1. 程式人生 > >使用python的Flask實現一個RESTful API伺服器端 使用python的Flask實現一個RESTful API伺服器端[翻譯]

使用python的Flask實現一個RESTful API伺服器端 使用python的Flask實現一個RESTful API伺服器端[翻譯]

使用python的Flask實現一個RESTful API伺服器端[翻譯]

最近這些年,REST已經成為web services和APIs的標準架構,很多APP的架構基本上是使用RESTful的形式了。

本文將會使用python的Flask框架輕鬆實現一個RESTful的服務。

REST的六個特性:

  • Client-Server:伺服器端與客戶端分離。
  • Stateless(無狀態):每次客戶端請求必需包含完整的資訊,換句話說,每一次請求都是獨立的。
  • Cacheable(可快取):伺服器端必需指定哪些請求是可以快取的。
  • Layered System(分層結構):伺服器端與客戶端通訊必需標準化,伺服器的變更並不會影響客戶端。
  • Uniform Interface(統一介面):客戶端與伺服器端的通訊方法必需是統一的。
  • Code on demand(按需執行程式碼?):伺服器端可以在上下文中執行程式碼或者指令碼?

Servers can provide executable code or scripts for clients to execute in their context. This constraint is the only one that is optional.(沒看明白)

RESTful web service的樣子

REST架構就是為了HTTP協議設計的。RESTful web services的核心概念是管理資源。資源是由URIs來表示,客戶端使用HTTP當中的'POST, OPTIONS, GET, PUT, DELETE'等方法傳送請求到伺服器,改變相應的資源狀態。

HTTP請求方法通常也十分合適去描述操作資源的動作:

HTTP方法 動作 例子
GET 獲取資源資訊

http://example.com/api/orders

(檢索訂單清單)

GET 獲取資源資訊

http://example.com/api/orders/123

