Slide 1

Slide 1 text

Django/WSGI ミドルウェア入門 (Django Congress JP 2018 talk)

Slide 2

Slide 2 text

お前誰よ / Who are you ? thinkAmi Python & Django Blog :メモ的な思考的な (http://thinkami.hatenablog.com/) ( 株) 日本システム技研 PyCon JP 2016-17 Silver Sponsor ギークラボ長野 Python Boot Camp in 長野 みんなのPython 勉強会 in 長野

Slide 3

Slide 3 text

Django/WSGI ミドルウェア入門 (Django Congress JP 2018 talk)

Slide 4

Slide 4 text

話すこと WSGI とは WSGI/Django ミドルウェアとは WSGI/Django ミドルウェアの実装 Tips 複数利用、例外ハンドリング、他

Slide 5

Slide 5 text

話す範囲 Django のリクエスト/ レスポンスの流れ

Slide 6

Slide 6 text

話さないこと デフォルトで設定されるDjango ミドルウェアについて すごいWSGI/Django ミドルウェアを作った話 WSGI/Django ミドルウェアのベストプラクティス

Slide 7

Slide 7 text

注意 このトークを聴いても、Django アプリは作れません ソースコードの一部は省略 GitHub に全体を公開済 https://github.com/thinkAmi/DjangoCongress_JP_2018_talk

Slide 8

Slide 8 text

こんなことありませんか このページは、ログイン必須にしてほしい

Slide 9

Slide 9 text

こんなことありませんか このページは、ログイン必須にしてほしい class FooView(LoginRequiredMixin, TemplateView):

Slide 10

Slide 10 text

こんなことありませんか このページも、ログイン必須にしてほしい class FooView(LoginRequiredMixin, TemplateView): # 増えた class BarView(LoginRequiredMixin, TemplateView):

Slide 11

Slide 11 text

こんなことありませんか やっぱり、全部のページをログイン必須にしてほしい

Slide 12

Slide 12 text

Django ミドルウェアを使う ログインしているかどうかの処理をまとめられた class LoginRequiredMiddleware: def process_view(self, request, view_func, view_args, view_kwargs): if not request.user.is_authenticated: return HttpResponse('permission denied!\n')

Slide 13

Slide 13 text

WSGI WSGI とは WSGI アプリケーションとは WSGI サーバとは

Slide 14

Slide 14 text

WSGI とは Python Web Server Gateway Interface (WSGI) PEP333 とPEP3333 PEP333 (Python2) PEP3333 (Python3 対応) https://www.python.org/dev/peps/pep-3333/ https://knzm.readthedocs.io/en/latest/pep-3333-ja.html

Slide 15

Slide 15 text

WSGI とは Web サーバソフトウェアとPython で記述されたWeb アプリケーシ ョンとの標準インターフェース https://docs.python.jp/3/library/wsgiref.html “ “

Slide 16

Slide 16 text

WSGI 登場前

Slide 17

Slide 17 text

WSGI 登場後

Slide 18

Slide 18 text

WSGI アプリケーションとは

Slide 19

Slide 19 text

WSGI アプリケーションとは 2 つの固定引数を持つアプリケーションオブジェクト environ CGI スタイルの環境変数を含む辞書オブジェクト start_response 2 つの固定引数(status ・response_headers) と、1 つの任意引数 (exc_info) を受け付ける、呼び出し可能なオブジェクト 戻り値は、HTTP レスポンスボディ(iterable なバイト文字列) 関数ベース・クラスベースの2 種類

Slide 20

Slide 20 text

関数ベースのWSGI アプリ def hello_wsgi(environ, start_response): start_response('200 OK', [('Content-Type', 'text/plain')]) return [b'Hello, WSGI function\n']

Slide 21

Slide 21 text

クラスベースのWSGI アプリ class HelloWSGI: def __call__(self, environ, start_response): start_response('200 OK', [('Content-Type', 'text/plain')]) return [b'Hello, WSGI class\n']

Slide 22

Slide 22 text

WSGI 登場前のDjango Apache + mod_python の組み合わせを推奨 http://djangoproject.jp/doc/ja/1.0/howto/deployment/modpython .html

Slide 23

Slide 23 text

WSGI 登場後のDjango Django 1.5 で、mod_python のサポートが削除 http://django.readthedocs.io/en/1.4.X/howto/deployment/modpy thon.html Django の主要なデプロイプラットフォームは、 Web サーバとWeb アプリケーションに関してPython の標準であるWSGI です。 https://docs.djangoproject.com/ja/2.0/howto/deployment/wsgi/ “ “

Slide 24

Slide 24 text

Django のWSGI 実装 Django のソースコードを追ってみる WSGI_APPLICATION より デフォルトは your_project/wsgi.py application = get_wsgi_application()

Slide 25

Slide 25 text

Django とWSGI django.core.wsgi.py def get_wsgi_application(): ... return WSGIHandler()

Slide 26

Slide 26 text

Django とWSGI django.core.handlers.wsgi.py class WSGIHandler(base.BaseHandler): def __call__(self, environ, start_response): ... start_response(status, response_headers) ... return response Django はクラスベース

Slide 27

Slide 27 text

WSGI サーバとは uWSGI Gunicorn mod_wsgi WSGI サーバ(WSGI アプリケーションコンテナ)は、WSGI アプリ ケーションを常駐させ、HTTP クライアントからリクエストを受け 取るごとに、WSGI アプリケーションのcallable オブジェクトを呼び 出す https://ja.wikipedia.org/wiki/Web_Server_Gateway_Interface “ “

Slide 28

Slide 28 text

WSGI アプリとWSGI サーバ WSGI アプリを動かすためには、WSGI サーバが必要 標準モジュール wsgiref のWSGI サーバ wsgiref.simple_server.make_server Django の開発サーバも、標準モジュール wsgiref を使用 $ python manage.py runserver django.core.management.commands.runserver.py django.core.servers.basehttp.py

Slide 29

Slide 29 text

関数ベースのWSGI アプリを動かす WSGI サーバにWSGI アプリを乗せる from wsgiref.simple_server import make_server from wsgi_function import hello_wsgi if __name__ == '__main__': server = make_server('', 15000, hello_wsgi) server.serve_forever()

Slide 30

Slide 30 text

関数ベースのWSGI アプリを動かす WSGI アプリ def hello_wsgi(environ, start_response): return [b'Hello, WSGI function\n'] WSGI サーバを起動後、確認 $ curl http://localhost:15000 Hello, WSGI function

Slide 31

Slide 31 text

クラスベースのWSGI アプリを動かす WSGI サーバにWSGI アプリを乗せる from wsgiref.simple_server import make_server from wsgi_class import HelloWSGI if __name__ == '__main__': server = make_server('', 15001, HelloWSGI()) server.serve_forever()

Slide 32

Slide 32 text

クラスベースのWSGI アプリを動かす WSGI アプリ class HelloWSGI: def __call__(self, environ, start_response): return [b'Hello, WSGI class\n'] WSGI サーバを起動後、確認 $ curl http://localhost:15001 Hello, WSGI class

Slide 33

Slide 33 text

WSGI ミドルウェア WSGI ミドルウェアとは WSGI ミドルウェアを実装 WSGI ミドルウェアをDjango へ組み込み

Slide 34

Slide 34 text

ミドルウェアとは Web サーバ アプリケーションサーバ データベースサーバ コンピュータの基本的な制御を行うオペレーティングシステム(OS) と、各業務処理を行うアプリケーションソフトウェアとの中間に入 るソフトウェアのこと https://ja.wikipedia.org/wiki/ ミドルウェア “ “

Slide 35

Slide 35 text

WSGI ミドルウェアとは Beaker wsgi_lineprof サーバとWSGI アプリケーションの両方のインターフェースを持つ オブジェクト Web サーバ側からはWSGI アプリケーションのように見え,WSGI アプリケーション側からはWeb サーバのように見える http://gihyo.jp/dev/feature/01/wsgi/0003 “ “

Slide 36

Slide 36 text

WSGI ミドルウェア class WSGIMiddleware: def __init__(self, app): self.app = app def __call__(self, environ, start_response): # WSGI アプリの処理前に、何か処理をする場所 response = self.app(environ, start_response) # WSGI アプリの処理後に、何か処理をする場所 return response

Slide 37

Slide 37 text

( 例) レスポンスを追加するミドルウェア class HelloWSGIMiddleware: def __init__(self, app): self.app = app def __call__(self, environ, start_response): response = self.app(environ, start_response) response[0] += b'Hello, WSGI middleware\n' return response

Slide 38

Slide 38 text

WSGI ミドルウェア + WSGI アプリを動かす WSGI アプリ + WSGI ミドルウェアを、WSGI サーバで動かす from wsgiref.simple_server import make_server from wsgi_class import HelloWSGI from wsgi_middleware import HelloWSGIMiddleware if __name__ == '__main__': wsgi_app = HelloWSGI() app_with_middleware = HelloWSGIMiddleware(wsgi_app) server = make_server('', 15002, app_with_middleware) server.serve_forever()

Slide 39

Slide 39 text

WSGI ミドルウェア + WSGI アプリを動かす WSGI アプリ class HelloWSGI: def __call__(self, environ, start_response): return [b'Hello, WSGI class\n'] WSGI サーバを起動後、確認 $ curl http://localhost:15002 Hello, WSGI class Hello, WSGI middleware

Slide 40

Slide 40 text

Django にWSGI ミドルウェアを組み込む

Slide 41

Slide 41 text

Django にWSGI ミドルウェアを組み込む Django アプリのView # `localhost:8000/myapp/hello` で呼ばれるView class HelloView(View): def get(self, request, *args, **kwargs): print('called: HelloView') return HttpResponse('Hello world\n')

Slide 42

Slide 42 text

Django にWSGI ミドルウェアを組み込む WSGI ミドルウェア class HelloWSGIMiddleware: def __init__(self, app): self.app = app def __call__(self, environ, start_response): print('[wsgi_middleware] before view') response = self.app(environ, start_response) print('[wsgi_middleware] after view') return response

Slide 43

Slide 43 text

Django にWSGI ミドルウェアを組み込む wsgi.py に組み込み os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings") application = get_wsgi_application() # 追加 application = HelloWSGIMiddleware(application)

Slide 44

Slide 44 text

実行結果 # curl 結果 $ curl localhost:8000/myapp/hello Hello world # Django ログ [wsgi_middleware] before view called: HelloView [wsgi_middleware] after view

Slide 45

Slide 45 text

Django ミドルウェア Django ミドルウェアとは Django ミドルウェアを実装 Django ミドルウェアによるフック

Slide 46

Slide 46 text

Django ミドルウェアとは 関数ベースとクラスベース Django のリクエスト/ レスポンス処理にフックを加えるためのフレ ームワーク Django の入力あるいは出力をグローバルに置き換えるための、軽 量で低レベルの「プラグイン」システム https://docs.djangoproject.com/ja/2.0/topics/http/middleware/ “ “

Slide 47

Slide 47 text

Django ミドルウェアの全体像

Slide 48

Slide 48 text

Django ミドルウェアを実装

Slide 49

Slide 49 text

Django ミドルウェア class SimpleMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): # Django アプリの処理前に、何か処理をする場所 response = self.get_response(request) # Django アプリの処理後に、何か処理をする場所 return response https://docs.djangoproject.com/ja/2.0/topics/http/middleware/#writin g-your-own-middleware

