middleware がどう動くか実装をみる
前回セッションのバックエンド実装について書いた。
実際に使うときは、ミドルウェアで指定する必要がある。
Django のミドルウェアは公式ドキュメントによると object を継承したクラスをかけばいいという。自分もちょっとだけ書いたことがある。ただこのとき、固定されたメソッド名をどう拾って処理できるようにしているか、あと object を継承していてどこから定義したものがつかえるのかよくわからなかった。
参考
おさらい
設定する
MIDDLEWARE_CLASSES
でタブル内に文字列でクラスのパスを指定する。
Middleware | Django documentation | Django
実装する
先ほどの SessionMiddleware
の場合こうなっている
django/middleware.py at master · django/django · GitHub
Python の object を継承したクラスをつくる。あと必要なメソッドを実装すればOK。 Middleware | Django documentation | Django によると以下の5つ
- process_request
- process_view
- process_exception
- process_template_response
- process_response
SessionMiddleware
の場合、 Django 読み込み時に backend のエンジンを読み込み、 SessionStore
クラスのインスタンスを保持しておく。 リクエストが来たら都度 SessionStore
のインスタンスを作成、レスポンスを返すときにもろもろの処理をはさんでいる。
実装をみる
実装がどこにあるか
実装を調べようにも SessionMiddleware
は MIDDLEWARE_CLASSES
で読み込むよう定義しているだけ。なので、 MIDDLEWARE_CLASSES
そのものを検索する。すると、 django.core.handlers.base
でなにか処理していることがわかる。
django/base.py at master · django/django · GitHub
BaseHandler
BaseHandler
クラスでやってることの大枠はたぶんこう
- init
- load_middleware
- get_response
初期化時に process_xxx
のメソッドに対応する変数を初期化しておく。
継承先の __call__
で呼ばれる load_middleware
で MIDDLEWARE_CLASSES
で定義したミドルウェアを取り出し、ミドルウェアのインスタンスの属性に process_xxx
があれば追加しておく。
レスポンスを返すときにフックポイントにきたら先ほど登録したミドルウェアを取り出して実行する。
WSGIHandler
先ほどの BaseHandler
クラスを使っているのが WSGIHandler
。 __call__
されたらレスポンスを返す。どこかで while True: のようなループがあって、そこでインスタンスになっているものが待ち受けている?
django/wsgi.py at master · django/django · GitHub
使われているところを探すと、 django.core.wsgi#get_wsgi_application
で使われていることがわかった。これは django.setup()
して return WSGIServer
するだけの関数のようだった。これは django-admin.py startproject proj
したときに生成される wsgi.py
で使われている。gunicorn などで動かすときに使う。
おわりに
- 文字列で指定したクラスを import する
- middleware から固定のメソッド名を探して格納、フック時に適用する
というような話だった。
次は余力があれば runserver か render あたりをかく。
文字列で指定しているモジュールを import する
前回のセッションバックエンドについて書いた。 Djangoのセッションバックエンドを調べる(file, db) - そのあれ
そのとき、 django の設定ファイルでは文字列でパスを指定している。ただ、文字列をパスで指定しているだけでどう読み込んでいるかまでよく知らなかった。
セッションバックエンドの指定では django/middleware.py at master · django/django · GitHub で使われている。
結論
__import__
をつかうか importlib.import_module
をつかう。
- Pythonでスクリプトを動的にimportする - Qiita
- 31.5. importlib – The implementation of import — Python 3.4.3 ドキュメント
あまり詳しく調べてないけど、特に理由がなければ importlib.import_module
のほうが標準ライブラリなのでいいんじゃないかなという感じがする。実際、 import_module
関数の中ではキャッシュやロックの管理のようななにかが書かれている。
検証
リポジトリはこちら altnight/djsession at importlib · GitHub
こういう構成になっている。
$ cat app_a/__init__.py print('init %s' % __file__) $ cat hello.py print('hello %s' % __file__)
__init__.py
と hello.py
という import すると自身のファイルパスを表示するだけのモジュール。
$ tree app_a app_a ├── __init__.py ├── hello.py └── level1 ├── __init__.py ├── hello.py └── level2 ├── __init__.py └── hello.py 2 directories, 6 files
ためしにつかう main.py
はこういう内容。
import_module
でモジュール指定__import__
でモジュール指定import_module
でパッケージ指定__import__
でパッケージ指定
from importlib import import_module import_module('hello') import_module('app_a.hello') import_module('app_a.level1.hello') import_module('app_a.level1.level2.hello') #__import__('hello') #__import__('app_a.hello') #__import__('app_a.level1.hello') #__import__('app_a.level1.level2.hello') #import_module('app_a') #import_module('app_a.level1') #import_module('app_a.level1.level2') #__import__('app_a') #__import__('app_a.level1') #__import__('app_a.level1.level2')
結果
$ python main.py hello /Users/altnight/u/src/djsession/djsession/hello.py init /Users/altnight/u/src/djsession/djsession/app_a/__init__.py hello /Users/altnight/u/src/djsession/djsession/app_a/hello.py init /Users/altnight/u/src/djsession/djsession/app_a/level1/__init__.py hello /Users/altnight/u/src/djsession/djsession/app_a/level1/hello.py init /Users/altnight/u/src/djsession/djsession/app_a/level1/level2/__init__.py hello /Users/altnight/u/src/djsession/djsession/app_a/level1/level2/hello.py
ちなみにパッケージじゃなくても(=ディレクトリに init.py がなくても) import できる。
Djangoのセッションバックエンドを調べる(file, db)
セッションキーがどうも取得できないと思っていたら、DBじゃなくてfileバックエンドになっていたことがあった。
参考
基本的には公式ドキュメントを読むのが一番だと思う。
- https://docs.djangoproject.com/en/1.8/topics/http/sessions/
- http://docs.python.jp/3/library/tempfile.html
- http://docs.python.jp/3.3/library/shutil.html
準備
今回試した結果のリポジトリはこれ https://github.com/altnight/djsession
pythonのインストールなど
だいたいこういうことをした
virtualenv env -p `which python3` . ./env/bin/activate python --version # Python 3.4.3 pip install django # Successfully installed django-1.8.3 pip install ipython django-admin.py startproject djsession django-admin.py startapp app
初期化
runserver の前に migate or syncdb
が必要。今回はなんとなく admin を使いたかったので syncdb にしたけれど、どうやら syncdb
は django 1.9
で廃止されるらしい。
$ python manage.py syncdb /path/to/djsession/virtualenv/lib/python3.4/site-packages/django/core/management/commands/syncdb.py:24: RemovedInDjango19Warning: The syncdb command will be removed in Django 1.9 warnings.warn("The syncdb command will be removed in Django 1.9", RemovedInDjango19Warning) Operations to perform: Synchronize unmigrated apps: messages, staticfiles Apply all migrations: contenttypes, sessions, auth, admin Synchronizing apps without migrations: Creating tables... Running deferred SQL... Installing custom SQL... Running migrations: Rendering model states... DONE Applying contenttypes.0001_initial... OK Applying auth.0001_initial... OK Applying admin.0001_initial... OK Applying contenttypes.0002_remove_content_type_name... OK Applying auth.0002_alter_permission_name_max_length... OK Applying auth.0003_alter_user_email_max_length... OK Applying auth.0004_alter_user_username_opts... OK Applying auth.0005_alter_user_last_login_null... OK Applying auth.0006_require_contenttypes_0002... OK Applying sessions.0001_initial... OK You have installed Django's auth system, and don't have any superusers defined. Would you like to create one now? (yes/no): yes Username (leave blank to use 'altnight'): admin Email address: admin@admintestsite.com Password: Password (again): Superuser created successfully.
アプリの準備
- installed app 追加
- viewの作成
- url 登録
- template 作成
どこにソース上の記述があるか
だいたい django.contrib.sessions
以下をみればよさそう。今回はセッションで使っている値がどこに保存されているかを調べたかったので、 backends
であっていた。
file
django.contrib.sessions.backends.file
db
django.contrib.sessions.backends.db
どこで指定するか
設定ファイルなのだけど。 django-admin.py startproject djsession
で実行した時に生成された設定ファイルには記述がなかった。
Django の設定値の初期値は django.conf.global_settings
にある。それをみると以下の記述があったので、DBだった。
SESSION_ENGINE = 'django.contrib.sessions.backends.db'
file
モジュールを文字列で指定しているので、 file
だった。
SESSION_ENGINE = 'django.contrib.sessions.backends.file' SESSION_FILE_PATH = os.path.join(BASE_DIR, 'session_file')
SESSION_FILE_PATH
はディレクトリである必要がある。最初存在しないてきとうなパスにしたら以下のエラーが出た。
ImproperlyConfigured at / The session storage path '/path/to/djsession/djsession/session_file' doesn't exist. Please set your SESSION_FILE_PATH setting to an existing directory in which Django can store session data.
db
SESSION_ENGINE = 'django.contrib.sessions.backends.db'
他
backends 以下のモジュールは以下ななので、他の方法もある。
- (init)
- (base)
- cache
- acched_db
- signed_cookies
どこに保存されているか
file
SESSION_FILE_PATH
で指定したディレクトリ以下に保存されている。
$ ls session_file sessionidnbd3c1righob86pm4pdjuvvo8u45bud4 $cat session_file/sessionidnbd3c1righob86pm4pdjuvvo8u45bud4 Njk2ZWY1MDRmMzcwZjA0NGNlZDQzNzc5YTdjMWY3NjIwMmVhMjBkNDp7InVzZXJfaWQiOjF9%
db
django_session
テーブルに保存されている。
$ sqlite3 --line db.sqlite3 sqlite> .table auth_group auth_user_user_permissions auth_group_permissions django_admin_log auth_permission django_content_type auth_user django_migrations auth_user_groups django_session sqlite> select * from django_session; session_key = 5lonf9nkfy1167jvlwpygz1s0dmdi6ih session_data = MTdlZTUxZWNkMzc4NDNhODZmZGUzOGViNTdlZTc3YTA5N2Y5YjlkODp7Il9hdXRoX3VzZXJfaWQiOiIxIiwiX2F1dGhfdXNlcl9iYWNrZW5kIjoiZGphbmdvLmNvbnRyaWIuYXV0aC5iYWNrZW5kcy5Nb2RlbEJhY2tlbmQiLCJfYXV0aF91c2VyX2hhc2giOiJiN2Y0MzM0ODcxOTI2ZjQ1NGJhZTk0MjdiMmU4NzNjZWUwODEwZWZlIiwidXNlcl9pZCI6MX0= expire_date = 2015-08-01 10:00:19.658846
どういう内容になっているか
file
実装をみると base64
でエンコードされているので、デコードでOKだった。
$ ipython In [1]: import base64 In [2]: base64.b64decode('Njk2ZWY1MDRmMzcwZjA0NGNlZDQzNzc5YTdjMWY3NjIwMmVhMjBkNDp7InVzZXJfaWQiOjF9') Out[2]: b'696ef504f370f044ced43779a7c1f76202ea20d4:{"user_id":1}'
key:value
という形になっている。これの key は cookie の sessionid
の値。
db
こちらも base64
でデコードすればOK。
In [3]: base64.b64decode('MTdlZTUxZWNkMzc4NDNhODZmZGUzOGViNTdlZTc3YTA5N2Y5YjlkODp7Il9hdXRoX3VzZXJfaWQiOiIxIiwiX2F1dGhfdXNlcl9iYWNrZW5kIjoiZGphbmdvLmNvbnRyaWIuYXV0aC5iYWNrZW5kcy5Nb2RlbEJhY2tlbmQiLCJfYXV0aF91c 2VyX2hhc2giOiJiN2Y0MzM0ODcxOTI2ZjQ1NGJhZTk0MjdiMmU4NzNjZWUwODEwZWZlIiwidXNlcl9pZCI6MX0=') Out[3]: b'17ee51ecd37843a86fde38eb57ee77a097f9b9d8:{"_auth_user_id":"1","_auth_user_backend":"django.contrib.auth.backends.ModelBackend","_auth_user_hash":"b7f4334871926f454bae9427b2e873cee0810efe","user_id":1}'
こちらも key:value
という形になっている。なんだか value に auth_user 系のものがついてて、DBの場合挙動が違うかと思ったけど、単純に admin にログインしたままだった。
ログアウトするとこう。
In [6]: base64.b64decode('Njk2ZWY1MDRmMzcwZjA0NGNlZDQzNzc5YTdjMWY3NjIwMmVhMjBkNDp7InVzZXJfaWQiOjF9') Out[6]: b'696ef504f370f044ced43779a7c1f76202ea20d4:{"user_id":1}'
どういう実装になっているか(backend)
基本は base.py にある SessionBase を継承している。クラス名は SessionStore
以下のメソッドを実装する必要がある
- exists
- create
- save
- delete
- load
- clear_expired
file
os.write, shutil, tempfile あたりをつかっている。あんまり馴染みがなかったので tempfile
を使ってみる。
In [1]: import tempfile In [2]: tempfile. tempfile.NamedTemporaryFile tempfile.TMP_MAX tempfile.TemporaryFile tempfile.gettempprefix tempfile.mkstemp tempfile.tempdir tempfile.SpooledTemporaryFile tempfile.TemporaryDirectory tempfile.gettempdir tempfile.mkdtemp tempfile.mktemp tempfile.template In [2]: tempfile.get tempfile.gettempdir tempfile.gettempprefix In [2]: tempfile.gettempdir() Out[2]: '/var/folders/fd/3vccp68n6n50prd2zj2x67mc0000gn/T' In [3]: tempfile.gettempprefix() Out[3]: 'tmp'
ちなみに上のほうで出てきたディレクトリを指定する必要があったという判定は、こういうコードだった。
# Make sure the storage path is valid. if not os.path.isdir(storage_path): raise ImproperlyConfigured( "The session storage path %r doesn't exist. Please set your" " SESSION_FILE_PATH setting to an existing directory in which" " Django can store session data." % storage_path)
db
Session
というモデルを使って CRUD していた。
おわりに
fileの場合サーバーが複数台になるとそれぞれのサーバー上でセッションが保存されることになるので、リクエストしたタイミングなどによってはセッションが取得できない。そのセッションファイルはそれぞれのサーバー上の tempfile で生成される一時的なディレクトリに存在している。DBに保存しているのであれば、masterとなるDBがひとつなので、セッションファイルがわかれることはなかった。ただ、masterが複数台になるようなパターンがあったらだめなのでは。
という、一言で言えばそういう話。