使用Flask建立web services超级简单。

当然,也有很多Flask extensions可以帮助建立RESTful services,但是这个例实在太简单了,不需要使用任何扩展。

这个web service提供增加,删除、修改任务清单,所以我们需要将任务清单存储起来。最简单的做法就是使用小型的数据库,但是数据库并不是本文涉及太多的。可以参考原文作者的完整教程。Flask Mega-Tutorial series

在这里例子我们将任务清单存储在内存中,这样只能运行在单进程和单线程中,这样是不适合作为生产服务器的,若非就必需使用数据库了。

现在我们准备实现第一个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)

正如您所见,并没有改变太多代码。我们将任务清单存储在list内(内存),list存放两个非常简单的数组字典。每个实体就是我们上面定义的字段。

而 index 入口点有一个get_tasks函数与/todo/api/v1.0/tasks URI关联,只接受http的GET方法。

这个响应并非一般文本,是JSON格式的数据,是经过Flask框架的 jsonify模块格式化过的数据。

使用浏览器去测试web service并不是一个好的办法,因为要创建不同类弄的HTTP请求,事实上,我们将使用curl命令行。如果没有安装curl,快点去安装一个。

像刚才一样运行app.py。

打开一个终端运行以下命令:

$ 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]})

第二个函数稍稍复杂了一些。任务的id包含在URL内,Flask将task_id参数传入了函数内。

通过参数,检索tasks数组。如果参数传过来的id不存在于数组内,我们需要返回错误代码404,按照HTTP的规定,404意味着是"Resource Not Found",资源未找到。

如果找到任务在内存数组内,我们通过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>

当我们请求#2 id的资源时,可以获取,但是当我们请求#3的资源时返回了404错误。并且返回了一段奇怪的HTML错误,而不是我们期望的JSON,这是因为Flask产生了默认的404响应。客户端需要收到的都是JSON的响应,因此我们需要改进404错误处理:

from flask import make_response

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

这样我们就得到了友好的API错误响应:

$ 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

request.json里面包含请求数据,如果不是JSON或者里面没有包括title字段,将会返回400的错误代码。

当创建一个新的任务字典,使用最后一个任务id数值加1作为新的任务id(最简单的方法产生一个唯一字段)。这里允许不带description字段,默认将done字段值为False。

将新任务附加到tasks数组里面,并且返回客户端201状态码和刚刚添加的任务内容。HTTP定义了201状态码为“Created”。

测试上面的新功能:

$ 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"
}
}

注意:如果使用原生版本的curl命令行提示符,上面的命令会正确执行。如果是在Windows下使用Cygwin bash版本的curl,需要将body部份添加双引号:

