Pythonで、自作したWSGI準拠のWebサーバ上で、自作のWSGIアプリを動かしてみた

前回WSGI準拠のWebサーバを自作しました。

その時はWSGIアプリケーションフレームワークとしてBottleを使いましたが、今回はWSGIアプリケーションも自作してみます。

 
目次

 

環境

  • Windows10 x64
  • Python 3.5.2 32bit
  • Jinja2 2.8

 
WSGIアプリケーションのテンプレートエンジンはどうしようかと考えました。ただ、以下を読むと、自作に手間がかかりそうでした。
みんなのPython Webアプリ編 - 標準モジュールを使ったテンプレートエンジン | TRIVIAL TECHNOLOGIES 4 @ats のイクメン日記

そこで今回は、Django・Flask・Bottleなどに対応しているJinja2をテンプレートエンジンとして使うことにしました。
Welcome to Jinja2 — Jinja2 Documentation (2.8-dev)

 
また、自作したWSGI準拠のWebサーバは以下を流用します。
wsgi_webserver-sample/multi_response_wsgi_server.py at master · thinkAmi-sandbox/wsgi_webserver-sample

 

最も単純なWSGIプリケーション

関数一つで動作します。
第1回 WSGIの概要:WSGIとPythonでスマートなWebアプリケーション開発を|gihyo.jp … 技術評論社

Python3版の場合、戻り値はバイト列とする必要があるため、

hello_wsgi_app_by_function.py

