Django・WSGIミドルウェア入門/django-congress-jp-2018-talk

 Django・WSGIミドルウェア入門/django-congress-jp-2018-talk

Django Congress JP 2018の発表資料です

D484f3a4d5f516f943b29b9ff55a2040?s=128

thinkAmi

May 18, 2018
Tweet

Transcript

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

  2. お前誰よ / Who are you ? thinkAmi Python & Django

    Blog :メモ的な思考的な (http://thinkami.hatenablog.com/) ( 株) 日本システム技研 PyCon JP 2016-17 Silver Sponsor ギークラボ長野 Python Boot Camp in 長野 みんなのPython 勉強会 in 長野
  3. Django/WSGI ミドルウェア入門 (Django Congress JP 2018 talk)

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

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

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

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

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

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

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

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

  12. 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')
  13. WSGI WSGI とは WSGI アプリケーションとは WSGI サーバとは

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

  16. WSGI 登場前

  17. WSGI 登場後

  18. WSGI アプリケーションとは

  19. WSGI アプリケーションとは 2 つの固定引数を持つアプリケーションオブジェクト environ CGI スタイルの環境変数を含む辞書オブジェクト start_response 2 つの固定引数(status

    ・response_headers) と、1 つの任意引数 (exc_info) を受け付ける、呼び出し可能なオブジェクト 戻り値は、HTTP レスポンスボディ(iterable なバイト文字列) 関数ベース・クラスベースの2 種類
  20. 関数ベースのWSGI アプリ def hello_wsgi(environ, start_response): start_response('200 OK', [('Content-Type', 'text/plain')]) return

    [b'Hello, WSGI function\n']
  21. クラスベースのWSGI アプリ class HelloWSGI: def __call__(self, environ, start_response): start_response('200 OK',

    [('Content-Type', 'text/plain')]) return [b'Hello, WSGI class\n']
  22. WSGI 登場前のDjango Apache + mod_python の組み合わせを推奨 http://djangoproject.jp/doc/ja/1.0/howto/deployment/modpython .html

  23. 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/ “ “
  24. Django のWSGI 実装 Django のソースコードを追ってみる WSGI_APPLICATION より デフォルトは your_project/wsgi.py application

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

  26. 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 はクラスベース
  27. WSGI サーバとは uWSGI Gunicorn mod_wsgi WSGI サーバ(WSGI アプリケーションコンテナ)は、WSGI アプリ ケーションを常駐させ、HTTP

    クライアントからリクエストを受け 取るごとに、WSGI アプリケーションのcallable オブジェクトを呼び 出す https://ja.wikipedia.org/wiki/Web_Server_Gateway_Interface “ “
  28. 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
  29. 関数ベースの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()
  30. 関数ベースのWSGI アプリを動かす WSGI アプリ def hello_wsgi(environ, start_response): return [b'Hello, WSGI

    function\n'] WSGI サーバを起動後、確認 $ curl http://localhost:15000 Hello, WSGI function
  31. クラスベースの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()
  32. クラスベースのWSGI アプリを動かす WSGI アプリ class HelloWSGI: def __call__(self, environ, start_response):

    return [b'Hello, WSGI class\n'] WSGI サーバを起動後、確認 $ curl http://localhost:15001 Hello, WSGI class
  33. WSGI ミドルウェア WSGI ミドルウェアとは WSGI ミドルウェアを実装 WSGI ミドルウェアをDjango へ組み込み

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

    “ “
  35. WSGI ミドルウェアとは Beaker wsgi_lineprof サーバとWSGI アプリケーションの両方のインターフェースを持つ オブジェクト Web サーバ側からはWSGI アプリケーションのように見え,WSGI

    アプリケーション側からはWeb サーバのように見える http://gihyo.jp/dev/feature/01/wsgi/0003 “ “
  36. 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
  37. ( 例) レスポンスを追加するミドルウェア 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
  38. 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()
  39. 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
  40. Django にWSGI ミドルウェアを組み込む

  41. 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')
  42. 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
  43. Django にWSGI ミドルウェアを組み込む wsgi.py に組み込み os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings") application = get_wsgi_application()

    # 追加 application = HelloWSGIMiddleware(application)
  44. 実行結果 # curl 結果 $ curl localhost:8000/myapp/hello Hello world #

    Django ログ [wsgi_middleware] before view called: HelloView [wsgi_middleware] after view
  45. Django ミドルウェア Django ミドルウェアとは Django ミドルウェアを実装 Django ミドルウェアによるフック

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

    https://docs.djangoproject.com/ja/2.0/topics/http/middleware/ “ “
  47. Django ミドルウェアの全体像

  48. Django ミドルウェアを実装

  49. 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
  50. 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')
  51. 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
  52. Django ミドルウェアを組み込む settings にて設定 MIDDLEWARE = [ 'myproject.middlewares.hello_middleware.HelloDjangoMiddleware', ]

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

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

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

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

  57. process_view() の戻り値 None か HttpResponse オブジェクト None の場合、他の process_view() へ遷移

    メソッドの戻り値が無い場合も同じ HttpResponse オブジェクトの場合、そのオブジェクトに差し替わる render() メソッドを持つオブジェクトを返す場合、 process_template_response() が呼ばれる
  58. 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')
  59. 結果確認 クエリ文字列にfoo があるリクエストの実行結果 クエリ文字列にfoo が無いリクエストの実行結果

  60. クエリ文字列に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
  61. クエリ文字列に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 が呼ばれていない
  62. process_template_response() View の実行直後のフック render() メソッドを持つレスポンスのみをフック TemplateResponse オブジェクトなどを、引数として受け取る https://github.com/django/django/blob/2.0.5/django/core/handlers/ base.py#L144

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

  64. 使用する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 }}
  65. process_template_response() を実装 class ProcessTemplateResponseDjangoMiddleware: ... def process_template_response(self, request, response): response.context_data['message']

    = 'overwrite by middleware' return response
  66. 結果確認 HttpResponse を返すView の場合 TemplateResponse を返すView の場合

  67. HttpResponse を返すView の場合 View class HelloView(View): return HttpResponse('Hello world\n') 実行結果

    $ curl localhost:8000/myapp/hello Hello world
  68. TemplateResponse を返すView の場合 View class HelloTemplateView(TemplateView): extra_context = {'message': 'hello'}

    実行結果 $ curl localhost:8000/myapp/template overwrite by middleware # 値が差し替わった
  69. process_exception() View がException を投げたときのフック request やView の例外を、引数として受け取る https://github.com/django/django/blob/2.0.5/django/core/handlers/ base.py#L128

  70. process_exception() の戻り値 None か HttpResponse オブジェクト None の場合、他の process_exception() へ遷移

    メソッドの戻り値が無い場合も同じ HttpResponse オブジェクトの場合、そのオブジェクトに差し替わる render() メソッドを持つオブジェクトを返す場合、 process_template_response() が呼ばれる
  71. 使用するView # `localhost:8000/myapp/error` で呼ばれるView class ExceptionView(View): def get(self, request, *args,

    **kwargs): raise ValueError('Oops!')
  72. 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
  73. 動作確認 HttpResponse オブジェクトを返す場合 クエリ文字列 http あり render() を持つオブジェクトを返す場合 クエリ文字列 template

    あり None を返す場合 いずれのクエリ文字列も無い
  74. HttpResponse オブジェクトを返す場合 $ curl localhost:8000/myapp/error?http=0 HttpResponse by process_exception

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

  76. None を返す場合 curl の結果 $ curl localhost:8000/myapp/error ... <title>ValueError at

    /myapp/error</title> ... Django のログ Internal Server Error: /myapp/error Traceback (most recent call last): ... raise ValueError('Oops!') ValueError: Oops!
  77. Tips WSGI/Django ミドルウェアの複数利用 ミドルウェアでの例外送出 Django1.9 以前のミドルウェアのアップグレード Django ミドルウェアの動的解除

  78. ミドルウェアの複数利用

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

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

    application = SecondWSGIMiddleware(application) application = FirstWSGIMiddleware(application)
  81. 各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
  82. 実行結果 [wsgi1] before view [wsgi2] before view [wsgi3] before view

    called: HelloTemplateView [wsgi3] after view [wsgi2] after view [wsgi1] after view
  83. Django ミドルウェアの複数利用

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

  85. 各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')
  86. 結果 [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
  87. Django/WSGI ミドルウェアの複数利用

  88. 各ミドルウェアの設定 WSGI application = SecondWSGIMiddleware(application) application = FirstWSGIMiddleware(application) Django MIDDLEWARE

    = [ 'myproject.middlewares.multi_middleware.FirstDjangoMiddleware', 'myproject.middlewares.multi_middleware.SecondDjangoMiddleware', ]
  89. 結果 [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
  90. ミドルウェアでの例外送出

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

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

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

    ミドルウェアが動作した後に、WSGI ミド ルウェアが動作するため
  94. Django ミドルウェアで例外を送出 WSGI ミドルウェアでのハンドリング Django ミドルウェアでのハンドリング

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

    ミドルウェアで処理する には不適切
  96. Django ミドルウェアでのハンドリング process_exception() では、例外をハンドリングできない 上位ミドルウェアの self.get_response(request) に HttpResponse が返 る

    HTTP ステータスコード 500 など MIDDLEWARE = [ 'myproject.xxx.HandlingExceptionDjangoMiddleware', 'myproject.xxx.RaisingExceptionInProcessViewDjangoMiddleware', ]
  97. Django1.9 以前の ミドルウェアのアップグレード

  98. 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
  99. アップグレード方法 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

  100. 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
  101. 実行結果 [mixin] process_request called: HelloTemplateView [mixin] process_template_response [mixin] process_response

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

  103. 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
  104. 実行結果 curl $ curl localhost:8000/myapp/template hello Django ログ [unused] one-time

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

    !!