本系列教程分为四个阶段

1.flask restful web service

2.flask restful api

3.flask httpauth实现权限管控

4.uwsgi管理flask应用

文章摘自http://www.pythondoc.com/flask-restful/

并由本人修正其中的一些bug及版本更新导致的程序问题,去其糟粕,取其精华。

flask是当下python比较流行的web开发框架,遂在使用flask做了大大小小的项目之后,便想着做一篇博客,一方面以用来记录关键性的知识,另一方面也便以那些想入门或者想了解python、flask的人。

----

本文需要读者有一定的python基础

----

  1. 一个flask小程序(使用pycharm开发)
from flask import Flask

app = Flask(__name__)

@app.route('/')
def index():
        return 'hello world'

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

如上一些参数的说明:

  1. from flask import Flask 引入flask中的Flask类模块
  2. app = Flask(__name__) 实例化Flask类
  3. @app.route('/') flask路由注释器

    运行如上的程序便可以启动一个flask程序,此时访问http://localhost:5000, 便可以访问flask的程序。
    如若想对程序的端口自定义,则在最后一行进行如下修改:

    app.run('0.0.0.0', 8080)
    注意:此种方式只适用于使用python flask_app.py方式,若使用flask run方式则不生效,如有需要请自行查询资料,后续生产环境不会采用任何直接运行的方式,均会使用uwsgi等代理方式管理程序,后续我们会介绍uwsgi相关使用。

此时程序将会启动在本地的8080端口;

什么是 REST?

六条设计规范定义了一个 REST 系统的特点:

客户端-服务器: 客户端和服务器之间隔离,服务器提供服务,客户端进行消费。
无状态: 从客户端到服务器的每个请求都必须包含理解请求所必需的信息。换句话说, 服务器不会存储客户端上一次请求的信息用来给下一次使用。
可缓存: 服务器必须明示客户端请求能否缓存。
分层系统: 客户端和服务器之间的通信应该以一种标准的方式,就是中间层代替服务器做出响应的时候,客户端不需要做任何变动。
统一的接口: 服务器和客户端的通信方法必须是统一的。
按需编码: 服务器可以提供可执行代码或脚本,为客户端在它们的环境中执行。这个约束是唯一一个是可选的。

使用flask实现restful services

实现web services的第一个入口

#!flask/bin/python
from flask import Flask, jsonify

app = Flask(__name__)

tasks = [
    {
        'id': 1,
        'title': u'Buy geries',
        '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)

如上tasks列表为我们模拟的数据属性,接口返回必须以json或str格式,否则报错。
这里的@app.route中我们定义了web URL访问的路由,及http请求资源(methods)的方式,这里涉及到一个知识点,http请求方式,下面列出常用的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]  删除任务
==========  ================================================ =============================

下面我们根据常用的http请求资源方式进行接下来的restful services的编写,在上面一个例子中我们已经实现了第一种GET方法,获取任务列表中的所有数据,但很多时候我们页面上并不需要展示所有数据,只需要展示部分数据,然后根据需要进行多次获取,那就用到第二种GET方法,准确的说应该是GET路由参数。

注意:在浏览器中,所有请求均为GET方法,之所以HTTP方法分以上几种,大部分时候是由前端通过ajax/axios等技术请求获取数据后进行数据渲染到页面中,所以在测试restful services时,建议使用curl,或者借助postman工具做专业化的开发测试。
注意:使用postman调用service时,要在请求头中添加Content-Type: application/json

这里我们借助curl访问我们刚才创建的tasks函数:

$ curl -i http://localhost:5000/todo/api/v1.0/tasks
# Result:
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 geries"
    },
    {
      "description": "Need to find a good Python tutorial on the web",
      "done": false,
      "id": 2,
      "title": "Learn Python"
    }
  ]
}


可见tasks函数只是将tasks字典返回,并没有什么变化,接下来我们将尝试对数据的其他操作:
获取指定数据、修改数据、删除数据

获取单个任务数据:

@app.route('/todo/api/v1.0/tasks/<int:id>', methods=['GET'])
def get_task():
    task = list(filter(lambda t: t['id'] == task_id, tasks))
    if len(task) == 0:
                # abort(404)
        return jsonify({'error': 'no such data.'})
    return jsonify({'task': task[0]})


这里我们用到了filter()高阶函数及lambda实现字典过滤,后续还会用到map()函数;

注意:在python3中需要对filter()、map()函数进行转 <list> 后方可赋值使用,否则报错。


访问get_task方法:

# 访问<id>为2的数据
$ 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"
  }
}

# 访问不存在的<id>数据
$ 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

{'error': 'no such data.'}


上述测试中我们进行了2次访问,一次为字典中存在的<id>为2的数据,一个是<id>为3不存在的数据,我们会看到数据返回是友好的自定义异常返回,在原文中使用的abort方法笔者不太建议使用,在实际应用中我们会有多个接口可能会出现访问不到数据返回404的情况,我们并不希望所有的404返回都是如下信息:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>404 Not Found</title>
<h1>Not Found</h1>

这样并不友好,且并不能帮助我们前端开发又或自测的时候有效的发现问题,所以通过return方法返回我们定义的异常问题。
当然这里我们也把自定义abort异常方法实现,方法如下:

from flask import make_response

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

说明:

@app.errorhandler(404)为flask中的异常装饰器

def not_found(error) 函数定义中的传参error不可留空,原因很简单,装饰器会去捕获traceback并传入not_found函数,可以自行查看error打印具体信息。
make_response函数为flask服务返回方法。


对应的abort返回信息为:

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


上述介绍获取单个数据的方法,及flask的异常处理,接下来就是POST方法, 一般用于数据的添加、更新,表单提交等,方法如下:

from flask import request  #http请求处理模块,获取请求头、参数、提交数据等信息

@app.route('/todo/api/v1.0/tasks', methods=['POST'])
def create_task():
    if not request.json or not 'title' in request.json:
        return jsonify({'error': 'args not matching'})
    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


请求该方法:

$ 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


再次请求我们刚开始的GET方法请求数据,查看数据变化:

$ 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 geries"
    },
    {
      "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"
    }
  ]
}


此时可以看到我们定义的tasks原始字典中,多出了<id>为3 的新增数据,说明我们POST数据添加接口正常。

关于剩下的更新、删除方法,就不再一一阐述了,以下直接列出实现方法:

@app.route('/todo/api/v1.0/tasks/<int:task_id>', methods=['PUT'])
def update_task(task_id):
    task = list(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 = list(filter(lambda t: t['id'] == task_id, tasks))
    if len(task) == 0:
        abort(404)
    tasks.remove(task[0])
    return jsonify({'result': True})


以上就是flask实现restful web services的方法,当然flask可以实现的远不止于此,这里只是起到抛砖引玉的效果,后续的工作开发中大家会慢慢的发现flask的魅力,强大。

原文中这里紧接着介绍了关于web services的基础认证,笔者思量之后决定不在此处进行阐述,在后续的关于restful services的httpauth中会进行系统的介绍。


写在最后的话:
技术这一行,是一个不断进步学习的过程,我们在学习别人的东西的同时一定要多思考,多实践,才能变成自己的知识,以用于后续的变现。