Responder使ってみた際のポイント・注意点（Pytestも書いてみたよ）

# はじめに

最近流行っている（？） Pythonの新しい Webフレームワークの[Responder](https://github.com/kennethreitz/responder)を使って、ちょっとしたAPIを作ってみたところ、少し詰まったところがあったので書きます。
かなり高機能ながら、使っている事例が少なくて参考になるコードも少ないので少しでも参考になればと思います。

またPytestの事例も少なかったのでご参考になればと思います。

# 基本編

まずは基本編
ここらへんは結構記事がありますがかゆいところに手が届かない気もするのでもう少し書いてみます。

## Route, Background tasks, JSON response

Responderの特徴の一つとして非同期のAPIを簡単に実装できることがありますが、本当に簡単です。
下のように書くだけで簡単に非同期なAPIを作れます。

ここらへんは公式の[Quick Start!](https://python-responder.org/en/latest/quickstart.html#quick-start)にだいたい書いてあります。


```python:slow_job.py

import responder

api = responder.API()

@api.route("/data/upload") # 1.デコレータで簡単route、Flaskと同じですね
async def call_slow_job(req, resp): # 2.request, responseの順でパラメータに
    request = await req.media() # ★注意1
    slow_job(request)
    resp.media = {"status": "ok"} # 4.request, responseも.mediaでDictとしてJSONを扱える


@api.background.task # 3.非同期で処理させたい関数
def slow_job(request):
    # 重たい処理
    sleep(30)
    return True
```

### ポイント

1. エンドポイントになる関数に`route` デコレータをつけることで簡単に作れます。 ここらへんはFlaskと同じですね
2. request, responseは関数のパラメータとして設定されます
3. `background.task`を利用することで、非同期なAPIが簡単に作れます。重たいバッチ処理を受け付けるようなAPIに最適です
4. requestもresponseも`.media()`でDict型としてJSONを扱えます

#### 注意1

Background taskを使った場合、エンドポイントになる関数内の処理は意図せず非同期で処理されてしまうみたい？
関数の戻り値を後続の処理で使いたい場合、`async` で非同期関数を宣言し、`await` で処理を待ちましょう。

## MarshmallowでValidationしてError Responseを返す

[marshmallow](https://github.com/marshmallow-code/marshmallow)を使って定義したSchemaに沿ったRequestか検証することができます
また、その際のハンドリング方法を紹介します

どちらかというと、Responderというよりmarshmallowの解説っぽい・・・

公式ではSchema/Validationではなく、OpenAPIの説明でちらっと載ってます
[OpenAPI Schema Support](https://python-responder.org/en/latest/tour.html#openapi-schema-support)

```python:schema_example.py
from marshmallow import Schema, fields, ValidationError

@api.schema("DownloadReq")
class DownloadReqSchema(Schema):
    uploadId = fields.Str(required=True) # 1. Schema定義


@api.schema("ErrorResp")
class ErrorRespSchema(Schema):
    error = fields.Str()
    errorDate = fields.Date()


class ErrorModel:
    def __init__(self, error):
        self.error = str(error)
        self.errorDate = datetime.datetime.now()


@api.route("/convert/pdf/download")
async def download_result(req, resp):
    request = await req.media()
    try:
        data = DownloadReqSchema(strict=True).load(request).data # 2. RequestをSchemaで検証

    except ValidationError as error: # 3. VaridationErrorが返る
        resp.status_code = api.status_codes.HTTP_400
        resp.media = ErrorRespSchema().dump(ErrorModel(error)).data # 4. ResponseもSchema定義可能

        return # 5. Returnすればその時responseに設定されているものがresponse
    with open(os.path.join(upload_id, "result.pdf"), "rb") as result_pdf:
        resp.headers["Content-Type"] = "application/pdf"
        resp.content = result_pdf.read() # 6. おまけ ファイルをダウンロードする方法
```

### ポイント

1. marshmallowの`Schema`, `fields`を使ってSchemaを定義します `required=True`をfieldsに設定することでvalidationの対象になります
2. requestを1で設定したSchemaでロードすることでDeserializingとValidateをすることができます `strict=True`を設定しないとvalidationErrorがでません
3. validateに失敗すると`VaridationError`が返るのでexceptします
4. responseもschemaでserializeできます　あらかじめモデルを作ってそこに格納した後dumpします　ここではErrorModelクラスを作っていますがnamedtupleで簡単に作ることもできます 
5. except節でAPI的にErrorを返したい場合はresponseに設定後、関数をreturnすればその時設定されている値が返却されます
6. おまけ: response.contentにbyteモードで開いたファイルを読み込ませることでファイルダウンロードを実現できます

# 応用編(Pytest)

Pytestでどう調理するかあまり記事がなかったのでまとめます。

## HTTPリクエストを疑似

Pytestのfixtureを使えば簡単にテストできます。

```python:test_slow_job.py
import json
from unittest.mock import patch

import pytest
import hogehoge as target

@pytest.fixture # 1. fixtureを設定
def api():
    return target.api 

def test_call_slow_job (api):
    event = json.dumps({"fileName": "test.pdf",
                        "contentType": "image/png"})
    with patch.object(target, "slow_job", return_value=True) as mock_heavy_job: # Unit Test的にBackground TaskはMock化
        r = api.requests.post("/data/upload", event) # 2. 実際にコールしてresponseを取得
        assert r.status_code == 200
        json_response = json.loads(r.text)
        assert "id" in json_response
        assert "release_date" in json_response
        assert json_response["id"] == "hoge"
```

### ポイント

1. `pytest.fixture` であらかじめResponderAPIをインスタンス化しておきます　ここらへんは公式Docにも書いてありますので、普通によくやる方法らしい [Building and Testing with Responder](https://python-responder.org/en/latest/testing.html)
2. get methodは`requests.get` post methodは`requests.post`でコールします　第一引数にURI, 第二引数にpayloadを入れます

## Background TaskをTest

Background Taskを設定した関数へのTestはBackground Taskの挙動からFutureオブジェクトが返ります。
[Source](https://github.com/kennethreitz/responder/blob/master/responder/background.py)
下記のようにテストしてあげればよいです。

```python:test_slow_job.py
def test_slow_job(tmpdir):
    future = target.heavy_job(["hoge"], tmpdir) # 1. Background Taskの戻りはFutureが返ります
    actual = future.result() # 2. result()で結果を取得　★注意2
    expected = True
    assert actual == expected
```

### ポイント

1. Background Taskでデコレートした関数の戻りはFutureインスタンスになります
2. futureインスタンスに対してresult()メソッドをコールすることで、結果が取得できます

#### 注意2

IDEとかでLinterを使っているとデコレータの挙動なんてお構いなしなので、関数の戻り値にresult()なんてメソッドないですよと怒られる。

![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97408/c33cdbaf-bf9e-a812-1156-3f0adb3e70a2.png)


# おわりに

ResponderはほかにもOpenAPIやWebsocketやgrapheneなどできることは山ほどありそうです。素晴らしいですね。

今回勉強したことを使ってちょっとしたAPIを作ってみました。
今回紹介したこと以外にも[OpenAPI](https://python-responder.org/en/latest/tour.html#openapi-schema-support)や[CORS](https://python-responder.org/en/latest/tour.html#cors)とかも使ってみました。

[Ebook_homebrew　Github](https://github.com/tubone24/ebook_homebrew)

もう少し色々触ってみようかと思います。

## おまけ

★Herokuにもデプロイしております [https://ebook-homebrew.herokuapp.com/docs](https://ebook-homebrew.herokuapp.com/docs)

★Vue.jsを使ったフロントも拙作ながら作ってみました [Source](https://github.com/tubone24/ebook-homebrew-vue-client) [Heroku](https://ebook-homebrew-vue-client.herokuapp.com/#/)

※ 特にHerokuのスケールダウンの細工をしていないので、立ち上がりが悪いです。

## 参考

[公式Doc](https://python-responder.org/en/latest/index.html)

[Python responder 入門のために… 下調べ](https://qiita.com/y_k/items/6e4da3bef5c8d8bd2730)

[marshmallow公式Doc](https://marshmallow.readthedocs.io/en/3.0/index.html)