Slide 50

Slide 50 text

Django ミドルウェアを使ってみる Django アプリのView ( 再掲) # `localhost:8000/myapp/hello` で呼ばれるView class HelloView(View): def get(self, request, *args, **kwargs): print('called: HelloView') return HttpResponse('Hello world\n')

Slide 51

Slide 51 text

Django ミドルウェアを使ってみる ミドルウェアの実装 class HelloDjangoMiddleware: def __init__(self, get_response): self.get_response = get_response print('[hello] one-time configuration') def __call__(self, request): print('[hello] before view') response = self.get_response(request) print('[hello] after view') return response

Slide 52

Slide 52 text

Django ミドルウェアを組み込む settings にて設定 MIDDLEWARE = [ 'myproject.middlewares.hello_middleware.HelloDjangoMiddleware', ]

Slide 53

Slide 53 text

動作確認 起動時 $ python manage.py runserver --settings=myproject.settings.hello ... [hello] one-time configuration

Slide 54

Slide 54 text

動作確認 curl でアクセスした時のDjango ログ [hello] before view called: HelloView [hello] after view

Slide 55

Slide 55 text

Django ミドルウェアによるフック process_view() process_template_response() process_exception()

Slide 56

Slide 56 text

process_view() Django がビューを呼び出す直前のフック https://github.com/django/django/blob/2.0.5/django/core/handle rs/base.py#L118 request ・view_func などを、引数として受け取る

