もうちょっとだけがんばってみた

分かったのは
root.py の RootController にexposeで修飾したメソッドを追加すると、そのメソッドをurlで指定できる。
クラス変数で他のコントローラを設定しておくと、その名前とメソッドをurlで指定できる。
文で分かりやすく書けないのだが、

class RootController(TurboGearsController):
        
    hello = Tg2HelloController()
    
    @expose('tutorial.templates.index')
    def index(self):
        from datetime import datetime
        return dict(now=datetime.now())

こうすると、/hello/ でアクセスできる。

...CherryPyをエミュレートしている?
とりあえず以前のTGと同じようにコントローラーのツリーを構成できるようだ。

TurboGears2を予習してみる。

以下の内容は、Subversionのtrunkから取得したコードで試しています。

http://svn.turbogears.org/trunk/INSTALL.txt を見ると、PylonsTurboGearsをそれぞれSuversionのtrunkからとってくるように書いてある。

    $ svn co http://pylonshq.com/svn/Pylons/trunk pylons
    $ svn co http://svn.turbogears.org/trunk tg2

and install TurboGears 2 in development mode::

    $ python ./pylons/setup.py develop
    $ python ./tg2/setup.py develop
    

でもこのままやるとPasteScriptでVersionConflictしますから。
Pylonsが0.9.6っていうバージョンで入るけど、tg2のcommandが、'Pylons==dev,<0.9.6' こんな指定の仕方。
直接Subversionから入れるとdevになるようなので、そっちの方法で。

    $ easy_install http://pylonshq.com/svn/Pylons/trunk pylons

さて、ベースのMVCフレームワークCherryPyからPylonsに移行することになるらしいTurboGears2.0。
quickstartはPasteScriptのコマンドになります。

$ paster quickstart tutorial

ez_setup がねーぞとエラーになります。orz
まあ、終盤でエラーになるし、プロジェクトディレクトリ内はそれなりなので、気にしないことにします。
とりあえずidentityオプションを有効にしときました。
が、modelsパッケージ内には特にそんなものは生成されていない模様。
paste のテンプレートを除いてもそれらしきテンプレートは見つからないので、まだ実装されていないのでしょう。

さて、フレームワークをまとめあげるメガフレームワークであるところのTurboGearsですが2.0になってどんなフレームワークの選択になったのでしょう。
paster tginfo で依存するモジュール一覧と現在インストールされているバージョンを確認できました。
requiresだけだと以下のようなもの。

* TurboGears2 2.0a1dev-r3239
* SQLAlchemy 0.3.7
* Genshi 0.4.2
* Pylons 0.9.6dev-r2126
* Mako 0.1.8
* nose 0.9.3
* decorator 2.0.1
* simplejson 1.7.1
* FormEncode 0.7.1
* PasteScript 1.3.5
* PasteDeploy 1.3.1
* Paste 1.4
* Beaker 0.7.4
* WebHelpers 0.3.1dev-r2064
* Routes 1.7
* Beaker 0.7.4
* PasteDeploy 1.3.1
* simplejson 1.7.1
* Routes 1.7

うむ。いかにもon Pylons
SQLAlchemyとGenshiは前評判どおりの内容。
FormEncodeは続投。
Pylonsがベースになったので、CherryPyが脱落。
って、Makoはrequiresなの?
ユニットテストはnoseになっていくのかなぁ
と、いろいろ思いをはせることはできますが、不思議なのはElixirがrequiresじゃない?
template内にはElixirの文字が見えるのだが...
まだ本採用ではないのかな。

じゃあ、動かしてみるか。

$ paster serve development.ini

