5. WSGIとPylons

ここまでに、Pythonの基本からO/RマッパーやテンプレートエンジンなどのWebフレームワークの部品について説明しました。
この章では,PythonのWebフレームワークを概観して,これまでに説明した部品を使って簡単なPylonsのアプリケーションを作成します。

WSGIはフィルタ

Webフレームワークの前に、最近PythonのWebアプリケーションの開発でよく取り上げられるWSGI(Web Server Gateway Interface)があります。先日公開されたGoogle App EngineもWSGIの実装を提供しています。

では、WSGIとは何でしょうか?WSGIとは、WebサーバとWebアプリケーション間のインターフェースを定義した仕様です。WSGIが標準化される以前は、Webアプリケーションの開発時にWebサーバでどのように動作させるか想定しておく必要がありました。例えば、WebアプリケーションはCGIとして動作するのか、mod_pythonで動作するのかをあらかじめ知っておく必要がありました。WSGIにより、Webサーバとのインターフェースが統一され、Webアプリケーションが実際にどのような環境で動作するか知っておく必要がありません。

Pythonには有名なWebフレームワークがいくつもあります。そうしたWebフレームワークで使い回せる機能も少なくありません。例えば、セッション管理やユーザ認証、キャッシュ機能などです。WSGIにはミドルウェアという概念があり、設定ファイルに記述するだけでそうした共通機能を利用することができます。

複雑な仕組みのようですが、図のように、Webサーバから複数のミドルウェアを経由してWebアプリケーションにリクエストが伝搬されます。ミドルウェアはフィルタのように動作し、データを加工したり、必要であればミドルウェアが(キャッシュなどの)レスポンスを返したりします。


さて、WSGIの実体はどのようなものでしょうか?WSGIが規定しているインターフェースは、次の関数一つだけです。

  def app(environ, start_response)

environは辞書です。CGIの環境変数のようなものです。environの中にはリクエストされたURLやCGIパラメータなど必要なものが格納されています。ミドルウェアが、セッション情報など、独自にenvironに情報を使用することもあります。

start_responseはC言語での関数ポインタです。HTTPヘッダーの情報をこの関数で設定します。この関数の戻り値は、文字列の配列(*1)で、実際にレスポンスの本文になります。

*1 正確にはイテレーション可能なものです。

これらのインターフェースは全てPEP(Python Enhancement Proposals)と呼ばれるPythonでのRFCによって決められます。すべての仕様がオープンな手続きによって決められるのもPythonの特徴の一つです。

フレームワークのフレームワークPylons

Pythonは、Webフレームワークの多さでも有名です。最も有名で開発も活発なWebフレームワークには、TurboGearsとDjangoがあります。特にDjangoは,Google App Engineで標準で利用できるなど、注目を集めています。Google App Engineは、WSGIを実装したアプリケーションであればすべて動作させることができます。TurboGearsもDjangoもWSGIのミドルウェアとして機能するので、Google App Engineへの移植は比較的簡単です。

さて、WSGIをもっと効率的に使おうというプロジェクトがあります。それがPylonsです。Pylonsは「フレームワークのフレームワーク」というとらえどころのない特徴を標榜しています。TuboGearsの次期バージョン(バージョン2.0)では、Pylons上でTurboGearsが動作すると言えば、「フレームワークのフレームワーク」が少しは理解できるかもしれません。