Slide 57

Slide 57 text

process_view() の戻り値 None か HttpResponse オブジェクト None の場合、他の process_view() へ遷移 メソッドの戻り値が無い場合も同じ HttpResponse オブジェクトの場合、そのオブジェクトに差し替わる render() メソッドを持つオブジェクトを返す場合、 process_template_response() が呼ばれる

Slide 58

Slide 58 text

process_view() を実装したミドルウェア ( 例) クエリ文字列に foo が無い場合、HttpResponse を返す class ProcessViewDjangoMiddleware: ... def process_view(self, request, view_func, view_args, view_kwargs): if not request.GET.get('foo'): return HttpResponse('overwrite by process_view\n')

Slide 59

Slide 59 text

結果確認 クエリ文字列にfoo があるリクエストの実行結果 クエリ文字列にfoo が無いリクエストの実行結果

Slide 60

Slide 60 text

クエリ文字列にfoo があるリクエストの実行結果 curl 結果 $ curl localhost:8000/myapp/hello?foo=123 Hello world Django ログ [process_view] before view [process_view] hook! called: HelloView # View が呼ばれている [process_view] after view

Slide 61

Slide 61 text

クエリ文字列にfoo が無いリクエストの実行結果 curl 結果 $ curl localhost:8000/myapp/hello?bar=456 overwrite by process_view # ミドルウェアで差し替わった Django ログ [process_view] before view [process_view] hook! [process_view] after view # View が呼ばれていない

