FlaskアプリをuWSGIで動かす(2)

ユーザの作成

専用ユーザを作る。

takatoh@nightschool $ sudo adduser bruschetta
[sudo] password for takatoh: 
ユーザー `bruschetta' を追加しています...
新しいグループ `bruschetta' (1003) を追加しています...
新しいユーザー `bruschetta' (1003) をグループ `bruschetta' に追加しています...
ホームディレクトリ `/home/bruschetta' を作成しています...
`/etc/skel' からファイルをコピーしています...
新しい UNIX パスワードを入力してください: 
新しい UNIX パスワードを再入力してください: 
passwd: パスワードは正しく更新されました
bruschetta のユーザ情報を変更中
新しい値を入力してください。標準設定値を使うならリターンを押してください
	フルネーム []: bruschetta
	部屋番号 []: 
	職場電話番号 []: 
	自宅電話番号 []: 
	その他 []: 
以上で正しいですか? [Y/n] Y

sudo する権限をつける。

takatoh@nightschool $ sudo gpasswd -a bruschetta sudo
ユーザ bruschetta をグループ sudo に追加

アプリの配置と起動確認

今作った新しいユーザでログインしなおして GitHub から clone。

bruschetta@nightschool:~$ git clone https://github.com/takatoh/Bruschetta.git bruschetta
Cloning into 'bruschetta'...
remote: Counting objects: 345, done.
remote: Compressing objects: 100% (126/126), done.
remote: Total 345 (delta 231), reused 319 (delta 205), pack-reused 0
Receiving objects: 100% (345/345), 31.03 KiB | 0 bytes/s, done.
Resolving deltas: 100% (231/231), done.
Checking connectivity... done.

virtualenv の作成と必要なライブラリのインストール。

bruschetta@nightschool:~$ cd bruschetta
bruschetta@nightschool:~/bruschetta$ virtualenv env
New python executable in env/bin/python
Installing setuptools, pip, wheel...done.
bruschetta@nightschool:~/bruschetta$ source env/bin/activate
(env)bruschetta@nightschool:~/bruschetta$ pip install -r requirements.txt
Collecting Flask (from -r requirements.txt (line 1))
/home/bruschetta/bruschetta/env/local/lib/python2.7/site-packages/pip/_vendor/requests/packages/urllib3/util/ssl_.py:90: InsecurePlatformWarning: A true SSLContext object is not available. This prevents urllib3 from configuring SSL appropriately and may cause certain SSL connections to fail. For more information, see https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning.
  InsecurePlatformWarning
  Downloading Flask-0.10.1.tar.gz (544kB)
    100% |████████████████████████████████| 544kB 720kB/s 
Collecting Flask-SQLAlchemy (from -r requirements.txt (line 2))
  Downloading Flask-SQLAlchemy-2.1.tar.gz (95kB)
    100% |████████████████████████████████| 98kB 1.6MB/s 
Collecting Flask-Script (from -r requirements.txt (line 3))
  Downloading Flask-Script-2.0.5.tar.gz (42kB)
    100% |████████████████████████████████| 45kB 2.4MB/s 
Collecting Werkzeug>=0.7 (from Flask->-r requirements.txt (line 1))
  Downloading Werkzeug-0.11.2-py2.py3-none-any.whl (304kB)
    100% |████████████████████████████████| 307kB 1.1MB/s 
Collecting Jinja2>=2.4 (from Flask->-r requirements.txt (line 1))
  Downloading Jinja2-2.8-py2.py3-none-any.whl (263kB)
    100% |████████████████████████████████| 266kB 1.4MB/s 
Collecting itsdangerous>=0.21 (from Flask->-r requirements.txt (line 1))
  Downloading itsdangerous-0.24.tar.gz (46kB)
    100% |████████████████████████████████| 49kB 1.8MB/s 
Collecting SQLAlchemy>=0.7 (from Flask-SQLAlchemy->-r requirements.txt (line 2))
  Downloading SQLAlchemy-1.0.9.tar.gz (4.7MB)
    100% |████████████████████████████████| 4.7MB 120kB/s 
