使用 Bottle 框架構建 Rest API

已發表: 2022-03-11

REST API 已成為在 Web 後端和前端之間以及不同 Web 服務之間建立接口的常用方法。 這種接口的簡單性,以及對 HTTP 和 HTTPS 協議在不同網絡和框架中的普遍支持,使其在考慮互操作性問題時成為一個簡單的選擇。

Bottle 是一個極簡的 Python Web 框架。 它輕量、快速且易於使用,非常適合構建 RESTful 服務。 Andriy Kornatskyy 進行的基本比較將其列為響應時間和吞吐量(每秒請求數)的前三名框架。 在我自己對 DigitalOcean 提供的虛擬服務器的測試中,我發現 uWSGI 服務器堆棧和 Bottle 的組合可以實現低至每個請求 140μs 的開銷。

在本文中,我將介紹如何使用 Bottle 構建 RESTful API 服務。

Bottle:一個快速、輕量級的 Python Web 框架

安裝和配置

Bottle 框架之所以能取得令人印象深刻的性能,部分原因在於其重量輕。 事實上,整個庫是作為一個文件模塊分發的。 這意味著它不像其他框架那樣牽手,但它也更靈活,可以適應許多不同的技術堆棧。 因此,Bottle 最適合那些性能和可定制性非常重要的項目,並且不太需要考慮更多重型框架的省時優勢。

Bottle 的靈活性使得對設置平台的深入描述有點徒勞,因為它可能無法反映您自己的堆棧。 但是,這裡適合快速瀏覽這些選項,以及在哪裡了解有關如何設置它們的更多信息:

安裝

安裝 Bottle 就像安裝任何其他 Python 包一樣簡單。 您的選擇是:

  • 使用系統的包管理器在您的系統上安裝。 Debian Jessie(當前穩定版)將 0.12 版打包為python-bottle
  • 使用 Python 包索引和pip install bottle在您的系統上安裝。
  • 在虛擬環境中安裝(推薦)。

要在虛擬環境中安裝 Bottle,您需要virtualenvpip工具。 要安裝它們,請參閱 virtualenv 和 pip 文檔,儘管您可能已經在系統上安裝了它們。

在 Bash 中,使用 Python 3 創建一個環境:

 $ virtualenv -p `which python3` env

抑制-p `which python3`參數將導致安裝系統上存在的默認 Python 解釋器——通常是 Python 2.7。 支持 Python 2.7,但本教程假定使用 Python 3.4。

現在激活環境並安裝 Bottle:

 $ . env/bin/activate $ pip install bottle

而已。 瓶子已安裝並可以使用。 如果您不熟悉virtualenvpip ,他們的文檔是一流的。 看一看! 他們非常值得。

服務器

Bottle 符合 Python 的標準 Web 服務器網關接口 (WSGI),這意味著它可以與任何 WSGI 兼容的服務器一起使用。 這包括 uWSGI、Tornado、Gunicorn、Apache、Amazon Beanstalk、Google App Engine 等。

設置它的正確方法因每個環境而略有不同。 Bottle 暴露了一個符合 WSGI 接口的對象,服務器必須配置為與這個對象交互。

要了解有關如何設置服務器的更多信息,請參閱服務器的文檔和 Bottle 的文檔,請點擊此處。

數據庫

Bottle 與數據庫無關,不關心數據來自哪裡。 如果您想在應用程序中使用數據庫,Python 包索引有幾個有趣的選項,例如 SQLAlchemy、PyMongo、MongoEngine、CouchDB 和 Boto for DynamoDB。 您只需要適當的適配器即可使其與您選擇的數據庫一起使用。

Bottle 框架基礎知識

現在,讓我們看看如何在 Bottle 中製作一個基本的應用程序。 對於代碼示例,我將假設 Python >= 3.4。 但是,我將在這裡寫的大部分內容也適用於 Python 2.7。

Bottle 中的一個基本應用程序如下所示:

 import bottle app = application = bottle.default_app() if __name__ == '__main__': bottle.run(host = '127.0.0.1', port = 8000)

當我說基本時,我的意思是這個程序甚至沒有“Hello World”你。 (您最後一次訪問回答“Hello World”的 REST 接口是什麼時候?)所有對127.0.0.1:8000的 HTTP 請求都將收到 404 Not Found 響應狀態。

瓶中應用

