Flask 設計 RESTful API
使用 Python 和 Flask 設計 RESTful API
近些年來 REST (REpresentational State Transfer) 已經變成了 web services 和 web APIs 的標配。
在本文中我將向你展示如何簡單地使用 Python 和 Flask 框架來建立一個 RESTful 的 web service。
什麼是 REST?
六條設計規範定義了一個 REST 系統的特點:
- 客戶端-伺服器: 客戶端和伺服器之間隔離,伺服器提供服務,客戶端進行消費。
- 無狀態: 從客戶端到伺服器的每個請求都必須包含理解請求所必需的資訊。換句話說, 伺服器不會儲存客戶端上一次請求的資訊用來給下一次使用。
- 可快取: 伺服器必須明示客戶端請求能否快取。
- 分層系統: 客戶端和伺服器之間的通訊應該以一種標準的方式,就是中間層代替伺服器做出響應的時候,客戶端不需要做任何變動。
- 統一的介面: 伺服器和客戶端的通訊方法必須是統一的。
- 按需編碼: 伺服器可以提供可執行程式碼或指令碼,為客戶端在它們的環境中執行。這個約束是唯一一個是可選的。
什麼是一個 RESTful 的 web service?
REST 架構的最初目的是適應全球資訊網的 HTTP 協議。
RESTful web services 概念的核心就是“資源”。 資源可以用URI來表示。客戶端使用 HTTP 協議定義的方法來發送請求到這些 URIs,當然可能會導致這些被訪問的”資源“狀態的改變。
HTTP 標準的方法有如下:
========== ===================== ================================== HTTP 方法 行為 示例 ========== ===================== ================================== GET 獲取資源的資訊 http://example.com/api/orders GET 獲取某個特定資源的資訊 http://example.com/api/orders/123 POST 建立新資源 http://example.com/api/orders PUT 更新資源 http://example.com/api/orders/123 DELETE 刪除資源 http://example.com/api/orders/123 ========== ====================== ==================================
REST 設計不需要特定的資料格式。在請求中資料可以以JSON形式, 或者有時候作為 url 中查詢引數項。
設計一個簡單的 web service
堅持 REST 的準則設計一個 web service 或者 API 的任務就變成一個標識資源被展示出來以及它們是怎樣受不同的請求方法影響的練習。
比如說,我們要編寫一個待辦事項應用程式而且我們想要為它設計一個 web service。要做的第一件事情就是決定用什麼樣的根 URL 來訪問該服務。例如,我們可以通過這個來訪問:
http://[hostname]/todo/api/v1.0/
在這裡我已經決定在 URL 中包含應用的名稱以及 API 的版本號。在 URL 中包含應用名稱有助於提供一個名稱空間以便區分同一系統上的其它服務。在 URL 中包含版本號能夠幫助以後的更新,如果新版本中存在新的和潛在不相容的功能,可以不影響依賴於較舊的功能的應用程式。
下一步驟就是選擇將由該服務暴露(展示)的資源。這是一個十分簡單地應用,我們只有任務,因此在我們待辦事項中唯一的資源就是任務。
我們的任務資源將要使用 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] 刪除任務 ========== ================================================ =============================
我們定義的任務有如下一些屬性:
- id: 任務的唯一識別符號。數字型別。
- title: 簡短的任務描述。字串型別。
- description: 具體的任務描述。文字型別。
- done: 任務完成的狀態。布林值。
目前為止關於我們的 web service 的設計基本完成。剩下的事情就是實現它!
Flask 框架的簡介
如果你讀過Flask Mega-Tutorial 系列,就會知道 Flask 是一個簡單卻十分強大的 Python web 框架。
在我們深入研究 web services 的細節之前,讓我們回顧一下一個普通的 Flask Web 應用程式的結構。
我會首先假設你知道 Python 在你的平臺上工作的基本知識。 我將講解的例子是工作在一個類 Unix 作業系統。簡而言之,這意味著它們能工作在 Linux,Mac OS X 和 Windows(如果你使用Cygwin)。 如果你使用 Windows 上原生的 Python 版本的話,命令會有所不同。
讓我們開始在一個虛擬環境上安裝 Flask。如果你的系統上沒有 virtualenv,你可以從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,現在開始建立一個簡單地網頁應用,我們把它放在一個叫 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看看這個小應用程式的效果。
簡單吧?現在我們將這個應用程式轉換成我們的 RESTful service!
使用 Python 和 Flask 實現 RESTful services
使用 Flask 構建 web services 是十分簡單地,比我在Mega-Tutorial中構建的完整的服務端的應用程式要簡單地多。
在 Flask 中有許多擴充套件來幫助我們構建 RESTful services,但是在我看來這個任務十分簡單,沒有必要使用 Flask 擴充套件。
我們 web service 的客戶端需要新增、刪除以及修改任務的服務,因此顯然我們需要一種方式來儲存任務。最直接的方式就是建立一個小型的資料庫,但是資料庫並不是本文的主體。學習在 Flask 中使用合適的資料庫,我強烈建議閱讀Mega-Tutorial。
這裡我們直接把任務列表儲存在記憶體中,因此這些任務列表只會在 web 伺服器執行中工作,在結束的時候就失效。 這種方式只是適用我們自己開發的 web 伺服器,不適用於生產環境的 web 伺服器, 這種情況一個合適的資料庫的搭建是必須的。
我們現在來實現 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)
正如你所見,沒有多大的變化。我們建立一個任務的記憶體資料庫,這裡無非就是一個字典和陣列。陣列中的每一個元素都具有上述定義的任務的屬性。
取代了首頁,我們現在擁有一個 get_tasks 的函式,訪問的 URI 為 /todo/api/v1.0/tasks,並且只允許 GET 的 HTTP 方法。
這個函式的響應不是文字,我們使用 JSON 資料格式來響應,Flask 的 jsonify 函式從我們的資料結構中生成。
使用網頁瀏覽器來測試我們的 web service 不是一個最好的注意,因為網頁瀏覽器上不能輕易地模擬所有的 HTTP 請求的方法。相反,我們會使用 curl。如果你還沒有安裝 curl 的話,請立即安裝它。
通過執行 app.py,啟動 web service。接著開啟一個新的控制檯視窗,執行以下命令:
$ 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]})
第二個函式有些意思。這裡我們得到了 URL 中任務的 id,接著 Flask 把它轉換成 函式中的 task_id 的引數。
我們用這個引數來搜尋我們的任務陣列。如果我們的資料庫中不存在搜尋的 id,我們將會返回一個類似 404 的錯誤,根據 HTTP 規範的意思是 “資源未找到”。
如果我們找到相應的任務,那麼我們只需將它用 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>
當我們請求 id #2 的資源時候,我們獲取到了,但是當我們請求 #3 的時候返回了 404 錯誤。有關錯誤奇怪的是返回的是 HTML 資訊而不是 JSON,這是因為 Flask 按照預設方式生成 404 響應。由於這是一個 Web service 客戶端希望我們總是以 JSON 格式迴應,所以我們需要改善我們的 404 錯誤處理程式:
from flask import make_response
@app.errorhandler(404)
def not_found(error):
return make_response(jsonify({'error': 'Not found'}), 404)
我們會得到一個友好的錯誤提示:
$ 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
新增一個新的任務也是相當容易地。只有當請求以 JSON 格式形式,request.json 才會有請求的資料。如果沒有資料,或者存在資料但是缺少 title 項,我們將會返回 400,這是表示請求無效。
接著我們會建立一個新的任務字典,使用最後一個任務的 id + 1 作為該任務的 id。我們允許 description 欄位缺失,並且假設 done 欄位設定成 False。
我們把新的任務新增到我們的任務陣列中,並且把新新增的任務和狀態 201 響應給客戶端。
使用如下的 curl 命令來測試這個新的函式:
$ 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
當然在完成這個請求後,我們可以得到任務的更新列表:
$ 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'