用Flask搭建你自己的Restful API

背景

最近在过一个《30天学习30种新技术》,过到第10天用Phonegap开发APP的时候,发现作者提供的API不可用,所以费心研究了一下Restful API, 顺利构建出了环境写完了那个APP,下面是一些摘要。
我用的Tutorial来自这里:
http://blog.miguelgrinberg.com/post/designing-a-restful-api-with-python-and-flask
开发环境是ArchLinux.

Hello Flask

原Tutorial中给出的是一个关于todo-list的实现,我们从最简单的"Hello Flask"开始:
首先安装python虚拟环境和flask,注意因为Arch默认的python版本是3,所以这里我们使用了virtualenv2来创建python虚拟运行环境。

$ mkdir todo-api
$ cd todo-api
$ virtualenv2 flask
$ source flask/bin/activate
(flask) $ pip install flask

在当前目录下建立app.py文件, 输入以下内容:

#!flask/bin/python
from flask import Flask

app = Flask(__name__)

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

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

现在改变文件属性,运行之:

$ chmod a+x app.py
$ ./app.py

打开浏览器访问http://127.0.0.1:5000,,我们可以看到Hello Flask已经出现在浏览器里了。

实现最简单的Restful API

Flask本身支持很多插件,可以用于实现Restful API,由于我们这里只是做DEMO使用,需求比较简单,我们抛弃那些繁琐的插件,手动来写。
这里我们也不会引入数据库等内容,我们将task任务列表直接保存在内存中,所以一旦断电这些数据就将消失。在实际的生产环境中,我们是需要引入不同的数据库来存储这些数据的。
在上面生成的app.py中添加以下内容:

#!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)

现在打开终端,用curl就可以访问到我们新添加的API了:

$ curl -i http://127.0.0.1:5000/todo/api/v1.0/tasks
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 316
Server: Werkzeug/0.9.6 Python/2.7.8
Date: Wed, 03 Dec 2014 08:59:55 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的API就这样创建成功了。

添加第二个RESTful API

我们接着来添加第二个RESTful API, 加入下列代码到已有的app.py中:

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

现在用curl来测试,结果应该是这样的:

(flask)[Trusty@~/code/30days/todo-api]$ curl -i http://127.0.0.1:5000/todo/api/v1.0/tasks/2
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 151
Server: Werkzeug/0.9.6 Python/2.7.8
Date: Wed, 03 Dec 2014 09:10:25 GMT

{
  "task": {
    "description": "Need to find a good Python tutorial on the web", 
    "done": false, 
    "id": 2, 
    "title": "Learn Python"
  }
}%                                                                                                                                                   (flask)[Trusty@~/code/30days/todo-api]$ curl -i http://127.0.0.1:5000/todo/api/v1.0/tasks/3
HTTP/1.0 404 NOT FOUND
Content-Type: text/html
Content-Length: 233
Server: Werkzeug/0.9.6 Python/2.7.8
Date: Wed, 03 Dec 2014 09:10:27 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.  If you entered the URL manually please check your spelling and try again.</p>

由于我们引用了abort方法,所以当我们给出的task id错误时,将弹出404错误。

错误提示JSON化

通过引入make_response模块我们可以把404返回错误JSON化,添加以下代码到app.py中:

from flask import make_response

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

现在访问一个不存在的task id将返回如下结果:

(flask)[Trusty@~/code/30days/todo-api]$ curl -i http://127.0.0.1:5000/todo/api/v1.0/tasks/3
HTTP/1.0 404 NOT FOUND
Content-Type: application/json
Content-Length: 26
Server: Werkzeug/0.9.6 Python/2.7.8
Date: Wed, 03 Dec 2014 09:16:50 GMT

{
  "error": "Not found"
}%             

实现POST方法

添加以下代码以实现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

用curl测试的结果如下:

(flask)[Trusty@~/code/30days/todo-api]$ curl -i -H "Content-Type: application/json" -X POST -d '{"title":"Read a book"}' http://127.0.0.1:5000/todo/api/v1.0/tasks
HTTP/1.0 201 CREATED
Content-Type: application/json
Content-Length: 104
Server: Werkzeug/0.9.6 Python/2.7.8
Date: Wed, 03 Dec 2014 09:18:55 GMT

{
  "task": {
    "description": "", 
    "done": false, 
    "id": 3, 
    "title": "Read a book"
  }
}% 
 (flask)[Trusty@~/code/30days/todo-api]$ curl -i http://127.0.0.1:5000/todo/api/v1.0/tasks 
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 423
Server: Werkzeug/0.9.6 Python/2.7.8
Date: Wed, 03 Dec 2014 09:19:24 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"
    }
  ]
}%           

从上面的测试中我们可以看到一个新的任务被添加到了task列表中。

最后两个RESTful API

