Python 3 + Falcon を使った RESTful API の開発/テスト/デバッグ

  • 2
    いいね
  • 0
    コメント

はじめに

単純な Web API を用意する必要があり、Falcon という若干マイナーな WSGI フレームワークを使ってみました。Falcon の公式サイトのサンプルやドキュメントの情報は十分なので、インストールしてすぐに使い始めるのはとても簡単です。しかし、細かいところで追加情報が必要だと感じることが多かったので、この記事で補いたいと思います。

Falcon - The minimalist Python WSGI framework
https://falconframework.org/

公式情報を日本語訳するだけのことは避けたいので、インストール方法や使い方を日本語で読みたいという方は、Qiita にある他の記事が参考になると思います。既に Falcon タグが作られています。この記事が記念すべき 10 件目。

Falconに関する9件の投稿 - Qiita
http://qiita.com/tags/Falcon

本記事における開発環境は以下の通りです。

  • Ubuntu 16.04.2 LTS
  • Python 3.5.2
  • Falcon 1.1.0
  • pytest 3.07

なお、私の Python における戦闘力は、例外捕捉の文法が分からなくてググるレベルのゴミです。実質ほとんど Python 書いてないからつれーわー。実質ほとんど Python 書いてないからなー。

ところでなぜ Falcon

記事の主旨からは逸れますが、そもそもの Falcon の選定理由と、本記事を書くに至った経緯をそれぞれ書いておきます。

Web API を書くにあたり、ゴミ初心者から脱却するために言語は Python 3 で、というのは決まっていたのですが、フレームワークを選定するために小一時間インターネット上を徘徊していました。小一時間 (いや、見栄を張った。実際は 2 時間ぐらいだたぶん。) 何をやっていたかというと、次のような感じです。

  1. Django と Flask が人気らしい。
  2. Django は大きすぎる。OpenSSL で済むところを ADCS、Python の SimpleHTTPServer で済むところを IIS でやるようなものだ。
  3. では Flask 一択、ということで Flask のチュートリアルを始める。
  4. チュートリアルが微妙。データベースやセットアップ部分にはあまり興味がない。
  5. しかも flask と flask_restful というパッケージが分かれている。美しくない。
  6. というわけで Flask 却下。他に何かないか。
  7. Falcon 発見。シンプルは正義。速いのも正義。
  8. チュートリアルクイックスタートもいい感じ。決定。

もちろんデメリットもあります。

  1. マイナー
    Flask などの有名なフレームワークに比べて知名度が低く、情報が少ないです。何か困ったことがあったときに調べるのが大変かもしれません。

  2. 公式ドキュメントもシンプル
    とりあえず動くものを作ることは簡単なのですが、実際に使い始めてドキュメントを参照すると、記述が簡潔過ぎて知りたい情報が書かれていなかったりします。シンプルなドキュメントはそれほど正義ではない。本記事を書くに至った主な動機はこの点です。

Application Factory パターンでの実装

チュートリアルで最初に作る Hello world は、2 つのファイルから構成されています。これを Gunicorn で実行するときのコマンドは gunicorn app です。app というのは app.py のファイル名に起因します。

ディレクトリ構造
.
|-- app.py
`-- images.py
app.py
import falcon
import images

api = application = falcon.API()
images = images.Resource()
api.add_route('/images', images)
images.py
import falcon

class Resource(object):
  def on_get(self, req, resp):
    resp.body = '{"message": "Hello world!"}'
    resp.status = falcon.HTTP_200

WSGI におけるアプリケーション オブジェクトをグローバル スコープで定義しています。これはチュートリアルの最後までこのままです。また、クイックスタートにある 2 つの例でも同じです。これでも別に構わないのですが、グローバル スコープはあまり使いたくないですし、テストを書くときに不便なことが後で分かりました。

Flask のドキュメントでは、Application Factory というデザイン パターンが紹介されており、こちらのほうが使いやすいと思います。

Application Factories — Flask Documentation (0.12)
http://flask.pocoo.org/docs/0.12/patterns/appfactories/

Application Factory を使うと、先ほどの Hello world は次のような構成になります。Gunicorn での実行コマンドは gunicorn tutorial.wsgi になります。

ディレクトリ構造
.
`-- tutorial
    |-- api
    |   |-- __init__.py
    |   `-- resources.py
    |-- __init__.py
    `-- wsgi.py

tutorial/api/__init__.py
# 空でいい
tutorial/api/resources.py
import falcon

class Resource(object):
  def on_get(self, req, resp):
    resp.body = '{"message": "Hello world!"}'
    resp.status = falcon.HTTP_200