Collecting MarkupSafe (from Jinja2>=2.4->Flask->-r requirements.txt (line 1))
  Downloading MarkupSafe-0.23.tar.gz
Building wheels for collected packages: Flask, Flask-SQLAlchemy, Flask-Script, itsdangerous, SQLAlchemy, MarkupSafe
  Running setup.py bdist_wheel for Flask
  Stored in directory: /home/bruschetta/.cache/pip/wheels/d2/db/61/cb9b80526b8f3ba89248ec0a29d6da1bb6013681c930fca987
  Running setup.py bdist_wheel for Flask-SQLAlchemy
  Stored in directory: /home/bruschetta/.cache/pip/wheels/79/20/fe/49ca207b5445eacf8f34ac4c7f2365dd344e17668cd63b8fdd
  Running setup.py bdist_wheel for Flask-Script
  Stored in directory: /home/bruschetta/.cache/pip/wheels/42/85/3d/fca6d4a04f7852ce8322be36ff8918ceff9bce568e7144c813
  Running setup.py bdist_wheel for itsdangerous
  Stored in directory: /home/bruschetta/.cache/pip/wheels/97/c0/b8/b37c320ff57e15f993ba0ac98013eee778920b4a7b3ebae3cf
  Running setup.py bdist_wheel for SQLAlchemy
  Stored in directory: /home/bruschetta/.cache/pip/wheels/f7/c3/1f/a136a98dbce9c82df10df168813df34bba6d039787e4327634
  Running setup.py bdist_wheel for MarkupSafe
  Stored in directory: /home/bruschetta/.cache/pip/wheels/94/a7/79/f79a998b64c1281cb99fa9bbd33cfc9b8b5775f438218d17a7
Successfully built Flask Flask-SQLAlchemy Flask-Script itsdangerous SQLAlchemy MarkupSafe
Installing collected packages: Werkzeug, MarkupSafe, Jinja2, itsdangerous, Flask, SQLAlchemy, Flask-SQLAlchemy, Flask-Script
Successfully installed Flask-0.10.1 Flask-SQLAlchemy-2.1 Flask-Script-2.0.5 Jinja2-2.8 MarkupSafe-0.23 SQLAlchemy-1.0.9 Werkzeug-0.11.2 itsdangerous-0.24

データベースの初期化。

(env)bruschetta@nightschool:~/bruschetta$ python manage.py init_db

起動確認。

(env)bruschetta@nightschool:~/bruschetta$ python manage.py runserver
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

OK。

uWSGIを使った起動

uWSGI 用の設定ファイル。

[uwsgi]
uid = root
gid = root
http = :9090
venv = /home/bruschetta/bruschetta/env
python-path = /home/bruschetta/bruschetta
wsgi-file = /home/bruschetta/bruschetta/manage.py
callable = app
master=true
pidfile=/var/run/bruschetta.pid
logger=file:/home/bruschetta/bruschetta/bruschetta.log

起動確認。

bruschetta@nightschool:~/bruschetta$ sudo uwsgi bruschetta.ini
[sudo] password for bruschetta: 
[uWSGI] getting INI configuration from bruschetta.ini

OK。sudo をつけてるのは、/var/run/bruschetta.pid に書き込むため。

起動用スクリプト

PATH=/sbin:/usr/local/bin:/usr/bin:/bin
APP_ROOT=/home/bruschetta/bruschetta

case "$1" in
start)
    cd ${APP_ROOT}
    uwsgi bruschetta.ini
    ;;
stop)
    PID_FILE=/var/run/bruschetta.pid
    PID=`cat ${PID_FILE}`
    kill -INT ${PID}
    rm ${PID_FILE}
    ;;
*)
    echo "Usage: bruschetta {start|stop}" >&2
    exit 1
    ;;
esac

exit 0

起動確認。

