Pyramid Tutorial for PyCon JP Sprint 1.0 documentation

Pyramid チュートリアル

Contents

Pyramid チュートリアル

インストール

基本のインストール方法 : easy_installでインストールする

Pyramid をインストールする場合は、まずpython本体とeasy_installが使えるようにしておきましょう。 そうすれば以下の通りコマンドを実行すると Pyramid のインストールができます。:

$ easy_install pyramid

easy_installがない人の為に : easy_installをインストールする

easy_installが入っていない人は以下の手順でeasy_installを入れましょう。:

$ wget http://python-distribute.org/distribute_setup.py
$ python distribute_setup.py

或いはaptやyumでインストールすることも可能です。:

$ yum install python-setuptools
$ apt-get install python-setuptools

別の方法 : pipでインストールする

pythonにはeasy_installよりも多機能なpipというツールもあります。 普段pipを使っている人はこちらでインストールすることもできます。 pipでインストールしてみたい人は以下の手順でインストールします。

まずpipをインストールします。pipはeasy_installからインストールできます。:

$ easy_install pip

次に、pipを使ってPyramidをインストールします。:

$ pip install pyramid

pipの良いところはアンインストールできる点です。 easy_installはアンインストールの方法が提供されておらず、同等のことをする為に多少面倒なことをする必要があります。 また、pipはインストール中に表示されるメッセージの内容がわかりやすく親切です。

Windows環境下ではpipを使わない方がいい

pipは便利なツールですが、Windowsの環境ではpipを使わない方が無難です。

pipはeggからのインストールを行いません。必ずソースからビルドを行います。 この為、gccが予め用意されていないWindowsの環境下では問題が起き易くなります。 対して、easy_installはPyPIにビルド済みeggが存在すればそちらを利用します。

Windowsを使って Pyramid を試したい人は、pipでなくeasy_installを使って Pyramid をインストールした方が良いでしょう。

最初の一歩

Hello, world

Pyramid をインストールしたら、 hello.py という名前のファイルで以下のようなスクリプトを作成してみましょう。 python hello.py と実行すると、 http://localhost:8080/ でこのアプリケーションにアクセスできます。 hello.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from waitress import serve
from pyramid.config import Configurator
from pyramid.response import Response

def hello_world(request):
    return Response('Hello world!')

if __name__ == '__main__':
    config = Configurator()
    config.add_view(hello_world)
    app = config.make_wsgi_app()
    serve(app, host='0.0.0.0')

Pyramid は、 Configuratoradd_view 関数でビューを追加します。 hello_world 関数がビューです。

ルーティング

routingを追加して、URLごとに別の処理を行うようにします。

routing.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pyramid.config import Configurator
from pyramid.response import Response
from pyramid.view import view_config
from waitress import serve

@view_config(route_name='home')
def index(request):
    return Response("home")

@view_config(route_name='hello')
def hello(request):
    return Response("hello")

if __name__ == '__main__':
    config = Configurator()
    config.add_route('home', '/')
    config.add_route('hello', '/hello')

    config.scan()

    app = config.make_wsgi_app()
    serve(app, host='0.0.0.0')

Configuratoradd_route でルートを追加します。 第一引数がルートの名前( route_name ) で、第二引数がURLのパターンです。

また、さきほどと違い、 View の登録を view_config デコレータで行っています。 このデコレータをつけておくと、 Configuratorscan したときに、その関数が add_view されます。

python routing.py と実行して、 http://localhost:8080/ と、 http://localhost:8080/hello にそれぞれアクセスしてみましょう。

makoテンプレート

テンプレートエンジンを使って HTML の編集を簡単に行えるようにします。 Pyramidは、 Chameleon, Mako を標準でサポートしています。 今回は、 Mako を使います。

makoテンプレートを使うために、 Configuratorにテンプレートが置かれているディレクトリを指定します。 まずは、テンプレート用のディレクトリを作成しましょう。:

$ mkdir templates

テンプレートを書いてみましょう。 templates/index.mak:

1
2
3
4
5
<html>
<body>
<h1>Hello, ${name}</h1>
</body>
</html>

name 変数を表示するテンプレートです。 ${} で囲まれた部分は、pythonの式として評価されて、結果がHTML中に表示されます。

templating.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
from waitress import serve
from pyramid.config import Configurator
from pyramid.view import view_config

@view_config(route_name="home", renderer="index.mak")
def index(request):
    return dict(name="pyramid")