tutorial/__init__.py
import falcon
from tutorial.api.resources import Resource

def create_app():
  api = falcon.API()
  images = Resource()
  api.add_route('/images', images)
  return api
tutorial/wsgi.py
from tutorial import create_app

application = create_app()

ファクトリー メソッドを分離して __init__.py に置き、アプリケーションの外側と内側とでディレクトリを分けました。

ネーミング規則などは、Google 様の Flask プロジェクトである timesketch を参考にしました。困ったときは、Google の GitHub リポジトリから同じ言語で書かれたプロジェクトを適当に探して、それの真似をしておけば間違いはないはず。優秀な人のコードを読むのはいつでも勉強になります。なんていい時代だ。

pytest でテスト

Falcon にはテスト用のモジュールが用意されています。

Testing — Falcon 1.1.0 documentation
http://falcon.readthedocs.io/en/stable/api/testing.html

unittest と pytest のそれぞれを使ったサンプルが書かれているのは有難いのですが、これ ↓

# -----------------------------------------------------------------
# unittest-style
# -----------------------------------------------------------------

(..snip..)

class MyTestCase(testing.TestCase):
    def setUp(self):
        super(MyTestCase, self).setUp()

        # Assume the hypothetical `myapp` package has a
        # function called `create()` to initialize and
        # return a `falcon.API` instance.
        self.app = myapp.create()

全部書いてくれよ・・・。しかもこの myapp.create() は Application Factory におけるファクトリー メソッドを想定しているわけで、だったらチュートリアルのデザインも Application Factory 合わせてほしいですね。

さて、Python のテストに unittest を使うのか pytest を使うのかも迷うところでしょう。この選択にも正解はないのでしょうが、pytest の方が高機能っぽいので pytest を選びました。ざっと見たのは ↓ のブログなど。あと、pytest は公式ドキュメントがとても優れています。unittest の方は見ていないので、もっと凄いドキュメントだったらごめんなさい。

pythonのテストにpytestを使ってみた · I Will Survive
http://blog.restartr.com/2013/04/05/my-first-pytest/

[Python] 初中級者のためのpytest入門 - くろのて
http://note.crohaco.net/2016/python-pytest/

というわけで、上記 Hello world プロジェクトに pytest のテスト コードを追加します。

ディレクトリ構造
.
`-- tutorial
    |-- api
    |   |-- __init__.py
    |   `-- resources.py
    |-- __init__.py
    |-- tests
    |   |-- conftest.py
    |   |-- __init__.py
    |   `-- tutorial_test.py
    `-- wsgi.py

tutorial/tests ディレクトリに新しくファイルを作っただけで、他はそのままです。

tutorial/tests/conftest.py
import pytest
from falcon import testing
from tutorial import create_app

@pytest.fixture(scope = 'module')
def client():
  return testing.TestClient(create_app())
tutorial/tests/__init__.py
# 空でいい
tutorial/tests/tutorial_test.py
import pytest

def test_resource(client):
  resp = client.simulate_get('/images')
  assert(resp.status_code == 200)
  assert(resp.json == {u'message': u'Hello world!'})

  resp = client.simulate_get('/invalid')
  assert(resp.status_code == 404)

pytest には Auto-discovery という機能があり、ファイル名などから勝手にテストを見つけて実行してくれるので、シェルで pytest と打つだけでテストが実行されます。簡単でいいですね。

$ pytest
======================================= test session starts ========================================
platform linux -- Python 3.5.2, pytest-3.0.7, py-1.4.33, pluggy-0.4.0
rootdir: /data/src/falcon-tutorial, inifile:
collected 1 items

tutorial/tests/tutorial_test.py .
===================================== 1 passed in 0.18 seconds =====================================

pytest も覚えたばかりなので詳しくは説明できませんが、ポイントを書いておきます。

まずディレクトリ構造ですが、プロジェクトの規模などによってパターンがあるようで、公式ドキュメントでそれぞれ丁寧に説明されています。今回書いたテスト コードは tutorial ディレクトリの中に tests ディレクトリを作ったので、下記ドキュメントにおける "Tests as part of application code" です。

Good Integration Practices — pytest documentation
https://docs.pytest.org/en/latest/goodpractices.html

Falcon の Testing ページで省略されていた myapp.create() は、本記事のコードでは create_app() をそのまま使うことができます。私の拙い美的感覚では、 create_app() を呼び出す Fixture の定義は、テスト メソッドと分けておきたいところです。pytest のドキュメントによると、conftest.py に書いておけば自動的にインポートされるみたいなので、conftest.py に書きました。

Basic patterns and examples — pytest documentation
https://docs.pytest.org/en/latest/example/simple.html

