この記事は、 PMOB Advent Calendar 2016 の 1日目 の記事です。
- 前の記事 ないよ
- 次の記事 PMOB Advent Calendar 2016 2日目
現在Webアプリケーションを作っており、 アプリケーションサーバーはPythonのなんかにするつもりなんですが、 なんかDjango使えって言われたので使ってみました。 使ってみたところこれちげえなってなってFlaskに戻ってきたんですが、 せっかくなので他のも試してみるかとBottle使ってみたらこれが案外良かった。 のでメモ的な。
Tutoriaるぞ
ということでBottleのチュートリアルです。 ドキュメントにもあるけどなんかふーんって感じだったので 実際のところ使いそうなものを実際に使いそうな感じで書いていきます。
ハロワ
import bottle
app = bottle.Bottle()
@app.route('/hello')
def hello():
return "Hello World!"
app.run(port=8080, debug=True)
ルーティング
@app.route('/hello')
@app.route('/hello/<name>')
def hello(name='Somebody'):
return "Hello %s!" % name
Flaskの系譜感じるね。 どっちが先か知らんけど。 デコレータは可読性高いっすよ。
でももしかしてこの辺ってReact.jsがなんかするんですかね? もうアプリ鯖がどこまですりゃいいのかわからんよね。 まあ私がしっくりくる感じにすると思います。
アノーテーション
# with annotation
@app.route('/add_one/<num:int>')
def add_one(num):
return "%d + 1 = %d" % (num, num+1)
@app.route('/user/<id:re:[0-9A-Za-z]+>')
def user(id):
return "your id is %s" % id
アノーテーションはいっぱいあるよ
annotation | 値 |
---|---|
:int |
整数 |
:float |
実数 |
:path |
マッチしたパス |
:re:<exp> |
<exp> にマッチした正規表現 |
アノーテーションにマッチしないURIは、 普通にルートにマッチしないということで、 ルートが存在すればそこに、存在しなければ404を返します。 普通ですね。
リクエストのハンドリング
# @app.route('/login') or
@app.get('/login')
def login():
return '''
<form action="/login" method="post">
Username: <input name="username" type="text">
Password: <input name="password" type="password">
<input value="Login" type="submit">
</form>
'''
from bottle import request
# @app.route('/login', method='POST') or
@app.post('/login')
def do_login():
username = request.forms.get('username')
password = request.forms.get('password')
if check_login(username, password):
return "<p>Your login information was correct.</p>"
else:
return "<p>Login failed.</p>"
読みやすいね。いいね。
静的ファイル
from bottle import static_file
@app.route('/static/<filename>')
def server_static(filename):
return static_file(filename,
root='/path/to/your/static/files', mimetype='image/png')
# Force download
@app.route('/download/<filename:path>')
def download(filename):
return static_file(filename, root='/path/to/static/files', download=filename)
まあCDN使うから使わないんだけどね。
エラーハンドリングとリダイレクト
@app.error(404)
def error404(error):
return 'Nothing here, sorry'
from bottle import abort
@app.route('/restricted')
def restricted():
abort(401, "Sorry, access denied.")
from bottle import redirect
@app.route('/wrong/url')
def wrong():
redirect("/right/url")
クッキー☆
from bottle import request
@app.route('/hello')
def hello_again():
if request.get_cookie("visited", secret='secret-key'):
return "Welcome back! Nice to see you again"
else:
response.set_cookie("visited", "yes", secret='secret-key')
return "Hello there! Nice to meet you"
secret
なしだと生のクッキーができます。
毎回secret
渡すのキモいのでPluginだかでどうにかしたいですね。
HTTPヘッダ一発芸
from bottle import request
@app.route('/is_ajax')
def is_ajax():
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return 'This is an AJAX request'
else:
return 'This is a normal request'
ファイルアップロード
@app.post('/upload')
def do_upload():
category = request.forms.get('category')
upload = request.files.get('upload')
name, ext = os.path.splitext(upload.filename)
if ext not in ('.png','.jpg','.jpeg'):
return 'File extension not allowed.'
save_path = get_save_path_for_category(category)
upload.save(save_path) # appends upload.filename automatically
return 'OK'
ちょっと物足りなさありますね。werkzeugのsecure_file的なもの作った方がいいかも。
テンプレーティング
from bottle import jinja2_view as view
# project_dir/
# |- this_app.py
# |- views/
# |- test.html
@app.route('/test')
@view('test.html')
def testing():
return dict(title='test')
テンプレートにdict
を渡すイメージ。dict
の中身はもちろんテンプレートに渡す値です。
テンプレートの探索場所は、実行ソースから見て./
と./views
です。
テンプレートのパス追加は後で書きます。
テンプレートエンジンはいくつかあるけど私はJinja2使うと思います。 そういえばセキュリティマンがテンプレートエンジンの脆弱性がうんぬんって 言ってたんですけど、ちょっと気になりますね。 ちょっときになる程度ですけどね。
Flaskもそうですけど、 テンプレートはキャッシュするのでデバッグの時は設定をいじって、 変更を反映させるようにした方が楽です。 デバッグ用の設定も後で書くよ。
Plugins
BottleのPluginは、Routeコールバックのラッパです。
ドキュメントの例
では、Routeコールバック (@app.route
でデコレートされた関数のことです)
にdb
という引数が設定された場合、
引数db
の中にsqlite3でアクセスしたデータベースのコネクションを渡しています。
ラッパでコールバックを拡張するのは珍しいですね。面白いと思います。 でもクッキーのデコードに常にsecretを渡すのはPluginではできなそうです。 フレームワークの拡張自体は普通にオーバーライドになりそう。
コンテクストマネージャ
import bottle
with bottle.Bottle() as app:
@app.route('/')
def hello():
return "Hello, World!"
app.run()
マジで意味がわからんのだけどコンテクストマネージャに対応している。 いくらミニマルだからってそんな使い方はせんだろ。
Tipsっぽくなるぞ
ここからTipsっぽくなるぞ。てぃップス。ポてぃてぅてぃップス。うすしお。
モジュール化
モジュール化はするよね。 今の所モジュール化するときにやってること。
こんなディレクトリ構造でね
# project/
# |- main.py
# |- myapp/
# |- cool_feature.py
# |- templates/
# |- test.html
こんなモジュールがあったとするじゃないですか
# myapp/cool_feature.py
import bottle
app = bottle.Bottle()
@app.route('/hello')
def hello():
return "Hello, World!"
モジュールを統合したいならこう
# main.py
import bottle
from myapp import cool_feature
mainapp = bottle.Bottle()
mainapp.merge(cool_feature.app)
mainapp.run()
mainapp
にcool_feature.app
のルールがそのまま追加されるぞ。
つまり/hello
にアクセスできる。
モジュールをマウントしたいならこう
# main.py
import bottle
from myapp import cool_feature
mainapp = bottle.Bottle()
mainapp.mount('/prefix/', cool_feature.app)
mainapp.run()
メインアプリの下に置く感じ。
つまり/prefix/hello
にアクセスできる。
テンプレート使う
モジュールの中にテンプレートを閉じたい場合は、
TEMPLATE_PATH
にソースのディレクトリを追加してやる必要があるぞ。
import os
from bottle import TEMPLATE_PATH
from bottle import jinja2_view as view
pwd = os.path.dirname( os.path.abspath(__file__) )
TEMPLATE_PATH.append( os.path.join(pwd, 'templates') )
@app.route('/test')
@view('test.html')
def testing():
return dict(title='test')
Bottle自体まだ対して触ってないのでもっと色々あるかも。
開発中に役立つ設定
app.run(port=8080, reloader=True, debug=True)
ポートを適当な場所に指定します。 ファイルが変更されると再起動します。 あとロガーがデバッグ情報まで吐くようになります。 詳しいことは以下のドキュメント。
http://bottlepy.org/docs/dev/api.html#bottle.run
Jinja2にカスタムフィルタ渡す
Jinja2を使っていてカスタムフィルタを渡したい時は以下のようにするといいよ。
こんなテンプレートを用意しましょう
This is { { title | titled } }.
このtitle
を先頭大文字にする自作フィルタtitled
にかけたいとする。
こんなコードを書くといいよ。
from bottle import jinja2_view as view
app = bottle.Bottle()
template_settings = {
'filters': {
'titled': lambda s: s.title(),
},
}
@app.route('/test')
@view('test.html', template_settings=template_settings)
def testing():
return dict(title='test')
'filters'
のキーに加えたいフィルタの一覧を持った辞書を、
view
のtemplate_settings
引数に渡すのです。
最後に
ソースすごい短いから読んでいいと思いますよ。
Config
関連はまだいいやり方見つかってないので今回書きません。
おしまい。