Source

translations / docs-source / do-it-yourself-framework_ja.rst

A Do-It-Yourself Framework

Author: Ian Bicking <ianb@colorstudy.com>
translator:Atsushi Odagiri <aodagx@gmail.com>
Revision: 7181
Date: 2008-01-08 09:46:01 +0900 (火, 08 1 2008)
original:http://pythonpaste.org/do-it-yourself-framework.html

Introduction and Audience

この短いチュートリアルは、Pasteが可能にし、推奨するアーキテクチャの例です。 また、WSGIについても軽く触れます。

これは、Pasteのすべてに関する紹介ではありません。 説明しているのはほんの一部です。 また、すべての人に、フレームワークを乗り換え、自らのフレームワークを作成することを推奨するものでもありません。(正直なところそうなっても気にしません)。 目的は、この文書を読み終えた方が、このアーキテクチャを用いたフレームワークをより快適に使えるようになることと、覆い隠された内部をより理解することにあります。

What is WSGI?

最も単純言えば、WSGIとは、WebサーバーとWebアプリケーション間のインターフェイスです。 以下で説明するのは、WSGIの構造についです。 上位のレベルから見れば、WSGIはwebリクエストを非常に公平で公式な方法で、コードに引き渡します。 しかし、それだけではありません! WSGIは、単なるHTTP以上のものなのです。 これらの内容は、HTTPとほんの ささいな 違いにしか見えないでしょう。 しかし、この小さな違いが重要なのです。:

  • CGIのように環境をコードに引き渡します。 REMOTE_USER などのデータもsecureに引き渡されます。
  • CGIのような環境が、もっと多くのコンテキストと渡されます。特に、1つのパスではなく2つのパスが渡されます。 SCRIPT_NAMEPATH_INFO です。 SCRIPT_NAME はどのようにしてここまできたか、 PATH_INFO は、なにが残っているかを表しています。
  • 独自の拡張をWSGI環境に置くことができます。また通常はそうすべきです。 コールバックや追加情報など、あなたが望むあらゆるPythonオブジェクトを置くことが許されています。 これらは、HTTPヘッダーに置くことができないものです。

これらの特徴により、WSGIはWebサーバーとアプリケーションの間を取り持つだけでなく、すべてのレベルでのやりとりが可能になります。 つまり、Webアプリケーションを再利用可能なカプセル化したライブラリのように扱えるようになり、機能を再利用可能にします。

Writing a WSGI Application

最初に説明するのは、 WSGI の初歩的な使い方です。 ここで簡単に説明しますが、WSGIの規格を参照してください。

  • WSGI アプリケーション を書こうとしています。これは、要求に応じるオブジェクトです。 アプリケーションは単に environstart_response の2つの引数を受け取る呼び出し可能(関数のようなもの)なオブジェクトです。
  • 環境は、 REQUEST_METHOD, HTTP_HOST など、CGI環境変数を持っています。
  • また、 wsgi.input (POSTリクエストの入力内容) のような特殊なキーも持っています。
  • start_response は、レスポンスを返し始めるための関数です。 ここで、ステータスやヘッダを与えます。
  • 最終的に、アプリケーションは、レスポンスボディをイテレータで返します。 (通常は、複数の文字列のリストか、すべてをまとめた1つだけの文字列を含むリストになります。)

そして、一番簡単なアプリケーションは以下のようになります:

def app(environ, start_response):
    start_response('200 OK', [('content-type', 'text/html')])
    return ['Hello world!']

しかし、これではまだ不十分です。 想像がつくと思いますが、これではブラウザでアクセスできません。

良い方法はたくさんありましが、このチュートリアルでは良い方法ではなく分かりやすい方法を使います。

なので、以下の文をファイルの末尾に付け加えるだけにしましょう。:

if __name__ == '__main__':
    from paste import httpserver
    httpserver.serve(app, host='127.0.0.1', port='8080')

そして、http://localhost:8080 を確認してみましょう。 このアプリケーションが動いています。 WSGIサーバーの動作を理解したければ、WSGI規格の CGI WSGI server を確認することをお勧めします。

