コントローラ

MVC パラダイムでは、 コントローラ は入力を解釈し、モデル and/or ビューに対して適切に変化するように命令します。 Pylons の下ではこ の概念はわずかに拡張されています。Pylons コントローラは直接クライアント 要求を解釈するのではなく、モデルからデータを集め、正しいテンプレートで それをレンダリングする適切な方法を決定するために振る舞います。

コントローラはユーザからのリクエストを解釈して、そのリクエストを実現す るために必要に応じてモデルとビューの一部を呼びます。したがって、ユーザ がウェブリンクをクリックしたり HTML フォームを送信したりするとき、コン トローラ自体は何も出力せず、実際の処理は何も実行しません。 それはリクエ ストを受け取って、どのモデルの部品を呼び出すか、そして、結果として起こ るデータにどのフォーマットを適用したらよいかを決定します。

Pylons はクラスを使用します。その親クラスは WSGI インタフェース を提供し、サブクラスがアプリケーション固有のコントローラロジックを実装 します。

Pylons WSGI コントローラは Pylons WSGI アプリケーション PylonsApp からディスパッチされて来るウェブリク エストを扱います。

これらのリクエストによって WSGIController の新しいインスタンスが 作られます。それは次に、 Routes マッチからの dict オプションと共に呼ば れます。そして、 WSGI 仕様に従って start_response が呼ばれるとともに標 準のWSGI レスポンスが返ります。

Pylons コントローラが実際に WSGI インタフェースで呼ばれるので、通常の WSGI アプリケーションもまた Pylons コントローラ になることができます。

標準のコントローラ

標準のコントローラは、ウェブ開発者がサブクラス化することを意図しています。

メソッドをプライベートに保つ

デフォルトのルーティングはあらゆるコントローラとアクションをマッピング するので、おそらくコントローラメソッドのいくつかを URL から呼べないよう にしたいと思うでしょう。

Routes は、プライベートなメソッドを _ で始めるという Python のデフォ ルト規約を使用します。クラスの edit_generic メソッドを隠すためには、 単に _ で始まるように名前を変更するだけで十分です。

class UserController(BaseController):
    def index(self):
        return "This is the index."

    def _edit_generic(self):
        """I can't be called from the web!"""
        return True

特殊メソッド

以下の特殊コントローラメソッドを定義することができます:

__before__
このメソッドは、アクションが実行される前に呼ばれます。変数/オブジェ クトをセットアップしたり、他のアクションへのアクセスを制限したり、 またはアクションが呼ばれる前に実行すべき他のタスクのために使用でき ます。
__after__
このメソッドは、予期しない例外が raise されない限り、アクションが実 行された後で実行されます。 HTTPException のサブ クラス (例えば redirect_toabort で raise されるもの) は 予期された例外です。従ってリダイレクトされた場合も __after__ は 呼ばれます。

コントローラを動的に追加する

アプリケーションをリスタートすることなしにコントローラを加えることは可 能です。そのためには、 Routes にコントローラディレクトリを再スキャンす るように伝えます。

新しいコントローラは、コマンドラインから paster コマンドを用いるか (同 時にテストハーネスのファイルも作成されるのでおすすめです) 、またはコン トローラファイルを作成する他の手段で追加することができます。

Routes がコントローラディレクトリに存在している新しいコントローラを認識 するように、 Routes がディレクトリを再スキャンすべきことを示すための内 部フラグを切り換えます:

from routes import request_config

mapper = request_config().mapper
mapper._created_regs = False

次回のリクエストのときに Routes が controllers ディレクトリを再スキャン し、パスの動的部分に :controller を使っているルートが新しいコントロー ラにマッチするようになります。

WSGI アプリケーションを接続する

Note

このレシピは WSGI Specification (PEP 333) の基本的なレベルに馴染み があることを仮定しています。

WSGI は Pylons を深く貫いており、アーキテクチャの多くの部分に存在してい ます。 Pylons コントローラが実際に WSGI インタフェースで呼ばれるので、 通常の WSGI アプリケーションもまた Pylons ‘コントローラ’ になることがで きます。