Slide 62

Slide 62 text

process_template_response() View の実行直後のフック render() メソッドを持つレスポンスのみをフック TemplateResponse オブジェクトなどを、引数として受け取る https://github.com/django/django/blob/2.0.5/django/core/handlers/ base.py#L144

Slide 63

Slide 63 text

process_template_response() の戻り値 render() メソッドを持つオブジェクト django.template.response.TemplateResponse など

Slide 64

Slide 64 text

使用するView とTemplate View class HelloTemplateView(TemplateView): template_name = 'hello.html' extra_context = {'message': 'hello'} def get(self, request, *args, **kwargs): return super().get(request, args, kwargs) Template {{ message }}

Slide 65

Slide 65 text

process_template_response() を実装 class ProcessTemplateResponseDjangoMiddleware: ... def process_template_response(self, request, response): response.context_data['message'] = 'overwrite by middleware' return response

Slide 66

Slide 66 text

結果確認 HttpResponse を返すView の場合 TemplateResponse を返すView の場合

Slide 67

Slide 67 text

HttpResponse を返すView の場合 View class HelloView(View): return HttpResponse('Hello world\n') 実行結果 $ curl localhost:8000/myapp/hello Hello world

Slide 68

Slide 68 text

TemplateResponse を返すView の場合 View class HelloTemplateView(TemplateView): extra_context = {'message': 'hello'} 実行結果 $ curl localhost:8000/myapp/template overwrite by middleware # 値が差し替わった

Slide 69

Slide 69 text

process_exception() View がException を投げたときのフック request やView の例外を、引数として受け取る https://github.com/django/django/blob/2.0.5/django/core/handlers/ base.py#L128

Slide 70

Slide 70 text

process_exception() の戻り値 None か HttpResponse オブジェクト None の場合、他の process_exception() へ遷移 メソッドの戻り値が無い場合も同じ HttpResponse オブジェクトの場合、そのオブジェクトに差し替わる render() メソッドを持つオブジェクトを返す場合、 process_template_response() が呼ばれる

Slide 71

Slide 71 text

使用するView # `localhost:8000/myapp/error` で呼ばれるView class ExceptionView(View): def get(self, request, *args, **kwargs): raise ValueError('Oops!')

Slide 72