An Interactive App

さきほどのアプリケーションはとりたてて興味深いものではありませんでした。 最低限の対話性を付け加えてみましょう。 そうするには、フォームを表示し、そのフォームフィールドをパースするようにします。:

from paste.request import parse_formvars

def app(environ, start_response):
    fields = parse_formvars(environ)
    if environ['REQUEST_METHOD'] == 'POST':
        start_response('200 OK', [('content-type', 'text/html')])
        return ['Hello, ', fields['name'], '!']
    else:
        start_response('200 OK', [('content-type', 'text/html')])
        return ['<form method="POST">Name: <input type="text" '
                'name="name"><input type="submit"></form>']

parse_formvars 関数は、WSGI環境を受け取り、 cgi モジュール(FieldStorage クラス) を呼び出して、MultiDictにつめて返します。

Now For a Framework

これまでの内容は少々粗雑に感じるでしょう。 このままでは、REQUEST_METHODのようなものをもっと判定しなくてはならなくなりますし、 複数ページを取り扱う方法が分かりません。

一般的なアプリケーションのためのフレームワークが欲しくなります。 このチュートリアルでは、オブジェクトパブリッシャー を実装します。 これは、ZopeやQuixote、Cherrypy で見たことがあるかもしれません。

Object Publishing

典型的なPythonオブジェクトパブリッシャーは、 /. に変換します。 つまり、 /articles/view?id=5 は、 root.articles.view(id=5) と変換されます。

もちろん、なんらかのルートオブジェクトが必要なので、それを渡すようにします。

class ObjectPublisher(object):

def __init__(self, root):
self.root = root
def __call__(self, environ, start_response):
...

app = ObjectPublisher(my_root_object)

__call__ をオーバーライドするのは、 ObjectPublisher のインスタンスを関数のように呼び出し可能なWSGIアプリケーションにするためです。 このあとやらなくてはならないのは、 environ を表示しようとしているものに変換してWSGIのレスポンスとして返すことです。

The Path

WSGIは要求されたパスを SCRIPT_NAMEPATH_INFO の2つの変数にします。 SCRIPT_NAMEたどり着く のに使われたものです。 PATH_INFO は、まだ残されているものです。 フレームワークがオブジェクトを発見するために使うのは、この部分です。 この2つを一緒にすると、今リクエストされているフルパスになります。 これは、正しいURLを作成するのに役立ちます。 この状態を必ず守るようにします。

そして、これが __call__ の実装です。:

def __call__(self, environ, start_response):
    fields = parse_formvars(environ)
    obj = self.find_object(self.root, environ)
    response_body = obj(**fields.mixed())
    start_response('200 OK', [('content-type', 'text/html')])
    return [response_body]

def find_object(self, obj, environ):
    path_info = environ.get('PATH_INFO', '')
    if not path_info or path_info == '/':
        # We've arrived!
        return obj
    # PATH_INFO always starts with a /, so we'll get rid of it:
    path_info = path_info.lstrip('/')
    # Then split the path into the "next" chunk, and everything
    # after it ("rest"):
    parts = path_info.split('/', 1)
    next = parts[0]
    if len(parts) == 1:
        rest = ''
    else:
        rest = '/' + parts[1]
    # Hide private methods/attributes:
    assert not next.startswith('_')
    # Now we get the attribute; getattr(a, 'b') is equivalent
    # to a.b...
    next_obj = getattr(obj, next)
    # Now fix up SCRIPT_NAME and PATH_INFO...
    environ['SCRIPT_NAME'] += '/' + next
    environ['PATH_INFO'] = rest
    # and now parse the remaining part of the URL...
    return self.find_object(next_obj, environ)

これで、フレームワークを獲得しました。

Taking It For a Ride

では、小さなアプリケーションを書いてみましょう。 さきほどの ObjectPublisher クラスは、 objectpub モジュールにあります。:

from objectpub import ObjectPublisher

class Root(object):

    # The "index" method:
    def __call__(self):
        return '''
        <form action="welcome">
        Name: <input type="text" name="name">
        <input type="submit">
        </form>
        '''

    def welcome(self, name):
        return 'Hello %s!' % name

