2014年7月14日月曜日

bottleのテンプレートで共通ヘッダ、フッタ

前回はbottleのテンプレート機能をご紹介しました。今回はその延長です。

各ページで共通のヘッダ、フッタを使いたいニーズってありますよね。過去、自分はphpでヘッダ、フッタ出力用の関数を作って、各HTMLからそれを呼ぶ、というように実装していました。

それが正しいのかは分かりませんが、一つの例として、今回も基本は同じやり方で、実装してみたいと思います。ぶっちゃけ、php部分を、bottleテンプレートのコード埋め込み機能に変えるだけですね。


準備


ファイルフォルダ構成は以下の通りです。

kitabat@clientX% pwd
/home/kitabat/dev001/work
kitabat@clientX% tree .
.
├── headfoot.py
├── hello.py
├── static
│   └── css
│       └── default.css
└── views
    └── top.tpl

3 directories, 4 files


各ファイルの内容は以下の通りです。

hello.py


メイン処理です。今までと変更点はありません。(余計なコードは除きました)

from bottle import route, run, static_file, view

@route('/<hoge>')
@view('top')
def hello(hoge="root"):
    return dict(message = "hello, " + hoge)

@route('/file/<filename:path>')
def static(filename):
    return static_file(filename, root="/home/kitabat/dev001/work/static")

run(host='0.0.0.0', port=8080, debug=True, reloader=True)

headfoot.py


ヘッダ、フッタ出力用の関数を定義したファイルです。
header()関数で<head>タグの内容を共通化します。ページタイトルは引数で受け取ります。また、この中でcssファイルを指定しています。
footer()関数は、引数を与えると、それを表示するようにしています。

それとポイントとしては、ヒアドキュメントを使っています。pythonでは、' か " を3つ重ねる事で、ヒアドキュメントになります。

def header(title='title'):
        return '''
<head><meta http-equiv="Content-Style-Type" content="text/css">
<link rel="stylesheet" href="/file/css/default.css" type="text/css" />
<title>%s</title></head>
''' % title

def footer(msg=''):
        return '''
<hr><div align="right"><font class="s2">%s</font></div>
''' % msg


static/default.css


共通ヘッダ、フッタを使うならスタイルシートと組み合わせたいですよね。という事で、用意しておきました。背景色を変えるだけですが…。

BODY {
  background-color: green
}

top.tpl


テンプレート定義です。2〜4行目に埋め込んだコードのとおり、このファイルでは、上で定義したheadfoot.py内の関数を呼び出し、結果を格納した変数を、HTMLの表示したい部分に書いています。
注意点として、変数指定の際に {{!f}} というように、頭に!を付けてください。これをしないと、タグがそのまま表示されてしまいます。


<html>
% from headfoot import header, footer
% h = header()
% f = footer("bye,bye!")
{{!h}}
<body>
{{message}}
{{!f}}
</body>
</html>


最後に、アクセス結果を。こんな感じです。




はい、如何でしたか?
これでテンプレートファイルが複数存在する場合でも、中でheader()、footer()関数を呼ぶようにしておけば良いですね。


2014年7月8日火曜日

bottleのテンプレート機能を使う

人間、見た目が9割と言いますが…。

今回は見た目、てことでbottleのテンプレート機能を使ってみたいと思います。
ざっくり言うと、変数やコードを埋め込んだ、画面のひな形です。

早速使ってみる


まずは使ってみましょう。テンプレートファイルはbottleを実行したディレクトリ以下のviewsに置き、拡張子は*.tplとします。(static等は気にしないで下さい。前回利用したものです。)

(dev001)kitabat@clientX% pwd
/home/kitabat/dev001
(dev001)kitabat@clientX% tree work
work
├── hello.py
├── static
│   ├── css
│   │   └── default.css
│   └── img
│       └── city.jpg
└── views
    └── top.tpl

4 directories, 4 files

今回はtop.tplとしました。内容は以下の通りです。