オプションで、もし完全な WSGI アプリケーションをマウントして URL の残り の部分を処理させたいなら、 Routes は自動的に URL の正しい部分を SCRIPT_NAME に移動することができます。これによって WSGI アプ リケーションが適切に PATH_INFO 部分を処理できるようになります。

このレシピは、基本的な WSGI アプリケーションを Pylons コントローラとし て加えることを実演します。

Pylons プロジェクトディレクトリに新しいコントローラファイルを作成してく ださい:

$ paster controller wsgiapp

これは、他の WSGI アプリケーションを使用する際に利用したいであろう基本 的な インポートをセットアップします。

このようにコントローラを編集してください:

import logging

from YOURPROJ.lib.base import *

log = logging.getLogger(__name__)

def WsgiappController(environ, start_response):
    start_response('200 OK', [('Content-type', 'text/plain')])
    return ["Hello World"]

他の WSGI アプリケーションを接続するとき、それはこのコントローラを得る ために使用された URL の部分が SCRIPT_NAME に移動されているこ とを期待します。このコントローラのためのマップルートが config/routing.py ファイルに追加されるなら、 Routes は environ を 適切に調整することができます。

# CUSTOM ROUTES HERE

# Map the WSGI application
map.connect('wsgiapp/{path_info:.*}', controller='wsgiapp')

path_info 変数を指定することによって、 Routes は path_info に leading up to するすべてを SCRIPT_NAME に入れて、残りは PATH_INFO に入るでしょう。

WSGI サービスを提供するために WSGI コントローラを使用する

Pylons WSGI コントローラ

Pylons 自身の WSGI コントローラは、呼び出しと値の返却のために WSGI 仕様 に従います。

Pylons の WSGI コントローラは PylonsApp からディスパッチされて来る ウェブリクエストを扱います。これらのリクエストによって WSGIController の新しいインスタンスが作成されます。次に、 Routes マッ チからの dict オプションを伴って呼ばれます。そして、 WSGI 仕様に従っ て:meth:start_response が呼ばれ、標準の WSGI 応答を返します

WSGIController のメソッド

WSGIController の以下の特殊メソッドを定義することができます:

__before__
このメソッドは、アクションが実行される前に実行されます。変数/オブジェ クトをセットアップしたり、他のアクションへのアクセスを制限したり、 またはアクションが呼ばれる前に実行すべき他のタスクのために使用でき ます。
__after__
アクションが実行された後で実行されるメソッドです。このメソッドは、 たとえ例外が上がっても、リダイレクトしても、他のメソッドが呼ばれた 後に 必ず 呼ばれます。

(訳注: 特殊メソッド と重複している?)

呼ばれる各アクションは、 _inspect_call() で inspect されて Routes の match dict の中から必要な値だけが引数として渡されます。アクショ ンに渡される引数は _get_method_args() 関数をオーバーライドするこ とでカスタマイズできます。この関数は dict を返すことが期待されます。

リクエストを扱うアクションが見つからない場合、コントローラはデバッグモー ドでは “Action Not Found” エラーを raise します。デバッグモードでなけれ ば 404 Not Found エラーが返されます。

RESTful API で REST コントローラを使う

paster restcontroller テンプレートを使う

$ paster restcontroller --help

REST Controller とそれに付属する機能テストを作成してください。

RestController コマンドは REST ベースのディスパッチング resource() と共に使用される、 REST ベースのコ ントローラファイルを作成します。このテンプレートには resource() がディスパッチするメソッドと、その メソッドがいつ呼ばれるか明確にするための docstring が含まれています。

最初の引数は REST リソースの単数形であるべきです。 2番目の引数はその単 語の複数形です。 それが入れ子になったコントローラなら、以下の 2 番目の 例に示されるように、ディレクトリ情報をその前に入れてください。

使用例:

yourproj% paster restcontroller comment comments
Creating yourproj/yourproj/controllers/comments.py
Creating yourproj/yourproj/tests/functional/test_comments.py

コントローラをディレクトリの下に置きたければ、単にコントローラ名にパス を含めてください。そうすれば必要なディレクトリが作成されます:

$ paster restcontroller admin/trackback admin/trackbacks
Creating yourproj/controllers/admin
Creating yourproj/yourproj/controllers/admin/trackbacks.py
Creating yourproj/yourproj/tests/functional/test_admin_trackbacks.py

Atom スタイルのユーザ REST コントローラ

# From http://pylonshq.com/pasties/503
import logging

from formencode.api import Invalid
from pylons import url
from simplejson import dumps

from restmarks.lib.base import *

log = logging.getLogger(__name__)

class UsersController(BaseController):
    """REST Controller styled on the Atom Publishing Protocol"""
    # To properly map this controller, ensure your
    # config/routing.py file has a resource setup:
    #     map.resource('user', 'users')

    def index(self, format='html'):
        """GET /users: All items in the collection.<br>
            @param format the format passed from the URI.
        """
        #url('users')
        users = model.User.select()
        if format == 'json':
            data = []
            for user in users:
                d = user._state['original'].data
                del d['password']
                d['link'] = url('user', id=user.name)
                data.append(d)
            response.headers['content-type'] = 'text/javascript'
            return dumps(data)
        else:
            c.users = users
            return render('/users/index_user.mako')

    def create(self):
        """POST /users: Create a new item."""
        # url('users')
        user = model.User.get_by(name=request.params['name'])
        if user:
            # The client tried to create a user that already exists
            abort(409, '409 Conflict',
                  headers=[('location', url('user', id=user.name))])
        else:
            try:
                # Validate the data that was sent to us
                params = model.forms.UserForm.to_python(request.params)
            except Invalid, e:
                # Something didn't validate correctly
                abort(400, '400 Bad Request -- %s' % e)
            user = model.User(**params)
            model.objectstore.flush()
            response.headers['location'] = url('user', id=user.name)
            response.status_code = 201
            c.user_name = user.name
            return render('/users/created_user.mako')

    def new(self, format='html'):
        """GET /users/new: Form to create a new item.
            @param format the format passed from the URI.
        """
        # url('new_user')
        return render('/users/new_user.mako')

    def update(self, id):
        """PUT /users/id: Update an existing item.
            @param id the id (name) of the user to be updated
        """
        # Forms posted to this method should contain a hidden field:
        #    <input type="hidden" name="_method" value="PUT" />
        # Or using helpers:
        #    h.form(url('user', id=ID),
        #           method='put')
        # url('user', id=ID)
        old_name = id
        new_name = request.params['name']
        user = model.User.get_by(name=id)

        if user:
            if (old_name != new_name) and model.User.get_by(name=new_name):
                abort(409, '409 Conflict')
            else:
                params = model.forms.UserForm.to_python(request.params)
                user.name = params['name']
                user.full_name = params['full_name']
                user.email = params['email']
                user.password = params['password']
                model.objectstore.flush()
                if user.name != old_name:
                    abort(301, '301 Moved Permanently',
                          [('Location', url('users', id=user.name))])
                else:
                    return

    def delete(self, id):
        """DELETE /users/id: Delete an existing item.
            @param id the id (name) of the user to be updated
        """
        # Forms posted to this method should contain a hidden field:
        #    <input type="hidden" name="_method" value="DELETE" />
        # Or using helpers:
        #    h.form(url('user', id=ID),
        #           method='delete')
        # url('user', id=ID)
        user = model.User.get_by(name=id)
        user.delete()
        model.objectstore.flush()
        return

    def show(self, id, format='html'):
        """GET /users/id: Show a specific item.
            @param id the id (name) of the user to be updated.
            @param format the format of the URI requested.
        """
        # url('user', id=ID)
        user = model.User.get_by(name=id)
        if user:
            if format=='json':
                data = user._state['original'].data
                del data['password']
                data['link'] = url('user', id=user.name)
                response.headers['content-type'] = 'text/javascript'
                return dumps(data)
            else:
                c.data = user
                return render('/users/show_user.mako')
        else:
            abort(404, '404 Not Found')

    def edit(self, id, format='html'):
        """GET /users/id;edit: Form to edit an existing item.
            @param id the id (name) of the user to be updated.
            @param format the format of the URI requested.
        """
        # url('edit_user', id=ID)
        user = model.User.get_by(name=id)
        if not user:
            abort(404, '404 Not Found')
        # Get the form values from the table
        c.values = model.forms.UserForm.from_python(user.__dict__)
        return render('/users/edit_user.mako')