bruschetta@nightschool:~/bruschetta$ sudo service bruschetta start
[uWSGI] getting INI configuration from bruschetta.ini

あれ、デーモンにならない。
ググってみたら、ini ファイルに logger じゃなくて daemonize を書くみたい。次のように書きなおした。

[uwsgi]
uid = root
gid = root
http = :9090
venv = /home/bruschetta/bruschetta/env
python-path = /home/bruschetta/bruschetta
wsgi-file = /home/bruschetta/bruschetta/manage.py
callable = app
master=true
pidfile=/var/run/bruschetta.pid
#logger=file:/home/bruschetta/bruschetta/bruschetta.log
daemonize=/home/bruschetta/bruschetta/bruschetta.log

今度は OK。

Ubuntu 起動時に自動的に起動するように設定。sysv-rc-conf を使う。

bruschetta@nightschool:~/bruschetta$ sudo sysv-rc-conf
buschetta-service

Nginxの設定

upstream uwsgi-bruschetta {
    server 127.0.0.1:9090;
}

server {
    # port
    listen 80;

    # server name
    server_name bruschetta;

    # document root
    root /home/bruschetta/bruschetta;

    # index
    #index index.php index.html index.htm;

    # log files
    access_log /var/log/nginx/bruschetta/access.log combined;
    error_log /var/log/nginx/bruschetta/error.log warn;

    keepalive_timeout 60;
    proxy_connect_timeout 60;
    proxy_read_timeout 60;
    proxy_send_timeout 60;

    #client_max_body_size 20M;

    location / {
        #root /home/bruschetta/bruschetta;

        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_pass http://uwsgi-bruschetta;
    }

}

sites-enabled から sites-available へリンクを張る。

bruschetta@nightschool:/etc/nginx/sites-enabled$ sudo ln -s ../sites-available/bruschetta bruschetta

/etc/hosts に次の行を挿入。

127.0.0.1 bruschetta

ログファイル用のディレクトリを作成。

bruschetta@nightschool:~$ sudo mkdir /var/log/nginx/bruschetta

Nginx を再起動。

bruschetta@nightschool:/etc/nginx/sites-enabled$ sudo service nginx restart
 * Restarting nginx nginx

これで完了。

FlaskアプリをuWSGIで動かす

uWSGIのインストール

まずは uWSGI のインストール。

takatoh@nightschool $ sudo pip install uwsgi

Successfully installed uwsgi とでて完了。

簡単なアプリでテスト

$HOME/w/test/app.py を次のように書いてテスト。

from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello_worls():
    return 'Hello, world.'

if __name__ == '__main__':
    app.run()
takatoh@nightschool $ pwd
/home/takatoh
takatoh@nightschool $ uwsgi --http :9090 -H w/test/env --wsgi-file w/test/app.py --callable app

http://localhost:9090/ にブラウザでアクセスすると、無事 Hello, world. と表示されたのでOK。
-H オプションは virtualenv の環境のディレクトリを指定している。

書籍管理アプリ Bruschetta を動かす

上で結構すんなり動いたので、簡単にいくかと思ったらそうでもなかった。試行錯誤の上、次のようにして起動に成功。

takatoh@nightschool $ uwsgi --http :9090 --wsgi-file /home/takatoh/w/bruschetta/manage.py --python-path /home/takatoh/w/bruschetta --callable app -H /home/takatoh/w/bruschetta/env

ハマった点は次のとおり:

  • アプリのディレクトリを --python-path で指定しないとダメ
  • --wsgi-file はフルパスで指定しないとダメ
  • --callable は Flask のインスタンスを代入した変数名を指定

--callable について補足。アプリの起動ファイル manage.py は次のようになっている。

from __future__ import print_function
from flask.ext.script import Manager
from bruschetta import app, db

manager = Manager(app)

@manager.command
def init_db():
    db.create_all()

if __name__ == '__main__':
    manager.run()

なので、最初は --callable manager としたけどダメだった。Flask のインスタンスはあくまでも app なので、それを指定しなきゃいけないようだ。

