はじめに
Responder は、PythonのWebアプリケーションのフレームワークです。WebSocketやGraphQLもサポートしています。
同じくPythonのフレームワークである Flask と Falcon の優れた部分 + 作者のアイデアを足したようなものとなっています。Flaskはマイクロフレームワークと呼ばれる必要最低限の機能のみを持ったフレームワークで、Falconは軽量で高速なWebAPIのためのフレームワークです。
この記事は、Responderをざっくり触ってみたときのメモです。2019年1月4日時点の情報で、今も開発が進められているので、異なる部分もあるかと思いますがご了承ください。
実験環境について
記事を書いた時点でのPythonとResponderのバージョンは以下のようになっています。
バージョン | |
---|---|
Python | 3.7.1 |
Responder | 1.2.0 |
とりあえず動かす
Responderを試すにあたって pipenv で環境を作りました。公式ドキュメントにある通り、--pre
フラグをつけてインストールしています。
$ pipenv --three $ pipenv install responder --pre $ pipenv shell
とりあえずResponderで作成したWebアプリケーションを動かします。python <ファイル名>.py
で実行すればWebアプリケーションが立ち上がります。Webブラウザで、 http://localhost:5042
にアクセスすれば、hello responder
と表示されているはずです。
# responder_sampler.py import responder api = responder.API() @api.route("/") def index(req, resp): resp.text = "hello responder" if __name__ == '__main__': api.run()
Flaskを触ったことがある人にはなんだか既視感のあるコードではないでしょうか。Flaskで同じことを実現すると以下のような感じになります。
# flask_sampler.py from flask import Flask app = Flask(__name__) @app.route("/") def index(): return "hello flask" if __name__ == '__main__': app.run()
Responderのレスポンスの書き方はFalcon流です。Flaskは明示的に return
するのに対して、ResponderとFalconは、Responseオブジェクトの属性に値をセットします。
こんな感じで、FlaskやFalconに似た部分がたくさんあるので、どちらかを触ったことある人にとっては馴染みやすいかと思います。
Responderの特徴
サンプルコードと一緒に、Responderの特徴をざっくりピックアップしていきます。
- ASGI
- WSGI/ASGI Appのマウント
- メソッドベースのAPI定義
- APIドキュメントの生成
- テンプレートエンジン
- WebSocket
ResponderはGraphQLもサポートしています。他にもこの記事では触れていない特徴的な機能が色々あります。気になる方はぜひ 公式ドキュメント を参照ください。ちなみに GraphQLは graphene というパッケージで実現されています。
以下、箇条書きした項目について書いていきます。
ASGI
Responderは、ASGI(Asynchronous Server Gateway Interface)なアプリケーションとして動作します。ASGIであることは、Responderの大きな特徴の1つです。FlaskやFalconは WSGIなアプリケーションです。WSGIは同期的であるのに対し、ASGIは同期・非同期のどちらも扱えます。
ResponderはASGIフレームワークに Starlette を、ASGIサーバには Uvicorn を使っています。これらをいい感じにラップして作られたWebアプリケーションフレームワークが Responder であるとも言えます。
WSGI/ASGI Appのマウント
WSGI/ASGI なアプリケーションをResponderにマウントできます。
import flask_sampler api.mount("/flask", flask_sampler.app)
http://localhost:5042/flask/
にアクセスすると、hello flask というレスポンスが得られるはずです。既存のFlaskアプリケーションをマウントすることで、無理なくResponderに移行できたりしそうです。
メソッドベースのAPI定義
Falcon のように on_{HTTPメソッド名}
な関数を定義することで、HTTPメソッドに対応したルーティングを定義できます。その場合、以下のようにクラスとして定義します。
@api.route("/echo/{voice}") class Echo(): def on_get(self, req, resp, *, voice): resp.text = f"get {voice}" def on_post(self, req, resp, *, voice): resp.text = f"post {voice}"
ルーティングは f-string の要領で動的なパスを定義できます。Flaskは /echo/<voice>
のような固有の記法だったので、f-stringを知ってる人にとっては直感的に書けます。
Responderでは、すべてのメソッドに対応する on_request
を定義できます。 on_{HTTPメソッド名}
と on_request
の両方が定義されている場合は、on_request
がまず処理されたあとに on_{HTTPメソッド名}
が処理されるようです。
@api.route("/echo/{word}") class Echo(): def on_get(self, req, resp, *, word): resp.text = f"get {word}" def on_post(self, req, resp, *, word): resp.text = f"post {word}" def on_request(self, req, resp, *, word): resp.text = f"call on request {word}"
APIドキュメントの生成
インタラクティブなAPIドキュメントを自動生成できます。responder.API()
を実行する際に対応するパラメータを指定し、スキーマを定義することでAPIドキュメントが生成されます。openapi
version
docs_route
の3つのパラメータを指定します。
openapi_params = { "title": "Sample API", "openapi": "3.0.0", "version": "1.0", "docs_route": "/docs" } api = responder.API(**openapi_params)
これでAPIドキュメントが生成されました。http://localhost:5042/docs
にアクセスすると下図のようなページが表示されるはずです。見てわかる通り、Swagger なページです。
まだスキーマを定義していないので「No operations defined in spec!」と表示されています。先ほど作成した Echo のAPIに対してスキーマを定義してみます。コメント部分はOpenAPIの仕様に基づいています。OpenAPIの仕様については、OpenAPIのドキュメント や Swaggerのドキュメント を参照ください。
# スキーマ定義のために marshmallow からモジュールをインポート from marshmallow import Schema, fields @api.schema("Echo") class EchoSchema(Schema): message = fields.Str() @api.route("/echo/{word}") class Echo(): """ test docs --- get: description: echo back word. parameters: - name: word in: path schema: type: string responses: 200: description: return word schema: $ref = "#/components/schemas/Echo" """ def on_get(self, req, resp, *, word): resp.text = f"get {word}"
これでスキーマが定義されたので、以下のようなドキュメントが生成されています。お手軽に作成できて便利ですね。
テンプレートエンジン
Responderインストール時に、Pythonのテンプレートエンジンである Jinja2 も一緒にインストールされています。なので、新たに何かを追加することなく、API.template
を実行すればレンダリングしたHTMLを返せます。試しに、先ほどの on_get
をレンダリングしたHTMLを返すようにしてみます。
@api.route("/echo/{word}") class Echo(): def on_get(self, req, resp, *, word): resp.content = api.template("index.html", word=word)
<!-- templates/index.html --> <!DOCTYPE html> <html> <!-- 検証用なので雑 --> <body> echo {{word}} </body> </html>
デフォルトでは templates
ディレクトリ以下を参照します。上記の例だと、templates/index.html
をレンダリングして返します。これらのデフォルト値は、responder.API
を実行する際に引数を与えることで変更できます。引数とデフォルト値の組み合わせは、公式ドキュメントを参照してください。
API Documentation — responder 1.1.3 documentation
たとえばレンダリングの際に templatesディレクトリではなく、htmlsディレクトリを参照させたい場合は、以下のようにします。
api = responder.API(templates_dir="htmls")
WebSocket
ResponderはWebSocketをサポートしています。websocket=True
を指定することで、簡単にWebSocketをルーティングできちゃいます。WebSocketは、Starletteの機能で実現されています。
@api.route("/ws", websocket=True) async def websocket(ws): await ws.accept() while True: name = await ws.receive_text() await ws.send_text(f"hello websocket") await ws.close()
WebSocketなのでこれまでのようにブラウザでは疎通確認できません。Chromeの拡張機能である WebSocket Test Client や wscat などを利用すれば疎通確認できます。
さいごに
Responderは、「既存のライブラリを組み合わせてイイ感じにまとめてるフレームワーク」という印象を持ちました。そのことを実感してもらえるといいなぁと思い、本記事では意図的に依存パッケージ(Starlette や graphene など)の名前を挙げておりました。
Flaskをよく使っていたので、Responderは非常に手に馴染みました。WebSocketやAPIドキュメントの生成も簡単にできるのも感動ポイントでした。この記事には書いていない機能が他にも色々あるので、ぜひ一度公式ドキュメントを見てもらえればと思います。Responderは、今まさに活発に開発が進められているところなので、しばらく使ってみようと思います。
今回の記事にあるコードはGithubにも置いておきました。 github.com
備考
バージョン 1.1.2 のResponderにはバグがあり、WebSocket使用時に AssertionError が発生してしまいました。エラーが発生した場合は、Responderが依存しているパッケージである Starlette のバージョンを0.9未満に指定すればOKです。この問題については、すでに報告されており解消されています。次のリリースに修正が含まれるようです(参考)。
最新版をインストールすればこの問題は踏むことはありませんが、検証時にぶち当たったので一応書いておきます。