if __name__ == '__main__':
    import os
    here = os.path.dirname(__file__)
    settings = {
            'mako.directories':[
                os.path.abspath(os.path.join(here, 'templates')),
            ],
        }
    config = Configurator(settings=settings)
    config.add_route('home', '/')
    config.scan()
    app = config.make_wsgi_app()
    serve(app, host='0.0.0.0')

Configurator のコンストラクタに settings という、設定情報の dict を渡します。 settingsmako.directories に、テンプレートがおかれているディレクトリを指定します。

Note

  • os.path.dirname(__file__) は、スクリプトファイルが置かれているディレクトリを取得する定石的なコードです。

テンプレートを使う view は、 view_configrenderer でテンプレートを指定します。 テンプレートを使う場合は、 Response ではなく、 テンプレートに渡す変数を dict で返すようにします。

resource context

ビュー関数はあくまでビューなので、ここにロジックを書くのは推奨されません。 リソースを定義して、そちらにロジックを書きましょう。 また、リソースはリクエストごとに生成されたり、ロードされたりします。 リクエストに対応するリソースは、 context として、 request から取得できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
from datetime import datetime, date

from waitress import serve
from pyramid.config import Configurator
from pyramid.view import view_config
from pyramid.response import Response

class MyContext(object):
    def __init__(self, request):
        self.request = request

    def get_date(self):
        return date.today()

    def greeting(self):
        now = datetime.now()
        if now.hour < 12:
            return "Good morning"
        elif now.hour < 18:
            return "Good afternoon"
        else:
            return "Good evening"

@view_config(route_name="home", renderer="index.mak")
def index(request):
    return dict()

if __name__ == '__main__':
    import os
    here = os.path.dirname(__file__)
    settings = {
            'mako.directories':[
                os.path.abspath(os.path.join(here, 'templates')),
            ],
        }
    config = Configurator(settings=settings,
                          root_factory=MyContext)
    config.add_route('home', '/')
    config.scan()
    app = config.make_wsgi_app()
    serve(app, host='0.0.0.0')

MyContenxt クラスが今回定義したリソースです。 Configurator のコンストラクタに、 root_factory としてクラスを渡すことで、アプリケーション全体でのデフォルトリソースになります。

Note

  • route ごとに異なるリソースを使いたい場合は、 add_route のキーワード引数 resource_factory にクラスや関数を渡します。
1
2
3
4
5
6
<html>
<body>
${request.context.get_date()}
${request.context.greeting()}
</body>
</html>

request 変数はテンプレート内でいつでも参照可能です。 requestcontext 属性からコンテキストを参照できます。 上の例では、 MyContext クラスのインスタンスがコンテキストです。

実践1

プロジェクト、パッケージ、モジュール

先ほどまではpythonソースファイル1つと、テンプレートファイルだけで進めてきました。 実際のアプリケーションを作るときには、モジュールに分割して作業します。

実践的なファイルレイアウト:

project root
   package
       __init__.py
       views.py
       models.py
       templates
           index.mak
       static
           spam.js
           egg.css
   development.ini
   setup.py
   setup.cfg

project root はこれから作るアプリケーションのディレクトリです。 わかりやすい名前をつけましょう。

package はpythonパッケージです。 pythonパッケージは、モジュールを束ねる役割を持っています。 views.py や、 models.py がモジュールです。 また、 __init__.py はパッケージの初期化をします。

モジュールには、それぞれの役割を持っています。 views.py には、ビュー関数(これまでの例の indexhello など、リクエストを受け取って処理する関数) を書きます。 models.py には、実際の処理を行うクラスを書きます。 models.py に書かれるクラスのオブジェクトは多くの場合、DBなどに保存され永続的に状態を保持します。

ファイルレイアウトの作成

Pyramidは、 scaffold を使って、ファイルレイアウトを作成できるようになっています。 pcreate コマンドを実行して、新しいアプリケーションプロジェクトを作成してみましょう。

プロジェクト作成:

$ pcreate -t starter bankaccount
$ cd bankaccount

Note

以前のバージョンを使ったことのある人向けの注釈:

Pyramid 1.3 以前は paster コマンドを使っていました。 また scaffold の名前も違いました。

Pyramid 1.2 でのコマンド:

$ paster create -t pyramid_starter bankaccount

実行すると、 bankaccount ディレクトリが作成され、その下に bankaccount パッケージや、 views, models のファイルまで作成されます。