Bottle 可能創建了多個應用程序實例,但為了方便起見,為您創建了第一個實例; 這是默認應用程序。 Bottle 將這些實例保存在模塊內部的堆棧中。 每當您使用 Bottle 執行某些操作(例如運行應用程序或附加路由)並且未指定您正在談論的應用程序時,它指的是默認應用程序。 事實上, app = application = bottle.default_app()行甚至不需要在這個基本應用程序中存在,但它存在以便我們可以輕鬆地使用 Gunicorn、uWSGI 或一些通用 WSGI 服務器調用默認應用程序。

多個應用程序的可能性起初可能看起來令人困惑,但它們增加了 Bottle 的靈活性。 對於應用程序的不同模塊,您可以通過實例化其他 Bottle 類並根據需要使用不同的配置對其進行設置來創建專門的 Bottle 應用程序。 通過 Bottle 的 URL 路由器,可以通過不同的 URL 訪問這些不同的應用程序。 我們不會在本教程中深入研究,但我們鼓勵您在此處和此處查看 Bottle 的文檔。

服務器調用

腳本的最後一行使用指定的服務器運行 Bottle。 如果沒有指明服務器,就像這裡的情況,默認服務器是 Python 內置的 WSGI 參考服務器,只適合開發目的。 可以像這樣使用不同的服務器:

 bottle.run(server='gunicorn', host = '127.0.0.1', port = 8000)

這是語法糖,讓您通過運行此腳本來啟動應用程序。 例如,如果此文件名為main.py ,您可以簡單地運行python main.py來啟動應用程序。 Bottle 提供了相當多的可以以這種方式使用的服務器適配器列表。

一些 WSGI 服務器沒有 Bottle 適配器。 這些可以使用服務器自己的運行命令啟動。 例如,在 uWSGI 上,您所要做的就是像這樣調用uwsgi

 $ uwsgi --http :8000 --wsgi-file main.py

關於文件結構的說明

Bottle 讓您的應用程序的文件結構完全由您決定。 我發現我的文件結構策略會隨著項目的發展而變化,但往往基於 MVC 理念。

構建您的 REST API

當然,沒有人需要只為每個請求的 URI 返回 404 的服務器。 我已經向你保證我們會構建一個 REST API,所以讓我們開始吧。

假設您想構建一個操作一組名稱的接口。 在一個真實的應用程序中,您可能會為此使用數據庫,但對於這個示例,我們將只使用內存中的set數據結構。

我們的 API 的骨架可能看起來像這樣。 您可以將此代碼放在項目中的任何位置,但我的建議是單獨的 API 文件,例如api/names.py

 from bottle import request, response from bottle import post, get, put, delete _names = set() # the set of names @post('/names') def creation_handler(): '''Handles name creation''' pass @get('/names') def listing_handler(): '''Handles name listing''' pass @put('/names/<name>') def update_handler(name): '''Handles name updates''' pass @delete('/names/<name>') def delete_handler(name): '''Handles name deletions''' pass

路由

正如我們所見,Bottle 中的路由是使用裝飾器完成的。 導入的裝飾器postgetputdelete為這四個操作註冊處理程序。 了解這些工作如何分解如下:

  • 以上所有裝飾器都是default_app路由裝飾器的快捷方式。 例如, @get()裝飾器將bottle.default_app().get()應用於處理程序。
  • default_app上的路由方法都是route()的快捷方式。 所以default_app().get('/')等價於default_app().route(method='GET', '/')

所以@get('/')@route(method='GET', '/')一樣,和@bottle.default_app().route(method='GET', '/')一樣,並且這些可以互換使用。

關於@route裝飾器的一個有用的事情是,例如,如果您想使用相同的處理程序來處理對象更新和刪除,您可以傳遞它處理的方法列表,如下所示:

 @route('/names/<name>', method=['PUT', 'DELETE']) def update_delete_handler(name): '''Handles name updates and deletions''' pass

好吧,讓我們實現其中的一些處理程序。

使用 Bottle Framework 打造完美的 REST API。

RESTful API 是現代 Web 開發的主要內容。 使用 Bottle 後端為您的 API 客戶提供有效的混合物。
鳴叫

POST:資源創建

我們的 POST 處理程序可能如下所示:

 import re, json namepattern = re.compile(r'^[a-zA-Z\d]{1,64}$') @post('/names') def creation_handler(): '''Handles name creation''' try: # parse input data try: data = request.json() except: raise ValueError if data is None: raise ValueError # extract and validate name try: if namepattern.match(data['name']) is None: raise ValueError name = data['name'] except (TypeError, KeyError): raise ValueError # check for existence if name in _names: raise KeyError except ValueError: # if bad request data, return 400 Bad Request response.status = 400 return except KeyError: # if name already exists, return 409 Conflict response.status = 409 return # add name _names.add(name) # return 200 Success response.headers['Content-Type'] = 'application/json' return json.dumps({'name': name})

