Python Flask Restful API

【转载】文章转载自bestallen

目前Web应用这块,restufl API用得非常普遍,因为,你手上的前端设备五花八门,各种系统的手机,pad等等,而且网站和手机APP经常会有需要资源共享的时候。

如果网站做个app,手机端再独立一个app,要保持2者同步,估计做开发的要做死了。

所以,RESTFUL API提出了一个概念,就是资源为上,通俗地讲:就是,我有一个资源A,可以作为接口来提供出来,那么,前端设备的B,C,D 都可以通过申请这个API接口的方式,来进行获取资源,而最后显示画面如何渲染,让客户看到是怎么样的一个样子,那是前端的事情了。

学习API之前,有两个前置技能需要去了解下,一个是JSON数据格式,另外一个是curl或者HTTPie这样的http调试工具,我这里用的是HTTPie

最简单的Web API

先来看看通过Python Flask做一个非常简单的Web API接口范例

[python] view plain copy

  1. from flask import Flask,jsonify
  2. app = Flask(__name__)
  3. tasks =[
  4.     {
  5.         ‘id’:1,
  6.         ‘title’: u‘Buy groceries’,
  7.         ‘description’: u‘Milk, Cheese, Pizza, Fruit, Tylenol’,
  8.         ‘done’False
  9.     },
  10.     {
  11.         ‘id’2,
  12.         ‘title’: u‘Learn Python’,
  13.         ‘description’: u‘Need to find a good Python tutorial on the web’,
  14.         ‘done’False
  15.     }]
  16. @app.route(‘/’)
  17. def index():
  18.     return ‘Hello,world!’
  19. @app.route(‘/todo/api/v1.0/tasks’,methods=[‘GET’])
  20. def get_tasks():
  21.     return jsonify({‘tasks’:tasks})

如上面代码,当你请求 /todo/api/v1.0/tasks 的时候,通过路由get_tasks,就能返回当前所有的数据信息

他是通过jsonify来进行返回的,这里需要注意的是,有2种方法可以返回json数据,一个是jsonify,还有一个是json.dumps()

那么2者有什么区别呢?可以看下图

jsonify直接返回的是Content-type:application/json的响应对象(Response对象)

json.dumps返回的,则是Content-type:text/html,charset=utf-8的HTML格式

我们可以看下jsonfiy的官方解释


来看下效果图及两者的区别

返回单个数据

刚才是获取了所有的任务,那如果我需要其中某一个数据的话,如何做呢?

[python] view plain copy

  1. @app.route(‘/todo/api/v1.0/tasks/<int:task_id>’,methods=[‘GET’])
  2. def get_task(task_id):
  3.     task = list(filter(lambda t:t[‘id’]==task_id,tasks))   #检查tasks内部元素,是否有元素的id的值和参数id相匹配
  4.     if len(task)==0:                                       #有的话,就返回列表形式包裹的这个元素,如果没有,则报错404
  5.         abort(404)
  6.     return jsonify({‘tasks’:task[0]})                      #否则,将这个task以JSON的响应形式返回

在URL里加入变量,通过检查tasks内部每个元素的id对应的值,是否有和输入的id匹配的,如果有匹配的,那就返回成一个list给task变量

比如下面,他就获取了id为2的这个数据的内容,当然,如果你在URL后面添加的是3,那他就会报错了,因为暂时还没有id为3的这个数据

上面的报错画面有些难看,而且,这样的报错格式,不太利于别人接口的使用。

一般像正规的接口,他都会以JSON格式返回error内容或者代码,所以,我们也要优化一下这样的错误信息。

[python] view plain copy

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

 

这样,返回的格式,也是以JSON的格式,那样,如果别人调用你的接口,如果发生错误,别人就可以通过error信息,来进行渲染了。

POST方法添加信息

接下来,既然有数据,那肯定会涉及到添加数据,那我们就要用到POST方法了,就和上传FORM表单的性质是一样的

HTTPie的好处是,他默认的POST格式是JSON的,所以,你不必特意指定格式,只要写一个POST,就ok,如下

[python] view plain copy

  1. @app.route(‘/todo/api/v1.0/tasks’,methods=[‘POST’])
  2. def create_task():
  3.     if not request.json or not ‘title’ in request.json:   #如果请求里面没有JSON数据,或者在JSON数据里面,title的内容是空的
  4.         abort(404)                                    #返回404报错
  5.     task = {
  6.         ‘id’:tasks[-1][‘id’]+1,                       #取末尾task的id号,并加一作为新的数据的id号
  7.         ‘title’:request.json[‘title’],                #title必须要设置,不能为空
  8.         ‘description’:request.json.get(‘description’,””),     #描述可以添加,但是也可以不写,默认为空
  9.         ‘done’:False
  10.     }
  11.     tasks.append(task)                                    #完了以后,添加这个task进tasks列表
  12.     return jsonify({‘task’:task}),201                     #并返回这个添加的task内容,和状态码

 

