FlaskでLESSをあつかう方法を検討した結果watchdogかgrunt.jsでいいことになった

Flask+Twitter Bootstrapで作ることが非常に増えている。LESSを弄りたいときに

<link rel="stylesheet/less" href="less/style.less">
<script src="js/libs/less-1.2.1.min.js"></script>

とかやればいいんだけど、IEだとレンダリングがやたらと遅くなるので本番環境ではcssにコンパイルすることになるため、この部分を書きなおすのは面倒くさい。

だったら開発環境でも最初から更新時にCSSコンパイルするとか裏でよろしくやってくれる方法を導入したいなぁということで調べてみた。

これらは拡張子がcssのurlにアクセスした時にlessファイルを探してcssファイルが存在しなかったり、lessが変更されている(更新日時とかハッシュを見ている)ときにコンパイルする用になっている。before_requestのところで処理している。

難点は、cssとlessが同じディレクトリにあることを決め打ちしているところかなぁ。僕の場合bootstrapは別に管理しているのでコンパイル先を自分で決められないのは困る。

もう少し統一的に管理できそなうなものにFlask-Funnelというものがあったが、ちょっと面倒くさそう。これだったらGruntとかYeomanみたいなnode.js製の管理ツールでもいいかなぁと。

結論

Initializrのbootstrapだったら変更検知してstyle.lessをコンパイルすればいいのでwatchdog使えばいいかなぁ。grunt.jsでもいいや。

Githubのtwitter bootstrapだったらMakefileにwatchがついているのでそれを使ってもいいなぁと。ちなみにカスタマイズはここらへんを参考にしている。

Flaskでセッションが入れ替わるというバグに悩まされた

13.02.27

バグの発生箇所を掴んだ。OpenIDサーバーだった。開発環境ではエラーが出なくて本番環境でのみ出現するのでサーバーを疑っていたのだけど違った。

本番環境でデバッグログ出したら

Error attempting to use stored discovery information: Claimed ID does not match (different subjects!)
Expected <wrong-openid> got <right-openid>
Attempting discovery to verify endopoint
Performing discovery on <wrong-openid>

というメッセージが出ていた。

OpenIDサーバーから戻ってきた所でおかしくなっているようだ。referrer出力するようにしたらOpenIDサーバーが認証して戻すところで入れ替わっていた。

なんでそうなるのかワケがわからないが、OpenIDサーバーを再起動したらとりあえずは治った。ploneでも似たような症状を見つけたが、Flaskのせいではないということが分かったので一安心。

仕様をきちんと押さえておくか、本を読んでおくかしておいたほうがいいけど、とりあえずはいいや。

ProductName OpenID: the Definitive Guide
David Recordon
Oreilly & Associates Inc / ?円 ( 2012-04-30 )


13.02.26

初回のログイン時じゃなくて、クッキーがエクスパイアしたあとにアクセスしてもセッションの入れ替わりが起こるみたいだ。スレッドかなぁと思ったけど、帰りの車のなかでよく考えてみたらやっぱ違うよなぁと思った。

セッションのデータがサーバー側に残っていてクッキーが送られないときにそのデータが使われているんじゃないかなぁ。

sessionをよく読んでみよう。

13.02.25

なおってなかった。

open-idサーバーで認証するところまではちゃんとユーザーが認識されているんだけど、アプリケーションにリダイレクトされたところで、ユーザーのセッションが違うユーザーのものに入れ替わってしまうというパターン。

  • バージョン0.8でも0.9でも起きる
  • 開発環境では起きないが、本番環境(Gunicorn)で起きる
  • IE8でのみ確認されている

多分ブラウザは関係ないと思う。

@app.before_requestにセッションが残っているっぽいので@oid.loginhandlerにアクセスする時に@app.before_requestのユーザーをチェックする処理をとばしてログイン処理をすればいいような気がしてきたのであとでやってみる。


Flask-OpenIDでログインするとなぜか初回に別アカウントになってしまい、一度ログアウトしてから再ログインすると正常にログインできるというよくわからないバグが頻発していて悩んでいた。しかもdevelopmentでは全く再現できず、production(Flask+Gunicorn+Supervisord)でしか起こらないので厄介だった。

Flaskのバージョンを0.8から0.9にあげてみたけどなおらなかったので、調べたら既に報告されていた。

最初のスレッドは、「ユーザー付きあわせて違っていたらセッション破棄するぜ」みたいな解決方法だったのでちょっと使えなかったが、二番目のスレッドでsessionインターフェースを自前で用意したらなおったという報告があったので試してみた。

Flask-OpenIDでこのスニペットをそのまま利用するとopenid.yadis.manager.YadisServiceManagerがJSONシリアライズできないというエラーが出た。これはpython-openidに由来する問題らしく、コメントで指摘されているようにpickleでdumpすればsave_sessionはできるようになる。

しかし、今度はopen_sessionでコケる。managerがstrだ云々っていうエラーなのでpickle.loadsがうまくいってないようだ。Flask-OpenIDのソース読むとSessionWrapperで' p'っていうkeyのDictionaryを作っているので、Sessionのほうもそのようにしてみた。