嗯,這相當多。 讓我們逐步回顧這些步驟。

身體解析

此 API 要求用戶在正文中 POST 一個帶有名為“name”的屬性的 JSON 字符串。

之前從bottle中導入的request對象總是指向當前請求並保存所有請求的數據。 它的body屬性包含請求體的字節流,任何能夠讀取流對象(如讀取文件)的函數都可以訪問該字節流。

request.json()方法檢查“application/json”內容類型的請求標頭,如果正確,則解析正文。 如果 Bottle 檢測到格式錯誤的正文(例如:空或內容類型錯誤),則此方法返回None ,因此我們會引發ValueError 。 如果 JSON 解析器檢測到格式錯誤的 JSON 內容; 它引發了我們捕獲並重新引發的異常,再次作為ValueError

對象解析和驗證

如果沒有錯誤,我們已將請求的主體轉換為data變量引用的 Python 對象。 如果我們收到帶有“name”鍵的字典,我們將能夠通過data['name']訪問它。 如果我們收到一個沒有這個鍵的字典,嘗試訪問它會導致一個KeyError異常。 如果我們收到的不是字典,我們會得到一個TypeError異常。 如果出現這些錯誤中的任何一個,我們再次將其重新引發為ValueError ,表示輸入錯誤。

要檢查名稱鍵是否具有正確的格式,我們應該針對正則表達式掩碼對其進行測試,例如我們在此處創建的namepattern掩碼。 如果鍵name不是字符串, namepattern.match()將引發TypeError ,如果不匹配,則返回None

使用此示例中的掩碼,名稱必須是 ASCII 字母數字,不包含 1 到 64 個字符之間的空格。 這是一個簡單的驗證,例如,它不會測試帶有垃圾數據的對象。 更複雜和更完整的驗證可以通過使用諸如 FormEncode 之類的工具來實現。

測試存在

完成請求之前的最後一個測試是給定名稱是否已存在於集合中。 在一個更結構化的應用程序中,該測試可能應該由一個專用模塊完成,並通過一個專門的異常向我們的 API 發出信號,但由於我們直接操作一個集合,所以我們必須在這裡進行。

我們通過引發KeyError來表示名稱的存在。

錯誤響應

正如請求對象保存所有請求數據一樣,響應對像對響應數據執行相同的操作。 有兩種設置響應狀態的方法:

 response.status = 400

和:

 response.status = '400 Bad Request'

對於我們的示例,我們選擇了更簡單的形式,但第二種形式可用於指定錯誤的文本描述。 在內部,Bottle 將拆分第二個字符串並適當地設置數字代碼。

成功響應

如果所有步驟都成功,我們通過將名稱添加到集合_names 、設置Content-Type響應標頭並返迴響應來滿足請求。 該函數返回的任何字符串都將被視為200 Success響應的響應主體,因此我們只需使用json.dumps生成一個。

GET:資源列表

從名稱創建開始,我們將實現名稱列表處理程序:

 @get('/names') def listing_handler(): '''Handles name listing''' response.headers['Content-Type'] = 'application/json' response.headers['Cache-Control'] = 'no-cache' return json.dumps({'names': list(_names)})

列出名稱要容易得多,不是嗎? 與創建名稱相比,這裡沒什麼可做的。 只需設置一些響應標頭並返回所有名稱的 JSON 表示,我們就完成了。

PUT:資源更新

現在,讓我們看看如何實現更新方法。 和create方法差別不大,但是我們用這個例子來介紹URI參數。

 @put('/names/<oldname>') def update_handler(name): '''Handles name updates''' try: # parse input data try: data = json.load(utf8reader(request.body)) except: raise ValueError # extract and validate new name try: if namepattern.match(data['name']) is None: raise ValueError newname = data['name'] except (TypeError, KeyError): raise ValueError # check if updated name exists if oldname not in _names: raise KeyError(404) # check if new name exists if name in _names: raise KeyError(409) except ValueError: response.status = 400 return except KeyError as e: response.status = e.args[0] return # add new name and remove old name _names.remove(oldname) _names.add(newname) # return 200 Success response.headers['Content-Type'] = 'application/json' return json.dumps({'name': newname})

