Bottleでファイルを分割する方法
概要
Bottleに限った話ではありませんが、RESTful APIサーバを実装したい、となった場合当然複数のファイルに分割したくなりますよね。 ということで、Bottleでファイルを分割する方法を調べたので纏めます。
RESTful APIで以下のURLでアクセスできるサーバの想定です。
| URL |内容| |-------|-------| |http://localhost:8081/|トップページ| |http://localhost:8081/resources|resourcesのトップページ| |http://localhost:8081/resources/users|usersリソース(GET、PUT、POST、DELETE)| |http://localhost:8081/resources/users/123|特定のusersリソース(GET)|
ディレクトリ構成は以下のようになります。
Project/
|
| ---- main.py
| ---- settings.py
| ----resources/
|
| ---- resources.py
| ---- users.py
main.py
from bottle import route, run, Bottle
from bottle import post, get, put, delete, request, response, route
import json
import books
from resources import resources
root: Bottle = Bottle()
@root.route('/')
def index():
return {'message': 'Hello. This is top page.'}
if __name__ == '__main__':
# mergeでも同様のことがデキる。こちらはprefixは付けられない。
# さらに、booksの中でhookを指定していても、mergeで取り込まれている場合はhookは動作しない。
#root.merge(books.app)
# 以下のresources.py(resources/resources.py)で、さらに別のroutesを指定することも可能。
# このようにワンクッション置くことで、/reousrcesと言うようなURLのみの場合にも何かメッセージを表示することが可能。
root.mount('/resources', resources.app)
root.run (host='localhost', port=8081, debug=True, reloader=True)
このmain.pyがアプリケーション(Bottle)を起動します。
Bottleでファイルを分割する、つまりMVC
で言うところのController
を複数ファイルに分割する方法ですが、上記のコードにある通り、merge
かmount
関数を使うことで実現できます。
merge関数
これを使うと、第1引数で指定したPythonコード(Bottleアプリケーション)が単純に読み込まれます。
なので、アクセスするためのURLなどはその読み込んだBottleアプリケーション自体で指定しているものが利用されます。
非常にお手軽なのですが、アクセスするためのURLの指定を利用側(main.py)で指定できないので、main.pyで読み込む複数のBottleアプリケーションで同じURLを指定してしまうと期待した通りの動作にならない、という問題が有ります。
また、コメントにも有りますがmerge
でBottleアプリケーションを取り込んだ場合、hook
関数が動作しません。
そのため、基本的にはこのmerge
は使わずに、以下のmount
を利用することになります。
mount関数
mount
関数では、第1引数にアクセスするためのURL、第2引数の取り込むBottleアプリケーションを指定します。
merge
と違い、URLをこちら(main.py)で指定できるので、Bottleアプリケーション側でURLの重複を気にする必要がなくなります。
resources/resources.py
from bottle import Bottle, route, get, response, request, redirect
from resources import users
import settings
app = Bottle()
app.add_hook('after_request', settings.enable_cors)
# For CORS
# https://github.com/axios/axios/issues/475
# http://blog.yaorenjie.com/2015/06/03/Enable-CORS-in-bottle-py/
# https://github.com/bottlepy/bottle/issues/402
@app.route('/', method='OPTIONS')
def for_cors():
return {}
@app.get('/')
def index():
return {'message': 'This is api top page.'}
# resources/でアクセスできるRESTful API群を以下に羅列していく
app.mount('/users', users.app)
このファイルだけを見れば、URLはドメイン/
でアクセスできるようになっています。
しかし、このresources.py
を読み込んでいるmain.py
で、URLをresources/
と指定しているので、ドメイン/resources
というURLにアクセスした場合に、このresources.pyのindex関数が実行されます。
そしてさらに、このresources/配下で動作させたいRESTful APIを記述したBottleアプリケーションをココでもmountすればOKです。
ココでもmain.py同様にURLを指定できます。それが
app.mount('/users', users.app)
ですね。
なお、態々resources.py
でワンクッション挟まずに、main.py
の中で直接各Bottleアプリケーションをマウントすることも可能です。
# main.pyで直接URLも指定
root.mount('/resources/users/', users.app)
root.mount('/resources/books/', books.app)
root.mount('/resources/contents/', contents.app)
そこまでアプリケーションの規模が大きくないのであればコレでも全然OKだと思います。 ただある程度の規模になってくると管理も大変なので、個人的には最初からURLとディレクトリ構成/ファイル構成は一致させておいたが方が楽だと思います。
resources/users.py
from bottle import Bottle, get, response, request, hook
import settings
app: Bottle = Bottle()
app.add_hook('after_request', settings.enable_cors)
# add_hookじゃなくても以下のように指定することも出来る。
#@app.hook('after_request')
#def enable_cors():
# settings.enable_cors()
# For CORS
# https://github.com/axios/axios/issues/475
# http://blog.yaorenjie.com/2015/06/03/Enable-CORS-in-bottle-py/
# https://github.com/bottlepy/bottle/issues/402
@app.route('/', method='OPTIONS')
def for_cors():
return {}
@app.get('/')
def list():
return {'message': 'users#get'}
@app.get('/<id:int>')
def index(id):
return {'message': 'users#get({})'.format(id)}
@app.post('/')
def create():
return {'message': 'users#create(post), {}'.format(request.json['name'])}
@app.put('/')
def update():
return {'message': 'users#update(put)'}
@app.delete('/')
def delete():
return {'message': 'users#delete(delete)'}
コレがRESTful APIを提供するusers.py
です。
それぞれの関数はHTTPメソッド毎に切り分けられるので、URLは当然同じものです。(/
)
そして、このusers.py
を読み込んでいるBottleアプリケーション(resources.py
)では、このusers.py
へのURLをusers/
と指定しているので、ドメイン/resources/users
(IDありはドメイン/resources/users/123
)でこのusersリソースにアクセスすることが出来ます。
とても自然なRESTful APIのURLが実現できました。
settings.py
RESTful APIサーバなので当然CORSを設定したいですね。
ということで、すでにusers.py
に記載が有りましたが、app.add_hook('after_request', settings.enable_cors)
で実行している関数が以下のものになります。
単純にCORS用の情報をレスポンスヘッダに詰め込んでいるだけです。
from bottle import response
def enable_cors():
print("settings.enable_cors")
response.headers['Access-Control-Allow-Origin'] = '*'
response.headers['Access-Control-Allow-Methods'] = 'PUT, GET, POST, DELETE, OPTIONS'
response.headers['Access-Control-Allow-Headers'] = 'Origin, Accept, Content-Type, X-Requested-With, X-CSRF-Token'
注意点
mount
で読みこめば、@app.hook('after_request')
やapp.add_hook('after_request', settings.enable_cors)
はそれぞれで必ずちゃんと動作するので、例えば特定URL配下(/resource/*)にアクセスした場合にログを出力する、といった処理はresources/resource.py
のhook
で記述すればOK。
だけど、Bottleが持つresponse
オブジェクトの中身は、最後に実行されるBottleアプリケーションの中身で毎回初期化されているので、response
を弄るCORSなどは必ずそれぞれのBottleアプリケーションで実行しなければなりません。
今回の例ならusers.py
でapp.add_hook('after_request', settings.enable_cors)
をコメントアウトすると、/resources/users
では、CORSヘッダーが付与されなくなります。
(resources.pyのhookでCORSをresponseヘッダに付与しているけど、その後のusers.pyでresponseヘッダが初期値に戻る)
また、axiosなどでJavaScriptからJSONリクエストを送信すると、先ずは必ずOPTIONSメソッドでCORS情報自体の取得が実行されます。 そのため、各Bottleアプリケーションで必ずOPTIONSに応答する処理を書いておく必要が有ります。(空のJSONを返すだけでOK) 恐らくmain.pyなどでまとめて記述することが出来るとは思うのですが、まだそこまでは確認できていません。
公開日:2018/04/25