最后的两个RESTful API代码如下:

@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})

测试代码如下:

$ curl -i -H "Content-Type: application/json" -X PUT -d '{"done":true}' http://127.0.0.1:5000/todo/api/v1.0/tasks/2

运行上面的命令可以将第2条记录里的done字段由false改成true.

改进接口

加入以下代码后,我们调用tasks方法将不再返回id,而是返回URIs,这样取回来就能用了。

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

同时我们重写以下方法:

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

测试结果如下:

$ curl -i http://127.0.0.1:5000/todo/api/v1.0/tasks
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 406
Server: Werkzeug/0.9.6 Python/2.7.8
Date: Wed, 03 Dec 2014 09:32:12 GMT

{
  "tasks": [
    {
      "description": "Milk, Cheese, Pizza, Fruit, Tylenol", 
      "done": false, 
      "title": "Buy groceries", 
      "uri": "http://127.0.0.1:5000/todo/api/v1.0/tasks/1"
    }, 
    {
      "description": "Need to find a good Python tutorial on the web", 
      "done": false, 
      "title": "Learn Python", 
      "uri": "http://127.0.0.1:5000/todo/api/v1.0/tasks/2"
    }
  ]
}% 

加密RESTful网络接口

好了,我们的RESTful接口搭建完毕了,但是由于接口对所有人都是开放的,为了考虑安全因素,我们会采用简单加密。
首先安装flask-httpauth模块:

$ pip install flask-httpauth

而后添加以下代码:

from flask.ext.httpauth import HTTPBasicAuth
auth = HTTPBasicAuth()

@auth.get_password
def get_password(username):
    if username == 'miguel':
        return 'python'
    return None

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

加密路由实现如下:

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

测试结果如下, 未通过授权时:

$ 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.9.6 Python/2.7.8
Date: Wed, 03 Dec 2014 09:40:02 GMT

{
  "error": "Unauthorized access"
}%   

通过授权时:

$ curl -u miguel:python -i http://localhost:5000/todo/api/v1.0/tasks
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 406
Server: Werkzeug/0.9.6 Python/2.7.8
Date: Wed, 03 Dec 2014 09:40:24 GMT