Pylonsの面白い機能の一つとして、Pythonのコード中などで例外が発生した時に、ブラウザに例外情報を表示する機能があげられます。Pylonsでは、さらに、ブラウザ上からデバッグできるようになっています(下図。Pylonsはこの機能によって、後発ながらPython界で一躍有名なフレームワークに押し上げられました。今回は、このPylonsを使って、Webアプリケーションを作っていきましょう。


まずは、Pylonsのインストールです。次のコマンドでインストールします。

  > easy_install pylons

今回は、簡単な一行掲示板を作りながらPylonsでの開発方法を説明します。一行掲示板(下図)は、名前と一行のメッセージを入力することができます。入力したメッセージの一覧はすべての人が参照できます。

一行掲示板を作る

それでは、一行掲示板を作っていきます。まずは、Pylonsのプロジェクトを作成します。今回は、simplebbsという名前でプロジェクトを作ります。次のようにpasterコマンドを実行してください。最後の引数がプロジェクト名になります。

  c:¥> cd c:¥tmp
  c:¥tmp> paster create -t pylons simplebbs
  [省略]
  c:¥tmp>

無事に作成できたでしょうか?ここで、サーバを起動して、プロジェクトが正常に作成できたか確認してみます。次のコマンドで起動します。以降は、c:¥tmp¥simplebbsディレクトリで作業します。

 c:¥tmp> cd simplebbs
 c:¥ paster serve --reload development.ini

ブラウザでhttp://localhost:5000/にアクセスしてください。Pylonsの簡単な説明画面が表示されれば成功です。サーバを停止する場合は、Ctrl + Cを押してください。Pylonsのサーバは,ソースコードや設定ファイルを変更すると自動的にリロードします。この機能のおかげで,サーバを起動したままで開発を行えます。筆者はPylonsで開発する場合、OSを再起動するまで一月ぐらいはサーバを起動し続けています。


モデルの定義

PylonsはMVCモデルを採用していますが、モデルのライブラリは開発者自身が選択します。ここで言うモデルとは実質O/Rマッパーのことです。今回のシステムは第3章で説明したSQLAlchemyを使います。まずは、モデルを定義します。今回は次のようにbulletinテーブルを一つだけ作成します。bulletinテーブルには、プライマリキーにidフィールド、名前を保持するnameフィールド、一行掲示メッセージのcontentフィールドと作成時間を保持するcreatedフィールドを定義します。

simplebbs/model/__init__.py ---
from datetime import datetime
from pylons import config
from sqlalchemy import Column, MetaData, Table, types
from sqlalchemy.orm import mapper
from sqlalchemy.orm import scoped_session, sessionmaker


Session = scoped_session(sessionmaker(autoflush=True, transactional=True,
                                      bind=config['pylons.g'].sa_engine))


metadata = MetaData()


bulletin_table = Table('bulletin', metadata,
    Column('id', types.Integer, primary_key=True),
    Column('name', types.Unicode(40), default='Anonymous'),
    Column('content', types.Unicode(), default=''),
    Column("created", types.DateTime, default=datetime.now)
)


class Bulletin(object):
    pass


mapper(Bulletin, bulletin_table)


モデルの設定


モデルを使うためには、データベースを指定する必要があります。データベースの指定など、アプリケーションごとの設定はdevelopment.iniに記述します。development.iniの[app:main]セクションに次の一行を追加します。今回はデータベースにsqliteを使います。%(here)sはカレントディレクトリに置換されます。ここでは、c:¥tmp¥simplebbsにsimplebbs.dbというデータベースを作成します。

  sqlalchemy.url = sqlite:///%(here)s/simplebbs.db

次に、この設定ファイルを読み込んでSQLAlchemyのエンジンを初期化するコードを書きます。simplebbs/config/environment.pyの「import os」の後に次の行を追加します。

  from sqlalchemy import engine_from_config

エンジンを初期化するには,load_environment関数の最後に次の行を追加します。

  config["pylons.g"].sa_engine = engine_from_config(config, "sqlalchemy.")

アプリケーションをディプロイする場合やデバッグする場合に、データベースを初期化する必要があります。そのようなアプリケーションの下準備は、simplebbs/websetup.pyで行います。websetup.pyは次のように記述します。


simplebbs/websetup.py ---


"""Setup the simplebbs application"""
import logging


from paste.deploy import appconfig
from pylons import config


from simplebbs.config.environment import load_environment


log = logging.getLogger(__name__)


def setup_config(command, filename, section, vars):
    """Place any commands to setup simplebbs here"""
    conf = appconfig('config:' + filename)
    load_environment(conf.global_conf, conf.local_conf)
    
    import quickwiki.model as model


    log.info("Setting up database connectivity...")
    engine = config['pylons.g'].sa_engine
    log.info("Creating tables...")
    model.metadata.create_all(bind=engine)
    log.info("Successfully set up.")


次のコマンドを実行するとデータベースが作成されます。

  > paster setup-app development.ini
  >


データベースの作成が終わったらsimplebbs/lib/base.pyのBaseControllerを次のように変更してください。トランザクションの途中で問題が発生したときなど、データベースのセッション上に残ったゴミを片付けるおまじないです。

class BaseController(WSGIController):


    def __call__(self, environ, start_response):
        """Invoke the Controller"""
        # WSGIController.__call__ dispatches to the Controller method
        # the request is routed to. This routing information is
        # available in environ['pylons.routes_dict']
        try:
            return WSGIController.__call__(self, environ, start_response)
        finally:
            model.Session.remove()
 


ルーティング

ルーティングは、URLのパスをコントローラに結びつけるための仕組みです。PylonsではルーティングをRoutesモジュールによって提供しています。

ルーティングを制御するためには、simplebbs/config/routing.pyの# CUSTOM ROUTES HEREと書かれた行の下を変更します。今回は次のように変更します。

  map.connect(':action', controller='bbs', action='index')
  map.connect('*url', controller='template', action='view')

map.connectメソッドの第1引数はURLになります。「:contoller/:action/:id」となっている場合、:controllerはコントローラの名前、:actionはアクション名、:idはデータベースのレコードのIDなどにマップされます。map.connect(':action', controller='bbs', action='index')では、コントローラがsimplebbs/bbs.pyのBbsController、アクションが指定されていなければBbsControllerのindexメソッドがコールされます。アクションが指定されている場合は、BbsControllerの同名のメソッドがコールされます。

これだけの説明では、URLのルーティングパターンを複雑にしているだけのように感じるかもしれません。Routesモジュールの機能を使うと、プログラムの中ではURL生成の知識は必要なく、次のようにコントローラやアクション名を指定するだけでURLを生成できます。

 h.url_for(controller="bbs", action="save")

このメカニズムはアプリケーションが巨大になったり、URLが複雑になる場合にとても便利です。また、ディプロイ先の設定によってURLが変化する場合にも威力を発揮します。

コントローラ

ここまでに、モデルを定義してルーティングの設定を行いました。次に、コントローラを記述してモデルからデータを呼び出し、ビューにデータを渡します。今回はBbsControllerを作成します。コントローラを追加するには、次のコマンドを実行します。

  > paster controller bbs

コマンドを実行するとsimplebbs/controler/bbs.pyができています。プロジェクトをSubversionで管理している場合,bbs.pyを自動的にリポジトリに追加するという,面白い機能がPylonsにはあります。

bbs.pyにはBbsControllerクラスが定義されています。BbsControllerにはあらかじめ、indexメソッドが定義されています。indexメソッドは「Hello, World」という文字列を返します。では、「paster serve --reload development.ini」でサーバを起動してください。以降の操作はすべてサーバを起動し続けた状態で作業します。つまり、サーバを再起動する必要はありません。

次に、ブラウザでhttp://localhost:5000/にアクセスしてみます。最初の「Welcome」メッセージが表示されたままです。これは、simplebbs/public/index.htmlファイルがフィードされた結果です。
Pylonsでは,最初にsimplebbs/publicディレクトリ下にURLとマッチするファイルがないか検索します。そしてファイルが見つかればファイルをフィードします。見つからなければ、routes.pyで定義したコントローラにディスパッチされます。静的なファイルをフィードするかどうかは、WSGIのミドルウェアとして実装されています。アプリケーションのWSGIミドルウェアの設定は、simplebbs/config/middleware.pyに定義されています。

「Welcome」メッセージを消すためにsimplebbs/public/index.htmlを削除してください。ブラウザをリロードすると「Hello World」と表示されます。次にhttp://localhost:5000/indexにアクセスしてみてください。同様に「Hello World」と表示されるでしょうか。

それでは、実際にBbsControllerのindexメソッドを定義していきましょう。

indexメソッドでは一行掲示メッセージを一覧します。データベースにある全てのメッセージを一覧させると処理が重くなるため、最新のデータ20件だけを表示するようにします。今回は紙面の都合上、ページリストやユーザ認証は実装しません。

    def index(self):
        # 最新メッセージ20件のメッセージをデータベースから選択
        query = model.Session.query(model.Bulletin)
        order_query = query.order_by(sqlalchemy.sql.desc(model.Bulletin.id))
        c.messages = order_query[:20]
        
        # テンプレートのレンダリング結果を返す    
        return render("/main.mako")

まず、データベースにクエリを発行して最新20件のメッセージを取得しています。queryオブジェクトに対してorder_byメソッドをコールすることで、検索結果を並べ替えることができます。次に、検索結果をc.messageに格納しています。cオブジェクトはスレッドローカルの変数で、リクエストが到達してからレスポンスが返るまでの間生存しています。コントローラとビュー(テンプレート)の間のデータの受け渡しにcオブジェクトが使われます。cは辞書のようなオブジェクトです。

最後にrenderメソッドをコールして、テンプレートmain.makoでHTMLを作成しています。PylonsのデフォルトのテンプレートエンジンはMakoです。テンプレートエンジンにMakoを使えば、設定の変更なしに利用できます。Mako以外にもGenshiなど多くのテンプレートエンジンが使用できます。

Makoのテンプレートはsimplebbs/templatesディレクトリに作成します。今回はmain.makoという名前で次のように作成してください。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
  "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html>
  <head>
    <title>Simple BBS</title>
  </head>
  <body>
    <h1>Simple BBS</h1>
    <hr/>
    <div class="content">
      % for message in c.messages:
          <div class="message">
            <span class="message_header">${message.name | h} (${message.created})</span><br/>
            <span class="message_body">${message.content | h}</span>
          </div>
      % endfor
    </div>
  </body>
</html>


ブラウザでアクセスすると「Simple BBS」というタイトルだけが表示されます。次に同じテンプレートに次の入力フォームを追加してみましょう。フォームでは、名前を入力するフィールドと1行のメッセージを入力するフィールドの二つだけがあります。

    <form action="save" method="post">
      Name: <input name="name" type="text" size="20" /><br/>
      Comment: <input name="content" type="text" size="40" /><br/>
      <input type="submit" value="save" />
    </form>

次にコントローラに送信されたデータを保存するコードを追加します。BbsControllerにsaveメソッドを追加します。

    def save(self):
        # requestからCGIパラメータの抽出
        name = request.params.get("name")
        content = request.params.get("content")
        
        # フィールドが空の場合はエラーメッセージを表示
        if not name or not content:
            c.error = u"nameとmessageを入力してください"
            return self.index()
        
        try:
            # データベースに保存する
            bulletin = model.Bulletin()
            bulletin.name = name
            bulletin.content = content
            
            model.Session.save(bulletin)
            model.Session.commit()
        except Exception, e:
            # エラーが発生したらロールバックする
            log.error(e)
            model.Session.rollback()
            c.error = e.message
        
        return self.index()

saveメソッドは大きく3つのパートに分かれます。一つはリクエストからCGIパラメータを抽出するところ、次に、データをバリデーションするパート、最後にデータベースに保存する処理です。

CGIパラメータはrequestオブジェクトのparamsフィールドに保存されています。requestオブジェクトはスレッドローカルな変数で、コントローラの中からアクセスできます。paramsからパラメータの値を取得する関数は二つあります。一つはget(キー)で、CGIパラメータの最初の値を返します。もう一つがgetall(キー)で、CGIパララメータのすべての値を配列で返します。今回は値が一つだけなので、getを使っています。

次のバリデーションでは、nameとcontent両方に値があるかどうかだけをチェックしています。複雑なバリデーションや素敵なエラーメッセージを表示したい場合はFormEncodeモジュールを使うと便利です。TurboGearsやDjangoなどのより上位のフレームワークは統一的なバリデーションやエラーハンドリングの仕組みを提供しています。

最後にデータベースにデータを保存して、もう一度,一覧を表示しています。

saveメソッドの中では,バリデーションエラーが発生したときにc.errorにエラーメッセージをセットしています。しかし、テンプレートの中ではc.errorを表示していません。formタグの中に,次のようなコードを追加して,エラーを表示できるようにします。

      % if c.error:
        <div class="error">!!! ERROR !!! ${c.error | h}</div>
      % endif

さて、ブラウザからnameやcontentを空にしてSaveボタンを押してください。「!!! ERROR !!! nameとmessageを入力してください」と黒い文字で表示されるだけです。サンプルとしてもちょっと寂しすぎるので、少しだけCSSをあててみます。まずは、テンプレートからCSSを読み込めるように、main.makoのHTMLヘッダーに次の行を追加します。

  ${h.stylesheet_link_tag('/simplebbs.css')}

次に、CSSファイルを作成します。simplebbs/public/simplebbs.cssを次のように作成します。

.error {
    color: red;
}


.message {
    padding-top: 1em;
}


.message_header {
    font-weight: 700;
    padding-bottom: 1em;
}


.message_body {
    font-size: 120%;
}

これで、少しだけブラウザからの見栄えが良くなりました。

今回は、ブラウザからのレコードの削除や更新処理は行いませんが、保存とほとんど同じ処理で実現できます。興味のある方は、自分で実装してみてはどうでしょうか?

PylonsとAjax

最近のWebアプリケーションの多くがAjaxを利用してユーザにリッチな操作性を提供しています。Pylonsでは、お好きなAjaxのJavaScriptライブラリが利用できます。

Ajaxを利用する場合,サーバからのデータのフェッチはjson形式で行うことが多いです。json形式に変換するモジュールにsimplejsonがあります。simplejsonでは、辞書や配列、文字列や数字などをjson形式に変換できます。残念ながらSQLAlachemyでのクエリ結果など、Pythonオブジェクトをそのまま変換することはできません。自分でsimplejsonが変換できる形式にコンバートしてください。

Pylonsでは、simplejsonをさらに扱いやすいようにする機能をデコレータとして提供しています。デコレータとは、関数に対するフィルタを設定する仕組みです。まず、Pylonsのjson出力用のデコレータを読み込みます。

  from pylons.decorators import jsonify

次にコントローラのメソッドを追加します。今回はmessagesという名前のメソッドをBbsControllerに追加します。messagesメソッドは、indexメソッドとほぼ同じです。

    def messages(self):
        # 最新メッセージ20件のメッセージをデータベースから選択
        query = model.Session.query(model.Bulletin)
        order_query = query.order_by(sqlalchemy.sql.desc(model.Bulletin.id))
        messages = order_query[:20]
        
        # メッセージをsimplejsonが理解できる形式に変換
        return [dict(created=m.created.ctime(), name=m.name, content=m.content) for m in messages]

そして、def messagesの前の行に@jsonifyをつけてください。ブラウザでhttp://localhost:5000/messagesにアクセスするとjson形式で表示されます。

おわりに


1行掲示板を作りながら駆け足でPylonsの仕組みを説明してきましたが、どうだったでしょうか?Pylonsはとらえどころが難しいフレームワークですが、DjangoやTurboGearsに比べて"薄い"フレームワークです。TurboGearsやDjangoに比べて,Pylonsは自分の好きな部品を組み合わせる上での自由度が高いです。また、今回説明した内容は、Pylonsに限らずGoogle App Engineの開発にも役に立つはずです。

Python自体は欧米では人気のある言語で、Googleでも採用されています。言語仕様を覚えるだけでなく、なぜそのような言語仕様になっているかを考えるだけでも、プログラミングのスキルが上がると思います。ぜひ一度、試してみてください。

今回作成したサンプルコードはここからダウンロードできます。
ċ
simplebbs.zip
(20k)
Hiroki Ohtani,
2009/09/20 6:49
Comments