<html>
<head><title>hoge</title></head>
<body>
message:{{message}}
</body>
</html>

4行目の{{message}}部分が変数です。ここに色々な値を与える事が出来ます。
ではいつものhello.pyに、こんなコードを書いてみましょう。

hello.py
from bottle import route, run, static_file, view
import datetime

@route('/')
@route('/<hoge>')
@view('top')
def hello(hoge="root"):
    m = "today is %s" % str( datetime.date.today().isoformat() )
    return dict(message = m)

@route('/file/<filename:path>')
def static(filename):
    return static_file(filename, root="/home/kitabat/dev001/work/static")

run(host='0.0.0.0', port=8080, debug=True, reloader=True)

前回からの変更点のみ、記載します。
まず1行目、bottleのview機能を利用する定義を追記しています。
次のimport datetimeは日時関連の処理をする為の標準ライブラリです。
ポイントは6行目。これもデコレータですね。先ほど作成したテンプレートファイルの、拡張子を除いた名前を与えます。これで、以下のhello()関数ではtop.tplテンプレートを使う、という意味になります。
8行目は今日の日付を取得しているだけです。
9行目、ここもポイントです。こんな感じで辞書型の変数をreturnする事で、テンプレートに値が渡ります。

先ほど、テンプレートファイルに書いた変数を思い出してください。{{message}}でしたね。{{ }}にはreturnで返した辞書型変数のキーを書きます。そこがバリューに置換えられます。
このようにして、今回は1つでしたが、複数のキーとバリューをテンプレートに渡す事が出来るという訳です。

アクセス結果は以下の通り。


テンプレートにコードを埋め込む


ぶっちゃけ、今回くらいの処理であれば、わざわざ値を渡さなくても、テンプレート内にpythonコードを埋め込む事で、実現出来てしまいます。
では、コードを埋め込む方法をご紹介します。

<%
        import datetime
        m = "today is %s" % str( datetime.date.today().isoformat() )
%>

<html>
<head><title>hoge</title></head>
<body>
message : {{m}}
</body>
</html>


はい、この通り、<% と %> で囲った部分にコードを書けばOKです。
あるいはコードが1行だけなのであれば、行の頭に % を付けても良いです。
テンプレート内で定義した変数であっても、HTML内で参照する場合は {{ }} で囲みます。
ではサイトにアクセスしてみて下さい。テンプレートを変更しただけですが、先程と同じ結果が得られると思います。


もう少し複雑なコード


テンプレートにコードを埋め込む際の注意点ですが、インデントは無視されます(可読性のためにインデントするのは自由です)。
ではif文など、区切りはどう表現するかというと、end というキーワードを明示的に書きます。
例えばこんな、、、。

<%
        import datetime

        hour = datetime.datetime.now().hour
        if hour > 18 or hour < 5:
                m = "good evening."
        elif hour >= 5 and hour < 10:
                m = "good morning."
        else:
                m = "hello."
        end     
%>

<html>
<head><title>hoge</title></head>
<body>
message : {{m}}
</body>
</html>

if文だけでなく、for文や関数定義等、pythonでインデントして区切りを表す部分には、終わりに必ずendを書きます。

これもhello.py側の変更は不要です(無駄な処理は残ってますが)。アクセスしてみて下さい。適切な挨拶が、なされましたか?



はい、早く寝ましょうね。では今回はこの辺で。

2014年7月4日金曜日

bottle環境で静的ファイルを扱う

bottleでは、静的ファイルへのルーティングをする為に、コードを書いてあげないといけません。静的ファイルというのは、例えばスタイルシートや画像ファイルなんかですね。

前提ディレクトリ構造


今回は以下ディレクトリ構造を想定します。

(dev001)kitabat@clientX% tree work
work
├── hello.py
└── static
    ├── css
    │   └── default.css
    └── img
        └── city.jpg

3 directories, 3 files