{
  "tasks": [
    {
      "description": "Milk, Cheese, Pizza, Fruit, Tylenol", 
      "done": false, 
      "title": "Buy groceries", 
      "uri": "http://localhost:5000/todo/api/v1.0/tasks/1"
    }, 
.....

我们可以把出错时返回的错误号从401改变为403,这样返回的就是forbidden错误。

Virtual Node Environment For PhoneGap Development

Background

I met some tutorials which were write one or two years ago, while at that time the corresponding plugins are in an old version, so I need to find a whole “virtual” environment for developing these tutorials.

Install Nodeenv

“Node.js virtual environment builder” — is the introduction for Nodeenv.
Install it via:

$ virtualenv2 venv
source venv/bin/activate
(venv)[Trusty@~/code/30days/PhoneGap2.9.0]$ 
$ pip install nodeenv
(venv)[Trusty@~/code/30days/PhoneGap2.9.0]$ which nodeenv
/home/Trusty/code/30days/PhoneGap2.9.0/venv/bin/nodeenv
(venv)[Trusty@~/code/30days/PhoneGap2.9.0]$ nodeenv --version
0.11.1

With this nodeenv we could setup all kinds of the dev environments.

Wrok With Nodeenv

Install the node.js first, this may cause long time:

(venv)[Trusty@~/code/30days/PhoneGap2.9.0]$ nodeenv nodeenv 
 * Install node.js (0.10.33 
[Trusty@~/code/30days/PhoneGap2.9.0]$ du -hs *
170M    nodeenv
8.9M    venv
(venv)[Trusty@~/code/30days/PhoneGap2.9.0]$ ls
nodeenv  npm-debug.log  venv
(venv)[Trusty@~/code/30days/PhoneGap2.9.0]$ . nodeenv/bin/activate 
(nodeenv)(venv)[Trusty@~/code/30days/PhoneGap2.9.0]$ which npm
/home/Trusty/code/30days/PhoneGap2.9.0/nodeenv/bin/npm
(nodeenv)(venv)[Trusty@~/code/30days/PhoneGap2.9.0]$ which node
/home/Trusty/code/30days/PhoneGap2.9.0/nodeenv/bin/node

Now we could install the specified version of PhoneGap, first we should find the specified versions:

$ npm view phonegap


This will returns the specified versions of all of the phonegap.
Choose whichever you want.

$ npm install phonegap@2.9.0-rc1-0.12.2

This will install the phonegap around Dec,2013, which should be fit well to the tutorial.
Then re-activate , or manually modify the PATH as:

$ export PATH=/home/Trusty/code/30days/PhoneGap2.9.0/nodeenv/bin:$PATH
(nodeenv)(venv)[Trusty@~/code/30days/PhoneGap2.9.0]$ phonegap -v
2.9.0-rc1-0.12.2

Run Tutorial

Create Apps

Create the app via following command:

(nodeenv)(venv)[Trusty@~/code/30days/PhoneGap2.9.0]$ npm config set https_proxy http://1xx.x.xx.xxx:2xxxx
(nodeenv)(venv)[Trusty@~/code/30days/PhoneGap2.9.0]$ npm config set proxy http://1xx.x.xx.xxx:2xxx
$ phonegap create audero-feed-reade com.audero.free.utility.auderofeedreader "Audero Feed Reader"

Setup Dev Env On DO

Prepare

Install the following packages:

$ sudo apt-get install python-virtualenv
$ sudo apt-get install ruby-full ruby
$ sudo gem install rhc

Since DO’s network is pretty good, so it’s very swift for developing on it.

TextBlob

$ virtualenv venv --python=python2.7
$ . venv/bin/activate
$ pip install textblob
$ python -m textblob.download_corpora
$ pip install flask


Apache Parameter Adjust

Background

A wordpress machine runs on DigitalOcean often sudden the mysqld database lost error.

Analyze

This is because the memory is exhausted in DO, so first I enable the swap for machine. This method solved the problem for a long time.
But later it seems the robots who sent the rubbish comments continue to attack the system, causing the mysqld halt again, this time, I modified the apahce2’s works:

$ grep -Ri MaxRequestWorkers /etc/apache2/
/etc/apache2/mods-enabled/mpm_prefork.conf:# MaxRequestWorkers: maximum number of server processes allowed to start
/etc/apache2/mods-enabled/mpm_prefork.conf:     MaxRequestWorkers         150
/etc/apache2/mods-available/mpm_prefork.conf:# MaxRequestWorkers: maximum number of server processes allowed to start
/etc/apache2/mods-available/mpm_prefork.conf:   MaxRequestWorkers         150
$ vim /etc/apache2/mods-available/mpm_prefork.conf
$ vim /etc/apache2/mods-available/mpm_worker.conf
$ vim /etc/apache2/mods-available/mpm_itk.conf
$ vim /etc/apache2/mods-available/mpm_event.conf
$ service apache2 restart

The vim command is changing the MaxRequestWorkers from 150 to 10, hope this will solve the problem.

TBD

If the mysql runs into death again, then we must limit its connection towards apache2.

Tips on 30Days30Skills(3)

Day 7 - GruntJS(2)

GruntJS Markdown Plugin

First download the source file:

$ git clone https://github.com/shekhargulati/day7-gruntjs-livereload-example.git
$ cd day7-gruntjs-livereload-example
$ sudo npm install -g grunt
$ grunt $ cd day7-gruntjs-livereload-example
$ sudo npm install -g grunt
$ npm init
$ npm install grunt
$ sudo npm install grunt-contrib-uglify grunt-markdown grunt-contrib-watch -g
$ grunt
>> Local Npm module "grunt-contrib-watch" not found. Is it installed?

Running "uglify:build" (uglify) task
>> 1 file created.

Running "markdown:all" (markdown) task
File "docs/html/day1.html" created.

Done, without errors.

	<div id="main" class="container">
		<%=content%>
	</div>

After grunt, the generated html file is listed as :

	<div id="main" class="container">
		<p>Today is the fourth day of my challenge to learn 30 technologies in 30 days. So far I am enjoying it and getting good response from fellow developers. I am more than motivated to do it for full 30 days. In this blog, I will cover how we can very easily build b
.....
	</div>

grunt-contrib-watch

Following command will install grunt-contrib-watch and update the package.json:

$ npm install grunt-contrib-watch --save-dev

Add following code into grunt’s initConfig method, these code ensure that if file changes, grunt will run uglify and markdown task:

watch :{
    scripts :{
      files : ['js/app.js','*.md','css/*.css'],
      tasks : ['uglify','markdown']
    }
  }

Add following line to Gruntfile for watch task:

grunt.loadNpmTasks('grunt-contrib-watch');

Now run grunt watch, then modify some files, like app.js under js folder, then detect if the generated files are modified or not.

Now run grunt watch, then modify some files, like app.js under js folder, then detect if the generated files are modified or not.

livereload

Edit the watch’s configuration, enable the livereload, this will enable the service in 35729 port. Or you could modify the service port by yourself.
Add content into the templates/index.html:

<script src="http://localhost:9090/livereload.js"></script>

Now every single modification will let you see the final result immediately.

Day 8 Harp.JS

Install the Harp.JS via:

$ sudo npm install -g harp

Then initiate the blog via:

$ harp init blog

This step won’t be continue because the program will complain that Template not found.