その時はWSGIアプリケーションフレームワークとしてBottleを使いましたが、今回はWSGIアプリケーションも自作してみます。
目次
- 環境
- 最も単純なWSGIプリケーション
- Jinja2テンプレートを使う(関数版)
- Jinja2テンプレートを使う(クラス版)
- クエリパラメータをテンプレートへ反映する
- POSTを扱う
- ソースコード
- その他参考
環境
- 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で掲示板を作る」の掲示板アプリケーションを作ります。
このアプリケーションの流れは、
- POST
- 301リダイレクト
- 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継承で書くことにしました。