というわけで、無事動いたので今日はここまで。明日はデーモンとして動くようにしてみよう。

Pythonでのタイムゾーン

Python でタイムゾーンを考慮した時刻を扱うのはちょっと面倒。
datetime ライブラリの日付や時刻のオブジェクトには “naive” と “aware” の2種類があって、”naive” なオブジェクトはタイムゾーンに関する情報を持っていない。

>>> from datetime import datetime
>>> now = datetime.now()
>>> now
datetime.datetime(2015, 11, 23, 11, 9, 51, 831037)
>>> utc_now = datetime.utcnow()
>>> utc_now
datetime.datetime(2015, 11, 23, 2, 10, 6, 927019)

だからタイムゾーンを考慮した日付・時刻を扱おうとすると “aware” なオブジェクトにしないといけない。で、タイムゾーンを表すには datetime.tzinfo クラスを使うんだけど、これは抽象クラスであって実際に使うにはタイムゾーンそれぞれのサブクラスを作らなきゃいけない。
リファレンスマニュアルの例を参考に書いてみたのがこれ。

from datetime import datetime, timedelta, tzinfo

class UTC(tzinfo):
    def utcoffset(self, dt):
        return timedelta(0)

    def tzname(self, dt):
        return "UTC"

    def dst(self, dt):
        return timedelta(0)

class JST(tzinfo):
    def utcoffset(self, dt):
        return timedelta(hours=9)

    def tzname(self, dt):
        return "JST"

    def dst(self, dt):
        return timedelta(0)


if __name__ == '__main__':
    now = datetime.now(JST())
    print now
    print now.utcoffset()

    utc_now = now.astimezone(UTC())
    print utc_now
    print utc_now.utcoffset()

datetime.tzinfo のサブクラスとして UTC と JST を定義している。これを使うとタイムゾーンを考慮した時刻を扱うことができる。こんな感じ:

>>> import test_tzinfo
>>> jst = test_tzinfo.JST()
>>> now = datetime.now(jst)
>>> now
datetime.datetime(2015, 11, 23, 11, 21, 51, 47529, tzinfo=<test_tzinfo.JST object at 0x7f9fa1686b50>)
>>> utc = test_tzinfo.UTC()
>>> utc_now = now.astimezone(utc)
>>> utc_now
datetime.datetime(2015, 11, 23, 2, 21, 51, 47529, tzinfo=<test_tzinfo.UTC object at 0x7f9fa1686c90>)

タイムゾーンを簡単に扱えるようにするライブラリもあるようだけど、少なくとも標準の機能だけだとちょっと面倒、という話でした。

Flask-SQLAlchemyのモデルでのリレーションシップ

一昨日のアプリを作る段階で、Flask-SQLAlchemy のモデルでのリレーションシップの作り方を調べたのでメモ。↓ここに書いてある。

 cf. One-to-Many Relationships – Flask SQLAlchemy

簡単に言うと、主キーを持つテーブルにリレーションシップを作成して、外部キーを持つテーブルに db.ForeignKey を設定してやる。
次の例では Category と Format が Book に対して1対多リレーションになっている。つまり、Category と Format が主キーを持っていて、Book が外部キーを持っている。

class Book(db.Model):
    __tablename__ = 'books'
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String)
    volume = db.Column(db.String)
    series = db.Column(db.String)
    series_volume = db.Column(db.String)
    author = db.Column(db.String)
    translator = db.Column(db.String)
    publisher = db.Column(db.String)
    category_id = db.Column(db.Integer, db.ForeignKey('categories.id'))
    format_id = db.Column(db.Integer, db.ForeignKey('formats.id'))
    isbn = db.Column(db.String)
    published_on = db.Column(db.String)
    original_title = db.Column(db.String)
    note = db.Column(db.String)
    keyword = db.Column(db.String)
    disk = db.Column(db.String)
    disposed = db.Column(db.Boolean, default=False)