fixture を定義するデコレーターに scope = 'module' を指定していますが、これは、fixture をテスト モジュールがロードされる毎に呼ばれるようにするという意味です。上の例では、tutorial_test.py がロードされたときに一度 create_app() が呼ばれ、モジュール内では同じオブジェクトが使い回されます。

pytest fixtures: explicit, modular, scalable — pytest documentation
https://docs.pytest.org/en/latest/fixture.html

もし scope = 'module' の代わりに scope = 'function' を使うと、テスト メソッド毎にアプリケーション オブジェクトが作られるため、テストの実行が遅くなってしまいます。

pdb でデバッグ

実行環境をデバッグできないと開発は効率よく進みません。Python には pdb というデバッガーがあるので、これを使って Falcon の実行環境をデバッガーから見てみましょう。もしかすると pdb をそのまま使うのは時代遅れかもしれませんが、原始的なツールは使えるようになっておいて損はないはずです。

テスト コードをデバッグ

pytest には --pdb オプションがあり、テストが失敗すると自動的に pdb をアタッチしてくれます。これはとても便利です。実行例は以下の通りです。

$ pytest --pdb
======================================= test session starts ========================================
platform linux -- Python 3.5.2, pytest-3.0.7, py-1.4.33, pluggy-0.4.0
rootdir: /data/src/falcon-tutorial, inifile:
collected 1 items

tutorial/tests/tutorial_test.py F
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> traceback >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

client = <falcon.testing.client.TestClient object at 0x7faf472ab320>

    def test_resource(client):
      resp = client.simulate_get('/images')
      assert(resp.status_code == 200)
>     assert(resp.json == {u'message': u'Hello failure!'}) # demo purpose
E     AssertionError: assert {'message': 'Hello world!'} == {'message': 'Hello failure!'}
E       Differing items:
E       {'message': 'Hello world!'} != {'message': 'Hello failure!'}
E       Use -v to get the full diff

tutorial/tests/tutorial_test.py:6: AssertionError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> /data/src/falcon-tutorial/tutorial/tests/tutorial_test.py(6)test_resource()
-> assert(resp.json == {u'message': u'Hello failure!'}) # demo purpose

(Pdb) bt
  /data/src/py-qiita/lib/python3.5/site-packages/_pytest/runner.py(163)__init__()
-> self.result = func()
  /data/src/py-qiita/lib/python3.5/site-packages/_pytest/runner.py(151)<lambda>()
-> return CallInfo(lambda: ihook(item=item, **kwds), when=when)
(..snip..)
  /data/src/py-qiita/lib/python3.5/site-packages/_pytest/python.py(154)pytest_pyfunc_call()
-> testfunction(**testargs)
> /data/src/falcon-tutorial/tutorial/tests/tutorial_test.py(6)test_resource()
-> assert(resp.json == {u'message': u'Hello failure!'}) # demo purpose

(Pdb) p resp.content
b'{"message": "Hello world!"}'
(Pdb) c

===================================== 1 failed in 0.76 seconds =====================================

$

WSGI 実行下で直接デバッグ

これに一番苦労しました。未だにベストな方法が見つかっていませんが、Falcon のクイック スタートのサンプル コードにおけるコメントで 2 つの方法が示唆されているので、それを試します。

# Useful for debugging problems in your API; works with pdb.set_trace(). You
# can also use Gunicorn to host your app. Gunicorn can be configured to
# auto-restart workers when it detects a code change, and it also works
# with pdb.
if __name__ == '__main__':
    httpd = simple_server.make_server('127.0.0.1', 8000, app)
    httpd.serve_forever()

pdb.set_trace() を入れる

まあそのままなんですが、止めたいところで pdb.set_trace() を実行すると、Gunicorn 上で実行していても pdb でブレークしてくれます。ただ、予め set_trace() を書かないといけないので不便ですし、場合によっては使えません。これは使いたくない方法です。

wsgiref.simple_server を使う

apache2 では -X のようなデバッグ用の便利なオプションがありましたが、Gunicorn には存在しないようです。そこで、Gunicorn の代わりに、単純な WSGI である wsgiref.simple_server を使い、それを pdb でデバッグすることを考えます。

では Falcon のクイックスタートにあるように、if __name__ == '__main__': という条件を tutorial/wsgi.py に追加すればいいかというと、複数の厄介な問題に阻まれてうまくいきません。

問題 1. tutorial/wsgi.py はスクリプトとして実行できない

tutorial/wsgi.py はモジュールとして実行することが前提となっています。スクリプトとして実行しようとすると、from tutorial import create_app がエラーになります。

問題 2. モジュール実行すると、外部から pdb がロードできない