作成されたプロジェクトでは、pyramid以外のライブラリも必要となります。 必要なライブラリは setup.py に記述されているため、プロジェクトを develop 状態にすると自動でインストールされます。

必要なライブラリをインストール(pipを使っている場合):

$ pip install -e .

必要なライブラリをインストール(distributeのeasy_installを使っている場合):

$ python setup.py develop

pserve コマンドからの起動

development.ini ファイルはアプリケーションの設定ファイルです。

設定には以下のようなものが書かれています。

  • アプリケーションをどのパッケージからロードし、どのようなサーバーで実行するのか
  • アプリケーションの初期化に渡す内容
  • WSGIミドルウェアの設定

pserveコマンドは、この設定を使ってWebアプリケーションを起動します。

pserveコマンドでWebアプリケーションを起動:

$ pserve development.ini

starter scaffold では、サーバーのポートが6543に設定されています。 今起動したWebアプリケーションを http://localhost:6543 にアクセスして確認してみましょう。

これから作るアプリケーション

少し実践的な例として、銀行口座を模倣したアプリケーションを書いていきます。 これから作る銀行口座は次のようなものです。

  • 残高を持っていて、最初は0
  • 預け入れをできる。預け入れするとその金額分、残高が増える。
  • 引き落としができる。引き落とすとその金額分、残高が減る。
  • ただし、引き落としの結果、残高が0より少なくなる場合はエラーとなり、残高は減らない。

BankAccount

インタプリタで試してみましょう。

>>> class BankAccount(object):
...     def __init__(self):
...         self.balance = 0
...     def deposit(self, amount):
...         self.balance += amount
...     def withdraw(self, amount):
...         if self.balance - amount < 0:
...             raise NotEnoughFunds
...         self.balance -= amount
...
>>> class NotEnoughFunds(Exception):
...     pass
>>> b = BankAccount()
>>> b.balance
0
>>> b.deposit(100)
>>> b.balance
100
>>> b.withdraw(50)
>>> b.balance
50
>>> b.withdraw(100)
Traceback (most recent call last):
  ...
NotEnoughFunds
Traceback (most recent call last):
  ...
NotEnoughFunds

SQLAlchemy, sqlahelper

基本的に、Webアプリケーションは1リクエストごとにしかものごとを覚えていません。 あるリクエストで BankAccount のオブジェクトを作っても、次のリクエストではまた新しい BankAccount になってしまうのです。 BankAccount オブジェクトを保存する場所が必要です。

銀行口座としては、残高がリクエストするたびに毎回0になっては困ります。 このようにリクエストをまたいで必要な情報は、オンメモリセッションや、データベース、ファイルに保存します。

今回は、データベースに情報を保存します。 使うデータベースはPythonの標準ライブラリに入っている SQLite を使います。 また、直接SQLを書くのではなく、 SQLAlchemy を使って、オブジェクトをデータベースにマッピングします。 また、レスポンスを返すタイミングで、オブジェクトをデータベースに保存させるために、 pyramid_tmsqlahelper を使います。

必要なライブラリをインストール:

$ easy_install sqlalchemy pyramid_tm sqlahelper

pyramid_tm は、 Pyramid のアドオンパッケージです。 Configuratorinclude メソッドで呼び出すだけで有効になります。

__init__.pypyramid_tm を有効にする:

def main(global_conf, **settings):
    ...
    config.include('pyramid_tm')

include の引数は文字列であることに注意しましょう。

Note

includeの引数は文字列でなく、対象のアドオンパッケージをimportして渡すこともできます。

sqlahelper には、データベースへの接続情報を渡します。 接続情報は、 SQLAlchemyengine オブジェクトです。 development.ini に、データベースURLを書き、 そのURLから、 engine を作成して、 sqlahelperadd_engine メソッドで接続情報を追加します。

development.ini:

[app:main]
...
sqlalchemy.url = sqlite:///%(here)s/bankaccount.db
sqlalchemy.echo = true

SQLite の接続URLは、 sqlite:///ファイルパス のようになります。

development.ini の中で、 %(here)s は、 development.ini が置かれているディレクトリに変換されます。 sqlite:///%(here)s/bankaccount.db は、 development.ini が置かれているディレクトリに、 bankaccount.db という名前のファイルで、 SQLite データベースを作成して使用するという意味になります。