class Category(db.Model):
    __tablename__ = 'categories'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String)
    books = db.relationship('Book', backref='category', lazy='dynamic')

class Format(db.Model):
    __tablename__ = 'formats'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String)
    books = db.relationship('Book', backref='format', lazy='dynamic')

はじめ、ForeignKey を db.ForeignKey(‘category.id’) とやったらダメだった。モデル名 (category) じゃなくてテーブル名 (categories) でないといけないのね。

Flaskで書籍管理アプリを作った

作った、というか作り始めた。とりあえず基本的な機能として、新規追加と編集はできるようになった。削除はまだ。Flask で DELETE メソッドってどうやって発行するんだ?

メインの画面はこんな感じ。

bruschetta-index

デザインもまだなんにもしてない。スタイルシートはこれからだな。
書籍の詳細画面。

bruschetta-book-detail

参考にしたのは、先週のエントリで挙げたサイトのほかにこのあたり。

 cf. http://a2c.bitbucket.org/flask/quickstart.html
 cf. FlaskSQLAlchemy

Flaskにさわってみた

Flask は Python の軽量 Web ワークフレーム。Ruby の Sinatra に影響を受けてるらしい。
さわった、といってもほんのちょっとだけ。↓のページの 1 〜 4 をやってみた。

 cf. Pythonで学ぶwebアプリケーションの作り方by Flask

結果出来たのがこの Flaskr という小さなブログみたいなアプリケーション。login 機能がついてて、記事をポストできる。

flaskr

基本的に、上のページのチュートリアルのとおりにやればできたんだけど、ひとつだけ、アプリケーションを起動するときに Warning が発生する(アプリケーション自体は動く)。

(env)takatoh@nightschool $ python manage.py runserver
/home/takatoh/w/handson/env/local/lib/python2.7/site-packages/flask_sqlalchemy/__init__.py:800: UserWarning: SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and will be disabled by default in the future.  Set it to True to suppress this warning.
  warnings.warn('SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and will be disabled by default in the future.  Set it to True to suppress this warning.')
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

メッセージにあるとおり、この警告を抑制するには config.py の中で SQLALCHEMY_TRACK_MODIFICATIONS に True をセットしてやればいい。

SQLALCHEMY_DATABASE_URI = 'sqlite:///flaskr.db'
SECRET_KEY = '\xce\xa3/\x97\xf60\xc1\x9c\x10J\xa2)t\r\xfd\xa9.\xdf\x8d\xe3\xfc\xfd\xb4\x9a'
SQLALCHEMY_TRACK_MODIFICATIONS = True

というわけで、今日はここまで。もう少しさわってみたらなにか作ってみるかな。

短縮URLを展開する

ネットを見ていたら、古い記事だけどこんなのを見つけた。

 cf. 短縮URLを1行で展開する@Python – shibuya blog

スクリプトにするんなら1行にすることないだろ、とも思ったけどそれはそれとして、わざわざ短縮元の URL にアクセスしなくても、短縮 URL に HEAD メソッドでアクセスしてヘッダーだけ取ってきて Location を見ればいい、というのをどこかで読んだ覚えがある。
というわけで、Python で書いてみた。

import httplib
import urlparse
import sys

url = sys.argv[1]
o = urlparse.urlparse(url)
conn = httplib.HTTPConnection(o.netloc)
conn.request("HEAD", url)
res = conn.getresponse()
print res.getheader("Location")

今回調べたわかったけど、urllib じゃ HEAD メソッド使えないらしい。で、より低レベルの httplib を使ってる。

実行結果:

takatoh@nightschool $ python expandurl.py http://bit.ly/1axxIi4
https://blog.panicblanket.com/

urllibでユーザーエージェントを変更する

Python の urllib.urlopen や urllib.urlretrieve で URL にアクセすると、FancyURLopener オブジェクトが生成されて、これが目的の URL にアクセスする。ユーザーエージェントの値もこの FancyURLopener オブジェクトが持っている。なので、これを変えてやればいい。次のように FancyURLopener のサブクラスを作ってユーザーエージェントの値を設定し、urllib._urlopenr に代入してやればいい。