スクリプトとして実行できれば、python -m pdb tutorial/wsgi.py と実行して pdb をアタッチできますが、モジュール実行で同等のことができません。

問題 3. IPython を使うと、ブレークインの後に再開できない

IPython を使って ipython --pdb -m tutorial.wsgi を実行すると、モジュール実行で、かつ pdb をロードできます。しかし今度は IPython の側に問題があり、Ctrl+C でブレークインした後に実行を再開しようとするとプロセスが終了してしまいます。この問題は GitHub に既に Issue 登録されていました。

Feature request: resume execution after KeyboardInterrupt · Issue #9354 · ipython/ipython
https://github.com/ipython/ipython/issues/9354

実行するとこうなります。

$ ipython --pdb -m tutorial.wsgi
^C---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
/usr/lib/python3.5/runpy.py in run_module(mod_name, init_globals, run_name, alter_sys)
    194         run_name = mod_name
    195     if alter_sys:
--> 196         return _run_module_code(code, init_globals, run_name, mod_spec)
    197     else:
    198         # Leave the sys module alone

(..snip..)

KeyboardInterrupt:
> /usr/lib/python3.5/selectors.py(378)select()
    376                 fd_event_list = self._poll.poll(timeout)
    377             except InterruptedError:
--> 378                 return ready
    379             for fd, event in fd_event_list:
    380                 events = 0

ipdb> b /data/src/falcon-tutorial/tutorial/api/resources.py:5
Breakpoint 1 at /data/src/falcon-tutorial/tutorial/api/resources.py:5
ipdb> c
/data/src/py-qiita/lib/python3.5/site-packages/IPython/core/interactiveshell.py:2555: UserWarning: Unknown failure executing module: <tutorial.wsgi>
  warn('Unknown failure executing module: <%s>' % mod_name)

$

詰んだ・・・。

で、結局どうしたかというと、デバッグ用のスクリプトを新しく書き加えました。エレガントではないですが、これが一番現実的です。

tutorial/debug_harness.py
import pdb
from wsgiref import simple_server
from tutorial import create_app

if __name__ == '__main__':
  application = create_app()
  httpd = simple_server.make_server('127.0.0.1', 8000, application)
  pdb.run('httpd.serve_forever()')

デバッグしたいときは python -m tutorial.debug_harness を実行します。実行例は以下の通り。^C のところでブレークインしていますが、ブレークポイントを設定した後でも何事もなく再開できています。頼むよ IPython。

$ python -m tutorial.debug_harness
> <string>(1)<module>()
(Pdb) c

^C
Program interrupted. (Use 'cont' to resume).
--Call--
> /usr/lib/python3.5/signal.py(45)signal()
-> @_wraps(_signal.signal)

(Pdb) b /data/src/falcon-tutorial/tutorial/api/resources.py:5
Breakpoint 1 at /data/src/falcon-tutorial/tutorial/api/resources.py:5

(Pdb) c
> /data/src/falcon-tutorial/tutorial/api/resources.py(5)on_get()
-> resp.body = '{"message": "Hello world!"}'

(Pdb) bt
  /usr/lib/python3.5/runpy.py(184)_run_module_as_main()
-> "__main__", mod_spec)
  /usr/lib/python3.5/runpy.py(85)_run_code()
-> exec(code, run_globals)
  /data/src/falcon-tutorial/tutorial/debug_harness.py(8)<module>()
-> pdb.run('httpd.serve_forever()')
(..snip..)
  /usr/lib/python3.5/wsgiref/handlers.py(137)run()
-> self.result = application(self.environ, self.start_response)
  /data/src/py-qiita/lib/python3.5/site-packages/falcon/api.py(209)__call__()
-> responder(req, resp, **params)
> /data/src/falcon-tutorial/tutorial/api/resources.py(5)on_get()
-> resp.body = '{"message": "Hello world!"}'

(Pdb) c
127.0.0.1 - - [20/Apr/2017 13:05:58] "GET /images HTTP/1.1" 200 27

サンプル ソース

以下のリポジトリに置きました。

msmania/falcon-tutorial: Just another Falcon tutorial
https://github.com/msmania/falcon-tutorial

おわりに

RESTful な Web API を Falcon を使って書いて、テストして、デバッグもできるようになりました。私自身が書いている API はまだ開発中で、どこかにデプロイして動かすところまで辿り着いていません。開発が一段落してデプロイを試したらまた何か書き足すかもしれません。

今回使った IPython、Falcon などは全てオープンソースなので、記事で指摘した部分のうち、自分で直せそうなところは直していきたいところです。でも IPython のやつは絶対に難しいよなぁ。