后面加入的信息,你写title=test或者title=”test”都可以


PUT方法修改和DELETE方法删除

既然已经有了添加的功能,那么修改和删除的也必不可少,如下

[python] view plain copy

  1. @app.route(‘/todo/api/v1.0/tasks/<int:task_id>’,methods=[‘PUT’])
  2. def update_task(task_id):
  3.     task = list(filter(lambda t:t[‘id’]==task_id,tasks))     #检查是否有这个id的数据
  4.     if len(task)==0:
  5.         abort(404)
  6.     if not request.json:                                     #如果请求中没有附带json数据,则报错400
  7.         abort(400)
  8.     if ‘title’ in request.json and not isinstance(request.json[‘title’],str):   #如果title对应的值,不是字符串类型,则报错400
  9.         abort(400)
  10.     if ‘description’ in request.json and not isinstance(request.json[‘description’],str):  #同上,检查description对应的值是否为字符串
  11.         abort(400)
  12.     if ‘done’ in request.json and not isinstance(request.json[‘done’],bool):    #检查done对应的值是否是布尔值
  13.         abort(400)
  14.     task[0][‘title’] = request.json.get(‘title’,task[0][‘title’])          #如果上述条件全部通过的话,更新title的值,同时要设置默认值
  15.     task[0][‘description’] = request.json.get(‘description’, task[0][‘description’])  #修改description值
  16.     task[0][‘done’] = request.json.get(‘done’, task[0][‘done’])            #修改done的值
  17.     return jsonify({‘task’:task[0]})                                       #最后,返回修改后的数据
  18. @app.route(‘/todo/api/v1.0/tasks/<int:task_id>’,methods=[‘DELETE’])
  19. def delete_task(task_id):
  20.     task = list(filter(lambda t: t[‘id’]==task_id,tasks))     #检查是否有这个数据
  21.     if len(task) ==0:
  22.         abort(404)
  23.     tasks.remove(task[0])                                     #从tasks列表中删除这个值
  24.     return jsonify({‘result’True})                          #返回结果状态,自定义的result

优化接口

这样做接口系统,功能是达到了,但是你不见得让用户去背你的URL吧?这样不现实,像我们平时用的话,最好是可以自动生成URL

所以,这里我们需要写一个辅助函数,来返回一个完整的URL,这样可以就可以直接拿着URL用了

[python] view plain copy

  1. def make_public_task(task):
  2.     new_task={}             #新建一个对象,字典类型
  3.     for key in task:        #遍历字典内部的KEY
  4.         if key == ‘id’#当遍历到id的时候,为新对象增加uri的key,对应的值为完整的uri
  5.             new_task[‘uri’] = url_for(‘get_task’,task_id=task[‘id’],_external=True)
  6.         else:
  7.             new_task[key] = task[key]  #其他的key,分别一一对应加入新对象
  8.     return new_task                            #最后返回新对象数据

这样的话,你获取数据合集的路由,也需要修改了

[python] view plain copy

  1. @app.route(‘/todo/api/v1.0/tasks’,methods=[‘GET’])
  2. def get_tasks():
  3.     return jsonify({‘tasks’: list(map(make_public_task,tasks))})  #使用map函数,罗列出所有的数据,返回的数据信息,是经过辅助函数处理的

也就是说,经过辅助函数处理,你就可以直接获取整体URI了,而不是单个id号

接口安全性

接口的应用功能上,已经做得差不多了,但是现在的接口外圈是对外开放的,如果有人恶意破坏,数据就有可能有危险

所以,这里引入了认证机制,其实简单来说就是要求客户端的用户有使用使用权限,比如有账号和密码。

这里引入HTTPBasicAuth

auth=HTTPBasicAuth()

[python] view plain copy

  1. @auth.get_password
  2. def get_password(username):
  3.     if username == “allen”:
  4.         return “python”
  5.     return None
  6. @auth.error_handler
  7. def unauthorized():
  8.     return make_response(jsonify({‘error’:‘Unauthorized access’}),401)

通过客户端发送的用户名和密码,匹配get_password函数内部这个用户名和密码是否匹配

如果没有用户名密码,或者说是错误的,则返回401错误

于是,像查看资料啊这些的路由,都要给他挂一个login_required的装饰器了

[python] view plain copy

  1. @app.route(‘/todo/api/v1.0/tasks’,methods=[‘GET’])
  2. @auth.login_required
  3. def get_tasks():
  4.     return jsonify({‘tasks’: list(map(make_public_task,tasks))})

如果你没有附带用户名密码,那么,就会禁止访问了

附带了用户名和密码的话,就可以有权限进行操作