curl -i -H "Content-Type: application/json" -X POST -d "{"""title""":"""Read a book"""}" http://localhost:5000/todo/api/v1.0/tasks

基本上在Windows中需要使用双引号包括body部份在内,而且需要三个双引号转义序列。

完成上面的事情,就可以看到更新之后的list数组内容:

$ 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': True})

delete_task函数没什么太特别的。update_task函数需要检查所输入的参数,防止产生错误的bug。确保是预期的JSON格式写入数据库里面。

测试将任务#2的done字段变更为done状态:

$ curl -i -H "Content-Type: application/json" -X PUT -d '{"done":true}' http://localhost:5000/todo/api/v1.0/tasks/2
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 170
Server: Werkzeug/0.8.3 Python/2.7.3
Date: Mon, 20 May 2013 07:10:16 GMT {
"task": [
{
"description": "Need to find a good Python tutorial on the web",
"done": true,
"id": 2,
"title": "Learn Python"
}
]
}

改进Web Service接口

当前我们还有一个问题,客户端有可能需要从返回的JSON中重新构造URI,如果将来加入新的特性时,可能需要修改客户端。(例如新增版本。)

我们可以返回整个URI的路径给客户端,而不是任务的id。为了这个功能,创建一个小函数生成一个“public”版本的任务URI返回:

from flask import url_for

def make_public_task(task):
new_task = {}
for field in task:
if field == 'id':
new_task['uri'] = url_for('get_task', task_id=task['id'], _external=True)
else:
new_task[field] = task[field]
return new_task

通过Flask的url_for模块,获取任务时,将任务中的id字段替换成uri字段,并且把值改为uri值。

当我们返回包含任务的list时,通过这个函数处理后,返回完整的uri给客户端:

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

现在看到的检索结果:

$ curl -i http://localhost:5000/todo/api/v1.0/tasks
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 406
Server: Werkzeug/0.8.3 Python/2.7.3
Date: Mon, 20 May 2013 18:16:28 GMT {
"tasks": [
{
"title": "Buy groceries",
"done": false,
"description": "Milk, Cheese, Pizza, Fruit, Tylenol",
"uri": "http://localhost:5000/todo/api/v1.0/tasks/1"
},
{
"title": "Learn Python",
"done": false,
"description": "Need to find a good Python tutorial on the web",
"uri": "http://localhost:5000/todo/api/v1.0/tasks/2"
}
]
}

这种办法避免了与其它功能的兼容,拿到的是完整uri而不是一个id。

RESTful web service的安全认证

我们已经完成了整个功能,但是我们还有一个问题。web service任何人都可以访问的,这不是一个好主意。

当前service是所有客户端都可以连接的,如果有别人知道了这个API就可以写个客户端随意修改数据了。 大多数教程没有与安全相关的内容,这是个十分严重的问题。

最简单的办法是在web service中,只允许用户名和密码验证通过的客户端连接。在一个常规的web应用中,应该有登录表单提交去认证,同时服务器会创建一个会话过程去进行通讯。这个会话过程id会被存储在客户端的cookie里面。不过这样就违返了我们REST中无状态的规则,因此,我们需求客户端每次都将他们的认证信息发送到服务器。

为此我们有两种方法表单认证方法去做,分别是 Basic 和 Digest。

这里有有个小Flask extension可以轻松做到。首先需要安装 Flask-HTTPAuth :

$ flask/bin/pip install flask-httpauth

假设web service只有用户 ok 和密码为 python 的用户接入。下面就设置了一个Basic HTTP认证:

from flask.ext.httpauth import HTTPBasicAuth
auth = HTTPBasicAuth() @auth.get_password
def get_password(username):
if username == 'ok':
return 'python'
return None @auth.error_handler
def unauthorized():
return make_response(jsonify({'error': 'Unauthorized access'}), 401)

get_password函数是一个回调函数,获取一个已知用户的密码。在复杂的系统中,函数是需要到数据库中检查的,但是这里只是一个小示例。

当发生认证错误之后,error_handler回调函数会发送错误的代码给客户端。这里我们自定义一个错误代码401,返回JSON数据,而不是HTML。

将@auth.login_required装饰器添加到需要验证的函数上面:

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

现在,试试使用curl调用这个函数:

$ curl -i http://localhost:5000/todo/api/v1.0/tasks
HTTP/1.0 401 UNAUTHORIZED
Content-Type: application/json
Content-Length: 36
WWW-Authenticate: Basic realm="Authentication Required"
Server: Werkzeug/0.8.3 Python/2.7.3
Date: Mon, 20 May 2013 06:41:14 GMT {
"error": "Unauthorized access"
}

这里表示了没通过验证,下面是带用户名与密码的验证:

$ curl -u ok:python -i http://localhost:5000/todo/api/v1.0/tasks
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 316
Server: Werkzeug/0.8.3 Python/2.7.3
Date: Mon, 20 May 2013 06:46:45 GMT {
"tasks": [
{
"title": "Buy groceries",
"done": false,
"description": "Milk, Cheese, Pizza, Fruit, Tylenol",
"uri": "http://localhost:5000/todo/api/v1.0/tasks/1"
},
{
"title": "Learn Python",
"done": false,
"description": "Need to find a good Python tutorial on the web",
"uri": "http://localhost:5000/todo/api/v1.0/tasks/2"
}
]
}

这个认证extension十分灵活,可以随指定需要验证的APIs。

为了确保登录信息的安全,最好的办法还是使用https加密的通讯方式,客户端与服务器端传输认证信息都是加密过的,防止第三方的人去看到。

当使用浏览器去访问这个接口,会弹出一个丑丑的登录对话框,如果密码错误就回返回401的错误代码。为了防止浏览器弹出验证对话框,客户端应该处理好这个登录请求。

有一个小技巧可以避免这个问题,就是修改返回的错误代码401。例如修改成403(”Forbidden“)就不会弹出验证对话框了。

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

当然,同时也需要客户端知道这个403错误的意义。

最后

还有很多办法去改进这个web service。

事实上,一个真正的web service应该使用真正的数据库。使用内存数据结构有非常多的限制,不要用在实际应用上面。

另外一方面,处理多用户。如果系统支持多用户认证,则任务清单也是对应多用户的。同时我们需要有第二种资源,用户资源。当用户注册时使用POST请求。使用GET返回用户信息到客户端。使用PUT请求更新用户资料,或者邮件地址。使用DELETE删除用户账号等。

通过GET请求检索任务清单时,有很多办法可以进扩展。第一,可以添加分页参数,使客户端只请求一部份数据。第二,可以添加筛选关键字等。所有这些元素可以添加到URL上面的参数。

原文

http://blog.miguelgrinberg.com/post/designing-a-restful-api-with-python-and-flask

Python 和 Flask实现RESTful services的更多相关文章

  1. 使用 Python 和 Flask 设计 RESTful API

    近些年来 REST (REpresentational State Transfer) 已经变成了 web services 和 web APIs 的标配. 在本文中我将向你展示如何简单地使用 Pyt ...

  2. Python 和 Flask 设计 RESTful API

    #!flask/bin/python from flask import Flask, jsonify from flask import make_response app = Flask(__na ...

  3. Python使用Flask实现RESTful API,使用Postman工具、requests库测试接口

    RESTful是一种API设计规范.在RESTful架构中,主要使用POST,DELETE,PUT和GET四种HTTP请求方式分别对指定的URL资源进行增删改查操作. RESTful之前的做法: /u ...

  4. 使用python的Flask实现一个RESTful API服务器端[翻译]

    最近这些年,REST已经成为web services和APIs的标准架构,很多APP的架构基本上是使用RESTful的形式了. 本文将会使用python的Flask框架轻松实现一个RESTful的服务 ...

  5. 使用python的Flask实现一个RESTful API服务器端

    使用python的Flask实现一个RESTful API服务器端 最近这些年,REST已经成为web services和APIs的标准架构,很多APP的架构基本上是使用RESTful的形式了. 本文 ...

  6. 转:使用python的Flask实现一个RESTful API服务器端

    提示:可以学习一下flask框架中对于密码进行校验的部分.封装了太多操作. 最近这些年,REST已经成为web services和APIs的标准架构,很多APP的架构基本上是使用RESTful的形式了 ...

  7. Python flask 基于 Flask 提供 RESTful Web 服务

    转载自 http://python.jobbole.com/87118/ 什么是 REST REST 全称是 Representational State Transfer,翻译成中文是『表现层状态转 ...

  8. Designing a RESTful API with Python and Flask 201

    rest服务器的搭建 - CSDN博客 http://blog.csdn.net/zhanghaotian2011/article/details/8760794 REST的架构设计 REST(Rep ...

  9. Python的Flask框架开发RESTful API

    web框架选择 Django,流行但是笨重,还麻烦,人生苦短,肯定不选 web.py,轻量,但据说作者仙逝无人维护,好吧,先pass tornado,据说倡导自己造轮子,虽然是facebook开源的吧 ...

随机推荐

  1. bash 变量传递方法

    ###1.sh    ##(该sh 目的是 将变量env传入env.sh, 同时让env.sh在当前事物生效,最后执行env.sh 定义的变量envs) export ENV=prepareecho ...

  2. ImageView加载长图(适用不需要缩放的情况)

    此案例适用于加载网络长图且图片的宽和高已知的情况.由于ImageView加载图片有一个4096*4096的限制,所以对于巨长图的加载比较麻烦,需要我们自己去手动处理. 有两种解决方案:第一种就是比较l ...

  3. 机器学习概念之特征选择(Feature selection)之VectorSlicer算法介绍

    不多说,直接上干货! VectorSlicer 算法介绍: VectorSlicer是一个转换器,输入特征向量,输出原始特征向量子集.VectorSlicer接收带有特定索引的向量列,通过对这些索引的 ...

  4. postman断言分析

    最近测试中用到postman,使用后就简单总结下常用的断言,下面带图的自己最常用的,其他的没怎么用. postman断言是JavaScript语言编写的,在postman客户端指定区域编写即可. 断言 ...

  5. 微信环境支付宝服务窗环境app手机浏览器pc端混合判断

    //微信环境 if(userAgent.match(/micromessenger/) == 'micromessenger'){ }//支付宝服务窗环境else if(userAgent.match ...

  6. img 标签访问图片返回403forbidden

    做百度编辑器时,从秀米复制过来的文档,图片不无法加载,返回403的错 解决办法 解决这个问题只需要在头部添加一个meta <meta name="referrer" cont ...

  7. maven编译报错 -source 1.5 中不支持 lambda(或diamond) 表达式,编码 UTF-8 的不可映射字符

    在用maven编译项目是由于项目中用了jdk 1.8, 编译是报错  -source 1.5 中不支持 lambda 表达式. 错误原因: Maven Compiler 插件默认会加 -source ...

  8. AJPFX辨析Java中运算符 ++ 和 += 的区别

    我们都知道Java中 ++ 和 +=1  都是把数字增加一后,把值赋给左边,那二者有什么区别呢? i+=1 运行的底层Heap申请一个区域存放i,在数据区域开劈一个区域存放1,2个内存段被数据被送入到 ...

  9. Android开发二维码之坑

    之前一直做的是.NET开发用的是C#语言,近段时间由于做一个APP这才用上了java,在二维码扫描整合到APP里面遇到扫描二维码之后没有返回值,经过反复的尝试最后终于拿到了返回值,之后觉得很有必要记录 ...

  10. 【数据分析 R语言实战】学习笔记 第六章 参数估计与R实现(下)

    6.3两正态总体的区间估计 (1)两个总体的方差已知 在R中编写计算置信区间的函数twosample.ci()如下,输入参数为样本x, y,置信度α和两个样本的标准差. > twosample. ...