PylonsTurboGearsもtrunk版なのが原因なのか、がんがんWarningが出る。
でも、とりあえず動いている。
デフォルトのトップページはTurboGearsを使っている人には、見慣れた画面。
さて、コントロールを追加してみるか。
controllers以下にそれぞれモジュールになっている。
pylonsと同じように、paster controllerでいいのだろうか。
root.pyを覗いてみると、RootControllerはTurboGearsControllerを継承している。
pasterのヘルプには、TurboGears2でコントローラー用のコマンドはないようなのだ。
とりあえず、Pylons的にコントローラーを作ってみると、BaseControllerを継承したものになった。
ところで、TurboGearsControllerって普通のとどう違うんだろう。
WSGIControllerを継承している。
出所はpylonsのcontrollers。
要するに、WSGI spec の__call__を実装したコントローラーのようだ。
BaseControllerもWSGIControllerを継承しているから正しい?
まあ、これはPylonsのまんまでやるってことかなー。
コントローラーはデフォルトのままにしておいて、ディスパッチャに設定してみる。
途中、Elixirインストールしてないせいでエラーになる。
templateに書くなら、requiresに入れてください。orz

この辺でやめとこう。

setuptoolsを使ってコマンドツールを作る

pasterでsetuptoolsを使った最小プロジェクトを作成する。

paster create hello

setup.pyを編集。
コマンドツールのスクリプトを追加する。
ここでは、helloモジュールのhello関数をpyhelloという名前のコマンドにする。
entory_pointsにconsole_scriptsセクションを作りその中に追加する。

from setuptools import setup, find_packages
import sys, os

version = '0.0'

setup(name='hello',
      version=version,
      description="",
      long_description="""\
""",
      classifiers=[], # Get strings from http://www.python.org/pypi?%3Aaction=list_classifiers
      keywords='',
      author='',
      author_email='',
      url='',
      license='',
      packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
      include_package_data=True,
      zip_safe=False,
      install_requires=[
          # -*- Extra requirements: -*-
      ],
      entry_points="""
      # -*- Entry points: -*-
      [console_scripts]
      pyhello = hello:hello
      """,
      )

setup.pyを変更したら、EGG_INFOを更新する。

python setup.py egg_info


hello.pyの中身。
普通のコマンドラインスクリプト

#
import sys
def hello():
    if len(sys.argv) > 1:
        print "Hello, %s!" % sys.argv[1]
    else:
        print "Hello, world!"

installしてもいいけど、developを使えば今編集しているソースツリーをsite-packagesから参照するようになる。

python setup.py develop

コマンドはプラットフォームごとに適当な方法で作成される。
Windowsなら、c:\Python25\Scripts 以下にpyhello.exeができているはず。
c:\Python25\Scripts にPATHを通してあるならそのまま pyhello と入力すれば実行できる。

いろんなもの使ってSBM その2 データアクセス

さて、ブックマークを保存する部分を考えよう。

深く考えずに、ひとまずDBに入れることにする。



データアクセスにはSQLAlchemyを使う。

オブジェクトをテーブルをまたいでマッピングできたり、継承構造をマッピングする方法を選択できたりと柔軟なO/Rマッパーだ。


DB情報保持のためのモジュールを先に作っておこう。

# database.py
from sqlalchemy import *
from sqlalchemy.ext.sessioncontext import SessionContext
meta = DynamicMetaData()
context = SessionContext(create_session)

metaはDBに関する情報を持っている。
contextはスレッドローカルなセッションを保持していて、実際のセッションにはcontex.current でアクセスできる。
どちらもグローバルに使用できるように作られていて、プログラム中では、直接この2つを扱えばいい。



ここで、モデルを考えよう。

主なモデルは2つブックマークとユーザーだ。

ユーザーはブックマークを複数持っている。

ここで、ブックマークはユーザーが自由に入力したものとして扱い、複数ユーザーで共有することはないものとする。

つまり1:N関連となるわけだ。

あとは最近のソーシャルブックマークから適当に引っ張ってこよう。

タグ付けは必須機能と言っていいほど、どのSBMも持っている機能だ。

また、ブックマークにメモ書きできる。

ラベルはユーザー共通としておこう。


u = User()

b = Bookmark()

l = Label()



u.name

u.bookmarks

u.labels

u.password



b.title

b.url

b.memo

b.user

b.labels



l.name

l.bookmarks