class Myopener(urllib.FancyURLopener):
    version = "Mozilla"

urllib._urlopener = Myopener()

これで、urllib のデフォルトでは跳ね返されてしまう URL にもアクセスできるようになる。

参考ページ:

 cf. http://docs.python.jp/2/library/urllib.html#urllib._urlopener

PILにかえてPillowをいれた

昨日 PIL をインストールしたけど、「python pil 使い方」でぐぐったらこんなページが最初に来た。

 cf. PIL 利用ノート [obsolete]

曰く、

Warning

PIL は随分長い間、Python のバージョンで言うと 2.7 くらいまでだろうか、 画像処理パッケージとして愛用してきたが、現在は Pillow というものに取って代わられたようである。 そこで、筆者の環境も PIL を放棄して Pillow を導入した。

だそうである。なんてこった…orz。

使ってる Python が 2.7.6 なのでこのままでもいいのかもしれないけど、せっかくなので Pillow に移行することにした。
Pillow のインストールは↓ここに詳しい。

 cf. Installation – Pillow (PIL Fork)

このページの Linux Installation の項に Ubuntu 14.04 LTS でのインストールの仕方も載っている。まずは依存ライブラリのインストール。

takatoh@nightschool $ sudo apt-get install libtiff5-dev libjpeg8-dev zlib1g-dev  libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python-tk

つづいて Pillow を pip でインストール。

takatoh@nightschool $ sudo pip install pillow

試してみよう。

takatoh@nightschool $ python
Python 2.7.6 (default, Mar 22 2014, 22:59:56) 
[GCC 4.8.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from PIL import Image
>>> img = Image.open("sample.jpg")
>>> img.info
{'jfif_version': (1, 1), 'jfif': 257, 'jfif_unit': 1, 'jfif_density': (72, 72), 'dpi': (72, 72)}
>>> img.size
(1280, 720)

フォーマットを変換するには、単に拡張子をかえて保存すればいい。

>>> img.save("sample.png")

縦横比を保ってサムネールを作るには thumbnail メソッドが使える。ただし、オブジェクト自身を変更するので注意。と言ってもいきなり上書きされるわけでもない。保存するには上にも書いた save メソッドを使う。

>>> img.thumbnail((360, 360))
>>> img.size
(360, 202)
>>> img.save("thumbnail.jpg")

とりあえずはこんなところか。うまくいってよかった。

Python Imaging Library (PIL)を入れてみた

pip で簡単にインストールできると書いてあるページもあるけど、少なくとも今回はうまくいかなかったので、↓このページの通りにインストールした。

 cf. Python Imaging Library (PIL)をインストール – rakkyooの備忘録

Python Imaging Library のサイト(http://www.pythonware.com/products/pil/)から、ソースをダウンロードして、展開。今回ダウンロードしたソースは、Python Imaging Library 1.1.7 Source Kit (all platforms) (November 15, 2009)。
展開したディレクトリに移動して setup.py を実行。

takatoh@nightschool $ sudo python setup.py install

ところが、python.h がない、っていうエラーが出た。ググってみたら、python-dev をインストールしておけばいいらしい。

 cf. [python]PILのインストールで「_imaging.c:75: fatal error: Python.h」- うえちょこ@ぼろぐ

takatoh@nightschool $ sudo apt-get install python-dev

再度 setup.py を実行。今度はうまくいったか。

試してみる。

takatoh@nightschool $ python
Python 2.7.6 (default, Mar 22 2014, 22:59:56) 
[GCC 4.8.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import Image
>>> im = Image.open("banner.gif")
>>> im.info
{'duration': 0, 'version': 'GIF89a', 'transparency': 57, 'background': 56}
>>> im.format
'GIF'
>>> im.size
(200, 40)
>>> 

OKっぽい。