hama_duのブログ

ノンジャンル記事置き場

Google App Engine用フレームワークtipfyの紹介とチュートリアル

このエントリについて

Google App Engine用に作られたフレームワーク「tipfy」の使い勝手がよかったので
チュートリアル形式で簡単に紹介をしたいと思います。
先日エントリにした「するめダイアリー」もGAE/Python + tipfyで作ってます。

おことわり

ちなみに、当方の開発環境は、

  • Max OS 10.6
  • GoogleAppEngineLauncher
  • TextMate

なので、説明もこれに準じたものになってます。

また、

に関しては説明を省略させていただきます。

追記しました(12/10)

  • テンプレートエンジン jinja2 について
  • エントリのデータストアモデルの変更

導入

tipfyのダウンロード

公式サイト
http://www.tipfy.org/
の右側、All-in-one packをダウンロードします。現時点での最新バージョンは0.6.4です。
ダウンロード・解凍が終わったら「project」フォルダを適当な場所に配置・リネームします。
これがプロジェクト全体のフォルダになります。(このパスを「/path/to/project」とします。)

GoogleAppEngineLauncherでプロジェクトをインポート

GoogleAppEngineLauncherを起動し、「Add Existing Application」を選択。
Pathに「/path/to/project/app」を指定します。以降、説明でパスを指定する時はこのフォルダが基準になります。
これで開発を進める準備は完了です。さっそく、左上の「Run」ボタンを押し、
ブラウザで「http://localhost:8080/pretty」にアクセスしてみましょう。
中央に大きく水色で「Hello, World」と表示されたらセットアップは完了です。簡単でしょ?

今回作るもの

早速、開発を進めたいと思います!
今回は例として、簡単なブログシステムを作りたいと思います。

作る画面(機能)一覧。

  • ブログのエントリ一覧
  • 投稿、更新、削除
  • エントリ詳細

データストアモデルの作成(12/10編集)

lib の下に datastore という名前のフォルダ*1を作成します。
そのフォルダの下に

  • __init__.py
  • model.py

の二つのファイルを作成します。
__init__.pyはPythonにここがパッケージのあるディレクトリであることを教えるためのものです。
model.pyには以下のようにモデルを定義します。

from google.appengine.ext import db

class Entry(db.Model):
  title = db.StringProperty(required=True)
  content = db.TextProperty()
  created_at = db.DateTimeProperty(auto_now_add=True)
  updated_at = db.DateTimeProperty(auto_now=True)

ブログのエントリを表すEntryモデルを定義しました。

  • エントリのタイトル
  • 内容
  • 作成日時
  • 更新日時

これがあればとりあえず十分でしょうか。
作成日時には、作成したら自動で現在時刻をつけてくれるオプション「auto_now_add」、
更新日時には、データを更新したら自動で現在時刻をつけてくれるオプション「auto_now」を指定しています。

モデル定義の方法、使えるタイプなどについては
http://code.google.com/intl/ja/appengine/docs/python/datastore/entitiesandmodels.html
http://code.google.com/intl/ja/appengine/docs/python/datastore/typesandpropertyclasses.html
などが詳しいです。

管理者用アプリの作成

一覧機能…を作る前に

さっそく、エントリ一覧機能を作りたい所なのですが、
まだ機能を作ってもデータストアの中身が空っぽなので一覧機能の動作確認ができません。
なので、ここは管理者用のモジュールを別に作って、
そこにテストデータをデータストアに入れる処理を書いていこうと思います。

アプリの追加

apps以下に internal という名前のフォルダを追加します。
既にサンプルとして hello_world というアプリが入っている*2ので、それをコピペするのが楽かと思います。

中身は以下の3つのファイルになっています。

  • __init__.py
  • handlers.py
  • urls.py

開発に入る前に、それぞれのファイルの役割を詳しく説明します。

ルーティング -> リクエストハンドラ -> テンプレート

リクエストハンドラ

handlers.py にはリクエストを受けた時の処理内容を書きます。中身はこんな感じになってるはずです。

from tipfy import RequestHandler, Response
from tipfy.ext.jinja2 import render_response

class HelloWorldHandler(RequestHandler):
    def get(self):
        """Simply returns a Response object with an enigmatic salutation."""
        return Response('Hello, World!')


class PrettyHelloWorldHandler(RequestHandler):
    def get(self):
        """Simply returns a rendered template with an enigmatic salutation."""
        return render_response('hello_world.html', message='Hello, World!')

RequestHandlerを拡張したHelloWorldHandler、PrettyHelloWorldHandlerというクラスが書かれています。
リクエストを受け取ったら、〜〜する、という処理内容が
def get(self):
の中に記述されています。たとえば、HelloWorldHandler であったら、

  • 中身が 'Hello, World!' となるレスポンスを返す