Better Client-side sessionsをちょっと変えたらうまくいった。

from werkzeug.datastructures import CallbackDict
from flask.sessions import SessionInterface, SessionMixin
from itsdangerous import URLSafeTimedSerializer, BadSignature
import pickle

class ItsdangerousSession(CallbackDict, SessionMixin):

    def __init__(self, initial=None):
        def on_update(self):
            self.modified = True
        CallbackDict.__init__(self, initial, on_update)
        self.modified = False

class ItsdangerousSessionInterface(SessionInterface):
    salt = 'cookie-session'
    session_class = ItsdangerousSession

    def get_serializer(self, app):
        if not app.secret_key:
            return None
        return URLSafeTimedSerializer(app.secret_key,
                                      salt=self.salt)

    def open_session(self, app, request):
        s = self.get_serializer(app)
        if s is None:
            return None
        val = request.cookies.get(app.session_cookie_name)
        if not val:
            return self.session_class()
        max_age = app.permanent_session_lifetime.total_seconds()
        try:
            data = s.loads(val, max_age=max_age)
            data = data[' p']
            for k in data:
                if k.startswith('yoc') or k.startswith('lt')\
                                       or k.startswith('_openid_consumer_last_token')\
                                       or k.startswith('_yadis_services__openid_consumer_'):
                    data[k] = pickle.loads(data[k])
            return self.session_class(data)
        except BadSignature:
            return self.session_class()

    def save_session(self, app, session, response):
        domain = self.get_cookie_domain(app)
        if not session:
            if session.modified:
                response.delete_cookie(app.session_cookie_name,
                                   domain=domain)
            return
        expires = self.get_expiration_time(app, session)
        d = {}
        for k in session:
            if k.startswith('yoc') or k.startswith('lt')\
                                   or k.startswith('_openid_consumer_last_token')\
                                   or k.startswith('_yadis_services__openid_consumer_'):
                d[k] = pickle.dumps(session[k])
            else:
                d[k] = session[k]
        val = self.get_serializer(app).dumps({' p': d})
        response.set_cookie(app.session_cookie_name, val,
                            expires=expires, httponly=True,
                            domain=domain)

FlaskでRESTful

ちょっと色々あってSpine.jsからBackbone.jsに乗り換えることにした。で、REST-APIが使いたかったので、Flask-RESTfulで書いてみた。

from flask import Flask, request
from flask.ext.restful import Resource, Api

app = Flask(__name__)
api = Api(app)

todos = {}
i = 0

class TodoSimple(Resource):

    def get(self, tid):
        return {"tid": tid, "todo": todos[tid]}

    def put(self):
        global i
        tid = i
        i = i + 1
        todos[tid] = request.form['data']
        return {"tid": tid, "todo": todos[tid]}

    def post(self, tid):
        todos[tid] = request.form['data']
        return {"todo": todos[tid]}

    def delete(self, tid):
        deldata = todos[tid]
        del(todos[tid])
        return {"tid": tid, "todo": deldata}

api.add_resource(TodoSimple, '/', '/<int:tid>')

if __name__ == '__main__':
    app.run(debug=True)

curlで確かめた。

$ curl http://localhost:5000/ -d "data=Remember the Flask" -X PUT
{"tid": 0, "todo": "Remember the Flask"}
$ curl http://localhost:5000/ -d "data=Remember the Python" -X PUT
{"tid": 1, "todo": "Remember the Python"}
$ curl http://localhost:5000/0
{"tid": 0, "todo": "Remember the Flask"}
$ curl http://localhost:5000/0 -d "data=Remember the Sphinx" -X POST
{"todo": "Remember the Sphinx"}
$ curl http://localhost:5000/0
{"tid": 0, "todo": "Remember the Sphinx"}
$ curl http://localhost:5000/0 -X DELETE
{"tid": 0, "todo": "Remember the Sphinx"}
$ curl http://localhost:5000/0
{"status": 500, "message": "Internal Server Error"}

今回はMongoDBを使いたかったので Flask-RESTfulを検討しているのだけど、SQLAlchemyでいいんだったらFlask-RESTlessでいいかもしれない。メソッド自分で書かなくていいし。

werkzeug.contrib.atomを使ってみた

このblogはfeedgeneratorを使っているんだけど、blohgのソースコードを読んでいたらwerkzeug.contrib.atomを使っていたのでちょっと興味があった。

最近あたらしいコードを書いていてせっかくなので使ってみた。調べたらFlaskのサイトにも書いてあった

実際使ってみるとrssを束ねて再配信するような場合にはちょっと考えなきゃいけないことが多いかなぁと。

rssにauthorとかそういう属性ないからどうしようかねぇ。

Flask-WTFのCSRF対策でちょいはまった

ドキュメントによると

<form method="POST" action=".">
    {{ form.csrf }}
    {{ form.name.label }} {{ form.name(size=20) }}
    <input type="submit" value="Go">
</form>

って書いてあるんだけど、

'csrf_token': [u'CSRF token missing']