Slide 72 text

process_exception() を実装したミドルウェア class ProcessExceptionDjangoMiddleware: def process_exception(self, request, exception): if request.GET.get('http'): return HttpResponse('HttpResponse by process_exception\n') elif request.GET.get('template'): return HelloTemplateView(request=request).render_to_response( {'message': 'TemplateResponse by process_exception\n'}) def process_template_response(self, request, response): response.context_data['message'] += \ 'called process_template_response' return response

Slide 73

Slide 73 text

動作確認 HttpResponse オブジェクトを返す場合 クエリ文字列 http あり render() を持つオブジェクトを返す場合 クエリ文字列 template あり None を返す場合 いずれのクエリ文字列も無い

Slide 74

Slide 74 text

HttpResponse オブジェクトを返す場合 $ curl localhost:8000/myapp/error?http=0 HttpResponse by process_exception

Slide 75

Slide 75 text

render() を持つオブジェクトを返す場合 $ curl localhost:8000/myapp/error?template=0 TemplateResponse by process_exception called process_template_response

Slide 76

Slide 76 text

None を返す場合 curl の結果 $ curl localhost:8000/myapp/error ... ValueError at /myapp/error ... Django のログ Internal Server Error: /myapp/error Traceback (most recent call last): ... raise ValueError('Oops!') ValueError: Oops!

Slide 77

Slide 77 text

Tips WSGI/Django ミドルウェアの複数利用 ミドルウェアでの例外送出 Django1.9 以前のミドルウェアのアップグレード Django ミドルウェアの動的解除

Slide 78

Slide 78 text

ミドルウェアの複数利用

Slide 79

Slide 79 text

WSGI ミドルウェアの複数利用

Slide 80

Slide 80 text

WSGI ミドルウェアの設定 Django View に近いWSGI ミドルウェアから設定 wsgi.py application = ThirdWSGIMiddleware(application) application = SecondWSGIMiddleware(application) application = FirstWSGIMiddleware(application)

Slide 81

Slide 81 text

各WSGI ミドルウェア class FirstWSGIMiddleware: def __init__(self, app): self.app = app def __call__(self, environ, start_response): print('[wsgi1] before view') response = self.app(environ, start_response) print('[wsgi1] after view') return response

Slide 82

Slide 82 text

実行結果 [wsgi1] before view [wsgi2] before view [wsgi3] before view called: HelloTemplateView [wsgi3] after view [wsgi2] after view [wsgi1] after view

Slide 83

Slide 83 text

Django ミドルウェアの複数利用

Slide 84

Slide 84 text

Django ミドルウェアの設定 MIDDLEWARE = [ 'myproject.middlewares.multi_middleware.FirstDjangoMiddleware', 'myproject.middlewares.multi_middleware.SecondDjangoMiddleware', 'myproject.middlewares.multi_middleware.ThirdDjangoMiddleware', ]

Slide 85

Slide 85 text

各Django ミドルウェア class FirstDjangoMiddleware: def __init__(self, get_response): print('[django1] one-time configuration') def __call__(self, request): print('[django1] before view') response = self.get_response(request) print('[django1] after view') def process_view(self, request, view_func, view_args, view_kwargs): print('[django1] process view') def process_template_response(self, request, response): print('[django1] process template response')

Slide 86

Slide 86 text

結果 [django3] one-time configuration [django2] one-time configuration [django1] one-time configuration [django1] before view [django2] before view [django3] before view [django1] process view [django2] process view [django3] process view called: HelloTemplateView [django3] process template response [django2] process template response [django1] process template response [django3] after view [django2] after view [django1] after view

Slide 87

Slide 87 text

Django/WSGI ミドルウェアの複数利用

Slide 88

Slide 88 text

各ミドルウェアの設定 WSGI application = SecondWSGIMiddleware(application) application = FirstWSGIMiddleware(application) Django MIDDLEWARE = [ 'myproject.middlewares.multi_middleware.FirstDjangoMiddleware', 'myproject.middlewares.multi_middleware.SecondDjangoMiddleware', ]

Slide 89

Slide 89 text