ラベルの識別子はラベル名(name)、bookmarkはユーザーごとにurlが識別子、ユーザーはユーザー名が識別子となる。

label = Label.byName(labelname)

user = User.byName(username)

bookmark = user.bookmarkByUrl(url)

bookmarks = user.selectBookmarkByLabel(label)

Label.byNameでは該当するラベルがなかった場合は、その名前で作成したラベルを作ってしまえばいい。

user.bookmarkByUrlも同様に考えることにしよう

としても、既存のものかどうかは知りたいと思うので、それを調べるメソッドも考えよう。

Label.exists(labelname)

User.exists(username)

user.hasBookmark(url)

さてこいつらをテーブルにマッピングしよう。

基盤として、クラスをそのままテーブルに割り当ててみる

from database import meta

user_table = Table("User", meta)

bookmark_table = Table("Bookmark", meta)

label_table = Table("Label", meta)

それぞれに機械的なIDをつける

user_table = Table("User", meta,
                                  Column("user_id", Integer, primary_key=True))
bookmark_table = Table("Bookmark", meta,
                                  Column("bookmark_id", Integer, primary_key=True))
label_table = Table("Label", meta)
                                  Column("label_id", Integer, primary_key=True))

関連付けのために外部キーを付ける

user_table = Table("User", meta,
                                  Column("user_id", Integer, primary_key=True))
bookmark_table = Table("Bookmark", meta,
                                  Column("bookmark_id", Integer, primary_key=True),
                                  Column("user_id", Integer, ForeignKey("user.user_id")))
label_table = Table("Label", meta,
                                  Column("label_id", Integer, primary_key=True))

N:M関連のためにブリッジテーブルを追加する

label_bookmark_table = Table("label_bookmark", meta,
                                  Column("label_id", Integer, ForeignKey("label.label_id")),
                                  Column("bookmark_id", Integer, ForeignKey("bookmark.bookmark_id")))

識別子の列を追加する

user_table = Table("User", meta,
                                  Column("user_id", Integer, primary_key=True),
                                  Column("name", String, unique=True))
bookmark_table = Table("Bookmark", meta,
                                  Column("bookmark_id", Integer, primary_key=True),
                                  Column("user_id", Integer, ForeignKey("user.user_id")),
                                  Column("url", String))
bookmark_table.append_constraint(UniqueConstraints("user_id", "url"))
label_table = Table("Label", meta,
                                  Column("label_id", Integer, primary_key=True),
                                  Column("name", Unicode, unique=True))

追加の情報について列を追加

bookmark_table = Table("Bookmark", meta,
                                  Column("bookmark_id", Integer, primary_key=True),
                                  Column("user_id", Integer, ForeignKey("user.user_id")),
                                  Column("url", String),
                                  Column("title", Unicode),
                                  Column("memo", Unicode))
bookmark_table.append_constraint(UniqueConstraints("user_id", "url"))

マッピング
assign_mapperを使って少し楽に使えるようにしておこう。

#database.py
from sqlalchemy import *
from sqlalchemy.ext.sessioncontext import SessionContext
from sqlalchemy.ext.assignmapper import assign_mapper

meta = DynamicMetaData()
context = SessionContext(create_session)
def assgin(*args, **kwargs):
    return assign_mapper(context, *args, **kwargs)

とりあえずそれぞれのクラス定義をしておく

class Bookmark(object):
    pass

class Label(object):
    pass

class User(object):
    pass

クラスとテーブルをマッピング

assgin(User, user_table)
assign(Bookmark, bookmark_table)
assign(label, label_table)

関連をマッピング

assign(Bookmark, bookmark_table,
       properties=dict(user=relation(User, 
                                     backref="bookmarks"),
                       labels=relation(Label, 
                                       secondary=label_bookmark_table,
                                       backref="bookmarks"))

Paste 1.4リリース

http://pythonpaste.org/news.html#id1
WSGIベースのWebアプリケーションを作るのに便利なツール集Pasteの最新バージョン1.4が昨日リリースされたようです。
PasteDeploy, PasteScriptは変わっていない模様。