├── .gitignore
├── LICENSE
├── README.rst
├── demos
├── file_session.py
├── memory_session.py
└── redis_session.py
├── setup.py
└── torndsession
├── __init__.py
├── compat.py
├── driver.py
├── filesession.py
├── memcachedsession.py
├── memorysession.py
├── redissession.py
├── session.py
├── sessionhandler.py
└── test
└── TEST_DIRECTORY
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.pyc
3 | *.pyo
4 | *.so
5 | *.class
6 | .*
7 | !/.gitignore
8 | build/
9 | /dist/
10 | /torndsession.egg-info/
11 | /env/
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright © 2014, Mitchell Chu
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | Torndsession Session
2 | ====================
3 |
4 |
5 | `Torndsession `_ is a session extension for `Tornado `__ web framework.
6 | Torndsession support application memory, file, redis or memcached to save session data for request, and it's easy to extend for developer.
7 |
8 | Quick links
9 | ===========
10 |
11 | * `Documentation `_
12 |
13 | * `Source (github) `_
14 |
15 | * `Torndsession License `_
16 |
17 | * `Examples `_
18 |
19 |
20 | Hello, Session
21 | ==============
22 |
23 | Here is a simple "Hello, Session" example web app for Tornado with Torndsession.::
24 |
25 |
26 | import tornado.web
27 | import tornado.httpserver
28 | import tornado.ioloop
29 | import torndsession
30 |
31 |
32 | class Application(tornado.web.Application):
33 | def __init__(self):
34 | handlers = [
35 | (r"/", MainHandler),
36 | ]
37 | settings = dict(
38 | debug=True,
39 | )
40 | # sid_name, lifetime added in 1.1.5.0
41 | # sid_name: the name of session id in cookies.
42 | # lifetime: session default expires seconds.
43 | session_settings = dict(
44 | driver='memory',
45 | driver_settings={'host': self},
46 | force_persistence=True,
47 | sid_name='torndsessionID',
48 | session_lifetime=1800
49 | ),
50 | settings.update(session=session_settings)
51 | tornado.web.Application.__init__(self, handlers, **settings)
52 |
53 |
54 | class MainHandler(torndsession.sessionhandler.SessionBaseHandler):
55 | def get(self):
56 | self.write("Hello, Session.
")
57 | if 'data' in self.session:
58 | data = self.session['data']
59 | else:
60 | data = 0
61 | self.write('data=%s' % data)
62 | self.session["data"] = data + 1
63 |
64 |
65 | def main():
66 | http_server = tornado.httpserver.HTTPServer(Application())
67 | http_server.listen(8000)
68 | tornado.ioloop.IOLoop.instance().start()
69 |
70 |
71 | if __name__ == "__main__":
72 | main()
73 |
74 |
75 | In this example, Request handler obtain memory session feature, it just inherit from SessionBaseHandler. more session example see `torndsession demos `_.
76 |
77 |
78 | Installation
79 | ============
80 |
81 | **Automatic installation**:
82 |
83 | ::
84 |
85 | pip install torndsession
86 |
87 | Torndsession is listed in `PyPI `__ and can be installed with `pip` or `easy_install`. Note that this installation can not install demos applicatinos which be included in source code.
88 |
89 | The another way is use `git+` install torndsession from github.
90 |
91 | ::
92 |
93 | pip install git+https://github.com/mitchellchu/torndsession
94 |
95 |
96 |
97 | **Manual installation**:
98 |
99 | In this way, you need download the source from `PyPI `__.::
100 |
101 | tar xvzf torndsession.tar.gz
102 | cd torndsession
103 | python setup.py build
104 | sudo python setup.py install
105 |
106 |
107 | The Torndsession source code is hosted on `GitHub `_.
108 |
109 |
110 | Updated
111 | =======
112 |
113 | Torndsession 1.1.5:
114 |
115 | - fixed bug in 1.1.4
116 | - default session id value generator changed. see `#ISSUE 12# `_.
117 | - added two custom key in settings.
118 |
119 | - sid_name: session's cookie name.
120 | - session_lifetime: default expired seconds for session.
121 |
122 | Torndsession 1.1.4:
123 |
124 | - fixed bug
125 |
126 | Torndsession 1.1.3 fixed some bug and supported python 3.x.
127 |
128 |
129 | Requires
130 | ========
131 |
132 |
133 | + `Tornado `__
134 | + `Redis (Optional) `_
135 | + `Memcached (Optional) `_
136 |
137 |
138 |
139 | LICENSE
140 | =======
141 | Torndsession is licensed under MIT.
142 |
143 |
144 |
--------------------------------------------------------------------------------
/demos/file_session.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Copyright @ 2014 Mitchell Chu
5 |
6 | from sys import version
7 | import tornado.web
8 | import tornado.httpserver
9 | import tornado.ioloop
10 | from torndsession.sessionhandler import SessionBaseHandler
11 |
12 |
13 | class Application(tornado.web.Application):
14 | def __init__(self):
15 | handlers = [
16 | (r"/", MainHandler),
17 | ]
18 | settings = dict(
19 | debug=True,
20 | )
21 | session_settings = dict(
22 | driver="file",
23 | driver_settings=dict(
24 | host="#_sessions",
25 | ),
26 | sid_name='torndsessionID',
27 | session_lifetime=1800,
28 | force_persistence=True,
29 | )
30 | settings.update(session=session_settings)
31 | tornado.web.Application.__init__(self, handlers, **settings)
32 |
33 |
34 | class MainHandler(SessionBaseHandler):
35 | def get(self):
36 | self.write("File Session Example:
")
37 | # print self.session.keys()
38 | if "sv" in self.session:
39 | self.write('sv in session
')
40 | sv = self.session["sv"]
41 | else:
42 | self.write('sv not in session
')
43 | sv = 0
44 | if sv == None:
45 | sv = 0
46 | else:
47 | sv = int(sv) + 1
48 | self.write('Current Session Value:%d
' % sv)
49 | self.write('Current Python Version: %s' % version)
50 | self.session["sv"] = sv
51 |
52 |
53 | def main():
54 | http_server = tornado.httpserver.HTTPServer(Application())
55 | http_server.listen(8888)
56 | tornado.ioloop.IOLoop.instance().start()
57 |
58 | if __name__ == "__main__":
59 | main()
60 |
--------------------------------------------------------------------------------
/demos/memory_session.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Copyright @ 2014 Mitchell Chu
5 |
6 | from sys import version
7 | import tornado.web
8 | import tornado.httpserver
9 | import tornado.ioloop
10 | from torndsession.sessionhandler import SessionBaseHandler
11 |
12 |
13 | class Application(tornado.web.Application):
14 | def __init__(self):
15 | handlers = [
16 | (r'/', MainHandler),
17 | (r'/del', DeleteHandler),
18 | ]
19 | settings = dict(
20 | debug=True,
21 | )
22 | session_settings = dict(
23 | driver="memory",
24 | driver_settings=dict(
25 | host=self,
26 | ),
27 | sid_name='torndsession-mem', # default is msid.
28 | session_lifetime=1800, # default is 1200 seconds.
29 | force_persistence=True,
30 | )
31 | settings.update(session=session_settings)
32 | tornado.web.Application.__init__(self, handlers=handlers, **settings)
33 |
34 |
35 | class MainHandler(SessionBaseHandler):
36 | def get(self):
37 | self.write("Memory Session Object Demo:
")
38 | if "sv" in self.session:
39 | current_value = self.session["sv"]
40 | else:
41 | current_value = 0
42 | if not current_value:
43 | self.write("current_value is None(0)
")
44 | current_value = 1
45 | else:
46 | current_value = int(current_value) + 1
47 | self.write('
Current Value is: %d' % current_value)
48 | self.write('
Current Python Version: %s' % version)
49 | self.session["sv"] = current_value
50 |
51 |
52 | class DeleteHandler(SessionBaseHandler):
53 | def get(self):
54 | '''
55 | Please don't do this in production environments.
56 | '''
57 | self.write("Memory Session Object Demo:")
58 | if "sv" in self.session:
59 | current_value = self.session["sv"]
60 | self.write("current sv value is %s, and system will delete this value.
" % self.session["sv"])
61 | self.session.delete("sv")
62 | if "sv" not in self.session:
63 | self.write("current sv value is empty")
64 | else:
65 | self.write("Session data not found")
66 |
67 |
68 | def main():
69 | http_server = tornado.httpserver.HTTPServer(Application())
70 | http_server.listen(8888)
71 | tornado.ioloop.IOLoop.instance().start()
72 |
73 | if __name__ == "__main__":
74 | main()
75 |
--------------------------------------------------------------------------------
/demos/redis_session.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Copyright @ 2014 Mitchell Chu
5 |
6 | import tornado.web
7 | import tornado.httpserver
8 | import tornado.ioloop
9 |
10 | from torndsession.sessionhandler import SessionBaseHandler
11 |
12 |
13 | class Application(tornado.web.Application):
14 | def __init__(self):
15 | handlers = [
16 | (r'/', MainHandler),
17 | ]
18 | settings = dict(
19 | debug=True,
20 | )
21 | session_settings = dict(
22 | driver="redis",
23 | driver_settings=dict(
24 | host='localhost',
25 | port=6379,
26 | db=0,
27 | max_connections=1024,
28 | )
29 | )
30 | settings.update(session=session_settings)
31 | tornado.web.Application.__init__(self, handlers, **settings)
32 |
33 |
34 | class MainHandler(SessionBaseHandler):
35 | def get(self):
36 | self.write("Redis Session Example:
")
37 | if 'sv' in self.session:
38 | sv = self.session["sv"]
39 | else:
40 | sv = 0
41 | self.write('Current Session Value:%s' % sv)
42 | self.session['sv'] = sv + 1
43 |
44 |
45 | def main():
46 | http_server = tornado.httpserver.HTTPServer(Application())
47 | http_server.listen(8888)
48 | tornado.ioloop.IOLoop.instance().start()
49 |
50 | if __name__ == "__main__":
51 | main()
52 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Copyright @ 2014 Mitchell Chu
5 |
6 | import io
7 |
8 | import torndsession
9 |
10 | try:
11 | from setuptools import setup
12 | except ImportError:
13 | from distutils.core import setup
14 |
15 |
16 | def read(*filenames, **kwargs):
17 | encoding = kwargs.get('encoding', 'utf-8')
18 | sep = kwargs.get('sep', '\n')
19 | buf = []
20 | for filename in filenames:
21 | with io.open(filename, encoding=encoding) as des_f:
22 | buf.append(des_f.read())
23 | return sep.join(buf)
24 |
25 | version = torndsession.version
26 | long_description = read("README.rst")
27 |
28 | setup(
29 | name='torndsession',
30 | version=version,
31 | description="Session extensions for Tornado",
32 | long_description=long_description,
33 | classifiers=[
34 | 'License :: OSI Approved :: MIT License',
35 | 'Topic :: Internet :: WWW/HTTP :: Session',
36 | 'Programming Language :: Python :: 2.7',
37 | 'Programming Language :: Python :: 3',
38 | 'Environment :: Web Environment',
39 | 'Intended Audience :: Developers',
40 | 'Operating System :: OS Independent',
41 | ],
42 | keywords='torndsession tornado session redis memcached memory file python',
43 | author="MitchellChu",
44 | author_email="zxdjsj@126.com",
45 | url="http://github.com/mitchellchu/torndsession",
46 | license="MIT",
47 | packages=["torndsession"],
48 | include_package_data=True,
49 | zip_safe=True,
50 | install_requires=['tornado',],
51 | )
52 |
--------------------------------------------------------------------------------
/torndsession/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 |
4 | # Copyright 2014 Mitchell Chu
5 |
6 | """This is a Tornado Session Extension """
7 |
8 | from __future__ import absolute_import, division, print_function, with_statement
9 |
10 | version = "1.1.5.1"
11 | version_info = (1, 1, 5, 1)
12 |
--------------------------------------------------------------------------------
/torndsession/compat.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | #
4 | # Copyright (c) 2014 Mitchell Chu
5 |
6 | import sys
7 |
8 | # __all__ = (
9 | # 'text_type', 'string_types', 'izip', 'iteritems', 'itervalues',
10 | # 'with_metaclass',
11 | # )
12 |
13 | PY3 = sys.version_info >= (3,)
14 |
15 | if PY3:
16 | text_type = str
17 | string_types = (str, )
18 | integer_types = int
19 | izip = zip
20 | _xrange = range
21 | MAXSIZE = sys.maxsize
22 |
23 | def iteritems(o):
24 | return iter(o.items())
25 |
26 | def itervalues(o):
27 | return iter(o.values())
28 |
29 | def bytes_from_hex(h):
30 | return bytes.fromhex(h)
31 |
32 | def reraise(exctype, value, trace=None):
33 | raise exctype(str(value)).with_traceback(trace)
34 |
35 | def _unicode(s):
36 | return s
37 | else:
38 | text_type = unicode
39 | string_types = (basestring, )
40 | integer_types = (int, long)
41 | from itertools import izip
42 | _xrange = xrange
43 | MAXSIZE = sys.maxint
44 |
45 | def b(s):
46 | # See comments above. In python 2.x b('foo') is just 'foo'.
47 | return s
48 |
49 | def iteritems(o):
50 | return o.iteritems()
51 |
52 | def itervalues(o):
53 | return o.itervalues()
54 |
55 | def bytes_from_hex(h):
56 | return h.decode('hex')
57 |
58 | # "raise x, y, z" raises SyntaxError in Python 3
59 | exec("""def reraise(exctype, value, trace=None):
60 | raise exctype, str(value), trace
61 | """)
62 |
63 | _unicode = unicode
64 |
65 |
66 | def with_metaclass(meta, base=object):
67 | return meta("NewBase", (base,), {})
68 |
--------------------------------------------------------------------------------
/torndsession/driver.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Copyright @ 2014 Mitchell Chu
5 |
6 | from __future__ import (absolute_import, division, print_function,
7 | with_statement)
8 |
9 |
10 | class SessionDriver(object):
11 | '''
12 | abstact class for all real session driver implements.
13 | '''
14 | def __init__(self, **settings):
15 | self.settings = settings
16 |
17 | def get(self, session_id):
18 | raise NotImplementedError()
19 |
20 | def save(self, session_id, session_data, expires=None):
21 | raise NotImplementedError()
22 |
23 | def clear(self, session_id):
24 | raise NotImplementedError()
25 |
26 | def remove_expires(self):
27 | raise NotImplementedError()
28 |
29 | class SessionDriverFactory(object):
30 | '''
31 | session driver factory
32 | use input settings to return suitable driver's instance
33 | '''
34 | @staticmethod
35 | def create_driver(driver, **settings):
36 | module_name = 'torndsession.%ssession' % driver.lower()
37 | module = __import__(module_name, globals(), locals(), ['object'])
38 | # must use this form.
39 | # use __import__('torndsession.' + driver.lower()) just load torndsession.__init__.pyc
40 | cls = getattr(module, '%sSession' % driver.capitalize())
41 | if not 'SessionDriver' in [base.__name__ for base in cls.__bases__]:
42 | raise InvalidSessionDriverException(
43 | '%s not found in current driver implements ' % driver)
44 | return cls
45 |
46 |
47 | class InvalidSessionDriverException(Exception):
48 | pass
49 |
--------------------------------------------------------------------------------
/torndsession/filesession.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Copyright @ 2014 Mitchell Chu
5 |
6 | from __future__ import (absolute_import, division, print_function,
7 | with_statement)
8 |
9 | from datetime import datetime
10 | import os
11 | from os.path import exists, isdir, join
12 |
13 | from torndsession.driver import SessionDriver
14 | from torndsession.session import SessionConfigurationError
15 |
16 | utcnow = datetime.utcnow
17 | try:
18 | import cPickle as pickle # py2
19 | except ImportError:
20 | import pickle # py3
21 |
22 |
23 | class FileSession(SessionDriver):
24 | """
25 | System File to save session object.
26 | """
27 | # default host is '#_sessions' directory which is in current directory.
28 | DEFAULT_SESSION_POSITION = './#_sessions'
29 | """
30 | Session file default save position.
31 | In a recommendation, you need give the host option.
32 | when host is missed, system will use this value by default.
33 |
34 | Additional @ Version: 1.1
35 | """
36 |
37 | def __init__(self, **settings):
38 | """
39 | Initialize File Session Driver.
40 | settings section 'host' is recommended, the option 'prefix' is an optional.
41 | if prefix is not given, 'default' is the default.
42 | host: where to save session object file, this is a directory path.
43 | prefix: session file name's prefix. session file like: prefix@session_id
44 | """
45 | super(FileSession, self).__init__(**settings)
46 | self.host = settings.get("host", self.DEFAULT_SESSION_POSITION)
47 | self._prefix = settings.get("prefix", 'default')
48 | if not exists(self.host):
49 | os.makedirs(self.host, 448) # only owner can visit this session directory.
50 |
51 | if not isdir(self.host):
52 | raise SessionConfigurationError('session host not found')
53 |
54 | def get(self, session_id):
55 | session_file = join(self.host, self._prefix + session_id)
56 | if not exists(session_file):
57 | return {}
58 |
59 | with open(session_file, 'rb') as rf:
60 | session = pickle.load(rf)
61 |
62 | now = utcnow()
63 | expires = session.get('__expires__', now)
64 | if expires > now:
65 | return session
66 | return {}
67 |
68 | def save(self, session_id, session_data, expires=None):
69 | session_file = join(self.host, self._prefix + session_id)
70 | session_data = session_data if session_data else {}
71 |
72 | if expires:
73 | session_data.update(__expires__=expires)
74 | with open(session_file, 'wb') as wf:
75 | pickle.dump(session_data, wf)
76 |
77 | def clear(self, session_id):
78 | session_file = join(self.host, self._prefix + session_id)
79 | if exists(session_file):
80 | os.remove(session_file)
81 |
82 | def remove_expires(self):
83 | if not exists(self.host) or not isdir(self.host): return
84 | now = utcnow()
85 | for session_file in os.listdir(self.host):
86 | if session_file.startswith(self._prefix):
87 | session_file = join(self.host, session_file)
88 | with open(session_file, 'rb') as sfile:
89 | session = pickle.load(sfile)
90 | expires = session.get('__expires__', now)
91 | if expires <= now:
92 | os.remove(session_file)
93 |
--------------------------------------------------------------------------------
/torndsession/memcachedsession.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Copyright @ 2014 Mitchell Chu
5 |
6 | from __future__ import (absolute_import, division, print_function,
7 | with_statement)
8 |
9 | from datetime import datetime
10 | import numbers
11 | from copy import copy
12 |
13 | from torndsession.driver import SessionDriver
14 |
15 | import memcache
16 |
17 | try:
18 | import cPickle as pickle # py2
19 | except:
20 | import pickle # py3
21 |
22 | """
23 | NOTICE:
24 | This session extension was not be tested.
25 | you must test all functions before use.
26 | """
27 |
28 |
29 | class MemcachedSession(SessionDriver):
30 | """
31 | Use memcached to save session object
32 | """
33 | DEFAULT_MEMCACHED_HOST = '127.0.0.1'
34 | DEFAULT_MEMCACHED_PORT = '11211'
35 |
36 | def __init__(self, **settings):
37 | super(MemcachedSession, self).__init__(**settings)
38 | self.client = self.__create_memcached_client()
39 |
40 | def get(self, session_id):
41 | mem_data = self.client.get(session_id)
42 | if not mem_data: return {}
43 | return pickle.loads(mem_data)
44 |
45 | def save(self, session_id, session_data, expires=None):
46 | session_data = session_data if session_data else {}
47 | if expires:
48 | session_data.update(__expires__=expires)
49 | mem_data = pickle.dumps(session_data)
50 | expires = self.__get_expires_seconds(expires)
51 | self.client.set(session_id, mem_data, expires)
52 |
53 | def clear(self, session_id):
54 | self.client.delete(session_id)
55 |
56 | def remove_expires(self):
57 | pass
58 |
59 | def __create_memcached_client(self):
60 | settings = copy(self.settings)
61 | host = settings.pop('host', self.DEFAULT_MEMCACHED_HOST)
62 | port = settings.pop('port', self.DEFAULT_MEMCACHED_PORT)
63 | servers = '%s:%s' % (host, port)
64 | return memcache.Client(servers, **settings)
65 |
66 | def __get_expires_seconds(self, expires):
67 | if isinstance(expires, numbers.Real):
68 | return int(expires)
69 | elif isinstance(expires, datetime):
70 | now = datetime.utcnow()
71 | return int((expires - now).total_seconds())
72 | else:
73 | return 0
74 |
--------------------------------------------------------------------------------
/torndsession/memorysession.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Copyright @ 2014 Mitchell Chu
5 |
6 | from __future__ import (absolute_import, division, print_function,
7 | with_statement)
8 |
9 | from datetime import datetime
10 |
11 | from torndsession.driver import SessionDriver
12 | from torndsession.session import SessionConfigurationError
13 | from torndsession.compat import iteritems
14 |
15 |
16 | class MemorySession(SessionDriver):
17 | """
18 | save session data in process memory
19 | """
20 |
21 | MAX_SESSION_OBJECTS = 1024
22 | """The max session objects save in memory.
23 | when session objects count large than this value,
24 | system will auto to clear the expired session data.
25 | """
26 |
27 | def __init__(self, **settings):
28 | # check settings
29 | super(MemorySession, self).__init__(**settings)
30 | host = settings.get("host")
31 | if not host:
32 | raise SessionConfigurationError(
33 | 'memory session driver can not found persistence position')
34 | if not hasattr(host, "session_container"):
35 | setattr(host, "session_container", {})
36 | self._data_handler = host.session_container
37 |
38 | def get(self, session_id):
39 | """
40 | get session object from host.
41 | """
42 | if session_id not in self._data_handler:
43 | return {}
44 |
45 | session_obj = self._data_handler[session_id]
46 | now = datetime.utcnow()
47 | expires = session_obj.get('__expires__', now)
48 | if expires > now:
49 | return session_obj
50 | return {}
51 |
52 | def save(self, session_id, session_data, expires=None):
53 | """
54 | save session data to host.
55 | if host's session objects is more then MAX_SESSION_OBJECTS
56 | system will auto to clear expired session data.
57 | after cleared, system will add current to session pool, however the pool is full.
58 | """
59 | session_data = session_data or {}
60 | if expires:
61 | session_data.update(__expires__=expires)
62 | if len(self._data_handler) >= self.MAX_SESSION_OBJECTS:
63 | self.remove_expires()
64 | if len(self._data_handler) >= self.MAX_SESSION_OBJECTS:
65 | print("system session pool is full. need more memory to save session object.")
66 | self._data_handler[session_id] = session_data
67 |
68 | def clear(self, session_id):
69 | if self._data_handler.haskey(session_id):
70 | del self._data_handler[session_id]
71 |
72 | def remove_expires(self):
73 | keys = []
74 | for key, val in iteritems(self._data_handler):
75 | now = datetime.utcnow()
76 | expires = val.get("__expires__", now)
77 | if now >= expires:
78 | keys.append(key)
79 | for key in keys:
80 | del self._data_handler[key]
81 |
--------------------------------------------------------------------------------
/torndsession/redissession.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Copyright @ 2014 Mitchell Chu
5 |
6 | from __future__ import (absolute_import, division, print_function,
7 | with_statement)
8 |
9 | from copy import copy
10 | from datetime import datetime
11 |
12 | from torndsession.driver import SessionDriver
13 |
14 | import redis
15 |
16 | try:
17 | import cPickle as pickle # py2
18 | except ImportError:
19 | import pickle # py3
20 |
21 |
22 | class RedisSession(SessionDriver):
23 | """
24 | Use Redis to save session object.
25 | """
26 |
27 | def get(self, session_id):
28 | self.__create_redis_client()
29 | session_data = self.client.get(session_id)
30 | if not session_data:
31 | return {}
32 | return pickle.loads(session_data)
33 |
34 | def save(self, session_id, session_data, expires=None):
35 | session_data = session_data if session_data else {}
36 | if expires:
37 | session_data.update(__expires__=expires)
38 | session_data = pickle.dumps(session_data)
39 | self.__create_redis_client()
40 | self.client.set(session_id, session_data)
41 | if expires:
42 | delta_seconds = int((expires - datetime.utcnow()).total_seconds())
43 | self.client.expire(session_id, delta_seconds)
44 |
45 | def clear(self, session_id):
46 | self.__create_redis_client()
47 | self.client.delete(session_id)
48 |
49 | def remove_expires(self):
50 | pass
51 |
52 | def __create_redis_client(self):
53 | if not hasattr(self, 'client'):
54 | if 'max_connections' in self.settings:
55 | connection_pool = redis.ConnectionPool(**self.settings)
56 | settings = copy(self.settings)
57 | del settings['max_connections']
58 | settings['connection_pool'] = connection_pool
59 | else:
60 | settings = self.settings
61 | self.client = redis.Redis(**settings)
62 |
--------------------------------------------------------------------------------
/torndsession/session.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Copyright @ 2014 Mitchell Chu
5 | #
6 |
7 | from __future__ import (absolute_import, division, print_function,
8 | with_statement)
9 |
10 | from datetime import datetime, timedelta
11 | # from uuid import uuid4
12 | from os import urandom
13 | from binascii import b2a_base64
14 |
15 | from torndsession.driver import SessionDriverFactory
16 | from torndsession.compat import _xrange
17 |
18 | l = [c for c in map(chr, _xrange(256))]
19 | l[47] = '-'
20 | l[43] = '_'
21 | l[61] = '.'
22 | _smap = str('').join(l)
23 | del l
24 |
25 |
26 | class SessionManager(object):
27 |
28 | SESSION_ID = 'msid'
29 | DEFAULT_SESSION_LIFETIME = 1200 # seconds
30 |
31 | def __init__(self, handler):
32 | self.handler = handler
33 | self.settings = {}
34 | self.__init_settings()
35 | self._default_session_lifetime = datetime.utcnow() + timedelta(
36 | seconds=self.settings.get('session_lifetime', self.DEFAULT_SESSION_LIFETIME))
37 | self._expires = self._default_session_lifetime
38 | self._is_dirty = True
39 | self.__init_session_driver()
40 | self.__init_session_object() # initialize session object
41 |
42 | def __init_session_object(self):
43 | cookiename = self.settings.get('sid_name', self.SESSION_ID)
44 | session_id = self.handler.get_cookie(cookiename)
45 | if not session_id:
46 | session_id = self._generate_session_id(30)
47 | self.handler.set_cookie(cookiename,
48 | session_id,
49 | **self.__session_settings())
50 | self._is_dirty = True
51 | self.session = {}
52 | else:
53 | self.session = self._get_session_object_from_driver(session_id)
54 | if not self.session:
55 | self.session = {}
56 | self._is_dirty = True
57 | else:
58 | self._is_dirty = False
59 | cookie_config = self.settings.get("cookie_config")
60 | if cookie_config:
61 | expires = cookie_config.get("expires")
62 | expires_days = cookie_config.get("expires_days")
63 | if expires_days is not None and not expires:
64 | expires = datetime.utcnow() + timedelta(days=expires_days)
65 | if expires and isinstance(expires, datetime):
66 | self._expires = expires
67 | self._expires = self._expires if self._expires else self._default_session_lifetime
68 | self._id = session_id
69 |
70 | def __init_session_driver(self):
71 | """
72 | setup session driver.
73 | """
74 |
75 | driver = self.settings.get("driver")
76 | if not driver:
77 | raise SessionConfigurationError('driver not found')
78 | driver_settings = self.settings.get("driver_settings", {})
79 | if not driver_settings:
80 | raise SessionConfigurationError('driver settings not found.')
81 |
82 | cache_driver = self.settings.get("cache_driver", True)
83 | if cache_driver:
84 | cache_name = '__cached_session_driver'
85 | cache_handler = self.handler.application
86 | if not hasattr(cache_handler, cache_name):
87 | setattr(
88 | cache_handler,
89 | cache_name,
90 | SessionDriverFactory.create_driver(driver, **driver_settings))
91 | session_driver = getattr(cache_handler, cache_name)
92 | else:
93 | session_driver = SessionDriverFactory.create_driver(driver, **driver_settings)
94 | self.driver = session_driver(**driver_settings) # create session driver instance.
95 |
96 | def __init_settings(self):
97 | """
98 | Init session relative configurations.
99 | all configuration settings as follow:
100 | settings = dict(
101 | cookie_secret = "00a03c657e749caa89ef650a57b53ba()(",
102 | debug = True,
103 | session = {
104 | driver = 'memory',
105 | driver_settings = {'host': self,}, # use application to save session data.
106 | force_persistence = True,
107 | cache_driver = True, # cache driver in application.
108 | cookie_config = {'expires_days': 10, 'expires': datetime.datetime.utcnow(),}, # tornado cookies configuration
109 | },
110 | )
111 |
112 | driver: default enum value: memory, file, redis, memcache.
113 | driver_settings: the data driver need. settings may be the host, database, password, and so on.
114 | redis settings as follow:
115 | driver_settings = {
116 | host = '127.0.0.1',
117 | port = '6379',
118 | db = 0, # where the session data to save.
119 | password = 'session_db_password', # if database has password
120 | }
121 | force_persistence: default is False.
122 | In default, session's data exists in memory only, you must persistence it by manual.
123 | Generally, rewrite Tornado RequestHandler's prepare(self) and on_finish(self) to persist session data is recommended.
124 | when this value set to True, session data will be force to persist everytime when it has any change.
125 |
126 | """
127 | session_settings = self.handler.settings.get("session")
128 | if not session_settings: # use default
129 | session_settings = {}
130 | session_settings.update(
131 | driver='memory',
132 | driver_settings={'host': self.handler.application},
133 | force_persistence=True,
134 | cache_driver=True)
135 | self.settings = session_settings
136 |
137 | def _generate_session_id(self, blength=24):
138 | """generate session id
139 |
140 | Implement: https://github.com/MitchellChu/torndsession/issues/12
141 |
142 | :arg int blength: give the bytes to generate.
143 | :return string: session string
144 |
145 | .. versionadded:: 1.1.5
146 | """
147 | session_id = (b2a_base64(urandom(blength)))[:-1]
148 | if isinstance(session_id, str):
149 | # PY2
150 | return session_id.translate(_smap)
151 | return session_id.decode('utf-8').translate(_smap)
152 |
153 | def _get_session_object_from_driver(self, session_id):
154 | """
155 | Get session data from driver.
156 | """
157 | return self.driver.get(session_id)
158 |
159 | def get(self, key, default=None):
160 | """
161 | Return session value with name as key.
162 | """
163 | return self.session.get(key, default)
164 |
165 | def set(self, key, value):
166 | """
167 | Add/Update session value
168 | """
169 | self.session[key] = value
170 | self._is_dirty = True
171 | force_update = self.settings.get("force_persistence")
172 | if force_update:
173 | self.driver.save(self._id, self.session, self._expires)
174 | self._is_dirty = False
175 |
176 | def delete(self, key):
177 | """
178 | Delete session key-value pair
179 | """
180 | if key in self.session:
181 | del self.session[key]
182 | self._is_dirty = True
183 | force_update = self.settings.get("force_persistence")
184 | if force_update:
185 | self.driver.save(self._id, self.session, self._expires)
186 | self._is_dirty = False
187 | __delitem__ = delete
188 |
189 | def iterkeys(self):
190 | return iter(self.session)
191 | __iter__ = iterkeys
192 |
193 | def keys(self):
194 | """
195 | Return all keys in session object
196 | """
197 | return self.session.keys()
198 |
199 | def flush(self):
200 | """
201 | this method force system to do session data persistence.
202 | """
203 | if self._is_dirty:
204 | self.driver.save(self._id, self.session, self._expires)
205 |
206 | def __setitem__(self, key, value):
207 | self.set(key, value)
208 |
209 | def __getitem__(self, key):
210 | val = self.get(key)
211 | if val:
212 | return val
213 | raise KeyError('%s not found' % key)
214 |
215 | def __contains__(self, key):
216 | return key in self.session
217 |
218 | @property
219 | def id(self):
220 | """
221 | Return current session id
222 | """
223 | if not hasattr(self, '_id'):
224 | self.__init_session_object()
225 | return self._id
226 |
227 | @property
228 | def expires(self):
229 | """
230 | The session object lifetime on server.
231 | this property could not be used to cookie expires setting.
232 | """
233 | if not hasattr(self, '_expires'):
234 | self.__init_session_object()
235 | return self._expires
236 |
237 | def __session_settings(self):
238 | session_settings = self.settings.get('cookie_config', {})
239 | session_settings.setdefault('expires', None)
240 | session_settings.setdefault('expires_days', None)
241 | return session_settings
242 |
243 |
244 | class SessionMixin(object):
245 |
246 | @property
247 | def session(self):
248 | return self._create_mixin(self, '__session_manager', SessionManager)
249 |
250 | def _create_mixin(self, context, inner_property_name, session_handler):
251 | if not hasattr(context, inner_property_name):
252 | setattr(context, inner_property_name, session_handler(context))
253 | return getattr(context, inner_property_name)
254 |
255 |
256 | class SessionConfigurationError(Exception):
257 | pass
258 |
--------------------------------------------------------------------------------
/torndsession/sessionhandler.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Copyright @ 2014 Mitchell Chu
5 |
6 | from __future__ import (absolute_import, division, print_function,
7 | with_statement)
8 |
9 | import tornado.web
10 | import torndsession.session
11 |
12 |
13 | class SessionBaseHandler(tornado.web.RequestHandler, torndsession.session.SessionMixin):
14 | """
15 | This is a tornado web request handler which is base on torndsession.
16 | Generally, user must persistent session object with manual operation when force_persistence is False.
17 | but when the handler is inherit from SessionBaseHandler, in your handler, you just need to add/update/delete session values, SessionBaseHandler will auto save it.
18 | """
19 |
20 | def prepare(self):
21 | """
22 | Overwrite tornado.web.RequestHandler prepare.
23 | """
24 | pass
25 |
26 | def on_finish(self):
27 | """
28 | Overwrite tornado.web.RequestHandler on_finish.
29 | """
30 | self.session.flush() # try to save session
31 |
--------------------------------------------------------------------------------
/torndsession/test/TEST_DIRECTORY:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------