適当おじさんの適当ブログ

技術のことやゲーム開発のことやゲームのことなど自由に雑多に書き連ねます

PythonのWebフレームワーク Responder を使ってみたメモ

はじめに

Responder は、PythonのWebアプリケーションのフレームワークです。WebSocketやGraphQLもサポートしています。

github.com

同じくPythonのフレームワークである FlaskFalcon の優れた部分 + 作者のアイデアを足したようなものとなっています。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 というパッケージで実現されています。

github.com

以下、箇条書きした項目について書いていきます。

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 なページです。

f:id:subarunari:20190105170717p:plain

まだスキーマを定義していないので「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}"

これでスキーマが定義されたので、以下のようなドキュメントが生成されています。お手軽に作成できて便利ですね。

f:id:subarunari:20190105172027p:plain

テンプレートエンジン

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 Clientwscat などを利用すれば疎通確認できます。

さいごに

Responderは、「既存のライブラリを組み合わせてイイ感じにまとめてるフレームワーク」という印象を持ちました。そのことを実感してもらえるといいなぁと思い、本記事では意図的に依存パッケージ(Starlette や graphene など)の名前を挙げておりました。

Flaskをよく使っていたので、Responderは非常に手に馴染みました。WebSocketやAPIドキュメントの生成も簡単にできるのも感動ポイントでした。この記事には書いていない機能が他にも色々あるので、ぜひ一度公式ドキュメントを見てもらえればと思います。Responderは、今まさに活発に開発が進められているところなので、しばらく使ってみようと思います。

今回の記事にあるコードはGithubにも置いておきました。 github.com

備考

バージョン 1.1.2 のResponderにはバグがあり、WebSocket使用時に AssertionError が発生してしまいました。エラーが発生した場合は、Responderが依存しているパッケージである Starlette のバージョンを0.9未満に指定すればOKです。この問題については、すでに報告されており解消されています。次のリリースに修正が含まれるようです(参考)。

最新版をインストールすればこの問題は踏むことはありませんが、検証時にぶち当たったので一応書いておきます。