なんじゃこりゃーって感じで2時間ほどはまったあげく、form.csrfじゃなくてform.csrf_tokenだということに気づいた。

WTFormsのドキュメント読まないとわからん。

Flask-SQLAlchemyとFlask-WTFの連携

最近Flask-WTFをよく使っている。

SQLAlchemyのデータをWTFに適用する時にはコンストラクタにobj引数で渡せばいいらしいんだが、逆にWTFのデータをSQLAlchemyはどうすんのかなぁと思いドキュメントを調べたら

form.populate_obj(user)

こんだけだった。

ProductName Pythonプロフェッショナルプログラミング
ビープラウド
秀和システム / 2940円 ( 2012-03-26 )


Flask+jqmでtweetできるモバイルサイトを作ったのとNode.jsハンズオンについて

昨日の三島バルのために自分用モバイルサイトをつくっておいたりとか。そもそもバルで酒ののみながらハッシュタグツイートって難易度高すぎですよね。そこを適当にボタン押せばいいだけにしときたいなぁと。それからバルイベントなので位置情報くっつけるべきでしょうとか思うんだけどAPI通さないといけないっぽいのでそこら辺面倒臭かった。

1336864962

Flask+pymongo+tweepy+jQuery Mobileでモバイルサイトをつくってsupervisorで死活監視しながらgunicornで動かしてみたのと、now.jsとgooglemapを使ったリアルタイムなやつをつくっておいた。

で、一緒のサーバーで動かしたかったのでこんな構成です。

ProductName jQuery Mobile
Jon Reid
オライリージャパン / 1995円 ( 2011-12-22 )


Node.jsハンズオンの宣伝

now.jsとgooglemapを使ったリアルタイムのやつは、位置情報付きのツイートがなくて残念だったので、日本の位置情報を拾うのに変えておいた。

眺めてるとわかるんだけど、デリヘル関係多いですね。なるほどなぁとは思うんだけど。

というわけで、来月の30日に静岡でNode.jsの勉強会があります。私はこれみたいなサイトをつくるハンズオンをやるので興味があれば参加すると良いと思います。

FlaskをNginx+gunicorn+supervisorで動かす

さくらのVPS 2Gに移行したので、ついでにHTTPサーバーをApacheからNginxに変えてみた。

Nginx+(gunicorn+Flask)は簡単に動かせたんだが、moniteringどうするかなぁとここ二日間悩んでいた。

ドキュメントにはCircus,Runit,Supervisorのやり方が載っていて、Circusが簡単そうに思えたんだが動かせなかったので、結局Supervisorで動かしている。

ProductName ハイパフォーマンスHTTPサーバ Nginx入門
Clement Nedelcu
アスキー・メディアワークス / 3150円 ( 2011-04-21 )


herokuでFlaskを動かしてみる

GW中にherokuを一度触ってみようと思っていたので、さっと手を動かしてみた。

基本的に、このチュートリアルの通りにやっただけだけど。

デプロイまわりがよく理解できてないので、Declaring and Scaling Process Types with Procfileでも読んでおけばいいのかな。

一度デプロイしてしまえば、アプリケーションの更新はコミットしてから

git push herroku master

するだけでいいので楽ですね。

destroy

webの管理画面からdestoryできるのは分かるのだけど、気軽に消すにはコマンドラインから

heroku destroy APP

チュートリアルだと最初APP名が分からなかったのだが、heroku create --stack cedarで自動的に付けられた名前を指定すればいいみたい。

これで削除される。

Backbone-HangmanをSpine+Flaskで書きなおしてみた

Spineはサンプルとかドキュメントがまだまだ少ないので苦労することが多い。sofにも質問が70位しか投稿されてないしね。

Backbone-HangmanっていうSinatra+Backbone.jsで書かれたよさげなサンプルがあったので、これをSpine+Flaskで書きなおしてみた。Flaskのほうはさくっと終わったんだけど、Spineで書きなおすのに1週間以上かかってしまった。

spine hangman

今思うと、Spineがわかっていないというよりは、イベントドリブンなコードの読み方がちゃんとわかっていなかったんだろうと。モジュールごとに読んでも混乱するだけで、イベントごとにちょっとづつ組み立てていかなきゃあかんわなと。そういえば、backbone.jsのチュートリアルもそんな感じだったし。

spine.appはディレクトリが存在するとappオプションで作成できないので適当なディレクトリでスケルトンを作成して中身をmvした。それからFlaskのstaticにbuildされるようにslug.jsonにpublicを追加

  "public": "./static",
  "dependencies": [
    "es5-shimify", 
    "json2ify", 
    ...

今回理解したこと

  • 独自イベントのトリガーとバインド
  • elの使い方
  • ecoのロジック
  • spine.appとFlaskの連携のさせ方(ディレクトリ構成)
  • jQueryとcssを使ったウェブアプリの作り方の理解が甘いこと

放置したものはテストのやり方。モデルはまぁ分かるんだけどコントローラーのテストは一度やっておく必要がある。console.logのデバッグは疲れる。