Balloonsys Inc.

Just a blog now.

基于Flask设计RESTful API

Flask是一个Python WEB开发框架。接下来一段闲暇时间打算做的“西子快讯”项目中,我打算拿它来做RESTful API。虽然之前对Python并不了解,对Flask更是陌生,但爱折腾的本性促使我不断去学习。于是今天看blog、看文档,小有收获,略记一二。

准备工作

确保机器上已经安装了Python、Pip、Virtualenv等工具。如果默认的Pip安装源比较慢,可以用v2ex的试试。具体方法是在~/.pip/下创建一个名为pip.conf的文件,输入新的pip源。

1
2
[global]
index-url = http://pypi.v2ex.com/simple

安装Flask

为了不影响系统的Python环境,我们用Virtualenv虚拟一个运行环境,然后在此环境下开发一个名为todo的示例小应用(纯RESTful API,JSON格式的数据)。

1
2
3
4
5
cd ~/Desktop
mkdir todo-api
cd todo-api
virtualenv venv
. venv/bin/activate

此时命令行窗口应出现(venv)mbpr2013:todo-api linkoubian$ 这样的提示符,注意最左边的venv表明当前运行的是虚拟出来的环境。工作完成后打算退出venv,只需执行deactivate命令即可。

这时执行pip install flask便可在venv下安装flask模块。

测试Flask是否正常工作

编写一个Hello World程序,如下:

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

app = Flask(__name__)

@app.route('/')
def index():
  return "Hello, World!"

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

保存为~/Desktop/todo-api/app.py,添加可执行权限后,敲python app.py开始运行Flask。此时打开浏览器,访问localhost:5000即可看到Hello, World!

开始设计RESTful API

获取所有的tasks

一个task由id、title、description、done等属性构成,获取task列表的url可以设计成/todo/tasks。具体代码如下,

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
from flask import Flask, jsonify

app = Flask(__name__)

tasks = [
  {
      'id': 1,
      'title': u'Buy groceries',
      'description': u'Milk, Cheese, Pizza, Fruit, Tylenol',
      'done': False
  },
  {
      'id': 2,
      'title': u'Learn Python',
      'description': u'Need to find a good Python tutorial on the web',
      'done': False
  },
]

@app.route('/todo/tasks', methods = ['GET'])
def get_tasks():
  return jsonify({'tasks': tasks})

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

因为Flask运行在debug模式下会自动reload,所以代码写完后保存文件即可。

Mac上有一款名为Paw的HTTP Client,算是我见过、用过的各种API测试工具中的佼佼者,力荐。

打开Paw,在url里输入http://localhost:5000/todo/tasks,选择GET,敲CMD+Enter即可听到表示成功的清脆提示音。Paw除了提供原生的HTTP响应格式,还提供格式化后浏览功能,选择JSON格式即可。

上面这段代码还是比较清晰好懂的,首先创建一个task数组,初始化两个task对象放进去。指定GET方式请求/todo/tasks资源时执行get_tasks方法,具体是把前面创建的tasks数组以JSON的格式返回。而将Python的数组转成JSON格式是通过Flask的jsonify函数实现的。

根据id获得特定task

通常我们将id放在URL中,然后对应的处理函数从URL中得到id以从数据源(此处为task数组,存在内存中)中查询相应地entity(此处为task)。

1
2
3
4
5
6
@app.route('/todo/tasks/<int:task_id>', methods = ['GET'])
def get_task(task_id):
  task = filter(lambda t: t['id'] == task_id, tasks)
  if len(task) == 0:
      abort(404)
  return jsonify({'task': task[0]})

为了正常使用abort函数,需要从flask模块import一下。通过Paw测试http://localhost:5000/todo/tasks/1,发现一切正常。

访问http://localhost:5000/todo/tasks/0时,返回一段出错的HTML,这是Flask对404的默认处理,我们可以自己定义一个返回JSON格式的错误消息。

1
2
3
@app.errorhandler(404)
def not_found(error):
    return make_response(jsonify( { 'error': 'Not found' } ), 404)

我们用make_response函数,将构造好的JSON作为404错误的响应,返回给客户端。

创建一个task