(檢索訂單 #123)

POST 建立一個次的資源

http://example.com/api/orders

(使用帶資料的請求,建立一個新的訂單)

PUT 更新一個資源

http://example.com/api/orders/123

(使用帶資料的請求,更新#123訂單)

DELETE 刪除一個資源

http://example.com/api/orders/123

刪除訂單#123

REST請求並不需要特定的資料格式,通常使用JSON作為請求體,或者URL的查詢引數的一部份。

設計一個簡單的web service

下面的任務將會練習設計以REST準則為指引,通過不同的請求方法操作資源,標識資源的例子。

我們將寫一個To Do List 應用,並且設計一個web service。第一步,規劃一個根URL,例如:

http://[hostname]/todo/api/v1.0/

上面的URL包括了應用程式的名稱、API版本,這是十分有用的,既提供了名稱空間的劃分,同時又與其它系統區分開來。版本號在升級新特性時十分有用,當一個新功能特性增加在新版本下面時,並不影響舊版本。

第二步,規劃資源的URL,這個例子十分簡單,只有任務清單。

規劃如下:

HTTP方法 URI 動作
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] 刪除一個任務

我們定義任務清單有以下欄位:

  • id:唯一標識。整型。
  • title:簡短的任務描述。字串型。
  • description:完整的任務描述。文字型。
  • done:任務完成狀態。布林值型。

以上基本完成了設計部份,接下來我們將會實現它!

 簡單瞭解Flask框架

Flask好簡單,但是又很強大的Python web 框架。這裡有一系列教程Flask Mega-Tutorial series。(注:Django\Tornado\web.py感覺好多框:()

在我們深入實現web service之前,讓我們來簡單地看一個Flask web 應用的結構示例。

這裡都是在Unix-like(Linux,Mac OS X)作業系統下面的演示,但是其它系統也可以跑,例如windows下的Cygwin。可能命令有些不同吧。(注:忽略Windows吧。)

先使用virtualenv安裝一個Flask的虛擬環境。如果沒有安裝virtualenv,開發python必備,最好去下載安裝。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的開發環境,開始建立一個簡單的web應用,在當前目錄裡面建立一個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去看看這個Hello,World!

好吧,十分簡單吧。我們開始轉換到RESTful service!

使用Python 和 Flask實現RESTful services

使用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

 

最近這些年,REST已經成為web services和APIs的標準架構,很多APP的架構基本上是使用RESTful的形式了。

本文將會使用python的Flask框架輕鬆實現一個RESTful的服務。

REST的六個特性:

  • Client-Server:伺服器端與客戶端分離。
  • Stateless(無狀態):每次客戶端請求必需包含完整的資訊,換句話說,每一次請求都是獨立的。
  • Cacheable(可快取):伺服器端必需指定哪些請求是可以快取的。
  • Layered System(分層結構):伺服器端與客戶端通訊必需標準化,伺服器的變更並不會影響客戶端。
  • Uniform Interface(統一介面):客戶端與伺服器端的通訊方法必需是統一的。
  • Code on demand(按需執行程式碼?):伺服器端可以在上下文中執行程式碼或者指令碼?

Servers can provide executable code or scripts for clients to execute in their context. This constraint is the only one that is optional.(沒看明白)

RESTful web service的樣子

REST架構就是為了HTTP協議設計的。RESTful web services的核心概念是管理資源。資源是由URIs來表示,客戶端使用HTTP當中的'POST, OPTIONS, GET, PUT, DELETE'等方法傳送請求到伺服器,改變相應的資源狀態。

HTTP請求方法通常也十分合適去描述操作資源的動作:

HTTP方法 動作 例子
GET 獲取資源資訊

http://example.com/api/orders

(檢索訂單清單)

GET 獲取資源資訊

http://example.com/api/orders/123

(檢索訂單 #123)

POST 建立一個次的資源

http://example.com/api/orders

(使用帶資料的請求,建立一個新的訂單)

PUT 更新一個資源

http://example.com/api/orders/123

(使用帶資料的請求,更新#123訂單)

DELETE 刪除一個資源

http://example.com/api/orders/123

刪除訂單#123

REST請求並不需要特定的資料格式,通常使用JSON作為請求體,或者URL的查詢引數的一部份。

設計一個簡單的web service

下面的任務將會練習設計以REST準則為指引,通過不同的請求方法操作資源,標識資源的例子。

我們將寫一個To Do List 應用,並且設計一個web service。第一步,規劃一個根URL,例如:

http://[hostname]/todo/api/v1.0/

上面的URL包括了應用程式的名稱、API版本,這是十分有用的,既提供了名稱空間的劃分,同時又與其它系統區分開來。版本號在升級新特性時十分有用,當一個新功能特性增加在新版本下面時,並不影響舊版本。

第二步,規劃資源的URL,這個例子十分簡單,只有任務清單。

規劃如下:

HTTP方法 URI 動作
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] 刪除一個任務

我們定義任務清單有以下欄位:

  • id:唯一標識。整型。
  • title:簡短的任務描述。字串型。
  • description:完整的任務描述。文字型。
  • done:任務完成狀態。布林值型。

以上基本完成了設計部份,接下來我們將會實現它!

 簡單瞭解Flask框架

Flask好簡單,但是又很強大的Python web 框架。這裡有一系列教程Flask Mega-Tutorial series。(注:Django\Tornado\web.py感覺好多框:()

在我們深入實現web service之前,讓我們來簡單地看一個Flask web 應用的結構示例。

這裡都是在Unix-like(Linux,Mac OS X)作業系統下面的演示,但是其它系統也可以跑,例如windows下的Cygwin。可能命令有些不同吧。(注:忽略Windows吧。)

先使用virtualenv安裝一個Flask的虛擬環境。如果沒有安裝virtualenv,開發python必備,最好去下載安裝。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的開發環境,開始建立一個簡單的web應用,在當前目錄裡面建立一個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去看看這個Hello,World!

好吧,十分簡單吧。我們開始轉換到RESTful service!

使用Python 和 Flask實現RESTful services

使用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