使用 Python 和 Flask 設計 RESTful API

近些年來 REST (REpresentational State Transfer) 已經變成了 web services 和 web APIs 的標配。html

在本文中我將向你展現如何簡單地使用 Python 和 Flask 框架來建立一個 RESTful 的 web service。python

什麼是 REST?

六條設計規範定義了一個 REST 系統的特色:web

  • 客戶端-服務器: 客戶端和服務器之間隔離,服務器提供服務,客戶端進行消費。
  • 無狀態: 從客戶端到服務器的每一個請求都必須包含理解請求所必需的信息。換句話說, 服務器不會存儲客戶端上一次請求的信息用來給下一次使用。
  • 可緩存: 服務器必須明示客戶端請求可否緩存。
  • 分層系統: 客戶端和服務器之間的通訊應該以一種標準的方式,就是中間層代替服務器作出響應的時候,客戶端不須要作任何變更。
  • 統一的接口: 服務器和客戶端的通訊方法必須是統一的。
  • 按需編碼: 服務器能夠提供可執行代碼或腳本,爲客戶端在它們的環境中執行。這個約束是惟一一個是可選的。

什麼是一個 RESTful 的 web service?

REST 架構的最初目的是適應萬維網的 HTTP 協議。數據庫

RESTful web services 概念的核心就是「資源」。 資源能夠用 URI 來表示。客戶端使用 HTTP 協議定義的方法來發送請求到這些 URIs,固然可能會致使這些被訪問的」資源「狀態的改變。json

HTTP 標準的方法有以下:flask

==========  =====================  ==================================
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 中查詢參數項。api

設計一個簡單的 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': True}) 

delete_task 函數沒有什麼特別的。對於 update_task 函數,咱們須要嚴格地檢查輸入的參數以防止可能的問題。咱們須要確保在咱們把它更新到數據庫以前,任何客戶端提供咱們的是預期的格式。

更新任務 #2 的函數調用以下所示:

$ 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 接口

目前 API 的設計的問題就是迫使客戶端在任務標識返回後去構造 URIs。這對於服務器是十分簡單的,可是間接地迫使客戶端知道這些 URIs 是如何構造的,這將會阻礙咱們之後變動這些 URIs。

不直接返回任務的 ids,咱們直接返回控制這些任務的完整的 URI,以便客戶端能夠隨時使用這些 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 

這裏全部作的事情就是從咱們數據庫中取出任務而且建立一個新的任務,這個任務的 id 字段被替換成經過 Flask 的 url_for 生成的 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"
    }
  ]
}

咱們將會把上述的方式應用到其它全部的函數上以確保客戶端一直看到 URIs 而不是 ids。

增強 RESTful web service 的安全性

咱們已經完成了咱們 web service 的大部分功能,可是仍然有一個問題。咱們的 web service 對任何人都是公開的,這並非一個好主意。

咱們有一個能夠管理咱們的待辦事項完整的 web service,但在當前狀態下的 web service 是開放給全部的客戶端。 若是一個陌生人弄清咱們的 API 是如何工做的,他或她能夠編寫一個客戶端訪問咱們的 web service 而且毀壞咱們的數據。

大部分初級的教程會忽略這個問題而且到此爲止。在我看來這是一個很嚴重的問題,我必須指出。

確保咱們的 web service 安全服務的最簡單的方法是要求客戶端提供一個用戶名和密碼。在常規的 web 應用程序會提供一個登陸的表單用來認證,而且服務器會建立一個會話爲登陸的用戶之後的操做使用,會話的 id 以 cookie 形式存儲在客戶端瀏覽器中。然而 REST 的規則之一就是 「無狀態」, 所以咱們必需要求客戶端在每一次請求中提供認證的信息。

咱們一直試着儘量地堅持 HTTP 標準協議。既然咱們須要實現認證咱們須要在 HTTP 上下文中去完成,HTTP 協議提供了兩種認證機制: Basic 和 Digest

有一個小的 Flask 擴展可以幫助咱們,咱們能夠先安裝 Flask-HTTPAuth:

$ flask/bin/pip install flask-httpauth

比方說,咱們但願咱們的 web service 只讓訪問用戶名 miguel 和密碼 python 的客戶端訪問。 咱們能夠設置一個基本的 HTTP 驗證以下:

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) 

get_password 函數是一個回調函數,Flask-HTTPAuth 使用它來獲取給定用戶的密碼。在一個更復雜的系統中,這個函數是須要檢查一個用戶數據庫,可是在咱們的例子中只有單一的用戶所以沒有必要。

error_handler 回調函數是用於給客戶端發送未受權錯誤代碼。像咱們處理其它的錯誤代碼,這裏咱們定製一個包含 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 miguel: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"
    }
  ]
}

認證擴展給予咱們很大的自由選擇哪些函數須要保護,哪些函數須要公開。

爲了確保登陸信息的安全應該使用 HTTP 安全服務器(例如: https://...),這樣客戶端和服務器之間的通訊都是加密的,以防止傳輸過程當中第三方看到認證的憑據。

讓人不舒服的是當請求收到一個 401 的錯誤,網頁瀏覽都會跳出一個醜陋的登陸框,即便請求是在後臺發生的。所以若是咱們要實現一個完美的 web 服務器的話,咱們就須要禁止跳轉到瀏覽器顯示身份驗證對話框,讓咱們的客戶端應用程序本身處理登陸。

一個簡單的方式就是不返回 401 錯誤。403 錯誤是一個使人青睞的替代,403 錯誤表示 「禁止」 的錯誤:

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

可能的改進

咱們編寫的小型的 web service 還能夠在很多的方面進行改進。

對於初學者來講,一個真正的 web service 須要一個真實的數據庫進行支撐。咱們如今使用的內存數據結構會有不少限制不該該被用於真正的應用。

另一個能夠提升的領域就是處理多用戶。若是系統支持多用戶的話,不一樣的客戶端能夠發送不一樣的認證憑證獲取相應用戶的任務列表。在這樣一個系統中的話,咱們須要第二個資源就是用戶。在用戶資源上的 POST 的請求表明註冊換一個新用戶。一個 GET 請求表示客戶端獲取一個用戶的信息。一個 PUT 請求表示更新用戶信息,好比多是更新郵箱地址。一個 DELETE 請求表示刪除用戶帳號。

GET 檢索任務列表請求能夠在幾個方面進行擴展。首先能夠攜帶一個可選的頁的參數,以便客戶端請求任務的一部分。另外,這種擴展更加有用:容許按照必定的標準篩選。好比,用戶只想要看到完成的任務,或者只想看到任務的標題以 A 字母開頭。全部的這些均可以做爲 URL 的一個參數項。

相關文章
相關標籤/搜索