結果 [django2] one-time configuration [django1] one-time configuration [wsgi1] before view [wsgi2] before view [django1] before view [django2] before view [django1] process view [django2] process view called: HelloTemplateView [django2] process template response [django1] process template response [django2] after view [django1] after view [wsgi2] after view [wsgi1] after view

Slide 90

Slide 90 text

ミドルウェアでの例外送出

Slide 91

Slide 91 text

WSGI ミドルウェアで例外を送出 WSGI ミドルウェアでのハンドリング Django ミドルウェアでのハンドリング

Slide 92

Slide 92 text

WSGI ミドルウェアでのハンドリング request/response に近いWSGI ミドルウェアで、ハンドリングする application = RaiseExceptionWSGIMiddleware(application) application = HandlingExceptionWSGIMiddleware(application)

Slide 93

Slide 93 text

Django ミドルウェアでのハンドリング Django ミドルウェアでは、WSGI 例外をハンドリングできない リクエスト時:Django ミドルウェアが動作する前に、WSGI ミド ルウェアが動作するため レスポンス時:Django ミドルウェアが動作した後に、WSGI ミド ルウェアが動作するため

Slide 94

Slide 94 text

Django ミドルウェアで例外を送出 WSGI ミドルウェアでのハンドリング Django ミドルウェアでのハンドリング

Slide 95

Slide 95 text

WSGI ミドルウェアでのハンドリング Django ミドルウェアの例外は、 django.http.response.HttpResponse オ ブジェクトに変換済 例外そのものは、ハンドリングできない Django の世界のオブジェクトなので、WSGI ミドルウェアで処理する には不適切

Slide 96

Slide 96 text

Django ミドルウェアでのハンドリング process_exception() では、例外をハンドリングできない 上位ミドルウェアの self.get_response(request) に HttpResponse が返 る HTTP ステータスコード 500 など MIDDLEWARE = [ 'myproject.xxx.HandlingExceptionDjangoMiddleware', 'myproject.xxx.RaisingExceptionInProcessViewDjangoMiddleware', ]

Slide 97

Slide 97 text

Django1.9 以前の ミドルウェアのアップグレード

Slide 98

Slide 98 text

Django1.9 と1.10 の違い 1.10 から、Django ミドルウェアの実装方法が変更 1.9 までの実装のままでは、TypeError: object() takes no parameters 主な変更点 MIDDLEWARE_CLASSES ではなく、MIDDLEWARE を使う __init__() メソッドに引数が必要 __call__() メソッドが必要 process_request() と process_response() がDjango2.0 で削除 https://docs.djangoproject.com/en/1.10/releases/1.10/#new- style-middleware

Slide 99

Slide 99 text

アップグレード方法 django.utils.deprecation.MiddlewareMixin を継承 process_request() と process_response() を呼ぶMixin https://docs.djangoproject.com/ja/2.0/topics/http/middleware/# upgrading-pre-django-1-10-style-middleware

Slide 100

Slide 100 text

MiddlewareMixin を使ったミドルウェア class DeprecationMixinDjangoMiddleware(MiddlewareMixin): def process_request(self, request): print('[mixin] process_request') def process_response(self, request, response): print('[mixin] process_response') return response def process_template_response(self, request, response): print('[mixin] process_template_response') response.context_data['message'] = 'overwrite by deprecation mixin middleware' return response

Slide 101

Slide 101 text

実行結果 [mixin] process_request called: HelloTemplateView [mixin] process_template_response [mixin] process_response

Slide 102

Slide 102 text

Django ミドルウェアの動的解除

Slide 103

Slide 103 text

Django ミドルウェアの動的解除 __init__() の中で、 django.core.exceptions.MiddlewareNotUsed を送出 class UnusedDjangoMiddleware: def __init__(self, get_response): self.get_response = get_response raise MiddlewareNotUsed def process_template_response(self, request, response): response.context_data['message'] = 'overwrite by unused' return response

Slide 104

Slide 104 text

実行結果 curl $ curl localhost:8000/myapp/template hello Django ログ [unused] one-time configuration called: HelloTemplateView

Slide 105

Slide 105 text

終わりに Django では、WSGI ミドルウェアとDjango ミドルウェアが使える Django ミドルウェアには、フックするポイントがいくつかある Enjoy, middleware life !!