app = ObjectPublisher(Root())

if __name__ == '__main__':
    from paste import httpserver
    httpserver.serve(app, host='127.0.0.1', port='8080')

これだけです! ただ、ちょっと待ってください。 まだ大きな問題が残っています。 例えば、ヘッダーを設定するにはどうすればいいのでしょう? 404 Not Found を返す代わりに、アトリビュートエラーとしたい場合もあるでしょう。 ここから少しずつ修正していきましょう。

Give Me More!

なにかが足りないと感じていることでしょう。

特に、ヘッダを設定する方法がありませんし、リクエストの情報が少ししかありません。

# これは、単なるdictのようなオブジェクトで、ケースセンシティブなキーを持ちます。
from paste.response import HeaderDict

class Request(object):
    def __init__(self, environ):
        self.environ = environ
        self.fields = parse_formvars(environ)

class Response(object):
    def __init__(self):
        self.headers = HeaderDict(
            {'content-type': 'text/html'})

では、ちょっとしたトリックを説明しましょう。(原文ではeachとなっているが、teachの間違い?) メソッドのシグネチャを変えたくはありません。 しかし、リクエストとレスポンスのオブジェクトを普通のグローバル変数に置くわけにはいきません。 なぜなら、スレッドセーフにしたいからです。 全てのスレッドが同じグローバル変数を見てしまうからです。(たとえそれが、違うリクエストの処理だったとしても)

しかし、Python2.4で、"スレッドローカル値"の概念が導入されました。 あるスレッドからしかアクセスできない値です。 これは、 threading.local にあります。 local インスタンスを作成したら、それに設定したすべてのアトリビュートは、 設定したスレッドからしかアクセスできないようになります。 では、リクエストとレスポンスのオブジェクトを設定するようにしましょう。

まず、 __call__ 関数が以下のようになっていることを思い出しましょう。:

class ObjectPublisher(object):
    ...

    def __call__(self, environ, start_response):
        fields = parse_formvars(environ)
        obj = self.find_object(self.root, environ)
        response_body = obj(**fields.mixed())
        start_response('200 OK', [('content-type', 'text/html')])
        return [response_body]

このように変更します。:

import threading
webinfo = threading.local()

    def __call__(self, environ, start_response):
        webinfo.request = Request(environ)
        webinfo.response = Response()
        obj = self.find_object(self.root, environ)
        response_body = obj(**dict(webinfo.request.fields))
        start_response('200 OK', webinfo.response.headers.items())
        return [response_body]

メソッドでは、このように使えます。:

class Root:
    def rss(self):
        webinfo.response.headers['content-type'] = 'text/xml'
        ...

その気になれば、 cookies のハンドルもこれらのオブジェクトでできます。 しかし、今回はそこまでやらないことにします。 フレームワークができました。

WSGI Middleware

Middleware WSGIやPasteの中で、Middlwareはややとっつきにくく感じるでしょう。

ミドルウェアは何ものでしょう? ミドルウェアは、仲介者として働くソフトウェアです。

早速1つ書いてみましょう。 あなたの挨拶を誰からでも見られてしまわないように、認証ミドルウェアを書いてみます。

HTTP認証を使いましょう。人々を少しだけけむに巻くことができます。 HTTP認証は非常に簡単です。:

  • 認証が必要になったら、 401 Authentication Required ステータスを、 WWW-Authenticate: Basic realm="This Realm" ヘッダーとともに、返します。
  • クライアントは、 Authorization: Basic encoded_info ヘッダーを送り返します。
  • この "encoded_info" は、 username:password をbase-64エンコーディングしたものです。

どうすれば実現できるでしょう? そう、"middleware"を書きます。 つまり、リクエストを違うアプリケーションに渡してしまいます。 リクエスト変更したり、レスポンスを変更したりできます。 ただし、この場合は、ときにリクエストを渡さないようにするだけです。 (401レスポンスを返したい場合など)