XML-RPC リクエストに XML-RPC コントローラを使う

このコントローラを deploy するために、少なくとも XML-RPC それ自身に対す るちょっとした慣れが必要でしょう。この文書では、最初に XML-RPC の基礎を 復習した後で、 Pylons XMLRPCController の働きについて説明します。最 後に、簡単なウェブサービスを実行するために、このコントローラをどのよう に使用するかに関する例を示します。

この文書を読んだ後で、 XML-RPC についてより詳しく説明している “A blog publishing web service in XML-RPC” を読んだほうが良いでしょう。このガイ ドでは MetaWeblog API (ポピュラーな XML-RPC サービス) の細部をカバーす るとともに、MetaWeblog ブログ公開サービスの中核として機能するいくつかの 基本サービス方法を構成する方法が示されています。

XML-RPC の簡単なイントロダクション

XML-RPC は Remote Procedure Call (RPC) インタフェースを記述する仕様です。 XML-RPC を使えば、アプリケーションはインターネットを介して特定のプロシー ジャ呼び出しをリモート XML-RPC サーバ上で実行することができます。呼び出 されるプロシージャの名前とすべての必須パラメータ値は XML 形式に “直列化” (marshal) されます。この XML は、 HTTP を経由して XML-RPC サーバへと送 信される POST リクエストのボディーを形成します。サーバではプロシージャ が実行され、その戻り値が XML 形式に直列化されてアプリケーションに返され ます。 XML-RPC は、できるだけ単純になるように設計されている一方で、複雑 なデータ構造を送受信して処理を行わせることができます。

WSGI を話す XML-RPC コントローラ

Pylons は Python の xmlrpclib ライブラリを使用して独自の XMLRPCController クラスを提供します。このクラスはサービスメソッ ドの中で使用することができる様々な XML-RPC イントロスペクション機能を提 供しています。また、(ブログ公開インタフェースのような) 便利なウェブサー ビスを提供する 1 セットの独自のサービスメソッドを構成するための基礎を提 供します。

このコントローラは XML-RPC レスポンスを扱い、 XML-RPC 仕様XML-RPC イントロスペクション 仕様に従います。

基本機能の一部として、 XML-RPC サーバは 3 つの標準的なイントロスペクショ ン・プロシージャ、あるいは「サービスメソッド」を提供します (as they are called)。 Pylons の XMLRPCController クラスは、これらの標 準サービスメソッドを ready-made で提供します:

  • system.listMethods() XML-RPC リソースのメソッド一覧を返します。
  • system.methodSignature() メソッドの有効なシグネチャを表す配列の配列を返します。それぞれの配列の最初の値はメソッドの戻り値です。 その結果はメソッドが処理できる複数のシグネチャを表す配列です。
  • system.methodHelp() メソッドのドキュメンテーションを返します

デフォルトでは、メソッド名に含まれるドットはアンダースコアに変換されま す。 例えば、 system.methodHelp はメソッド system_methodHelp() によって処理されることになります。

XML-RPC コントローラのメソッドは XML-RPC ボディに与えられたメソッドで呼 ばれます。 メソッドは signature 属性でアノテートすることによって、有効 な引数と戻り値の型を宣言することができます。

以下に例を示します:

class MyXML(XMLRPCController):
    def userstatus(self):
        return 'basic string'
    userstatus.signature = [['string']]

    def userinfo(self, username, age=None):
        user = LookUpUser(username)
        result = {'username': user.name}
        if age and age > 10:
            result['age'] = age
        return result
    userinfo.signature = [['struct', 'string'],
                          ['struct', 'string', 'int']]

XML-RPC メソッドは異なったデータセットを受け取ることができるので、それ ぞれの有効な引数のセットはそれ自身のリストです。 リストにおける最初の値 は戻り値の型です。 引数の残りはそれに対して渡さなければならないデータの 型です。

上の例における最後のメソッドでは、メソッドがオプションの整数値を取るこ とができるので、有効なパラメータリストの両方のセットを与える必要があり ます。