URL仍然用/todo/tasks,但HTTP方法为POST。根据Request对象中的数据构造一个新的task对象,插入数据源即可。

1
2
3
4
5
6
7
8
9
10
11
12
@app.route('/todo/tasks', methods = ['POST'])
def create_task():
  if not request.json or not 'title' in request.json:
      abort(400)
  task = {
      'id': tasks[-1]['id'] + 1,
      'title': request.json['title'],
      'description': request.json.get('description', ''),
      'done': False
  }
  tasks.append(task)
  return jsonify({'task': task}), 201

另外,我们需要定义一个400的处理器,类似于404,

1
2
3
@app.errorhandler(400)
def not_found(error):
    return make_response(jsonify( { 'error': 'Bad request' } ), 400)

在Paw中,POST请求http://localhost:5000/todo/tasks,同时Header里加一个Content-Type,值为application/json,Body里加入一段文本内容{“title”: “Buy a cell phone”},敲CMD+Enter执行。

根据REST Cookbook一书的建议,可以在返回的新增task里加入一个uri字段。为此,建一个名为make_public_task的函数,如下,

1
2
3
4
5
6
7
8
def make_public_task(task):
  new_task = {}
  for field in task:
      new_task[field] = task[field]
      if field == 'id':
          new_task['uri'] = url_for('get_task', task_id = task['id'], _external = True)

  return new_task

另外,create_task中改为,

1
return jsonify({'task': make_public_task(task)}), 201

get_tasks中改为,

1
return jsonify({'tasks': map(make_public_task, tasks)})

get_task中改为,

1
return jsonify({'tasks': map(make_public_task, tasks)})

更新一个task

显然此时的HTTP方法为PUT,相应的url和获取task类似。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@app.route('/todo/tasks/<int:task_id>', methods = ['PUT'])
def update_task(task_id):
    task = filter(lambda t: t['id'] == task_id, tasks)
    if len(task) == 0:
        abort(404)
    if not request.json:
        abort(400)
    if 'title' in request.json and type(request.json['title']) != unicode:
        abort(400)
    if 'description' in request.json and type(request.json['description']) is not unicode:
        abort(400)
    if 'done' in request.json and type(request.json['done']) is not bool:
        abort(400)
    task[0]['title'] = request.json.get('title', task[0]['title'])
    task[0]['description'] = request.json.get('description', task[0]['description'])
    task[0]['done'] = request.json.get('done', task[0]['done'])
    return jsonify( { 'task': make_public_task(task[0]) } )

删除一个task

1
2
3
4
5
6
7
@app.route('/todo/tasks/<int:task_id>', methods = ['DELETE'])
def delete_task(task_id):
    task = filter(lambda t: t['id'] == task_id, tasks)
    if len(task) == 0:
        abort(404)
    tasks.remove(task[0])
    return jsonify( { 'result': True } )

修改Python源文件后,Flask会reload,这将导致上一次create的task丢失,所以直接尝试delete之前增加的task可能会报404资源找不到错误。

为WEB服务增加安全机制

基于Flask的HTTBasicAuth扩展,定义两个函数如下,

1
2
3
4
5
6
7
8
9
10
11
auth = HTTPBasicAuth()

@auth.get_password
def get_password(username):
    if username == 'linkoubian':
        return '7c4a8d09ca3762af61e59520943dc26494f8941b'
    return None

@auth.error_handler
def unauthorized():
    return make_response(jsonify( { 'error': 'Unauthorized access' } ), 401)

其中get_password方法是返回指定用户的密码信息(通常是从数据库用户表查询得到),当此处返回的值和客户端传递的值匹配时(匹配过程由HTTPBasicAuth完成)成功,否则执行error_handler返回错误信息。

定义完校验相关的代码,还需将校验逻辑应用到需要校验的WEB服务。比如,希望只有授权用户才可以修改或删除task,则可以在update_task及delete_task方法上面加上@auth.login_required即可。

此时在Paw中需添加Authorization头,值为HTTP Basic Auth,在username及password文本框中分布输入信息,敲CMD+Enter进行测试。

Paw HTTP Client

说明

本文是根据Miguel Grinberg写的这篇文章翻译并整理的。