Flask-RESTful
Flask-RESTful 是一个 Flask 扩展,它添加了快速构建 REST APIs 的支持。它当然也是一个能够跟你现有的ORM/库协同工作的轻量级的扩展。Flask-RESTful 鼓励以最小设置的最佳实践。如果你熟悉 Flask 的话,Flask-RESTful 应该很容易上手。
安装
使用 pip
安装 Flask-RESTful:
pip install flask-restful
开发的版本可以从 GitHub 上的页面 下载
git clone https://github.com/twilio/flask-restful.git
cd flask-restful
python setup.py develop
Flask-RESTful 有如下的依赖包(如果你使用 pip
,依赖包会自动地安装):
- Flask 版本 0.8 或者更高
Flask-RESTful 要求 Python 版本为 2.6, 2.7, 或者 3.3。
快速入门
是时候编写你第一个 REST API。本指南假设你对 Flask 有一定的认识,并且已经安装了 Flask 和 Flask-RESTful。如果还没有安装的话,可以依照 安装 章节的步骤安装。
一个最小的 API
一个最小的 Flask-RESTful API 像这样:
from flask import Flask
from flask.ext import restful
app = Flask(__name__)
api = restful.Api(app)
class HelloWorld(restful.Resource):
def get(self):
return {'hello': 'world'}
api.add_resource(HelloWorld, '/')
if __name__ == '__main__':
app.run(debug=True)
把上述代码保存为 api.py 并且在你的 Python 解释器中运行它。需要注意地是我们已经启用了 Flask 调试 模式,这种模式提供了代码的重载以及更好的错误信息。调试模式绝不能在生产环境下使用。
$ python api.py
* Running on http://127.0.0.1:5000/
现在打开一个新的命令行窗口使用 curl 测试你的 API:
$ curl http://127.0.0.1:5000/
{"hello": "world"}
资源丰富的路由(Resourceful Routing)
Flask-RESTful 提供的最主要的基础就是资源(resources)。资源(Resources)是构建在 Flask 可拔插视图 之上,只要在你的资源(resource)上定义方法就能够容易地访问多个 HTTP 方法。一个待办事项应用程序的基本的 CRUD 资源看起来像这样:
from flask import Flask, request
from flask.ext.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)
你可以尝试这样:
$ curl http://localhost:5000/todo1 -d "data=Remember the milk" -X PUT
{"todo1": "Remember the milk"}
$ curl http://localhost:5000/todo1
{"todo1": "Remember the milk"}
$ curl http://localhost:5000/todo2 -d "data=Change my brakepads" -X PUT
{"todo2": "Change my brakepads"}
$ curl http://localhost:5000/todo2
{"todo2": "Change my brakepads"}
或者如果你安装了 requests 库的话,可以从 python shell 中运行:
>>> from requests import put, get
>>> put('http://localhost:5000/todo1', data={'data': 'Remember the milk'}).json()
{u'todo1': u'Remember the milk'}
>>> get('http://localhost:5000/todo1').json()
{u'todo1': u'Remember the milk'}
>>> put('http://localhost:5000/todo2', data={'data': 'Change my brakepads'}).json()
{u'todo2': u'Change my brakepads'}
>>> get('http://localhost:5000/todo2').json()
{u'todo2': u'Change my brakepads'}
Flask-RESTful 支持视图方法多种类型的返回值。同 Flask 一样,你可以返回任一迭代器,它将会被转换成一个包含原始 Flask 响应对象的响应。Flask-RESTful 也支持使用多个返回值来设置响应代码和响应头,如下所示:
class Todo1(Resource):
def get(self):
# Default to 200 OK
return {'task': 'Hello world'}
class Todo2(Resource):
def get(self):
# Set the response code to 201
return {'task': 'Hello world'}, 201
class Todo3(Resource):
def get(self):
# Set the response code to 201 and return custom headers
return {'task': 'Hello world'}, 201, {'Etag': 'some-opaque-string'}
端点(Endpoints)
很多时候在一个 API 中,你的资源可以通过多个 URLs 访问。你可以把多个 URLs 传给 Api 对象的 Api.add_resource()
方法。每一个 URL 都能访问到你的 Resource
api.add_resource(HelloWorld,
'/',
'/hello')
你也可以为你的资源方法指定 endpoint 参数。
api.add_resource(Todo,
'/todo/<int:todo_id>', endpoint='todo_ep')
参数解析
尽管 Flask 能够简单地访问请求数据(比如查询字符串或者 POST 表单编码的数据),验证表单数据仍然很痛苦。Flask-RESTful 内置了支持验证请求数据,它使用了一个类似 argparse 的库。
from flask.ext.restful import reqparse
parser = reqparse.RequestParser()
parser.add_argument('rate', type=int, help='Rate to charge for this resource')
args = parser.parse_args()
需要注意地是与 argparse 模块不同,reqparse.RequestParser.parse_args()
返回一个 Python 字典而不是一个自定义的数据结构。
使用 reqparse
模块同样可以自由地提供聪明的错误信息。如果参数没有通过验证,Flask-RESTful 将会以一个 400 错误请求以及高亮的错误信息回应。
$ curl -d 'rate=foo' http://127.0.0.1:5000/
{'status': 400, 'message': 'foo cannot be converted to int'}
inputs
模块提供了许多的常见的转换函数,比如 inputs.date()
和 inputs.url()
。
使用 strict=True
调用 parse_args
能够确保当请求包含你的解析器中未定义的参数的时候会抛出一个异常。
args = parser.parse_args(strict=True)
数据格式化
默认情况下,在你的返回迭代中所有字段将会原样呈现。尽管当你刚刚处理 Python 数据结构的时候,觉得这是一个伟大的工作,但是当实际处理它们的时候,会觉得十分沮丧和枯燥。为了解决这个问题,Flask-RESTful 提供了 fields
模块和 marshal_with()
装饰器。类似 Django ORM 和 WTForm,你可以使用 fields 模块来在你的响应中格式化结构。
from collections import OrderedDict
from flask.ext.restful import fields, marshal_with
resource_fields = {
'task': fields.String,
'uri': fields.Url('todo_ep')
}
class TodoDao(object):
def __init__(self, todo_id, task):
self.todo_id = todo_id
self.task = task
# This field will not be sent in the response
self.status = 'active'
class Todo(Resource):
@marshal_with(resource_fields)
def get(self, **kwargs):
return TodoDao(todo_id='my_todo', task='Remember the milk')
上面的例子接受一个 python 对象并准备将其序列化。marshal_with()
装饰器将会应用到由 resource_fields
描述的转换。从对象中提取的唯一字段是 task
。fields.Url
域是一个特殊的域,它接受端点(endpoint)名称作为参数并且在响应中为该端点生成一个 URL。许多你需要的字段类型都已经包含在内。请参阅 fields
指南获取一个完整的列表。
完整的例子
在 api.py 中保存这个例子
from flask import Flask
from flask.ext.restful import reqparse, abort, Api, Resource
app = Flask(__name__)
api = Api(app)
TODOS = {
'todo1': {'task': 'build an API'},
'todo2': {'task': '?????'},
'todo3': {'task': 'profit!'},
}
def abort_if_todo_doesnt_exist(todo_id):
if todo_id not in TODOS:
abort(404, message="Todo {} doesn't exist".format(todo_id))
parser = reqparse.RequestParser()
parser.add_argument('task', type=str)
# Todo
# show a single todo item and lets you delete them
class Todo(Resource):
def get(self, todo_id):
abort_if_todo_doesnt_exist(todo_id)
return TODOS[todo_id]
def delete(self, todo_id):
abort_if_todo_doesnt_exist(todo_id)
del TODOS[todo_id]
return '', 204
def put(self, todo_id):
args = parser.parse_args()
task = {'task': args['task']}
TODOS[todo_id] = task
return task, 201
# TodoList
# shows a list of all todos, and lets you POST to add new tasks
class TodoList(Resource):
def get(self):
return TODOS
def post(self):
args = parser.parse_args()
todo_id = int(max(TODOS.keys()).lstrip('todo')) + 1
todo_id = 'todo%i' % todo_id
TODOS[todo_id] = {'task': args['task']}
return TODOS[todo_id], 201
##
## Actually setup the Api resource routing here
##
api.add_resource(TodoList, '/todos')
api.add_resource(Todo, '/todos/<todo_id>')
if __name__ == '__main__':
app.run(debug=True)
用法示例
$ python api.py
* Running on http://127.0.0.1:5000/
* Restarting with reloader
获取列表
$ curl http://localhost:5000/todos
{"todo1": {"task": "build an API"}, "todo3": {"task": "profit!"}, "todo2": {"task": "?????"}}
获取一个单独的任务
$ curl http://localhost:5000/todos/todo3
{"task": "profit!"}
删除一个任务
$ curl http://localhost:5000/todos/todo2 -X DELETE -v
> DELETE /todos/todo2 HTTP/1.1
> User-Agent: curl/7.19.7 (universal-apple-darwin10.0) libcurl/7.19.7 OpenSSL/0.9.8l zlib/1.2.3
> Host: localhost:5000
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 204 NO CONTENT
< Content-Type: application/json
< Content-Length: 0
< Server: Werkzeug/0.8.3 Python/2.7.2
< Date: Mon, 01 Oct 2012 22:10:32 GMT
增加一个新的任务
$ curl http://localhost:5000/todos -d "task=something new" -X POST -v
> POST /todos HTTP/1.1
> User-Agent: curl/7.19.7 (universal-apple-darwin10.0) libcurl/7.19.7 OpenSSL/0.9.8l zlib/1.2.3
> Host: localhost:5000
> Accept: */*
> Content-Length: 18
> Content-Type: application/x-www-form-urlencoded
>
* HTTP 1.0, assume close after body
< HTTP/1.0 201 CREATED
< Content-Type: application/json
< Content-Length: 25
< Server: Werkzeug/0.8.3 Python/2.7.2
< Date: Mon, 01 Oct 2012 22:12:58 GMT
<
* Closing connection #0
{"task": "something new"}
更新一个任务
$ curl http://localhost:5000/todos/todo3 -d "task=something different" -X PUT -v
> PUT /todos/todo3 HTTP/1.1
> Host: localhost:5000
> Accept: */*
> Content-Length: 20
> Content-Type: application/x-www-form-urlencoded
>
* HTTP 1.0, assume close after body
< HTTP/1.0 201 CREATED
< Content-Type: application/json
< Content-Length: 27
< Server: Werkzeug/0.8.3 Python/2.7.3
< Date: Mon, 01 Oct 2012 22:13:00 GMT
<
* Closing connection #0
{"task": "something different"}
请求解析
Flask-RESTful 的请求解析接口是模仿 argparse
接口。它设计成提供简单并且统一的访问 Flask 中 flask.request
对象里的任何变量的入口。
基本参数
这里是请求解析一个简单的例子。它寻找在 flask.Request.values
字典里的两个参数。一个类型为 int
,另一个的类型是 str
from flask.ext.restful import reqparse
parser = reqparse.RequestParser()
parser.add_argument('rate', type=int, help='Rate cannot be converted')
parser.add_argument('name', type=str)
args = parser.parse_args()
如果你指定了 help 参数的值,在解析的时候当类型错误被触发的时候,它将会被作为错误信息给呈现出来。如果你没有指定 help 信息的话,默认行为是返回类型错误本身的信息。
默认下,arguments 不是 必须的。另外,在请求中提供的参数不属于 RequestParser 的一部分的话将会被忽略。
另请注意:在请求解析中声明的参数如果没有在请求本身设置的话将默认为 None
。
必需的参数
要求一个值传递的参数,只需要添加 required=True
来调用 add_argument()
。
parser.add_argument('name', type=str, required=True,
help="Name cannot be blank!")
多个值&列表
如果你要接受一个键有多个值的话,你可以传入 action='append'
parser.add_argument('name', type=str, action='append')
这将让你做出这样的查询
curl http://api.example.com -d "Name=bob" -d "Name=sue" -d "Name=joe"
你的参数将会像这样
args = parser.parse_args()
args['name'] # ['bob', 'sue', 'joe']
其它目标(Destinations)
如果由于某种原因,你想要以不同的名称存储你的参数一旦它被解析的时候,你可以使用 dest
kwarg。
parser.add_argument('name', type=str, dest='public_name')
args = parser.parse_args()
args['public_name']
参数位置
默认下,RequestParser
试着从 flask.Request.values
,以及 flask.Request.json
解析值。
在 add_argument()
中使用 location
参数可以指定解析参数的位置。flask.Request
中任何变量都能被使用。例如:
# Look only in the POST body
parser.add_argument('name', type=int, location='form')
# Look only in the querystring
parser.add_argument('PageSize', type=int, location='args')
# From the request headers
parser.add_argument('User-Agent', type=str, location='headers')
# From http cookies
parser.add_argument('session_id', type=str, location='cookies')
# From file uploads
parser.add_argument('picture', type=werkzeug.datastructures.FileStorage, location='files')
多个位置
通过传入一个列表到 location
中可以指定 多个 参数位置:
parser.add_argument('text', location=['headers', 'values'])
列表中最后一个优先出现在结果集中。(例如:location=[‘headers’, ‘values’],解析后 ‘values’ 的结果会在 ‘headers’ 前面)
继承解析
往往你会为你编写的每个资源编写不同的解析器。这样做的问题就是如果解析器具有共同的参数。不是重写,你可以编写一个包含所有共享参数的父解析器接着使用 copy()
扩充它。你也可以使用 replace_argument()
覆盖父级的任何参数,或者使用 remove_argument()
完全删除参数。 例如:
from flask.ext.restful import RequestParser
parser = RequestParser()
parser.add_argument('foo', type=int)
parser_copy = parser.copy()
parser_copy.add_argument('bar', type=int)
# parser_copy has both 'foo' and 'bar'
parser_copy.replace_argument('foo', type=str, required=True, location='json')
# 'foo' is now a required str located in json, not an int as defined
# by original parser
parser_copy.remove_argument('foo')
# parser_copy no longer has 'foo' argument
输出字段
Flask-RESTful 提供了一个简单的方式来控制在你的响应中实际呈现什么数据。使用 fields
模块,你可以使用在你的资源里的任意对象(ORM 模型、定制的类等等)并且 fields
让你格式化和过滤响应,因此您不必担心暴露内部数据结构。
当查询你的代码的时候,哪些数据会被呈现以及它们如何被格式化是很清楚的。
基本用法
你可以定义一个字典或者 fields
的 OrderedDict 类型,OrderedDict 类型是指键名是要呈现的对象的属性或键的名称,键值是一个类,该类格式化和返回的该字段的值。这个例子有三个字段,两个是字符串(Strings)以及一个是日期时间(DateTime),格式为 RFC 822 日期字符串(同样也支持 ISO 8601)
from flask.ext.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() # Some function that queries the db
这个例子假设你有一个自定义的数据库对象(todo
),它具有属性:name
, address
, 以及 date_updated
。该对象上任何其它的属性可以被认为是私有的不会在输出中呈现出来。一个可选的 envelope
关键字参数被指定为封装结果输出。
装饰器 marshal_with
是真正接受你的数据对象并且过滤字段。marshal_with
能够在单个对象,字典,或者列表对象上工作。
注意:marshal_with 是一个很便捷的装饰器,在功能上等效于如下的 return marshal(db_get_todo(), resource_fields), 200
。这个明确的表达式能用于返回 200 以及其它的 HTTP 状态码作为成功响应(错误响应见 abort
)。
重命名属性
很多时候你面向公众的字段名称是不同于内部的属性名。使用 attribute
可以配置这种映射。
fields = {
'name': fields.String(attribute='private_name'),
'address': fields.String,
}
lambda 也能在 attribute
中使用
fields = {
'name': fields.String(attribute=lambda x: x._private_name),
'address': fields.String,
}
默认值
如果由于某种原因你的数据对象中并没有你定义的字段列表中的属性,你可以指定一个默认值而不是返回 None
。
fields = {
'name': fields.String(default='Anonymous User'),
'address': fields.String,
}
自定义字段&多个值
有时候你有你自己定义格式的需求。你可以继承 fields.Raw
类并且实现格式化函数。当一个属性存储多条信息的时候是特别有用的。例如,一个位域(bit-field)各位代表不同的值。你可以使用 fields
复用一个单一的属性到多个输出值(一个属性在不同情况下输出不同的结果)。
这个例子假设在 flags
属性的第一位标志着一个“正常”或者“迫切”项,第二位标志着“读”与“未读”。这些项可能很容易存储在一个位字段,但是可读性不高。转换它们使得具有良好的可读性是很容易的。
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'),
}
Url & 其它具体字段
Flask-RESTful 包含一个特别的字段,fields.Url
,即为所请求的资源合成一个 uri。这也是一个好示例,它展示了如何添加并不真正在你的数据对象中存在的数据到你的响应中。
class RandomNumber(fields.Raw):
def output(self, key, obj):
return random.random()
fields = {
'name': fields.String,
# todo_resource is the endpoint name when you called api.add_resource()
'uri': fields.Url('todo_resource'),
'random': RandomNumber,
}
默认情况下,fields.Url
返回一个相对的 uri。为了生成包含协议(scheme),主机名以及端口的绝对 uri,需要在字段声明的时候传入 absolute=True
。传入 scheme
关键字参数可以覆盖默认的协议(scheme):
fields = {
'uri': fields.Url('todo_resource', absolute=True)
'https_uri': fields.Url('todo_resource', absolute=True, scheme='https')
}
复杂结构
你可以有一个扁平的结构,marshal_with 将会把它转变为一个嵌套结构
>>> from flask.ext.restful 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 字段并不真正地存在于数据对象中,但是任何一个子字段(sub-fields)可以直接地访问对象的属性,就像没有嵌套一样。
列表字段
你也可以把字段解组(unmarshal)成列表
>>> from flask.ext.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"}'
高级:嵌套字段
尽管使用字典套入字段能够使得一个扁平的数据对象变成一个嵌套的响应,你可以使用 Nested
解组(unmarshal)嵌套数据结构并且合适地呈现它们。
>>> from flask.ext.restful 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_with(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
构造函数把字段的字典作为子字段(sub-fields)来呈现。使用 Nested
和之前例子中的嵌套字典之间的重要区别就是属性的上下文。在本例中 “billing_address” 是一个具有自己字段的复杂的对象,传递给嵌套字段的上下文是子对象(sub-object),而不是原来的“数据”对象。换句话说,data.billing_address.addr1
是在这里的范围(译者:这里是直译),然而在之前例子中的 data.addr1
是位置属性。记住:嵌套和列表对象创建一个新属性的范围。
扩展 Flask-RESTful
我们认识到每一个人在 REST 框架上有着不同的需求。Flask-RESTful 试图尽可能的灵活,但是有时候你可能会发现内置的功能不足够满足你的需求。Flask-RESTful 有几个不同的扩展点,这些扩展在这种情况下会有帮助。
内容协商
开箱即用,Flask-RESTful 仅配置为支持 JSON。我们做出这个决定是为了给 API 维护者完全控制 API 格式支持,因此一年来的路上,你不必支持那些使用 API 且用 CSV 表示的人们,甚至你都不知道他们的存在。要添加其它的 mediatypes 到你的 API 中,你需要在 Api
对象中声明你支持的表示。
app = Flask(__name__)
api = restful.Api(app)
@api.representation('application/json')
def output_json(data, code, headers=None):
resp = make_response(json.dumps(data), code)
resp.headers.extend(headers or {})
return resp
这些表示函数必须返回一个 Flask Response
对象。
自定义字段 & 输入
一种最常见的 Flask-RESTful 附件功能就是基于你自己数据类型的数据来定义自定义的类型或者字段。
字段
自定义输出字段让你无需直接修改内部对象执行自己的输出格式。所有你必须做的就是继承 Raw
并且实现 format()
方法:
class AllCapsString(fields.Raw):
def format(self, value):
return value.upper()
# example usage
fields = {
'name': fields.String,
'all_caps_name': AllCapsString(attribute=name),
}
输入
对于解析参数,你可能要执行自定义验证。创建你自己的输入类型让你轻松地扩展请求解析。
def odd_number(value):
if value % 2 == 0:
raise ValueError("Value is not odd")
return value
请求解析器在你想要在错误消息中引用名称的情况下将也会允许你访问参数的名称。
def odd_number(value, name):
if value % 2 == 0:
raise ValueError("The parameter '{}' is not odd. You gave us the value: {}".format(name, value))
return value
你还可以将公开的参数转换为内部表示:
# maps the strings to their internal integer representation
# 'init' => 0
# 'in-progress' => 1
# 'completed' => 2
def task_status(value):
statuses = [u"init", u"in-progress", u"completed"]
return statuses.index(value)
然后你可以在你的 RequestParser 中使用这些自定义输入类型:
parser = reqparse.RequestParser()
parser.add_argument('OddNumber', type=odd_number)
parser.add_argument('Status', type=task_status)
args = parser.parse_args()
响应格式
为了支持其它的表示(像 XML,CSV,HTML),你可以使用 representation()
装饰器。你需要在你的 API 中引用它。
api = restful.Api(app)
@api.representation('text/csv')
def output_csv(data, code, headers=None):
pass
# implement csv output!
这些输出函数有三个参数,data
,code
,以及 headers
。
data
是你从你的资源方法返回的对象,code
是预计的 HTTP 状态码,headers
是设置在响应中任意的 HTTP 头。你的输出函数应该返回一个 Flask 响应对象。
def output_json(data, code, headers=None):
"""Makes a Flask response with a JSON encoded body"""
resp = make_response(json.dumps(data), code)
resp.headers.extend(headers or {})
return resp
另外一种实现这一点的就是继承 Api
类并且提供你自己输出函数。
class Api(restful.Api):
def __init__(self, *args, **kwargs):
super(Api, self).__init__(*args, **kwargs)
self.representations = {
'application/xml': output_xml,
'text/html': output_html,
'text/csv': output_csv,
'application/json': output_json,
}
资源方法装饰器
Resource()
有一个叫做 method_decorators 的属性。你可以继承 Resource 并且添加你自己的装饰器,该装饰器将会被添加到资源里面所有 method
函数。举例来说,如果你想要为每一个请求建立自定义认证。
def authenticate(func):
@wraps(func)
def wrapper(*args, **kwargs):
if not getattr(func, 'authenticated', True):
return func(*args, **kwargs)
acct = basic_authentication() # custom account lookup function
if acct:
return func(*args, **kwargs)
restful.abort(401)
return wrapper
class Resource(restful.Resource):
method_decorators = [authenticate] # applies to all inherited resources
由于 Flask-RESTful Resources 实际上是 Flask 视图对象,你也可以使用标准的 flask 视图装饰器。
自定义错误处理器
错误处理是一个很棘手的问题。你的 Flask 应用可能身兼数职,然而你要以正确的内容类型以及错误语法处理所有的 Flask-RESTful 错误。
Flask-RESTful 在 Flask-RESTful 路由上发生任何一个 400 或者 500 错误的时候调用 handle_error()
函数,不会干扰到其它的路由。你可能需要你的应用程序在 404 Not Found 错误上返回一个携带正确媒体类型(介质类型)的错误信息;在这种情况下,使用 Api
构造函数的 catch_all_404s
参数。
app = Flask(__name__)
api = flask_restful.Api(app, catch_all_404s=True)
Flask-RESTful 会处理除了自己路由上的错误还有应用程序上所有的 404 错误。
有时候你想在发生错误的时候做一些特别的东西 - 记录到文件,发送邮件,等等。使用 got_request_exception()
方法把自定义错误处理加入到异常。
def log_exception(sender, exception, **extra):
""" Log an exception to our logging framework """
sender.logger.debug('Got exception during processing: %s', exception)
from flask import got_request_exception
got_request_exception.connect(log_exception, app)
定义自定义错误消息
在一个请求期间遇到某些错误的时候,你可能想返回一个特定的消息以及/或者状态码。你可以告诉 Flask-RESTful 你要如何处理每一个错误/异常,因此你不必在你的 API 代码中编写 try/except 代码块。
errors = {
'UserAlreadyExistsError': {
'message': "A user with that username already exists.",
'status': 409,
},
'ResourceDoesNotExist': {
'message': "A resource with that ID no longer exists.",
'status': 410,
'extra': "Any extra information you want.",
},
}
包含 ‘status’
键可以设置响应的状态码。如果没有指定的话,默认是 500.
一旦你的 errors
字典定义,简单地把它传给 Api
构造函数。
app = Flask(__name__)
api = flask_restful.Api(app, errors=errors)
中高级用法
本页涉及构建一个稍微复杂的 Flask-RESTful 应用程序,该应用程序将会覆盖到一些最佳练习当你建立一个真实世界的基于 Flask-RESTful 的 API。快速入门 章节适用于开始你的第一个 Flask-RESTful 应用程序,因此如果你是 Flask-RESTful 的新用户,最好查阅该章节。
项目结构
有许多不同的方式来组织你的 Flask-RESTful 应用程序,但是这里我们描述了一个在大型的应用程序中能够很好地扩展并且维持一个不错的文件组织。
最基本的思路就是把你的应用程序分为三个主要部分。路由,资源,以及任何公共的基础部分。
下面是目录结构的一个例子:
myapi/
__init__.py
app.py # this file contains your app and routes
resources/
__init__.py
foo.py # contains logic for /Foo
bar.py # contains logic for /Bar
common/
__init__.py
util.py # just some common infrastructure
common 文件夹可能只包含一组辅助函数以满足你的应用程序公共的需求。例如,它也可能包含任何自定义输入/输出类型。
在 resource 文件夹中,你只有资源对象。因此这里就是 foo.py 可能的样子:
from flask.ext import restful
class Foo(restful.Resource):
def get(self):
pass
def post(self):
pass
app.py 中的配置就像这样:
from flask import Flask
from flask.ext import restful
from myapi.resources.foo import Foo
from myapi.resources.bar import Bar
from myapi.resources.baz import Baz
app = Flask(__name__)
api = restful.Api(app)
api.add_resource(Foo, '/Foo', '/Foo/<str:id>')
api.add_resource(Bar, '/Bar', '/Bar/<str:id>')
api.add_resource(Baz, '/Baz', '/Baz/<str:id>')
因为你可能编写一个特别大型或者复杂的 API,这个文件里面会有一个所有路由以及资源的复杂列表。你也可以使用这个文件来设置任何的配置值(before_request,after_request)。基本上,这个文件配置你整个 API。
完整的参数解析示例
在文档的其它地方,我们已经详细地介绍了如何使用 reqparse 的例子。这里我们将设置一个有多个输入参数的资源。我们将定义一个名为 “User” 的资源。
from flask.ext import restful
from flask.ext.restful import fields, marshal_with, reqparse
def email(email_str):
""" return True if email_str is a valid email """
if valid_email(email):
return True
else:
raise ValidationError("{} is not a valid email")
post_parser = reqparse.RequestParser()
post_parser.add_argument(
'username', dest='username',
type=str, location='form',
required=True, help='The user\'s username',
)
post_parser.add_argument(
'email', dest='email',
type=email, location='form',
required=True, help='The user\'s email',
)
post_parser.add_argument(
'user_priority', dest='user_priority',
type=int, location='form',
default=1, choices=range(5), help='The user\'s priority',
)
user_fields = {
'id': fields.Integer,
'username': fields.String,
'email': fields.String,
'user_priority': fields.Integer,
'custom_greeting': fields.FormattedString('Hey there {username}!'),
'date_created': fields.DateTime,
'date_updated': fields.DateTime,
'links': fields.Nested({
'friends': fields.Url('/Users/{id}/Friends'),
'posts': fields.Url('Users/{id}/Posts'),
}),
}
class User(restful.Resource):
@marshal_with(user_fields)
def post(self):
args = post_parser.parse_args()
user = create_user(args.username, args.email, args.user_priority)
return user
@marshal_with(user_fields)
def get(self, id):
args = get_parser.parse_args()
user = fetch_user(id)
return user
正如你所看到的,我们创建一个 post_parser
专门用来处理解析 POST 请求携带的参数。让我们逐步介绍每一个定义的参数。
post_parser.add_argument(
'username', dest='username',
type=str, location='form',
required=True, help='The user\'s username',
)
username
字段是所有参数中最为普通的。它从 POST 数据中获取一个字符串并且把它转换成一个字符串类型。该参数是必须得(required=True
),这就意味着如果不提供改参数,Flask-RESTful 会自动地返回一个消息是’用户名字段是必须‘的 400 错误。
post_parser.add_argument(
'email', dest='email',
type=email, location='form',
required=True, help='The user\'s email',
)
email
字段是一个自定义的 email
类型。在最前面几行中我们定义了一个 email
函数,它接受一个字符串,如果该字符串类型合法的话返回 True,否则会引起一个 ValidationError
异常,该异常明确表示 email 类型不合法。
post_parser.add_argument(
'user_priority', dest='user_priority',
type=int, location='form',
default=1, choices=range(5), help='The user\'s priority',
)
user_priority
类型充分利用了 choices
参数。这就意味着如果提供的 user_priority
值不落在由 choices
参数指定的范围内的话,Flask-RESTful 会自动地以 400 状态码以及一个描述性的错误消息响应。
下面该讨论到输入了。我们也在 user_fields
字典中定义了一些有趣的字段类型用来展示一些特殊的类型。
user_fields = {
'id': fields.Integer,
'username': fields.String,
'email': fields.String,
'user_priority': fields.Integer,
'custom_greeting': fields.FormattedString('Hey there {username}!'),
'date_created': fields.DateTime,
'date_updated': fields.DateTime,
'links': fields.Nested({
'friends': fields.Url('/Users/{id}/Friends', absolute=True),
'posts': fields.Url('Users/{id}/Posts', absolute=True),
}),
}
首先,存在一个 fields.FormattedString
。
'custom_greeting': fields.FormattedString('Hey there {username}!'),
此字段主要用于篡改响应中的值到其它的值。在这种情况下,custom_greeting
将总是包含从 username
字段返回的值。
下一步,检查 fields.Nested
。
'links': fields.Nested({
'friends': fields.Url('/Users/{id}/Friends', absolute=True),
'posts': fields.Url('Users/{id}/Posts', absolute=True),
}),
此字段是用于在响应中创建子对象。在这种情况下,我们要创建一个包含相关对象 urls 的 links
子对象。注意这里我们是使用了 fields.Nested
。
最后,我们使用了 fields.Url
字段类型。
'friends': fields.Url('/Users/{id}/Friends', absolute=True),
'posts': fields.Url('Users/{id}/Posts', absolute=True),
它接受一个字符串作为参数,它能以我们上面提到的 fields.FormattedString
同样的方式被格式化。传入 absolute=True
确保生成的 Urls 包含主机名。
API 文档
flask.ext.restful.marshal(data, fields, envelope=None)
Takes raw data (in the form of a dict, list, object) and a dict of fields to output and filters the data based on those fields.
Parameters:
- data – the actual object(s) from which the fields are taken from
- fields – a dict of whose keys will make up the final serialized response output
- envelope – optional key that will be used to envelop the serialized response
>>> from flask.ext.restful import fields, marshal
>>> data = { 'a': 100, 'b': 'foo' }
>>> mfields = { 'a': fields.Raw }
>>> marshal(data, mfields)
OrderedDict([('a', 100)])
>>> marshal(data, mfields, envelope='data')
OrderedDict([('data', OrderedDict([('a', 100)]))])
flask.ext.restful.marshal_with(fields, envelope=None)
A decorator that apply marshalling to the return values of your methods.
>>> from flask.ext.restful import fields, marshal_with
>>> mfields = { 'a': fields.Raw }
>>> @marshal_with(mfields)
... def get():
... return { 'a': 100, 'b': 'foo' }
...
...
>>> get()
OrderedDict([('a', 100)])
>>> @marshal_with(mfields, envelope='data')
... def get():
... return { 'a': 100, 'b': 'foo' }
...
...
>>> get()
OrderedDict([('data', OrderedDict([('a', 100)]))])
see flask.ext.restful.marshal()
flask.ext.restful.marshal_with_field(field)
A decorator that formats the return values of your methods with a single field.
>>> from flask.ext.restful import marshal_with_field, fields
>>> @marshal_with_field(fields.List(fields.Integer))
... def get():
... return ['1', 2, 3.0]
...
>>> get()
[1, 2, 3]
see flask.ext.restful.marshal_with()
flask.ext.restful.abort(http_status_code, **kwargs)
Raise a HTTPException for the given http_status_code. Attach any keyword arguments to the exception for later processing.
Api
class flask.ext.restful.Api(app=None, prefix='', default_mediatype='application/json', decorators=None, catch_all_404s=False, url_part_order='bae', errors=None)
The main entry point for the application. You need to initialize it with a Flask Application:
>>> app = Flask(__name__)
>>> api = restful.Api(app)
Alternatively, you can use init_app()
to set the Flask application after it has been constructed.
Parameters:
- app (flask.Flask) – the Flask application object
- prefix (str) – Prefix all routes with a value, eg v1 or 2010-04-01
- default_mediatype (str) – The default media type to return
- decorators (list) – Decorators to attach to every resource
- catch_all_404s (bool) – Use
handle_error()
to handle 404 errors throughout your app - url_part_order – A string that controls the order that the pieces of the url are concatenated when the full url is constructed. ‘b’ is the blueprint (or blueprint registration) prefix, ‘a’ is the api prefix, and ‘e’ is the path component the endpoint is added with
- errors () – A dictionary to define a custom response for each exception or error raised during a request
add_resource(resource, *urls, **kwargs)
Adds a resource to the api.
Parameters:
- resource (
Resource
) – the class name of your resource - urls (str) – one or more url routes to match for the resource, standard flask routing rules apply. Any url variables will be passed to the resource method as args.
- endpoint (str) – endpoint name (defaults to
Resource.__name__.lower()
Can be used to reference this route infields.Url
fields
Additional keyword arguments not specified above will be passed as-is to flask.Flask.add_url_rule()
.
Examples:
api.add_resource(HelloWorld, '/', '/hello')
api.add_resource(Foo, '/foo', endpoint="foo")
api.add_resource(FooSpecial, '/special/foo', endpoint="foo")
error_router(original_handler, e)
This function decides whether the error occured in a flask-restful endpoint or not. If it happened in a flask-restful endpoint, our handler will be dispatched. If it happened in an unrelated view, the app’s original error handler will be dispatched. In the event that the error occurred in a flask-restful endpoint but the local handler can’t resolve the situation, the router will fall back onto the original_handler as last resort.
Parameters:
- original_handler (function) – the original Flask error handler for the app
- e (Exception) – the exception raised while handling the request
handle_error(e)
Error handler for the API transforms a raised exception into a Flask response, with the appropriate HTTP status code and body.
Parameters: e (Exception) – the raised Exception object
init_app(app)
Initialize this class with the given flask.Flask
application or flask.Blueprint
object.
Parameters: app (flask.Blueprint) – the Flask application or blueprint object
Examples:
api = Api()
api.add_resource(...)
api.init_app(app)
make_response(data, *args, **kwargs)
Looks up the representation transformer for the requested media type, invoking the transformer to create a response object. This defaults to (application/json) if no transformer is found for the requested mediatype.
Parameters: data – Python object containing response data to be transformed
mediatypes()
Returns a list of requested mediatypes sent in the Accept header
mediatypes_method()
Return a method that returns a list of mediatypes
output(resource)
Wraps a resource (as a flask view function), for cases where the resource does not directly return a response object
Parameters: resource – The resource as a flask view function
owns_endpoint(endpoint)
Tests if an endpoint name (not path) belongs to this Api. Takes in to account the Blueprint name part of the endpoint name.
Parameters: endpoint – The name of the endpoint being checked
Returns: bool
representation(mediatype)
Allows additional representation transformers to be declared for the api. Transformers are functions that must be decorated with this method, passing the mediatype the transformer represents. Three arguments are passed to the transformer:
- The data to be represented in the response body
- The http status code
- A dictionary of headers
The transformer should convert the data appropriately for the mediatype and return a Flask response object.
Ex:
@api.representation('application/xml')
def xml(data, code, headers):
resp = make_response(convert_data_to_xml(data), code)
resp.headers.extend(headers)
return resp
resource(*urls, **kwargs)
Wraps a Resource
class, adding it to the api. Parameters are the same as add_resource()
.
Example:
app = Flask(__name__)
api = restful.Api(app)
@api.resource('/foo')
class Foo(Resource):
def get(self):
return 'Hello, World!'
unauthorized(response)
Given a response, change it to ask for credentials
url_for(resource, **values)
Generates a URL to the given resource.
class flask.ext.restful.Resource
Represents an abstract RESTful resource. Concrete resources should extend from this class and expose methods for each supported HTTP method. If a resource is invoked with an unsupported HTTP method, the API will return a response with status 405 Method Not Allowed. Otherwise the appropriate method is called and passed all arguments from the url rule used when adding the resource to an Api instance. See add_resource()
for details.
ReqParse
class reqparse.RequestParser(argument_class=<class 'reqparse.Argument'>, namespace_class=<class 'reqparse.Namespace'>)
Enables adding and parsing of multiple arguments in the context of a single request. Ex:
from flask import request
parser = RequestParser()
parser.add_argument('foo')
parser.add_argument('int_bar', type=int)
args = parser.parse_args()
add_argument(*args, **kwargs)
Adds an argument to be parsed.
Accepts either a single instance of Argument or arguments to be passed into Argument
‘s constructor.
See Argument
‘s constructor for documentation on the available options.
copy()
Creates a copy of this RequestParser with the same set of arguments
parse_args(req=None, strict=False)
Parse all arguments from the provided request and return the results as a Namespace
Parameters: strict – if req includes args not in parser, throw 400 BadRequest exception
remove_argument(name)
Remove the argument matching the given name.
replace_argument(name, *args, **kwargs)
Replace the argument matching the given name with a new version.
class reqparse.Argument(name, default=None, dest=None, required=False, ignore=False, type=<function <lambda> at 0x1065c6c08>, location=('json', 'values'), choices=(), action='store', help=None, operators=('=', ), case_sensitive=True, store_missing=True)
Parameters:
- name – Either a name or a list of option strings, e.g. foo or -f, –foo.
- default – The value produced if the argument is absent from the request.
- dest – The name of the attribute to be added to the object returned by
parse_args()
. - required (bool) – Whether or not the argument may be omitted (optionals only).
- action – The basic type of action to be taken when this argument is encountered in the request. Valid options are “store” and “append”.
- ignore – Whether to ignore cases where the argument fails type conversion
- type – The type to which the request argument should be converted. If a type raises a ValidationError, the message in the error will be returned in the response. Defaults to
unicode
in python2 andstr
in python3. - location – The attributes of the
flask.Request
object to source the arguments from (ex: headers, args, etc.), can be an iterator. The last item listed takes precedence in the result set. - choices – A container of the allowable values for the argument.
- help – A brief description of the argument, returned in the response when the argument is invalid with the name of the argument and the message passed to a ValidationError raised by a type converter.
- case_sensitive (bool) – Whether the arguments in the request are case sensitive or not
- store_missing (bool) – Whether the arguments default value should be stored if the argument is missing from the request.
__init__(name, default=None, dest=None, required=False, ignore=False, type=<function <lambda> at 0x1065c6c08>, location=('json', 'values'), choices=(), action='store', help=None, operators=('=', ), case_sensitive=True, store_missing=True)
handle_validation_error(error)
Called when an error is raised while parsing. Aborts the request with a 400 status and an error message
Parameters: error – the error that was raised
parse(request)
Parses argument value(s) from the request, converting according to the argument’s type.
Parameters: request – The flask request object to parse arguments from
source(request)
Pulls values off the request in the provided location :param request: The flask request object to parse arguments from
Fields
class fields.String(default=None, attribute=None)
Marshal a value as a string. Uses six.text_type
so values will be converted to unicode
in python2 and str
in python3.
format(value)
class fields.Url(endpoint=None, absolute=False, scheme=None)
FormattedString is used to interpolate other values from the response into this field. The syntax for the source string is the same as the string format
method from the python stdlib.
Ex:
fields = {
'name': fields.String,
'greeting': fields.FormattedString("Hello {name}")
}
data = {
'name': 'Doug',
}
marshal(data, fields)
output(key, obj)
class fields.Url(endpoint=None, absolute=False, scheme=None)
A string representation of a Url
output(key, obj)
class fields.DateTime(dt_format='rfc822', **kwargs)
Return a formatted datetime string in UTC. Supported formats are RFC 822 and ISO 8601.
Parameters: dt_format (str) – 'rfc822'
or 'iso8601'
format(value)
class fields.Float(default=None, attribute=None)
A double as IEEE-754 double precision. ex : 3.141592653589793 3.1415926535897933e-06 3.141592653589793e+24 nan inf -inf
format(value)
class fields.Integer(default=0, **kwargs)
Field for outputting an integer value.
Parameters:
- default (int) – The default value for the field, if no value is specified.
- attribute – If the public facing value differs from the internal value, use this to retrieve a different attribute from the response than the publicly named value.
format(value)
class fields.Arbitrary(default=None, attribute=None)
A floating point number with an arbitrary precision
ex: 634271127864378216478362784632784678324.23432
format(value)
class fields.Nested(nested, allow_null=False, **kwargs)
Allows you to nest one set of fields inside another. See 高级:嵌套字段 for more information
Parameters:
- nested (dict) – The dictionary to nest
- allow_null (bool) – Whether to return None instead of a dictionary with null keys, if a nested dictionary has all-null keys
- **kwargs – if
default
keyword argument is present, a nested dictionary will be marshaled as its value if nested dictionary is all-null keys (e.g. lets you return an empty JSON object instead of null)
:keyword default
output(key, obj)
class fields.List(cls_or_instance, **kwargs)
Field for marshalling lists of other fields.
See 列表字段 for more information.
Parameters: cls_or_instance – The field type the list will contain.
format(value)
output(key, data)
class fields.Raw(default=None, attribute=None)
Raw provides a base field class from which others should extend. It applies no formatting by default, and should only be used in cases where data does not need to be formatted before being serialized. Fields should throw a MarshallingException in case of parsing problem.
format(value)
Formats a field’s value. No-op by default - field classes that modify how the value of existing object keys should be presented should override this and apply the appropriate formatting.
Parameters: value – The value to format
Raises MarshallingException:
In case of formatting problem
Ex:
class TitleCase(Raw):
def format(self, value):
return unicode(value).title()
output(key, obj)
Pulls the value for the given key from the object, applies the field’s formatting and returns the result. If the key is not found in the object, returns the default value. Field classes that create values which do not require the existence of the key in the object should override this and return the desired value.
Raises MarshallingException:
In case of formatting problem
class fields.Boolean(default=None, attribute=None)
Field for outputting a boolean value.
Empty collections such as ""
, {}
, []
, etc. will be converted to False
.
format(value)
class fields.Fixed(decimals=5, **kwargs)
A decimal number with a fixed precision.
format(value)
fields.Price
alias of Fixed
Inputs
flask.ext.restful.inputs.url(value)
Validate a URL.
Parameters: value (string) – The URL to validate
Returns: The URL if valid.
Raises: ValueError
flask.ext.restful.inputs.regex
flask.ext.restful.inputs.date(value)
Parse a valid looking date in the format YYYY-mm-dd
flask.ext.restful.inputs.iso8601interval(value, argument='argument')
Parses ISO 8601-formatted datetime intervals into tuples of datetimes.
Accepts both a single date(time) or a full interval using either start/end or start/duration notation, with the following behavior:
- Intervals are defined as inclusive start, exclusive end
- Single datetimes are translated into the interval spanning the largest resolution not specified in the input value, up to the day.
- The smallest accepted resolution is 1 second.
- All timezones are accepted as values; returned datetimes are localized to UTC. Naive inputs and date inputs will are assumed UTC.
Examples:
"2013-01-01" -> datetime(2013, 1, 1), datetime(2013, 1, 2)
"2013-01-01T12" -> datetime(2013, 1, 1, 12), datetime(2013, 1, 1, 13)
"2013-01-01/2013-02-28" -> datetime(2013, 1, 1), datetime(2013, 2, 28)
"2013-01-01/P3D" -> datetime(2013, 1, 1), datetime(2013, 1, 4)
"2013-01-01T12:00/PT30M" -> datetime(2013, 1, 1, 12), datetime(2013, 1, 1, 12, 30)
"2013-01-01T06:00/2013-01-01T12:00" -> datetime(2013, 1, 1, 6), datetime(2013, 1, 1, 12)
Parameters: value (str) – The ISO8601 date time as a string
Returns: Two UTC datetimes, the start and the end of the specified interval
Return type: A tuple (datetime, datetime)
Raises: ValueError, if the interval is invalid.
flask.ext.restful.inputs.natural(value, argument='argument')
Restrict input type to the natural numbers (0, 1, 2, 3...)
flask.ext.restful.inputs.boolean(value)
Parse the string “true” or “false” as a boolean (case insensitive). Also accepts “1” and “0” as True/False (respectively). If the input is from the request JSON body, the type is already a native python boolean, and will be passed through without further parsing.
flask.ext.restful.inputs.rfc822(dt)
Turn a datetime object into a formatted date.
Example:
inputs.rfc822(datetime(2011, 1, 1)) => "Sat, 01 Jan 2011 00:00:00 -0000"
Parameters: dt (datetime) – The datetime to transform
Returns: A RFC 822 formatted date string
运行测试
Makefile
文件中照顾到搭建一个 virtualenv 为运行测试。所有你需要做的就是运行:
$ make test
要更改用于运行测试的 Python 版本(默认是 Python 2.7),修改上面 Makefile
文件中的 PYTHON_MAJOR
和 PYTHON_MINOR
变量。
你可以在所有支持的版本上运行测试:
$ make test-all
单个的测试可以使用如下格式的命令运行:
nosetests <filename>:ClassName.func_name
例如:
$ source env/bin/activate
$ nosetests tests/test_reqparse.py:ReqParseTestCase.test_parse_choices_insensitive
另外,提交你的更改到 Github 中你的分支上,Travis 将会自动地为你的分支运行测试。
也提供了一个 Tox 配置文件,因此你可以在本地在多个 python 版本(2.6,2.7,3.3,和 3.4)上测试
$ tox