エンドポイントの拡張
bamboo において,エンドポイントはクライアントからのリクエストデータを受け取り,そのリクエストに対して適切な処理を行い,その結果をレスポンスとして返すようなエンティティです.このエンドポイントはプロトコルやインターフェースによって異なるエンドポイントクラスを継承することで実装可能であることは既に述べましたが,エンドポイントがクラスであるという特性を活かした実装をすることで,開発効率は高まる可能性があります.このドキュメントでは,より柔軟なエンドポイントの実装のために必要な setup() メソッドについて紹介し,エンドポイントがクラスであるという側面を活かしたエンドポイントの拡張方法について述べます.
用語の確認
既に前のドキュメントまでで出てきた用語ではありますが,本ドキュメントでも頻繁に用いる bamboo フレームワーク特有の用語について確認しておきます.
エンドポイント
クライアントからのリクエストを受け付ける場所という概念をモデル化したものです.その場所の住所が URI に相当し,エンドポイントはアプリケーション内で固有の URI パターンを持つことになります.また,(特に HTTP の場合) エンドポイントはクライアントからのリクエストを処理するためのレスポンスメソッドを持つことが出来る.
アプリケーション
アプリケーションは1つ以上のエンドポイントを持ち,クライアントからのリクエストをその URI をもとに適切なエンドポイントへルーティングすることが出来るオブジェクトです.また,アプリケーションには WSGI や ASGI などの Python の Web アプリケーションのインターフェースが実装されており,そのインターフェースに沿ったリクエスト処理が記述されています.
レスポンスメソッド
特定の HTTP メソッドが指定された際に呼び出されるコールバック関数です.各 HTTP メソッド (GET
や POST
) に対応する命名規則 (do_GET()
や do_POST()
) を持っています.エンドポイントクラスにレスポンスメソッドを実装することで,対応する HTTP メソッドを処理することが出来るようになります.
setup() メソッド
エンドポイントクラスには setup() というインスタンスメソッドがあります.setup()
はエンドポイントオブジェクトが生成された際に呼び出されます.アプリケーションオブジェクトはクライアントからリクエストが来てリクエストされた URI に対応するエンドポイントクラスを見つけると次のような処理を行います:
- URI に対応するエンドポイントクラスのインスタンスを作る
- 生成されたエンドポイントオブジェクトの setup メソッドを呼び出す
- エンドポイントオブジェクトのリクエストされた HTTP メソッドに対応するレスポンスメソッドを呼び出す
つまり,setup()
は do_GET()
や do_POST()
のような実際の処理を行うレスポンスメソッドの前に実行される関数です.setup()
の実装は任意であり,デフォルトでは何もしません.このメソッドはエンドポイントオブジェクトの __init__()
の代わりとなるようなメソッドであり,名前の通りエンドポイントオブジェクトの前処理を行うためのものです.
setup() の例
それでは簡単な例を挙げて setup()
の使い方を説明します.まずは以下の例を見てください:
app = WSGIApp()
@app.route("mock")
class MockEndpoint(WSGIEndpoint):
# データベースを操作するための何らかのオブジェクト db を仮定
def setup(self, db) -> None:
# レスポンスメソッドで使えるようにインスタンス変数として代入
self.db = db
def do_GET(self) -> None:
# setup() 後に呼び出されるので self.db にアクセス可能
models = self.db.select()
上記の例ではリクエスト処理時にデータベースを操作するエンドポイントを仮定しました.そのエンドポイントにはデータベースを扱うための何らかのオブジェクトが必要になるでしょう.それをセットアップ時に引数で受け取りインスタンス変数としておいて,その後実行されるレスポンスメソッド (ここでは do_GET()
) で参照しています.ただ,これだけでは実際に setup()
の引数の db
に何が渡されるのかは決まっていません.それを指定するためには,以下のようにアプリケーションオブジェクトの set_parcel()
を使用します:
# データベースを操作するオブジェクト db を App オブジェクト経由で渡す
# この処理をすることで db が MockEndpoint オブジェクトの setup() 実行時に渡される
app.set_parcel(MockEndpoint, db)
これによってアプリケーションオブジェクト app
は MockEndpoint
のオブジェクト生成後の setup()
の実行時に db
オブジェクトを引数として渡すように設定されます.
Tip
bamboo では setup()
の引数のことを parcel と呼んでいます.parcel
の実体は単なるオブジェクトのタプルに過ぎませんが,parcel
は set_parcel()
によって設定された後,プログラム終了時まで存続します.これは parcel
は実際にはアプリケーションではなく,エンドポイントクラスの名前空間下に存在していることに起因します.
再利用可能性を高めるための setup()
上の例では回りくどい方法で db
というオブジェクトをエンドポイントオブジェクトの setup()
に渡しています.上記の例は以下のように記述しても同等の効果が得られます:
app = WSGIApp()
@app.route("mock")
class MockEndpoint(WSGIEndpoint):
def do_GET(self):
models = db.select()
後者の例の方がはるかに簡潔に見えますが,1つ欠点があります.それは,エンドポイントクラス内部で db
というオブジェクトが明示的に使用されている点です.これによって,MockEndpoint
というクラスは db
というオブジェクトに直接的に依存することになってしまいます.それに対し setup()
を使用した例では,db
を引数として受け取ることで,MockEndpoint
がデータベースを操作するオブジェクトに直接的に依存しないようになっています.
このような性質は特にエンドポイントクラスをコンポーネント化し,再利用したい場合に有用です.例えば,さきほどの MockEndpoint
を mocklib
というライブラリとしてまとめた場合,以下のようにして MockEndpoint
を再利用できます:
from bamboo import WSGIApp
from mocklib import MockEndpoint
app = WSGIApp()
app.route("mymock", MockEndpoint)
# mydb という別のデータベースオブジェクトを使用する
app.set_parcel(MockEndpoint, mydb)
状態を定義するための setup()
まずはエンドポイントオブジェクトのライフサイクルについて説明します.まずさきほど述べたように,エンドポイントオブジェクトはクライアントからのリクエストがあるたびに生成されます.また,生成されたエンドポイントオブジェクトはレスポンス処理が終了すると破棄されてしまいます.したがって,エンドポイントオブジェクトのライフサイクルは,そのエンドポイントの URI パターンに一致する URI を持つリクエストが検知された地点からそのリクエストが処理された地点までです.
ひとたびリクエストが処理されるとエンドポイントオブジェクトは破棄されることから,特に何もしないとレスポンスはステートレスになります.しかし,エンドポイントによっては状態を保持するべきものも存在します.そこで,エンドポイントに状態を定義するためにはエンドポイントオブジェクトより生存期間の長いオブジェクトをエンドポイントオブジェクト内で参照する必要があります.setup()
はそのような場合に有用です.なぜなら,アプリケーションオブジェクトの set_parcel()
によって設定された parcel
はエンドポイントクラスによってプログラム終了時まで保持されるからです (上記 Tip 参照) .つまり,setup()
に渡される parcel
のライフサイクルはプログラム開始時から終了時までであり,エンドポイントオブジェクトのライフサイクルに比べてはるかに長いため,parcel
はエンドポイントオブジェクトの状態変数としての役割を持つことが出来ます.
エンドポイントの拡張例
エンドポイントはクラスとして実装されるため,クラスに対する Python の言語仕様に則った実装を行うことが出来ます.ここではその例として,継承を利用したエンドポイントクラスの実装について紹介します.
作成したいエンドポイントとしては,ユーザーデータを管理しているデータベースを操作する必要があるエンドポイントを仮定します.そしてそのデータベースをコントロールするためのオブジェクトを db_controller
と名付け,このオブジェクトは何らかの形で用意できるものとしましょう.このようなエンドポイントは内部でデータを管理し,その内部データによってレスポンスの結果は変化するため,明らかに状態を持ちます.その状態をもたらすオブジェクトはいま db_controller
であり,これはユーザーデータを参照する必要があるエンドポイント全てについての状態変数になりえます.そこで以下のような基底クラスを作成すると,そのようなエンドポイントたちの実装を簡略化出来ます:
class UserDataEndpointBase(WSGIEndpoint):
def setup(self, db_controller) -> None:
self.db_controller = db_controller
そして db_controller
を必要とする全てのエンドポイントでこのエンドポイントを継承します:
@app.route(AnyStringLocation(), "images")
class UserImagesEndpoint(UserDataEndpointBase):
@basic_auth()
def do_GET(self, user_id: str, pw: str) -> None:
# 認証処理を行う
flag = self.db_controller.authenticate(user_id, pw)
# その他の処理 ...
このように作成されたエンドポイントクラスに用意した db_controller
をセットするには前述したようにアプリケーションオブジェクトの set_parcel()
を使用します:
app.set_parcel(UserImagesEndpoint, db_controller)