シグネチャでチェックできる有効な型と Python 型の対応表を以下の表に示します:

XMLRPC Python
string str
array list
boolean bool
int int
double float
struct dict
dateTime.iso8601 xmlrpclib.DateTime
base64 xmlrpclib.Binary

シグネチャを与えるかどうかはオプションであることに注意してください。

また、便利な fault handler 関数が提供されることに注意してください。

def xmlrpc_fault(code, message):
    """Convenience method to return a Pylons response XMLRPC Fault"""

(XML-RPC ホームページXML-RPC HOW-TO の両方が、 XML-RPC 仕様に関する詳細を提供します。)

単純な XML-RPC サービス

この単純なサービス test.battingOrder は、 posn というパラメタで 51 未満の正の整数を受け取り、憲法を批准した/組合に加盟した順番でランク 付けしたアメリカの州名を含む文字列を返します。

import xmlrpclib

from pylons import request
from pylons.controllers import XMLRPCController

states = ['Delaware', 'Pennsylvania', 'New Jersey', 'Georgia',
          'Connecticut', 'Massachusetts', 'Maryland', 'South Carolina',
          'New Hampshire', 'Virginia', 'New York', 'North Carolina',
          'Rhode Island', 'Vermont', 'Kentucky', 'Tennessee', 'Ohio',
          'Louisiana', 'Indiana', 'Mississippi', 'Illinois', 'Alabama',
          'Maine', 'Missouri', 'Arkansas', 'Michigan', 'Florida', 'Texas',
          'Iowa', 'Wisconsin', 'California', 'Minnesota', 'Oregon',
          'Kansas', 'West Virginia', 'Nevada', 'Nebraska', 'Colorado',
          'North Dakota', 'South Dakota', 'Montana', 'Washington', 'Idaho',
          'Wyoming', 'Utah', 'Oklahoma', 'New Mexico', 'Arizona', 'Alaska',
          'Hawaii']

class RpctestController(XMLRPCController):

    def test_battingOrder(self, posn):
        """This docstring becomes the content of the
        returned value for system.methodHelp called with
        the parameter "test.battingOrder"). The method
        signature will be appended below ...
        """
        # XML-RPC checks agreement for arity and parameter datatype, so
        # by the time we get called, we know we have an int.
        if posn > 0 and posn < 51:
            return states[posn-1]
        else:
            # Technically, the param value is correct: it is an int.
            # Raising an error is inappropriate, so instead we
            # return a facetious message as a string.
            return 'Out of cheese error.'
    test_battingOrder.signature = [['string', 'int']]

サービスをテストする

OS X を使用している開発者のために XML/RPC クライアント があります。 それは XML-RPC を開発 する際には非常に役に立つ診断用ツールです (それはフリーです… しかし、全 くバグがないわけではありません)。 あるいは Python インタプリタを使うこ ともできます:

>>> from pprint import pprint
>>> import xmlrpclib
>>> srvr = xmlrpclib.Server("http://example.com/rpctest/")
>>> pprint(srvr.system.listMethods())
['system.listMethods',
 'system.methodHelp',
 'system.methodSignature',
 'test.battingOrder']
>>> print srvr.system.methodHelp('test.battingOrder')
This docstring becomes the content of the
returned value for system.methodHelp called with
the parameter "test.battingOrder"). The method
signature will be appended below ...

Method signature: [['string', 'int']]
>>> pprint(srvr.system.methodSignature('test.battingOrder'))
[['string', 'int']]
>>> pprint(srvr.test.battingOrder(12))
'North Carolina'

Python から XML-RPC サーバをデバッグするには、クライアントオブジェクト を作成するときにオプショナルな verbose=1 パラメタを指定してください。そ うすると、クライアントを通常通り使うことができ、 XML-RPC リクエストとレ スポンスがコンソールに表示されるのを観察することができます。

Read the Docs v: v1.0.1rc1
Versions
latest
v1.0.1rc1
v0.9.7
Downloads
PDF
HTML
Epub
On Read the Docs
Project Home
Builds

Free document hosting provided by Read the Docs.