とても簡単なミドルウェアの例です。 レスポンスを大文字にしています。:

class Capitalizer(object):

    # We generally pass in the application to be wrapped to
    # the middleware constructor:
    def __init__(self, wrap_app):
        self.wrap_app = wrap_app

    def __call__(self, environ, start_response):
        # We call the application we are wrapping with the
        # same arguments we get...
        response_iter = self.wrap_app(environ, start_response)
        # then change the response...
        response_string = ''.join(response_iter)
        return [response_string.upper()]

技術的には、完全に正しい方法というわけではありません。 なぜなら、レスポンスボディを返す方法は2通りあるからです。 しかし、詳細には立ち入らないことにします。

paste.wsgilib.intercept_output は、 この部分をきちんと実装したものです。

それでは、いくらか使い物になるコードです。認証:

class AuthMiddleware(object):

    def __init__(self, wrap_app):
        self.wrap_app = wrap_app

    def __call__(self, environ, start_response):
        if not self.authorized(environ.get('HTTP_AUTHORIZATION')):
            # Essentially self.auth_required is a WSGI application
            # that only knows how to respond with 401...
            return self.auth_required(environ, start_response)
        # But if everything is okay, then pass everything through
        # to the application we are wrapping...
        return self.wrap_app(environ, start_response)

    def authorized(self, auth_header):
        if not auth_header:
            # If they didn't give a header, they better login...
            return False
        # .split(None, 1) means split in two parts on whitespace:
        auth_type, encoded_info = auth_header.split(None, 1)
        assert auth_type.lower() == 'basic'
        unencoded_info = encoded_info.decode('base64')
        username, password = unencoded_info.split(':', 1)
        return self.check_password(username, password)

    def check_password(self, username, password):
        # Not very high security authentication...
        return username == password

    def auth_required(self, environ, start_response):
        start_response('401 Authentication Required',
            [('Content-type', 'text/html'),
             ('WWW-Authenticate', 'Basic realm="this realm"')])
        return ["""
        <html>
         <head><title>Authentication Required</title></head>
         <body>
          <h1>Authentication Required</h1>
          If you can't get in, then stay out.
         </body>
        </html>"""]

どうやって使えばいいでしょう?

app = ObjectPublisher(Root())
wrapped_app = AuthMiddleware(app)

if __name__ == '__main__':
    from paste import httpserver
    httpserver.serve(wrapped_app, host='127.0.0.1', port='8080')

これでミドルウェアもできました! ヒャッホー!

Give Me More Middleware!

自分で作ったもの以外にも、他の人々が作ったミドルウェアを使うのも簡単です。 自分で作る必要はありません。 ここまで読んできたのであれば、恐らく何回か例外に出会っているでしょう。 そして、コンソールに表示されている例外レポートを目の当たりにしているに違いありません。 少し簡単にしましょう、ブラウザ上で例外を確認できるようにします。

app = ObjectPublisher(Root())
wrapped_app = AuthMiddleware(app)
from paste.exceptions.errormiddleware import ErrorMiddleware
exc_wrapped_app = ErrorMiddleware(wrapped_app)

簡単ですね!しかし、 もっと いいものが欲しいなら...

app = ObjectPublisher(Root())
wrapped_app = AuthMiddleware(app)
from paste.evalexception import EvalException
exc_wrapped_app = EvalException(wrapped_app)

エラーを発生させてみましょう。 ちいさな + をクリックしてください。 ボックスの中に何か入力してみましょう。

Configuration

フレームワークとアプリケーションを作成しました。 (私が示してきたものよりもはるかにいいものになっていると確信しています。) これらを手でつなぎ合わせるのは、幾分粗雑と感じるかもしれません。

そうでなくても、誰かが他の場所にインストールしたり、違う構成にしたりするときに、困ることでしょう。

だから、アプリケーションの構成から、設定を分離しておきたいのです。

What's Next?

まだ続きがあります, 今後構成についても話す予定です。(Paste Deploy を使います)。 そして、パッケージングやプラグインについても短い紹介ができたらいいと思います。 そのときは、 私のブログ で、告知します。