ようになっていて、PrettyHelloWorldHandlerであったら、

  • テンプレート 'hello_world.html' を message='Hello, World!' であるコンテキストを元に解釈して返す

という感じになっています。

テンプレート

テンプレート 'hello_world.html' は templates直下にあって、

<html>
    <head>
        <title>{{ message|e }}</title>
        <style type="text/css">
            /* 省略 */
        </style>
    </head>
    <body>
        <h1>{{ message|e }}</h1>
    </body>
</html>

こんなファイルになっています。ここで 「{{ }}」で囲まれた部分が、
コンテキストに設定した変数の内容を出力するようになっていて、
例えば、message='Hello, World!' のとき、{{ message }} ならば、
Hello, World!
と出力されます。

(12/10追記)
これはtipfyの機能ではなくて、jinja2 というテンプレートエンジンの機能です。
イメージ的には、tipfyが内部でjinja2を呼び出して使っている、といった感じです。
このようにテンプレートエンジンを使えば、htmlの動的生成がグッと楽になります。
jinja2の公式サイトはこちらにあります。↓
http://jinja.pocoo.org/

テンプレートのエスケープ処理

先ほどのファイル 'Hello, World!' には、

<h1>{{ message|e }}</h1>

という部分がありました。{{ message }} の横についている 「|e」とは何者なのか?
これはXSS*3対策のためのエスケープ処理を行っている部分です。
試しにテンプレートの

<h1>{{ message|e }}</h1>

この部分を

<h1>{{ message }}</h1>

として、hello_world/handlers.pyで

        return render_response('hello_world.html', message='Hello, World!')

の部分を

        return render_response('hello_world.html', message='Hello,<br/> World!')

で置き換えてみて、
http://localhost:8080/pretty
にアクセスしてみてください。エスケープ処理が行われず、改行が入っていることが確認できるかと思います。

ルーティング処理

説明が戻りますが、urls.pyを開いてみてください。
次のような内容になっているはずです。

from tipfy import Rule

def get_rules(app):
    rules = [
        Rule('/', endpoint='hello-world', handler='apps.hello_world.handlers.HelloWorldHandler'),
        Rule('/pretty', endpoint='hello-world-pretty', handler='apps.hello_world.handlers.PrettyHelloWorldHandler'),
    ]
    return rules

ここには、どのリクエストのURLが来た場合、どのリクエストハンドラを実行するか、
ということが書かれています。例えば、URLが
http://(ドメイン)/pretty
だった場合は、hello_worldモジュールのPrettyHelloWorldHandlerを実行するようになっています。
このrulesに、上記の例に倣ってRuleを追加していくことでルーティングのルールを増やせます。

管理者用アプリの作成(続き)

リクエストハンドラの作成

apps/internal/handlers.pyに以下の内容を追加します。

from tipfy import RequestHandler, Response
from tipfy.ext.jinja2 import render_response
from datastore.model import *

class AddEntryHandler(RequestHandler):
  def get(self):
    for i in range(10):
      entry_model = Entry(
        title = "entry " + str(i),
        content = "content\ncontent\ncontent",
      )
      entry_model.put()
    return Response('Done!')
ルーティング処理

apps/internal/urls.pyに以下の内容を追加します。

def get_rules(app):
    rules = [
        Rule('/internal/addentry', endpoint='internal/addentry', handler='apps.internal.handlers.AddEntryHandler'),
    ]
    return rules
アプリの追加

config.pyにアプリ「internal」を追加します。
これをしないとアプリが認識されません。

    'apps_installed': [
        'apps.hello_world',
        'apps.internal',
    ],
実行

urls.pyに書いた通り、
http://localhost:8080/internal/addentry
にアクセスして実行させます。Done! が表示されたら成功!
SDK Consoleを表示させ、Datastore Viewerをのぞいてデータが追加されているか確かめてみましょう!

データストアの一括削除

SDK Consoleには残念ながらデータストアの中身をまっさらに消すツールはついてきません。
なので、これも internalアプリに追加しておくことをお勧めします。

apps/internal/handlers.pyに以下の内容を追加します。

class InitHandler(RequestHandler):
  def get(self):
    for entry_model in Entry.all():
      entry_model.delete()
    return Response('Done!')

もちろん、ルーティング設定も忘れずに行います。

  rules = [
        Rule('/internal/addentry', endpoint='internal/addentry', handler='apps.internal.handlers.AddEntryHandler'),
        Rule('/internal/init', endpoint='internal/init', handler='apps.internal.handlers.InitHandler'),
    ]
実行

http://localhost:8080/internal/init
にアクセスして実行させます。Datastore Viewerを見てデータが消えていたら成功!

今回はここまで。

次回以降はエントリ一覧機能から作ります。

*1:実際はどんな名前でもよい

*2:最初にHello, Worldを表示させたあれです

*3:分からない人はググってください