BottleはPythonの軽量Webフレームワーク。
サーバサイドのPython製フレームワークではDjangoやFlaskが有名ですが、中でも1ファイルで実装されているというシンプルな構成の(マイクロ)フレームワークなのだそうです。
手軽にWeb APIを作りたいと思い立ち、今回はBottleを選択。データ形式はjsonで。
基本jsonデータを送受信できればいいので、テンプレートエンジンなどWebページ(html)関連の機能は利用しません。
インストールは公式からファイルをダウンロードするか、またはいつものpip
で可能。
ダウンロードする場合:
1 2 | $ wget http://bottlepy.org/bottle.py # Windowsならブラウザで開いても可 |
pipの場合:
1 | $ sudo pip install bottle |
ルーティングの基本
本題の前にとりあえず書いてみます。
マイクロフレームワークだけあってMVCなどモジュール構成などのお約束は緩め。逆に言うと、プログラマが設計しなければならないことが多いと言えます。
1 2 3 4 5 | (project directory) ├─app.py ├─routes.py ├─utils.py └─(bottle.py) # pipでインストールしない場合 |
制約というわけではありませんが、ここでは機能別にモジュールを分けて実装していきます。
上のように、サーバを立ち上げるapp.py
、ルーティングを記述するroutes.py
、ロジック部分のutils.py
の3ファイル構成としました。
とりあえずは解説のため書いたので、この段階ではおそらくちゃんと動きません(後半で一応動くものを書きます)。
app.py
1 2 3 4 5 | import bottle import routes if __name__ == "__main__": bottle.run(host='localhost', port='5555') |
サーバ、ポートの設定。ルーティングを記述するroutes
モジュールをインポートしておきます。
上の例で、サーバの設定(host='localhost'
)は、ローカルのみアクセスを許可する指定です。
設定にはサブネットアドレスを書くなどいくつかパターンがあり、他のマシンからも無制限に繋ぎたければhost='0.0.0.0'
とします。
ソースを見た限り、ポート番号のデフォルトは8080のようです。
routes.py
1 2 3 4 5 6 7 8 9 10 11 | from bottle import route, response, abort, request from bottle import get from utils import * @route('/', method='GET') def home(): abort(404) @get('/api/') def api_home(): return get_items() |
メソッドの書き方は2通りあり、@route
としてmethod
を指定するか、または@get
のようにメソッドを直接書くか。前者のほうが後々メソッドを追加できる点で汎用的かも。
ここでやっているのは単純にルーティングに応じて処理内容(utils.py
)の関数を呼び出しているだけです。404などhttpエラーステータスを返したければabort
を使うと楽。
utils.py
やりたい処理の本体。後ほどちゃんと作ります。
1 2 3 4 | import os def get_items(): return os.listdir(".") |
app.py
を呼んでやればローカル&指定したポートでサーバが立ち上がります。
メッセージにもある通りブラウザでURLを開けば(もちろんwget
とかでも可)動作が確認できます。
ファイアウォールが有効な場合などポートを開放する必要があるかも。
1 2 3 4 5 6 | $ cd (project directory) $ python app.py Bottle v0.12.13 server starting up (using WSGIRefServer())... Listening on http://localhost:5555/ Hit Ctrl-C to quit. |
APIの実装
さて、ここからはちゃんと動くものを作ります。
といっても、機能的にはあまり意味がないアプリケーションです(長くなってしまったので、意味があるものは別記事で)。
実装したいAPIの設計に従って、routes.py
にURIやメソッドを、utils.py
に処理の実体を書いていきます。
以下の例では、ファイル構成は前半と同じですが、内容を書き換えています。
サーバの起動設定(app.py)
前掲の例と特に大きな変更なし。アクセス元クライアントをセグメントで制限しない設定(host='0.0.0.0'
)、ポートをデフォルト(8080)にしてみました。
1 2 3 4 5 | import bottle import routes if __name__ == "__main__": bottle.run(host='0.0.0.0') |
ルーティング(routes.py)
HTTPメソッド、URIとクエリで処理を分岐させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | from bottle import route, response, abort, request from utils import * @route('/api/item', method=['GET', 'POST']) def handle_item(): try: response.headers['Content-Type'] = 'application/json' if request.method == 'GET': return get_items() else: return create_item(key=request.query.key, payload=request.body) except: # internal server error abort(500) @route('/api/item/<item_id>', method=['PUT', 'DELETE']) def modify_item(item_id): try: response.headers['Content-Type'] = 'application/json' if request.method == 'PUT': return create_item(item_id, request.body, False) else: # not implemented abort(501) except: abort(500) |
複数のメソッドを定義するため、@route
でmethod
に配列を渡し、対応する処理はrequest.method
で条件分岐させます。
クエリパラメータ(この例ではPOST時の/api/item?(name)=xxx
)はrequest.query.(name)
で取得できます。
もちろんダイナミックにルーティングさせる(この例ではPUTのURL<item_id>
の部分)こともでき、ここでは使っていませんが正規表現も使えるらしい。
レスポンスのContent-Typeに'application/json'
をセットし、JSON文字列(string型)を返しています。
ロジック
ルーティングさえ押さえておけば、あとは普段のPythonの知識で自由にロジックを組むことができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 | import os import json def get_items(): return json.dumps(os.listdir("."), indent=4) def create_item(key, payload, createnew=True): message = "updated: {}".format(key) if createnew: message = "created: {}".format(key) return json.dumps({"message": message, "payload": json.load(payload)}, indent=4) |
この例ではutils.py
がjson文字列を返します。
いろいろ試してみたのですが、サーバが受信したデータ、言い換えればクライアントが送信したボディ(request.body
で取り出したもの)はByteストリームで渡ってくるようです。そこで、リクエストのボディ(クライアントからjsonデータを渡したもの)から、json.load(f)
でオブジェクトを取り出しています。
実行
前半と同じコマンドpython app.py
で、とりあえず動かすことはできるはず。
ためしにPOST, PUTしてみるとこんな感じ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | (POST request) 127.0.0.1 - - [xx/xx/2017 xx:xx:xx] "POST /api/item?key=1 HTTP/1.1" 200 91 (response) Content-Type: application/json Body: { "message": "created: 1", "payload":{ "data": "this is test data (post)"} } (PUT request) 127.0.0.1 - - [xx/xx/2017 xx:xx:xx] "PUT /api/item/2 HTTP/1.1" 200 97 (response) Content-Type: application/json Body: { "message": "updated: 2", "payload":{ "data": "this is test data (put)"} } |
つづきは長くなってしまったので別記事で。