BottleでWeb APIを作成する~おかわり~

以前の記事でBottleを使ったAPIを作成しました。
ここではもうちょっと進んだ例を書いていきたいと思います。

ここでのアプリケーションは、事前に指定されたファイルシステム上のあるディレクトリの中で、ファイルやサブディレクトリを作成する、というものです。

書ききれなかったその他は別記事で。

アプリケーションの仕様

仕様というほどでもありませんが、期待動作を決めておきましょう。
要はリクエストを解析し、ファイルを操作するPython標準モジュールの機能(os.listdir()os.remove()open()など)を呼び出すことで実現します。

  • GET: アイテム(ファイルまたはディレクトリ)の参照
    • クエリパラメータで種類(ファイルまたはディレクトリ)を指定できる
    • ID(名前)を指定しない場合は一覧表示する
  • POST: アイテムの新規作成
    • クエリパラメータで種類を指定できる
    • ファイルの場合はポストされた内容(文字列)を書き込む
  • PUT: アイテムの変更
    • 指定されたIDがファイルの場合は内容を上書き
    • ディレクトリの場合はなにもしない
  • DELETE: アイテムの削除
    • 指定されたIDのアイテムを削除する

Bottleの良さを活かす意味で、できるだけPython標準のモジュールのみで実装したいと思います。
また、これは個人的な事情ですが、Linux (Python 2.7)でもWindows (Python 3.6)でも動くようにしたいと思います。ここではGitを介して転送しています。

検証した環境は以下の通りです。

  • Windows:
    • Windows 10 (x64)
    • Python 3.6.1 (Anaconda 4.4.0)
  • Linux:
    • Ubuntu (Server) 16.04 (Xenial)
    • Python 2.7.5

サーバの変更

Bottleのオプションとして、HTTPリクエストを処理するサーバ部分を変更することができます。
# もちろん別途インストールする必要があります。

デフォルトで読み込まれるもの(wsgiref: WSGI参照実装)は開発用途向けなのだそうで、確かにもっさりしているかも。まあPythonに速度を求められないよなあと思いながら、実用向けと言われているものを試してみます。

公式サイトはWSGIに対応した実装なら何でもいけるぜ!と言っていますが、ソースを見ると確かに結構な数が列挙されています(下記)。シンプルさと性能で、ここではWaitressを使うことにしました。

公式の説明によると、WaitressはPythonスクリプトとして動くのではなく、その下のCPython(C言語で実装されたPythonのインタプリタ)を直接たたいているので高速に動作するのだそうです。

ともあれ、簡単にpipでインストールしておきましょう。下記はUbuntuのaptを使った例です。

APIの実装

やっと本題です。
ファイル構成は次のようにしました。前記事ではフラットにおいていましたが、のちのちモジュールを増やすことを想定してサブディレクトリに配置しました。Python3では必須ではありませんが、Python2向けに__init__.pyファイルも置いておきます。

以下にそれぞれの詳細を記載しておきます。

サーバの起動設定: app.py

app.pyでは各モジュールのインポートと、エントリポイントを書いておきます。
ここでは、全てのセグメントからアクセスを受け付け、スクリプト実行時の引数(sys.argv)でリッスンポートとサーバを指定できるようにしています。

ルーティング: routes.py

routes.pyにルーティングを定義しています。

基本的には、クライアントからのリクエストに応じて、別ファイルに記載したutilsモジュールを呼んでいます。ロジックに必要なモジュールへの依存性は極力utilsモジュールに押し込め、ここでのインポートを最低限にします。
ロジックを実装するutilsモジュール内では一部の例外をハンドルしてInternal Server Error (500)を返し、utilsモジュールでハンドルしなかった例外が上がってくる場合はBad request (400)で応答しています。

ロジック: utils.py

utils.pyではルーティングから呼ばれる機能を実装しています。

GETリクエストには、ハードコードされたディレクトリ(base_dir)にあるアイテム(ディレクトリやファイル)を取得してリストオブジェクトを作成し、JSON文字列として返します。
POSTやPUTリクエストに対しては、ID(item_id)を使ってアイテムを作成・変更します。

スクリプトは見たままですが、以下に詳細を解説しておきます。APIという本旨からは脱線するので、読み飛ばしても大丈夫です。

LinuxとWindowsの判別

LinuxとWindowsではパスの体系が違うので、platformモジュールを使って判別します。
platform.uname()が返すリスト(厳密にはnamed tuple)の最初の要素がOSの違いを格納しているので、それを使って操作対象にするディレクトリ(base_dir)を切り替えています。

大抵のLinuxでは環境変数$USERにユーザ名が格納されているので、これをos.environ()で参照してホームディレクトリのパスを取得しています。

また、条件式は次のような書き方でも同じことです。

IDの作成

ここではID(item_id)をUUID(uuidモジュール)を使って作成しています。
アイテムの実体はファイルシステム上のディレクトリまたはファイルなので、重複しない文字列が必要です。

ここではuuid.uuid4()メソッドが返す値を文字列(string型)にフォーマットしていますが、例えば時刻を使っても(まず無いと思いますが作成タイミングが重複しなければ)一意な文字列を作れます。

Python2対応

私の場合、コーディングとデバッグはPython3環境(Windows)で、実行はPython2環境(Linux)で実施しています。
そこで、Python3でデバッグしたものがPython2でも通るようにする必要があります。

3to2を使うなどやり方はいろいろあるのですが、ここではそう複雑なことをしていないので、書き方に注意するくらいで基本的には同じコードを使うことにします。

Python2からPython3では、print文が関数へ変更されていますが、Python3の書き方をしておけばPython2(ここでは2.7)でも通ります。また、Python2ではサブディレクトリに配置したモジュールを認識させるため__init__.pyを作成しておく(空でok)必要があります。__init__.pyは置いておくだけならPython3でも動作に影響しないので、こちらはPython2に合わせています。

アプリケーションの実行

WindowsでもLinuxでも同じコマンドで実行できます。下記はLinuxでの実行例です。
なお、操作対象となるディレクトリを忘れずに作成しておきます。Linuxの場合はパーミッションに注意が必要ですが、ディレクトリの所有者と同じユーザで実行すればまず大丈夫でしょう。

実際にリクエストを投げてみましょう。
POST(アイテム作成)メソッドのパラーメータitemtypeでアイテムの種類を指定すると、同期的に指定された種類のものを作成したあと、新規に付与したIDを応答します。

GETリクエストでディレクトリにあるファイルとディレクトリの情報を取得できます。
長いので省略。

POSTリクエストでは、対象がファイルのIDを指定した場合、リクエストのボディにセットされたJSONデータで上書きします。
また、試しに存在しないIDを指定した場合はBad request (400)を応答していることがわかります。

その他の話題は続きで。