hello.pyを実行するディレクトリ(/home/kitabat/dev001/work)が、'/' でアクセスされた際のディレクトリになります。
この状態で、http://192.168.56.103:8080/static/img/city.jpg にアクセスしても、、、エラーになるのです。もちろん、前述の通り、静的ファイルへのルーティングが無いからですね。



静的ファイルへのルーティングコード追加


こんな感じです。
from bottle import route, run, static_file

@route('/')
@route('/<hoge>')
def hello(hoge="root"):
    return "Hello World!<br> path is " + hoge

@route('/file/<filename:path>')
def static(filename):
    return static_file(filename, root="/home/kitabat/dev001/work/static")

run(host='0.0.0.0', port=8080, debug=True, reloader=True)

まず1行目。static_fileというbottleの機能を使うので読み込みます。3〜6行目は今までに書いた部分。8〜10行目が今回追記したルーティング用コードです。
8行目の file/<filename:path> について。< >は前回説明した通り、アクセスされたURLを以後の関数内で使うための書き方です。:pathはfilenameに、パスを含んで良い事を意味します。単純に<filename>だけだった場合、/file/img/city.jpg へのアクセスは上手く行かず404になります。filenameに格納される部分が "img/city.jpg" と、パスを含むからですね。
10行目でstatic_file()関数を使います。第1引数が静的ファイル名、第2引数が、root=サーバのローカルパスです。

では、http://192.168.56.103:8080/file/img/city.jpg にアクセスしてみると、、、



やった〜!

注意すべき点として、アクセスするURLと、サーバ側のローカルパスは一致させる必要が無いので、混乱しないように、くらいですかね。今回はあえて、アクセスURL側を "file"、実際のディレクトリ側を "staic" としておりました。


ではまた。

2014年7月1日火曜日

軽量pythonフレームワーク「bottle」の導入

開発するにあたり、やはりフレームワークは有った方が良いですよね。
openstackなんかはDjangoを使っているようですが、なにぶん初心者なので、、、挫折しないシンプルなものは無いかと探していたところ、ありました!bottle!

なんと驚愕の1ファイル、146kbyte!
でもこれで、立派なフレームワークです。

では恒例のhello worldまでを見ていきましょう。

bottleインストール

公式webはこちら
wgetでbottle.pyを持ってきてカレントディレクトリに置くのでも良いのですが、せっかくpipを入れた事ですし、pipで入れましょう。

(dev001)kitabat@clientX% pip install bottle
(dev001)kitabat@clientX% pip list
bottle (0.12.7)
pip (1.5.6)
setuptools (3.6)
はい、終わりです。これで from bottle import すればbottleが使えます。
pip listを打つとbottleが入っている事を確認出来ます。今回は 0.12.7 というバージョンが入りました。


hello, world!!


まず、virtualenvで作ったディレクトリのルートにはbin、libなどがあるので、workというディレクトリを作ってそこで作業を進めます。

(dev001)kitabat@clientX% pwd
/home/kitabat/dev001
(dev001)kitabat@clientX% mkdir work
(dev001)kitabat@clientX% cd work
(dev001)kitabat@clientX% vim hello.py 

では公式webにもある例文を見てみます。以下をhello.pyとして作成しましょう。

from bottle import route, run

@route('/hello')
def hello():
    return "Hello, World!!"

run(host='0.0.0.0', port=8080, debug=True)


1行目はbottleの利用宣言です。bottleという機能群から、routeとrunという機能を使いますよ、の意味です。
説明順が前後しますが、7行目がmain処理です。bottleは自身がwebサーバとして動作し、この例だと http://<サーバIP>:8080/ でwebアクセスを待ち受けます(0.0.0.0とする事でサーバの全NICでListenします)。
debug=Trueで、ターミナルにデバッグメッセージが出ます。
ここまではbottleを利用する場合は毎回ほぼ同じです。

