らくがきちょう

なんとなく ~所属組織/団体とは無関係であり、個人の見解です~

Python responder の Quick Start を写経してみる

responderPython の Web フレームワークです。 類似のものには FlaskFalcon があります。 公式ページには以下の機能がある、と書かれています。

  • A pleasant API, with a single import statement.
  • Class-based views without inheritance.
  • ASGI framework, the future of Python web services.
  • WebSocket support!
  • The ability to mount any ASGI / WSGI app at a subroute.
  • f-string syntax route declaration.
  • Mutable response object, passed into each view. No need to return anything.
  • Background tasks, spawned off in a ThreadPoolExecutor.
  • GraphQL (with GraphiQL) support!
  • OpenAPI schema generation, with interactive documentation!
  • Single-page webapp support!

今回は Quick Start! を写経して勉強した際のメモです。

インストールする

事前に responder をインストールしておきます。

pip install responder

サンプル 1 (Hello World!)

基本機能のみ、実装したシンプルなサンプルです。

ソースコード

responder はデフォルトで 127.0.0.1 を Listen するようです。 その為、address='0.0.0.0' を指定して外部からアクセス出来るようにしています。

import responder

api = responder.API()

@api.route("/")
def hello_world(req, resp):
    resp.text = "hello, world!"

if __name__ == '__main__':
    api.run(address='0.0.0.0', port=5042)

以下のように実行します。

# python app.py
INFO:     Started server process [18284]
INFO:     Uvicorn running on http://0.0.0.0:5042 (Press CTRL+C to quit)
INFO:     Waiting for application startup.
INFO:     Application startup complete.

結果

$ curl http://127.0.0.1:5042/
hello, world!

サンプル 2 (Accept Route Arguments)

引数を処理するサンプルです。 結果は文字列として返します。

ソースコード

import responder

api = responder.API()

@api.route("/hello/{who}")
def hello_to(req, resp, *, who):
    resp.text = f"hello, {who}!"

if __name__ == '__main__':
    api.run(address='0.0.0.0', port=5042)

結果

curl http://127.0.0.1:5042/hello/alice
hello, alice!

サンプル 3 (Returning JSON / YAML)

(文字列では無く) JSON を返すサンプルです。

ソースコード

import responder

api = responder.API()

@api.route("/hello/{who}/json")
def hello_to(req, resp, *, who):
    resp.media = {"hello": who}

if __name__ == '__main__':
    api.run(address='0.0.0.0', port=5042)

Quick Start には下記と書かれていました。

If you want your API to send back JSON, simply set the resp.media property to a JSON-serializable Python object:

結果

curl http://127.0.0.1:5042/hello/alice/json
{"hello": "alice"}

サンプル 4 (Rendering a Template)

テンプレートを使ってレンダリングしてみます。

ソースコード

テンプレート自体は templates ディレクトリに保存しておきます。 今回は templates/hello.html を以下の内容で新規作成しました。

<html>
  <head>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/uikit@3.3.0/dist/css/uikit.min.css" />
    <script src="https://cdn.jsdelivr.net/npm/uikit@3.3.0/dist/js/uikit.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/uikit@3.3.0/dist/js/uikit-icons.min.js"></script>
  </head>
  <body>
    <div class="uk-section uk-section-primary">
      <h1 class='uk-heading-primary'>Hello, {{ who }}!</h1>
    </div>
  </body>
</html>

Python のサンプルコードは以下です。

import responder

api = responder.API()

@api.route("/hello/{who}/html")
def hello_html(req, resp, *, who):
    resp.html = api.template('hello.html', who=who)

if __name__ == '__main__':
    api.run(address='0.0.0.0', port=5042)

結果

f:id:sig9:20200125123544p:plain

サンプル 5 (Setting Response Status Code)

レスポンスコードを指定するサンプルです。

ソースコード

import responder

api = responder.API()

@api.route("/416")
def teapot(req, resp):
    resp.status_code = api.status_codes.HTTP_416

if __name__ == '__main__':
    api.run(address='0.0.0.0', port=5042)

結果

$ curl -I http://127.0.0.1:5042/416
HTTP/1.1 416 Requested Range Not Satisfiable
date: Sat, 25 Jan 2020 03:41:00 GMT
server: uvicorn
content-type: application/json
content-length: 4

サンプル 6 (Setting Response Headers)

レスポンスヘッダを追加するサンプルです。

ソースコード

import responder

api = responder.API()

@api.route("/pizza")
def pizza_pizza(req, resp):
    resp.headers['X-Pizza'] = '42'

if __name__ == '__main__':
    api.run(address='0.0.0.0', port=5042)

結果

curl -I http://127.0.0.1:5042/pizza
HTTP/1.1 200 OK
date: Sat, 25 Jan 2020 03:45:48 GMT
server: uvicorn
content-type: application/json
x-pizza: 42
content-length: 4

サンプル 7 (Receiving Data & Background Tasks)

ソースコード

import responder

api = responder.API()

@api.route("/")
async def upload_file(req, resp):

    @api.background.task
    def process_data(data):
        f = open('./{}'.format(data['file']['filename']), 'w')
        f.write(data['file']['content'].decode('utf-8'))
        f.close()

    data = await req.media(format='files')
    process_data(data)

    resp.media = {'success': 'ok'}

if __name__ == '__main__':
    api.run(address='0.0.0.0', port=5042)

結果 (Python でのテスト)

Responder 公式サイトのテスト用サンプルコードは、なぜか /file を指定しつつポート番号も 8210 になっています。 これはサーバ側のサンプルコードに合わせて / ディレクトリに対してアップロードするように修正します。

import requests

data = {'file': ('hello.txt', 'hello, world!', "text/plain")}
r = requests.post('http://127.0.0.1:5042/', files=data)

print(r.text)

実行すると以下のように表示されます。 サーバと同じディレクトリに hello.txt ファイルがアップロードされているはずです。

$ python uploader.py
{"success": "ok"}

結果 (curl でのテスト)

同じ内容を curl で試してみます。 以下の内容で data.txt というファイルを作成しておきます。

$ cat data.txt
hello, world!

これを curl で POST します。

$ curl -F 'file=@data.txt; type=text/plain' http://127.0.0.1:5042/
{"success": "ok"}