更新操作的主體模式與創建操作相同,但現在我們在 URI 中還有一個新的oldname參數,由路由@put('/names/<oldname>')定義。

URI 參數

如您所見,Bottle 的 URI 參數表示法非常簡單。 您可以使用任意數量的參數構建 URI。 Bottle 自動從 URI 中提取它們並將它們傳遞給請求處理程序:

 @get('/<param1>/<param2>') def handler(param1, param2): pass

使用級聯路由裝飾器,您可以使用可選參數構建 URI:

 @get('/<param1>') @get('/<param1>/<param2>') def handler(param1, param2 = None) pass

此外,Bottle 允許在 URI 中使用以下路由過濾器:

  • int

僅匹配可以轉換為int的參數,並將轉換後的值傳遞給處理程序:

 @get('/<param:int>') def handler(param): pass
  • float

int相同,但具有浮點值:

 @get('/<param:float>') def handler(param): pass
  • re (正則表達式)

僅匹配與給定正則表達式匹配的參數:

 @get('/<param:re:^[az]+$>') def handler(param): pass
  • path

以靈活的方式匹配 URI 路徑的子段:

 @get('/<param:path>/id>') def handler(param): pass

火柴:

  • /x/id ,將x作為param傳遞。
  • /x/y/id ,將x/y作為param傳遞。

DELETE:資源刪除

與 GET 方法一樣,DELETE 方法給我們帶來的消息也很少。 請注意,在不設置狀態的情況下返回None會返回帶有空正文和 200 狀態代碼的響應。

 @delete('/names/<name>') def delete_handler(name): '''Handles name updates''' try: # Check if name exists if name not in _names: raise KeyError except KeyError: response.status = 404 return # Remove name _names.remove(name) return

最後一步:激活 API

假設我們將名稱 API 保存為api/names.py ,我們現在可以在主應用程序文件main.py中啟用這些路由。

 import bottle from api import names app = application = bottle.default_app() if __name__ == '__main__': bottle.run(host = '127.0.0.1', port = 8000)

請注意,我們只導入了names模塊。 由於我們已經使用附加到默認應用程序的 URI 修飾了所有方法,因此無需進行任何進一步的設置。 我們的方法已經到位,可以訪問。

沒有什麼比製作精良的 REST API 更能讓前端開心了。奇蹟般有效!

您可以使用 Curl 或 Postman 等工具來使用 API 並手動對其進行測試。 (如果您使用的是 Curl,則可以使用 JSON 格式化程序來使響應看起來不那麼混亂。)

獎勵:跨域資源共享 (CORS)

構建 REST API 的一個常見原因是通過 AJAX 與 JavaScript 前端進行通信。 對於某些應用程序,應該允許這些請求來自任何域,而不僅僅是您的 API 的主域。 默認情況下,大多數瀏覽器都不允許這種行為,所以讓​​我向您展示如何在 Bottle 中設置跨域資源共享 (CORS) 以允許這種行為:

 from bottle import hook, route, response _allow_origin = '*' _allow_methods = 'PUT, GET, POST, DELETE, OPTIONS' _allow_headers = 'Authorization, Origin, Accept, Content-Type, X-Requested-With' @hook('after_request') def enable_cors(): '''Add headers to enable CORS''' response.headers['Access-Control-Allow-Origin'] = _allow_origin response.headers['Access-Control-Allow-Methods'] = _allow_methods response.headers['Access-Control-Allow-Headers'] = _allow_headers @route('/', method = 'OPTIONS') @route('/<path:path>', method = 'OPTIONS') def options_handler(path = None): return

hook裝飾器允許我們在每個請求之前或之後調用一個函數。 在我們的例子中,要啟用 CORS,我們必須為每個響應設置Access-Control-Allow-Origin-Allow-Methods-Allow-Headers標頭。 這些向請求者表明我們將提供指定的請求。

此外,客戶端可能會向服務器發出 OPTIONS HTTP 請求,以查看它是否真的可以使用其他方法發出請求。 使用這個包羅萬象的示例,我們使用 200 狀態代碼和空正文響應所有 OPTIONS 請求。

要啟用此功能,只需將其保存並從主模塊導入即可。

包起來

這裡的所有都是它的!

在本教程中,我嘗試介紹使用 Bottle Web 框架為 Python 應用程序創建 REST API 的基本步驟。

您可以通過訪問它的教程和 API 參考文檔來加深對這個小而強大的框架的了解。

相關:構建 Node.js/TypeScript REST API,第 1 部分:Express.js