アプリケーションの動作を決定するのは3〜5行目です。
/helloにアクセスがあれば(正確には、http://<サーバIP>:8080/hello にアクセスがあれば)、hello()以下が呼び出されます。
def xxx()はpythonの関数定義、@xxx()はデコレータと呼ばれる仕組みです。この辺りは後ほど書ければ書きます。

では実行してみましょう!
っとその前にこのサーバのIPを確認しておきます。192.168.56.103です(virtual boxなのがバレバレですね)。
(dev001)kitabat@clientX% python hello.py 
(dev001)kitabat@clientX% ip addr show | grep inet
    inet 127.0.0.1/8 scope host lo
    inet 192.168.56.103/24 brd 192.168.56.255 scope global eth0

おもむろにpython hello.pyとタイプします。
(dev001)kitabat@clientX% python hello.py 
Bottle v0.12.7 server starting up (using WSGIRefServer())...
Listening on http://0.0.0.0:8080/
Hit Ctrl-C to quit.


はい、ターミナル、何も返ってこなくなりましたね。これでOKです。
先ほど確認したip:8080/helloにブラウザでアクセスしてみましょう!

おお〜!皆さん、出ましたか?
ちなみにターミナルを見ると、アクセスログが表示されております。
(dev001)kitabat@clientX% python hello.py 
Bottle v0.12.7 server starting up (using WSGIRefServer())...
Listening on http://0.0.0.0:8080/
Hit Ctrl-C to quit.

192.168.56.1 - - [26/Jun/2014 17:56:56] "GET /hello HTTP/1.1" 200 12


もうちょっと使ってみる


例えば '/' (http://192.168.56.103:8080/)にアクセスすると、当然、定義が無いので怒られます。


以下のコードを追記すると、問題なくアクセス出来るようになります。

from bottle import route, run

@route('/')
@route('/hello')
def hello():
    return "Hello World!"

run(host='0.0.0.0', port=8080, debug=True, reloader=True)

3行目に @route('/') を加えています。こんな感じでデコレータは関数に対して幾つも記載可能です。それと、直接は関係無いのですがrun()に渡す引数として reloader=True を加えています。これはbottleが、コードの編集を認識し再読み込みしてくれるオプションです。付けない場合は、コードの反映にbottleの停止起動が必要になります。



はい、無事アクセス出来ました。

ちなみに以下のようにする事でURLのパスを取得出来ます。

from bottle import route, run

@route('/')
@route('/<hoge>')
def hello(hoge="root"):
    return "Hello World!<br> path is " + hoge

run(host='0.0.0.0', port=8080, debug=True, reloader=True)

4行目で、アクセスされたurlをhogeという変数で受け取り、それをhello()関数の引数として使っています。5行目で、hogeのデフォルト値を"root"にしていますが、これは '/' へのアクセス時に使われます。
6行目で、hoge変数も表示するよう設定しています。

/helloへアクセスした場合



行けますね。じゃあ次。

ルート('/')にアクセスした場合


問題無しです!デフォルト値の"root" が適用されている事が確認出来ます。


補足:注意点

reloader=Trueが悪いのか分からないのですが、コードを編集し、bottleがリロードされてから暫く、アクセス出来るはずのURLに、何故かアクセス出来なくなる事があります。その時は根気よくブラウザのリロードをすれば良いのですが、酷い場合は1分程、アクセス出来ません。



ひとたびアクセス出来れば、以後はスムーズなのですが。。。
この問題の困るところは、修正したコードに何か誤りが有ったから表示出来なくなったのだ、と勘違いする事です。
この思考に落ち入ると、せっかく書いたコードを過去に戻していき、しかし以前上手く行った筈のコードですらアクセス出来なくて、更に以前に戻し…、という非常に悲惨な循環にハマります。
pythonの構文でエラーが有るならば必ずターミナルに出力されますので、それが出ていないのにアクセス出来なかったら、この問題を疑って、ブラウザリロードを何度か、試みて下さい。

------

はい、という事で、如何でしたでしょうか。結構簡単に色々面白い事が出来そうですよね!
次回以降、アプリケーションを作る過程で、これはどうやるんだろう?に直面した事を、順にご紹介していきます。

2014年6月25日水曜日

pythonの環境整備(1)

記念すべき初投稿です。

突然ですが、日々、何を食べようか迷う事は無いですか?(自炊)
過去作ったレシピが検索出来て、作った履歴も残せて、
週の献立計画や、リコメンドなんかもしてくれるシステムがあったら良いな!と思い、
ちょうど学習したかったpythonで、作る事にしました。

暫くはpythonに関するネタが続くと思います。
前置きが長くなりました。初回は環境の整備についてです。

インストール


前提

環境は以後、特に記載がなければ、CentOS 6.5 x86_64(メディアからのインストール、yum update未実施) で、@Core に加えてsshやvim、manあたりのパッケージを入れた最低限のものです。

pythonインストール


既に入ってた。
pythonはVer.3系もありますが、まずは、って事で、このまま行きます。
# python --version
Python 2.6.6



パッケージ管理ソフトのインストール


世の方々が作ってくれた、ありがたいパッケージ群を簡単に使うべく、管理ソフト easy_install と pip を入れます。まずeasy_installを入れて、それを使ってpipを導入、という流れ。
この2つは、RHELで言うところの up2date と yum のような関係かなと勝手に思っています。
ちなみにeasy_installは開発が止まっており、distributeというものを入れます。


# yum -y install unzip  これすら入っていなかったので…
# curl -k https://pypi.python.org/packages/source/d/distribute/distribute-0.7.3.zip -o distribute-0.7.3.zip
# unzip distribute-0.7.3.zip
# cd distribute-0.7.3
# python setup.py install


これでeasy_install が使えるようになったので次にpip。

# easy_install pip

これでパッケージ管理の準備は完了。以下helpの通り、yum等を使っている方なら違和感なく利用出来るのではないかと思います。pip install xxxx 等ですね。
動きとしては https://pypi.python.org/ を探しにいってくれます。

# pip help

Usage:   
  pip  [options]

Commands:
  install                     Install packages.
  uninstall                   Uninstall packages.
  freeze                      Output installed packages in requirements format.
  list                        List installed packages.
  show                        Show information about installed packages.
  search                      Search PyPI for packages.
  wheel                       Build wheels from your requirements.
  zip                         DEPRECATED. Zip individual packages.
  unzip                       DEPRECATED. Unzip individual packages.
  bundle                      DEPRECATED. Create pybundles.
  help                        Show help for commands.


virtualenv


これは1つのOS内にpythonの実行環境を幾つも用意する為のもの。複数人が開発するケース、異なるバージョンのpythonやパッケージで試験をしたい場合、等に。とりあえず入れておきます。
いやしかしpip便利ですね。

# pip install virtualenv
Downloading/unpacking virtualenv
  Downloading virtualenv-1.11.6-py2.py3-none-any.whl (1.6MB): 1.6MB downloaded
Installing collected packages: virtualenv
Successfully installed virtualenv
Cleaning up...
# 

新たに仮想python実行環境を作るコマンドは以下の通り。
仕掛けは、pythonのバイナリやライブラリを指定フォルダにごっそりコピーしているだけみたい。

% virtualenv /home/kitabat/dev001

binフォルダにあるactivateシェルを source コマンドで読みこむ事で、環境に入ります(仮想環境をactivate)。
試しに仮想環境内でpip listを打ってみます。シンプルな感じですね。
抜ける場合はdeactivate。
作った環境の削除は、、、フォルダをrmすれば良いらしいです(´・ω・`)

kitabat@clientX% source /home/kitabat/dev001/bin/activate
(dev001)kitabat@clientX% 
(dev001)kitabat@clientX% pip list
pip (1.5.6)
setuptools (3.6)
(dev001)kitabat@clientX% deactivate
kitabat@clientX% 

では次回以降はこのdev001環境で、開発を進めていきます!