ボトルフレームワークを使用したRESTAPIの構築
公開: 2022-03-11REST APIは、Webバックエンドとフロントエンド間、および異なるWebサービス間のインターフェースを確立するための一般的な方法になりました。 この種のインターフェースのシンプルさと、さまざまなネットワークやフレームワークにわたるHTTPおよびHTTPSプロトコルのユビキタスなサポートにより、相互運用性の問題を検討する際に簡単に選択できます。
ボトルはミニマリストのPythonWebフレームワークです。 軽量、高速、使いやすく、RESTfulサービスの構築に最適です。 Andriy Kornatskyyによる最低限の比較では、応答時間とスループット(1秒あたりのリクエスト数)の点で上位3つのフレームワークに含まれています。 DigitalOceanから入手できる仮想サーバーでの私自身のテストでは、uWSGIサーバースタックとBottleの組み合わせにより、要求ごとに140μsのオーバーヘッドを達成できることがわかりました。
この記事では、Bottleを使用してRESTfulAPIサービスを構築する方法のウォークスルーを提供します。
インストールと構成
ボトルフレームワークは、その軽量性のおかげもあり、その印象的なパフォーマンスを実現しています。 実際、ライブラリ全体が1つのファイルモジュールとして配布されます。 これは、他のフレームワークほど手に負えないことを意味しますが、より柔軟性があり、多くの異なる技術スタックに適合するように適合させることができます。 したがって、Bottleは、パフォーマンスとカスタマイズ性が重要視され、より頑丈なフレームワークの時間節約の利点があまり考慮されていないプロジェクトに最適です。
ボトルの柔軟性により、プラットフォームのセットアップに関する詳細な説明は、独自のスタックを反映していない可能性があるため、少し無駄になります。 ただし、ここでは、オプションの概要と、それらの設定方法の詳細を知ることが適切です。
インストール
Bottleのインストールは、他のPythonパッケージをインストールするのと同じくらい簡単です。 オプションは次のとおりです。
- システムのパッケージマネージャーを使用してシステムにインストールします。 Debian Jessie(現在の安定版)はバージョン0.12をpython-bottleとしてパッケージ化します。
- pipinstallbottleを使用して
pip install bottle
を使用してシステムにインストールします。 - 仮想環境にインストールします(推奨)。
ボトルを仮想環境にインストールするには、 virtualenvツールとpipツールが必要です。 それらをインストールするには、virtualenvとpipのドキュメントを参照してください。ただし、おそらくすでにシステムにインストールされています。
Bashで、Python3を使用して環境を作成します。
$ virtualenv -p `which python3` env
-p `which python3`
パラメーターを抑制すると、システムに存在するデフォルトのPythonインタープリター(通常はPython 2.7)がインストールされます。 Python 2.7がサポートされていますが、このチュートリアルではPython3.4を想定しています。
次に、環境をアクティブにして、Bottleをインストールします。
$ . env/bin/activate $ pip install bottle
それでおしまい。 ボトルが取り付けられ、使用できる状態になっています。 virtualenvまたはpipに精通していない場合、それらのドキュメントは一流です。 見てください! 彼らはそれだけの価値があります。
サーバ
ボトルはPythonの標準Webサーバーゲートウェイインターフェイス(WSGI)に準拠しているため、WSGI準拠の任意のサーバーで使用できます。 これには、uWSGI、トルネード、Gunicorn、Apache、Amazon Beanstalk、GoogleAppEngineなどが含まれます。
正しい設定方法は、環境ごとに若干異なります。 ボトルはWSGIインターフェイスに準拠するオブジェクトを公開し、サーバーはこのオブジェクトと対話するように構成する必要があります。
サーバーのセットアップ方法の詳細については、サーバーのドキュメントと、Bottleのドキュメントを参照してください。
データベース
ボトルはデータベースに依存せず、データがどこから来ているかを気にしません。 アプリでデータベースを使用する場合、Python Package Indexには、SQLAlchemy、PyMongo、MongoEngine、CouchDB、BotoforDynamoDBなどのいくつかの興味深いオプションがあります。 選択したデータベースで動作させるには、適切なアダプタのみが必要です。
ボトルフレームワークの基本
それでは、Bottleで基本的なアプリを作成する方法を見てみましょう。 コード例として、Python>=3.4を想定します。 ただし、ここで説明する内容のほとんどは、Python2.7でも機能します。
ボトルの基本的なアプリは次のようになります。
import bottle app = application = bottle.default_app() if __name__ == '__main__': bottle.run(host = '127.0.0.1', port = 8000)
基本とは、このプログラムが「HelloWorld」ではないことを意味します。 (「HelloWorld?」と応答したRESTインターフェースに最後にアクセスしたのはいつですか?) 127.0.0.1:8000
へのすべてのHTTP要求は、404NotFound応答ステータスを受け取ります。
ボトルに入ったアプリ
ボトルにはアプリのインスタンスがいくつか作成されている場合がありますが、便宜上、最初のインスタンスが作成されます。 これがデフォルトのアプリです。 ボトルは、これらのインスタンスをモジュール内部のスタックに保持します。 Bottleで何かを実行し(アプリの実行やルートのアタッチなど)、話しているアプリを指定しない場合は常に、デフォルトのアプリを参照します。 実際、 app = application = bottle.default_app()
行はこの基本的なアプリに存在する必要はありませんが、Gunicorn、uWSGI、またはいくつかの汎用WSGIサーバーでデフォルトアプリを簡単に呼び出すことができるように存在します。
複数のアプリの可能性は最初は混乱しているように見えるかもしれませんが、Bottleに柔軟性を追加します。 アプリケーションのさまざまなモジュールについて、他のボトルクラスをインスタンス化し、必要に応じてさまざまな構成でセットアップすることにより、特殊なボトルアプリを作成できます。 これらのさまざまなアプリには、BottleのURLルーターを介してさまざまなURLからアクセスできます。 このチュートリアルではこれについて詳しく説明しませんが、こことここでボトルのドキュメントをご覧になることをお勧めします。
サーバーの呼び出し
スクリプトの最後の行は、指定されたサーバーを使用してBottleを実行します。 ここでの場合のようにサーバーが示されていない場合、デフォルトのサーバーはPythonの組み込みWSGI参照サーバーであり、開発目的にのみ適しています。 次のように別のサーバーを使用できます。
bottle.run(server='gunicorn', host = '127.0.0.1', port = 8000)
これは、このスクリプトを実行してアプリを起動できる構文糖衣です。 たとえば、このファイルの名前がmain.py
の場合、 python main.py
を実行するだけでアプリを起動できます。 ボトルには、この方法で使用できるサーバーアダプターの非常に広範なリストが含まれています。
一部のWSGIサーバーにはボトルアダプターがありません。 これらは、サーバー自体の実行コマンドで開始できます。 たとえば、uWSGIでは、次のようにuwsgi
を呼び出すだけです。
$ uwsgi --http :8000 --wsgi-file main.py
ファイル構造に関する注記
ボトルは、アプリのファイル構造を完全にあなたに任せます。 私のファイル構造ポリシーはプロジェクトごとに進化していることがわかりましたが、MVC哲学に基づいている傾向があります。
RESTAPIの構築
もちろん、要求されたURIごとに404を返すだけのサーバーは必要ありません。 REST APIを構築することを約束したので、それを実行しましょう。
名前のセットを操作するインターフェースを構築したいとします。 実際のアプリでは、おそらくこれにデータベースを使用しますが、この例では、インメモリset
のデータ構造を使用します。
APIのスケルトンは次のようになります。 このコードはプロジェクトのどこにでも配置できますが、 api/names.py
などの別のAPIファイルをお勧めします。
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でのルーティングはデコレータを使用して行われます。 インポートされたデコレータは、これら4つのアクションのレジスタハンドラをpost
、 get
、 put
、およびdelete
します。 これらの作業を次のように分類する方法を理解します。
- 上記のデコレータはすべて、
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
デコレータの便利な点の1つは、たとえば、同じハンドラを使用してオブジェクトの更新と削除の両方を処理する場合は、次のように処理するメソッドのリストを渡すことができることです。
@route('/names/<name>', method=['PUT', 'DELETE']) def update_delete_handler(name): '''Handles name updates and deletions''' pass
それでは、これらのハンドラーのいくつかを実装しましょう。
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では、ユーザーが「name」という名前の属性を持つ本文にJSON文字列をPOSTする必要があります。
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
を返します。
この例のマスクでは、名前は1〜64文字のブランクを含まないASCII英数字である必要があります。 これは単純な検証であり、たとえば、ガベージデータを含むオブジェクトをテストしません。 FormEncodeなどのツールを使用することで、より複雑で完全な検証を実現できます。
存在のテスト
要求を満たす前の最後のテストは、指定された名前がセットにすでに存在するかどうかです。 より構造化されたアプリでは、そのテストはおそらく専用モジュールによって実行され、特殊な例外を介してAPIに通知される必要がありますが、セットを直接操作しているため、ここで実行する必要があります。
KeyError
を発生させることにより、名前の存在を通知します。
エラー応答
リクエストオブジェクトがすべてのリクエストデータを保持するのと同じように、レスポンスオブジェクトもレスポンスデータに対して同じことを行います。 応答ステータスを設定するには、次の2つの方法があります。
response.status = 400
と:
response.status = '400 Bad Request'
この例では、より単純な形式を選択しましたが、2番目の形式を使用して、エラーのテキストの説明を指定できます。 内部的には、Bottleは2番目の文字列を分割し、数値コードを適切に設定します。
成功への対応
すべての手順が成功した場合、set _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:リソースの更新
それでは、updateメソッドを実装する方法を見てみましょう。 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})
更新アクションの本体スキーマは作成アクションの場合と同じですが、ルート@put('/names/<oldname>')
で定義されているように、URIに新しいoldname
パラメーターもあります。
URIパラメータ
ご覧のとおり、URIパラメーターのBottleの表記は非常に簡単です。 必要な数のパラメーターを使用してURIを作成できます。 ボトルはそれらを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
として渡します。
削除:リソースの削除
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で装飾しているため、これ以上設定する必要はありません。 私たちのメソッドはすでに用意されており、アクセスする準備ができています。
CurlやPostmanなどのツールを使用して、APIを使用し、手動でテストできます。 (Curlを使用している場合は、JSONフォーマッターを使用して、応答が乱雑に見えないようにすることができます。)
ボーナス:クロスオリジンリソースシェアリング(CORS)
REST APIを構築する一般的な理由の1つは、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リクエストに応答します。
これを有効にするには、保存してメインモジュールからインポートするだけです。
要約
これですべてです。
このチュートリアルでは、BottleWebフレームワークを使用してPythonアプリ用のRESTAPIを作成するための基本的な手順について説明しました。
チュートリアルとAPIリファレンスドキュメントにアクセスすることで、この小さいながらも強力なフレームワークについての知識を深めることができます。