近些年来 REST (REpresentational State Transfer) 已经变成了 web services 和 web APIs 的标配。html
在本文中我将向你展现如何简单地使用 Python 和 Flask 框架来建立一个 RESTful 的 web service。python
六条设计规范定义了一个 REST 系统的特色:web
REST 架构的最初目的是适应万维网的 HTTP 协议。数据库
RESTful web services 概念的核心就是“资源”。 资源能够用 URI 来表示。客户端使用 HTTP 协议定义的方法来发送请求到这些 URIs,固然可能会致使这些被访问的”资源“状态的改变。json
HTTP 标准的方法有以下:flask
========== ===================== ==================================
HTTP 方法 行为 示例
========== ===================== ==================================
GET 获取资源的信息 http://example.com/api/orders
GET 获取某个特定资源的信息 http://example.com/api/orders/123
POST 建立新资源 http://example.com/api/orders
PUT 更新资源 http://example.com/api/orders/123
DELETE 删除资源 http://example.com/api/orders/123
========== ====================== ==================================
REST 设计不须要特定的数据格式。在请求中数据能够以 JSON 形式, 或者有时候做为 url 中查询参数项。api
坚持 REST 的准则设计一个 web service 或者 API 的任务就变成一个标识资源被展现出来以及它们是怎样受不一样的请求方法影响的练习。数组
好比说,咱们要编写一个待办事项应用程序并且咱们想要为它设计一个 web service。要作的第一件事情就是决定用什么样的根 URL 来访问该服务。例如,咱们能够经过这个来访问:浏览器
http://[hostname]/todo/api/v1.0/缓存
在这里我已经决定在 URL 中包含应用的名称以及 API 的版本号。在 URL 中包含应用名称有助于提供一个命名空间以便区分同一系统上的其它服务。在 URL 中包含版本号可以帮助之后的更新,若是新版本中存在新的和潜在不兼容的功能,能够不影响依赖于较旧的功能的应用程序。
下一步骤就是选择将由该服务暴露(展现)的资源。这是一个十分简单地应用,咱们只有任务,所以在咱们待办事项中惟一的资源就是任务。
咱们的任务资源将要使用 HTTP 方法以下:
========== =============================================== =============================
HTTP 方法 URL 动做
========== =============================================== ==============================
GET http://[hostname]/todo/api/v1.0/tasks 检索任务列表
GET http://[hostname]/todo/api/v1.0/tasks/[task_id] 检索某个任务
POST http://[hostname]/todo/api/v1.0/tasks 建立新任务
PUT http://[hostname]/todo/api/v1.0/tasks/[task_id] 更新任务
DELETE http://[hostname]/todo/api/v1.0/tasks/[task_id] 删除任务
========== ================================================ =============================
咱们定义的任务有以下一些属性:
目前为止关于咱们的 web service 的设计基本完成。剩下的事情就是实现它!
若是你读过 Flask Mega-Tutorial 系列,就会知道 Flask 是一个简单却十分强大的 Python web 框架。
在咱们深刻研究 web services 的细节以前,让咱们回顾一下一个普通的 Flask Web 应用程序的结构。
我会首先假设你知道 Python 在你的平台上工做的基本知识。 我将讲解的例子是工做在一个类 Unix 操做系统。简而言之,这意味着它们能工做在 Linux,Mac OS X 和 Windows(若是你使用Cygwin)。 若是你使用 Windows 上原生的 Python 版本的话,命令会有所不一样。
让咱们开始在一个虚拟环境上安装 Flask。若是你的系统上没有 virtualenv,你能够从 https://pypi.python.org/pypi/virtualenv 上下载:
$ mkdir todo-api
$ cd todo-api
$ virtualenv flask
New python executable in flask/bin/python
Installing setuptools............................done.
Installing pip...................done.
$ flask/bin/pip install flask
既然已经安装了 Flask,如今开始建立一个简单地网页应用,咱们把它放在一个叫 app.py 的文件中:
#!flask/bin/python
from flask import Flask app = Flask(__name__) @app.route('/') def index(): return "Hello, World!" if __name__ == '__main__': app.run(debug=True)
为了运行这个程序咱们必须执行 app.py:
$ chmod a+x app.py
$ ./app.py
* Running on http://127.0.0.1:5000/
* Restarting with reloader
如今你能够启动你的网页浏览器,输入 http://localhost:5000 看看这个小应用程序的效果。
简单吧?如今咱们将这个应用程序转换成咱们的 RESTful service!
使用 Flask 构建 web services 是十分简单地,比我在 Mega-Tutorial 中构建的完整的服务端的应用程序要简单地多。
在 Flask 中有许多扩展来帮助咱们构建 RESTful services,可是在我看来这个任务十分简单,没有必要使用 Flask 扩展。
咱们 web service 的客户端须要添加、删除以及修改任务的服务,所以显然咱们须要一种方式来存储任务。最直接的方式就是创建一个小型的数据库,可是数据库并非本文的主体。学习在 Flask 中使用合适的数据库,我强烈建议阅读 Mega-Tutorial。
这里咱们直接把任务列表存储在内存中,所以这些任务列表只会在 web 服务器运行中工做,在结束的时候就失效。 这种方式只是适用咱们本身开发的 web 服务器,不适用于生产环境的 web 服务器, 这种状况一个合适的数据库的搭建是必须的。
咱们如今来实现 web service 的第一个入口:
#!flask/bin/python
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/api/v1.0/tasks', methods=['GET']) def get_tasks(): return jsonify({'tasks': tasks}) if __name__ == '__main__': app.run(debug=True)
正如你所见,没有多大的变化。咱们建立一个任务的内存数据库,这里无非就是一个字典和数组。数组中的每个元素都具备上述定义的任务的属性。
取代了首页,咱们如今拥有一个 get_tasks 的函数,访问的 URI 为 /todo/api/v1.0/tasks,而且只容许 GET 的 HTTP 方法。
这个函数的响应不是文本,咱们使用 JSON 数据格式来响应,Flask 的 jsonify 函数从咱们的数据结构中生成。
使用网页浏览器来测试咱们的 web service 不是一个最好的注意,由于网页浏览器上不能轻易地模拟全部的 HTTP 请求的方法。相反,咱们会使用 curl。若是你尚未安装 curl 的话,请当即安装它。
经过执行 app.py,启动 web service。接着打开一个新的控制台窗口,运行如下命令:
$ curl -i http://localhost:5000/todo/api/v1.0/tasks
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 294
Server: Werkzeug/0.8.3 Python/2.7.3
Date: Mon, 20 May 2013 04:53:53 GMT
{
"tasks": [
{
"description": "Milk, Cheese, Pizza, Fruit, Tylenol",
"done": false,
"id": 1,
"title": "Buy groceries"
},
{
"description": "Need to find a good Python tutorial on the web",
"done": false,
"id": 2,
"title": "Learn Python"
}
]
}
咱们已经成功地调用咱们的 RESTful service 的一个函数!
如今咱们开始编写 GET 方法请求咱们的任务资源的第二个版本。这是一个用来返回单独一个任务的函数:
from flask import abort @app.route('/todo/api/v1.0/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]})
第二个函数有些意思。这里咱们获得了 URL 中任务的 id,接着 Flask 把它转换成 函数中的 task_id 的参数。
咱们用这个参数来搜索咱们的任务数组。若是咱们的数据库中不存在搜索的 id,咱们将会返回一个相似 404 的错误,根据 HTTP 规范的意思是 “资源未找到”。
若是咱们找到相应的任务,那么咱们只需将它用 jsonify 打包成 JSON 格式并将其发送做为响应,就像咱们之前那样处理整个任务集合。
调用 curl 请求的结果以下:
$ curl -i http://localhost:5000/todo/api/v1.0/tasks/2
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 151
Server: Werkzeug/0.8.3 Python/2.7.3
Date: Mon, 20 May 2013 05:21:50 GMT
{
"task": {
"description": "Need to find a good Python tutorial on the web",
"done": false,
"id": 2,
"title": "Learn Python"
}
}
$ curl -i http://localhost:5000/todo/api/v1.0/tasks/3
HTTP/1.0 404 NOT FOUND
Content-Type: text/html
Content-Length: 238
Server: Werkzeug/0.8.3 Python/2.7.3
Date: Mon, 20 May 2013 05:21:52 GMT
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>404 Not Found</title>
<h1>Not Found</h1>
<p>The requested URL was not found on the server.</p><p>If you entered the URL manually please check your spelling and try again.</p>
当咱们请求 id #2 的资源时候,咱们获取到了,可是当咱们请求 #3 的时候返回了 404 错误。有关错误奇怪的是返回的是 HTML 信息而不是 JSON,这是由于 Flask 按照默认方式生成 404 响应。因为这是一个 Web service 客户端但愿咱们老是以 JSON 格式回应,因此咱们须要改善咱们的 404 错误处理程序:
from flask import make_response @app.errorhandler(404) def not_found(error): return make_response(jsonify({'error': 'Not found'}), 404)
咱们会获得一个友好的错误提示:
$ curl -i http://localhost:5000/todo/api/v1.0/tasks/3
HTTP/1.0 404 NOT FOUND
Content-Type: application/json
Content-Length: 26
Server: Werkzeug/0.8.3 Python/2.7.3
Date: Mon, 20 May 2013 05:36:54 GMT
{
"error": "Not found"
}
接下来就是 POST 方法,咱们用来在咱们的任务数据库中插入一个新的任务:
from flask import request @app.route('/todo/api/v1.0/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
添加一个新的任务也是至关容易地。只有当请求以 JSON 格式形式,request.json 才会有请求的数据。若是没有数据,或者存在数据可是缺乏 title 项,咱们将会返回 400,这是表示请求无效。
接着咱们会建立一个新的任务字典,使用最后一个任务的 id + 1 做为该任务的 id。咱们容许 description 字段缺失,而且假设 done 字段设置成 False。
咱们把新的任务添加到咱们的任务数组中,而且把新添加的任务和状态 201 响应给客户端。
使用以下的 curl 命令来测试这个新的函数:
$ curl -i -H "Content-Type: application/json" -X POST -d '{"title":"Read a book"}' http://localhost:5000/todo/api/v1.0/tasks
HTTP/1.0 201 Created
Content-Type: application/json
Content-Length: 104
Server: Werkzeug/0.8.3 Python/2.7.3
Date: Mon, 20 May 2013 05:56:21 GMT
{
"task": {
"description": "",
"done": false,
"id": 3,
"title": "Read a book"
}
}
注意:若是你在 Windows 上而且运行 Cygwin 版本的 curl,上面的命令不会有任何问题。然而,若是你使用原生的 curl,命令会有些不一样:
curl -i -H "Content-Type: application/json" -X POST -d "{"""title""":"""Read a book"""}" http://localhost:5000/todo/api/v1.0/tasks
固然在完成这个请求后,咱们能够获得任务的更新列表:
$ curl -i http://localhost:5000/todo/api/v1.0/tasks
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 423
Server: Werkzeug/0.8.3 Python/2.7.3
Date: Mon, 20 May 2013 05:57:44 GMT
{
"tasks": [
{
"description": "Milk, Cheese, Pizza, Fruit, Tylenol",
"done": false,
"id": 1,
"title": "Buy groceries"
},
{
"description": "Need to find a good Python tutorial on the web",
"done": false,
"id": 2,
"title": "Learn Python"
},
{
"description": "",
"done": false,
"id": 3,
"title": "Read a book"
}
]
}
剩下的两个函数以下所示:
@app.route('/todo/api/v1.0/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': task[0]}) @app.route('/todo/api/v1.0/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'