ボトラーになった

python


この記事は、 PMOB Advent Calendar 2016 の 1日目 の記事です。

現在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()

mainappcool_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'のキーに加えたいフィルタの一覧を持った辞書を、 viewtemplate_settings引数に渡すのです。

最後に

ソースすごい短いから読んでいいと思いますよ。

Config関連はまだいいやり方見つかってないので今回書きません。 おしまい。