また、 sqlalchemy.echotrue にしておくと、 SQLAlchemy が実行する SQL 文がコンソールに表示されます。 開発中はこの機能を有効にしておくと便利です。

__init__.py:

import sqlahelper
from sqlalchemy import engine_from_config

def main(global_conf, settings):
    engine = engine_from_config(settings)
    sqlahelper.add_engine(engine)
    ...

モデルを定義する

SQLAlchemy でモデルを定義

では、 BankAccount をどのようにデータベースに保存するか、 models.py に書いてみましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import sqlahelper
import sqlalchemy as sa

Base = sqlahelper.get_base()
DBSession = sqlahelper.get_session()

class NotEnoughFunds(Exception):
    pass

class BankAccount(Base):
    __tablename__ = 'bankaccounts'

    id = sa.Column(sa.Integer, primary_key=True)
    name = sa.Column(sa.Unicode(255), unique=True)
    balance = sa.Column(sa.Integer, default=0)

    def __init__(self, name):
        super(BankAccount, self).__init__(name=name, balance=0)

    def deposit(self, amount):
        self.balance += amount

    def withdraw(self, amount):
        if self.balance - amount < 0:
            raise NotEnoughFunds
        self.balance -= amount

定義した BankAccount モデルをインタプリタで扱ってみましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
>>> from bankaccount import models
>>> from sqlalchemy import create_engine
>>> models.DBSession.remove()
>>> models.sqlahelper.add_engine(create_engine('sqlite:///'))
>>> models.Base.metadata.create_all()
>>> b = models.BankAccount(name=u"default")
>>> b.balance
0
>>> b.deposit(100)
>>> b.balance
100
>>> models.DBSession.add(b)
>>> import transaction
>>> transaction.commit()
>>> del b

>>> b = models.DBSession.query(models.BankAccount).filter_by(name=u"default").one()
>>> b.balance
100
>>> b.withdraw(50)
>>> b.balance
50
>>> b.withdraw(100)
Traceback (most recent call last):
  ...
NotEnoughFunds
Traceback (most recent call last):
  ...
NotEnoughFunds

DBSession, transaction

モデルをロードする処理を Resource に追加

resources.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from sqlalchemy.orm.exc import NoResultFound
from . import models

class Root(object):
    def __init__(self, request):
        self.request = request

    def get_bankaccount(self):
        try:
            return models.DBSession.query(models.BankAccount).filter_by(name=u'default').one()
        except NoResultFound:
            return None
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
>>> from bankaccount import models
>>> from sqlalchemy import create_engine
>>> models.DBSession.remove()
>>> models.sqlahelper.add_engine(create_engine('sqlite:///'))
>>> models.Base.metadata.create_all()
>>> b = models.BankAccount(name=u"default")
>>> models.DBSession.add(b)
>>> import transaction
>>> transaction.commit()
>>> from bankaccount.resources import Root
>>> root = Root(None)
>>> b = root.get_bankaccount()
>>> b is not None
True

ビューとテンプレート

home ビュー

home ビューでやるのは、 BankAccount オブジェクトを表示することだけです。 BankAccount オブジェクトを context から取得して、そのまま テンプレートに渡します。

views.py:

1
2
3
def home(request):
    bankaccount = request.context.get_bankaccount()
    return dict(bankaccount=bankaccount)

deposit ビュー

deposit ビューでは、金額を受け取って、 BankAccount オブジェクトの deposit メソッドを実行させます。

BankAccount オブジェクトは home ビューと同じく、 context から取得します。 金額は、 request.paramsamount から取得します。 request.params から取得した内容は str なので、 int に変換します。 この金額を引数にして、 BankAccount オブジェクトの deposit メソッドを実行します。 その後は、リダイレクトして、 home ビューに戻ります。

views.py:

1
2
3
4
def deposit(request):
    bankaccount = request.context.get_bankaccount()
    bankaccount.deposit(int(request.params['amount']))
    return HTTPFound(location=request.route_url('home'))

withdraw ビュー

deposit ビューでは、金額を受け取って、 BankAccount オブジェクトの deposit メソッドを実行させます。 呼び出すメソッドが deposit に変わるくらいですが、 deposit メソッドは残高不足で引き出し不能な場合に、 NotEnoughFunds 例外を発生させます。 その場合には、エラーメッセージを表示します。

views.py:

1
2
3
4
5
6
7
def withdraw(request):
    bankaccount = request.context.get_bankaccount()
    try:
        bankaccount.withdraw(int(request.params['amount']))
    except NotEnoughFunds:
        request.session.flash("you don't have too much money.")
    return HTTPFound(location=request.route_url('home'))

index.mak テンプレート

home ビューのテンプレートでは、 BankAccount オブジェクトの balance (金額) を表示します。 また、 depositwithdraw を呼び出すフォームも表示します。

index.mak:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<html>
<body>
%if request.session.peek_flash():
<div>
<ul>
%for m in request.session.pop_flash():
<li>${m}</li>
%endfor
</ul>
</div>
%endif
Balance: ${bankaccount.balance}
<form action="${request.route_url('deposit')}" method="post">
<fieldset>
<legend>Despsit</legend>
<input name="amount">
<input type="submit">
</fieldset>
</form>
<form action="${request.route_url('withdraw')}" method="post">
<fieldset>
<legend>Withdraw</legend>
<input name="amount">
<input type="submit">
</fieldset>
</form>
</body>
</html>

3行目 エラーメッセージがないか確認しています。エラーメッセージがある場合は、4行目から10行目の内容を表示します。

4行目~10行目 エラーメッセージを表示します。エラーメッセージは複数取れる可能性があるので、 for ループで処理しています。

12行目 残高の表示です。

13行目~19行目 預入用のフォームです。 request.route_url を使うと、ルート名のURLを逆引きできます。

20行目~26行目 引出用のフォームです。

初期化処理

__init__.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pyramid.config import Configurator
import sqlahelper
from sqlalchemy import engine_from_config
from bankaccount.resources import Root
from pyramid.session import UnencryptedCookieSessionFactoryConfig

my_session_factory = UnencryptedCookieSessionFactoryConfig('itsaseekreet')

def main(global_config, **settings):
    """ This function returns a Pyramid WSGI application.
    """
    engine = engine_from_config(settings)
    sqlahelper.add_engine(engine)

    config = Configurator(root_factory=Root,
                          settings=settings,
                          session_factory=my_session_factory)
    config.include('pyramid_tm')
    config.add_route('home', '/')
    config.add_route('deposit', '/deposit')
    config.add_route('withdraw', '/withdraw')
    config.add_static_view('static', 'bankaccount:static')
    config.scan('.views')
    return config.make_wsgi_app()

session_factory

フラッシュメッセージ用に、セッションを使う準備をします。 今回は単純なCookieを利用したセッションファクトリを使用します。

sqlahelper, pyramid_tm

sqlahelperにエンジンを登録します。エンジンの接続文字列は development.iniに書かれています。 また、 pyramid_tm をインクルードしてトランザクションマネージャを有効にします。

route

home, deposit, withdraw の route をそれぞれ登録します。

scan

Configurator.scan によって、 views モジュール内の view_config を読み取り、設定します。

データベースを作成

sqlite用のデータベースを作成します。 development.iniにデータベース接続の設定を追加しましょう。

[bankaccount]
...
sqlalchemy.url = sqlite:///%(here)s/bankaccount.db

設定したら、 pshell コマンドを実行します。 このコマンドでは、 bankaccount.main 関数まで実行した状態で Python インタプリタを起動します。 sqlahelperへの接続設定まで済んでいます。

データベースとテーブルの作成をして、初期データを投入します。

>>> from bankaccount import models
>>> models.Base.metadata.create_all()
>>> b = models.BankAccount(name=u'default')
>>> models.DBSession.add(b)
>>> import transaction
>>> transaction.commit()

トランザクションマネージャを使っているため、 コミットはSQLAlchemyのSessionではなく、 transaction モジュール経由で行います。

では、アプリケーションをたちあげてみましょう。

$ pserve development.ini

これからさきは?

すでに Pyramid を使ったアプリケーションの方法は、身についています。 その他のアドオンやライブラリを使ったり、 Configurator の設定を掘り下げて、よりよい Webアプリケーションの作り方を探ってみてください。

  • authentication_policy や authorization_policy を使って、認証や認可を設定してみましょう。
  • ビューを関数のみで定義していましたが、クラスでの定義も可能です。
  • pyramid_exclog や pyramid_debugtoolbar などのアドオンを使ってみましょう。
  • nose や webtest を使って、テストをしてみましょう。
  • deform や colander を使って、フォームを生成したり、入力チェックをしてみましょう。
  • webhelpers で sqlalchemy のページング処理をしてみましょう。

Contents