def application(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/plain')])
    return [b"Hello, world."]

とします。

 
動作確認として

(env) >python multi_response_wsgi_server.py hello_wsgi_app_by_function:application

を実行し、ブラウザでhttp://localhost:8888/へアクセスすると、ブラウザ上に 「Hello, world」 が表示されました。

 

Jinja2テンプレートを使う(関数版)

Jinja2テンプレートエンジンを使う方法は、以下が参考になりました。

 
Jinja2をインストールします。

(env) >pip install jinja2

 
Jinja2向けのテンプレートファイルを用意します。

templates/hello.html

<html>
    <head>
        <meta charset="UTF-8">
        <title>hello</title>
    </head>
    <body>
        <h1>Hello, world part2</h1>
    </body>
</html>

 
Jinja2テンプレートエンジンをWSGIアプリケーションへと組み込みます。

jinja2_wsgi_app_by_function.py

from jinja2 import Environment, FileSystemLoader

def application(environ, start_response):
    jinja2_env = Environment(loader=FileSystemLoader('./templates', encoding='utf8'))
    template = jinja2_env.get_template('hello.html')
    html = template.render()

    start_response('200 OK', [('Content-Type', 'text/html')])
    return [html.encode('utf-8')]

 
動作確認として

(env) >python multi_response_wsgi_server.py jinja2_wsgi_app_by_function:application

を実行し、ブラウザでhttp://localhost:8888/へアクセスすると、ブラウザ上に 「Hello, world part2」 が表示されました。

 

Jinja2テンプレートを使う(クラス版)

次はクラス形式で書いてみます。

PEP3333によると、__call__メソッドを持つインスタンスを用意すれば良さそうでした。
Specification Overview | PEP 3333: Python Web Server Gateway Interface v1.0.1 — knzm.readthedocs.org 2012-12-31 documentation

jinja2_wsgi_app_by_class.py

from jinja2 import Environment, FileSystemLoader

class MyWSGIApplication(object):
    def __call__(self, environ, start_response):
        jinja2_env = Environment(loader=FileSystemLoader('./templates', encoding='utf8'))
        template = jinja2_env.get_template('hello.html')
        html = template.render()

        start_response('200 OK', [('Content-Type', 'text/html')])
        return [html.encode('utf-8')]

app = MyWSGIApplication()

 
動作確認として

(env) >python multi_response_wsgi_server.py jinja2_wsgi_app_by_function:application

を実行し、ブラウザでhttp://localhost:8888/へアクセスすると、ブラウザ上に 「Hello, world part2」 が表示されました。

 

クエリパラメータをテンプレートへ反映する

GETの動作は確認できたので、今度はGETのクエリパラメータを扱ってみます。

GETのクエリパラメータを扱いやすくする方法を探したところ、cgi.FieldStorageを使うのが良さそうでした。
Python(WSGI)でGET、POSTを処理する方法.-蔦箱(ツタハコ)

 
templates/query.html

<html>
    <head>
        <meta charset="UTF-8">
        <title>GETテスト</title>
    </head>
    <body>
        <h1>GETテスト</h1>
        <p>{{ title }} : {{ comment }}</p>
    </body>
</html>

 
get_query_wsgi_app_by_class.py

import cgi
from jinja2 import Environment, FileSystemLoader

class MyWSGIApplication(object):
    def __call__(self, environ, start_response):
        if environ['REQUEST_METHOD'].upper() == "GET":
            jinja2_env = Environment(loader=FileSystemLoader('./templates', encoding='utf8'))
            template = jinja2_env.get_template('get.html')

            fs = cgi.FieldStorage(
                environ=environ,
                keep_blank_values=True,
            )
            html = template.render(title=fs['title'].value, comment=fs['comment'].value)

            start_response('200 OK', [('Content-Type', 'text/html')])
            return [html.encode('utf-8')]


app = MyWSGIApplication()

 
ただ、これだけではクエリパラメータがWSGIアプリケーションに渡らないため、WSGIサーバも修正します。

クライアントからのリクエストにて、ステータスラインにクエリパラメータが含まれます(例:GET /?title=aaa&comment=bbb HTTP/1.1)。

ただ、ステータスラインからクエリパラメータだけを抜き出す良い方法が思い浮かばなかったため、

multi_response_wsgi_server.py

def get_environ(self, byte_request):
#...
    request_method, path, request_version = str_request_line_without_crlf.split()

    fullpath = "http://{server}:{port}{path}".format(
        server=self.server_name,
        port=str(self.server_port),
        path=path,
    )
    parsed_fullpath = urlparse(fullpath)

    env = {}
#...
    env['PATH_INFO']         = path                  # /
    env['QUERY_STRING']      = parsed_fullpath.query # query parameter

#...
    return env

と、一度URLへと変換してからurllib.parse.urlparse()で抜き出してenv['QUERY_STRING']へと設定しました。

 
動作確認として

(env) >python multi_response_wsgi_server.py get_query_wsgi_app_by_class:app

を実行し、http://localhost:8888/?title=aaa&comment=bbbへアクセスすると

GETテスト

aaa : bbb

と、クエリパラメータの値が表示されました。

 

POSTを扱う

POSTを扱うWSGIアプリケーションとして、「Webサーバを作りながら学ぶ 基礎からのWebアプリケーション開発入門」の「3.3.3. Tomcatで掲示板を作る」の掲示板アプリケーションを作ります。

このアプリケーションの流れは、

  1. POST
  2. 301リダイレクト
  3. GET

でした。

301リダイレクトをするには、レスポンスヘッダにLocationヘッダを追加します。

bbs_wsgi_app.app

import datetime
import cgi
import io
from jinja2 import Environment, FileSystemLoader

class Message(object):
    def __init__(self, title, handle, message):
        self.title = title
        self.handle = handle
        self.message = message
        self.created_at = datetime.datetime.now()


class MyWSGIApplication(object):
    def __init__(self):
        self.messages = []

    # https://knzm.readthedocs.io/en/latest/pep-3333-ja.html#the-application-framework-side
    def __call__(self, environ, start_response):
        if environ['REQUEST_METHOD'].upper() == "POST":
            # POSTヘッダとボディが一緒に格納されている
            # cgi.FieldStorageで使うために、
            #  ・リクエストをデコード
            #  ・ヘッダとボディを分離
            #  ・ボディをエンコード
            #  ・ボディをio.BytesIOに渡す
            # を行う
            decoded = environ['wsgi.input'].read().decode('utf-8')
            header_body_list = decoded.split('\r\n')
            body = header_body_list[-1]
            encoded_body = body.encode('utf-8')

            # http://docs.python.jp/3/library/io.html#io.BytesIO
            with io.BytesIO(encoded_body) as bytes_body:
                fs = cgi.FieldStorage(
                    fp=bytes_body,
                    environ=environ,
                    keep_blank_values=True,
                )
            # 念のためFieldStorageの内容を確認
            print('-'*20 + '\nFieldStorage:{}\n'.format(fs) + '-'*20)

            self.messages.append(Message(
                title=fs['title'].value,
                handle=fs['handle'].value,
                message=fs['message'].value,
            ))

            # リダイレクトはLocationヘッダをつけてあげれば、ブラウザがうまいことやってくれる
            location = "{scheme}://{name}:{port}/".format(
                scheme = environ['wsgi.url_scheme'],
                name = environ['SERVER_NAME'],
                port = environ['SERVER_PORT'],
            )
            start_response('301 Moved Permanently', [('Location', location)])
            # 適当な値を返しておく
            return [b'1']

        else:
            jinja2_env = Environment(loader=FileSystemLoader('./templates', encoding='utf8'))
            template = jinja2_env.get_template('bbs.html')
            html = template.render({'messages': self.messages})
            start_response('200 OK', [('Content-Type', 'text/html')])
            return [html.encode('utf-8')]


app = MyWSGIApplication()

 
動作確認として

(env) >python multi_response_wsgi_server.py bbs_wsgi_app:app

を実行し、ブラウザでhttp://localhost:8888/へアクセスすると、掲示板アプリが再現できました。

 

ソースコード

GitHubに上げました。
thinkAmi-sandbox/wsgi_application-sample

 

その他参考

クラス宣言について

いろいろなコードのクラス宣言を見ると、明示的なobjectの継承があったりなかったりしました。

どちらのほうが良いか調べてみましたところstackoverflowに情報がありました。
Python class inherits object - Stack Overflow

自分はPython2系で書く機会もあるため、今後も明示的なobject継承で書くことにしました。