underscorepy 0.5.1__tar.gz → 0.6.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. underscorepy-0.6.2/LICENSE +22 -0
  2. underscorepy-0.6.2/MANIFEST.in +2 -0
  3. underscorepy-0.6.2/PKG-INFO +23 -0
  4. underscorepy-0.6.2/README.md +7 -0
  5. underscorepy-0.6.2/_/__init__.py +32 -0
  6. underscorepy-0.6.2/_/application.py +221 -0
  7. underscorepy-0.6.2/_/auth.py +105 -0
  8. underscorepy-0.6.2/_/caches/__init__.py +50 -0
  9. underscorepy-0.6.2/_/caches/dbcache.py +117 -0
  10. underscorepy-0.6.2/_/caches/memory.py +71 -0
  11. underscorepy-0.6.2/_/caches/redis.py +95 -0
  12. underscorepy-0.6.2/_/components.py +100 -0
  13. underscorepy-0.6.2/_/databases/__init__.py +178 -0
  14. underscorepy-0.6.2/_/databases/postgres.py +97 -0
  15. underscorepy-0.6.2/_/databases/sqlite.py +151 -0
  16. underscorepy-0.6.2/_/handlers.py +40 -0
  17. underscorepy-0.6.2/_/logins/__init__.py +72 -0
  18. underscorepy-0.6.2/_/logins/dblogin.py +194 -0
  19. underscorepy-0.6.2/_/logins/facebook.py +40 -0
  20. underscorepy-0.6.2/_/logins/gitlab.py +49 -0
  21. underscorepy-0.6.2/_/logins/google.py +20 -0
  22. underscorepy-0.5.1/_/web/auth/simple.py → underscorepy-0.6.2/_/logins/htpasswd.py +24 -13
  23. underscorepy-0.6.2/_/logins/oauth2.py +52 -0
  24. underscorepy-0.6.2/_/logins/slap.py +60 -0
  25. underscorepy-0.6.2/_/records/Protobuf.proto +19 -0
  26. underscorepy-0.6.2/_/records/__init__.py +197 -0
  27. underscorepy-0.6.2/_/records/data.py +186 -0
  28. underscorepy-0.6.2/_/records/protobuf.py +149 -0
  29. underscorepy-0.6.2/_/settings.py +149 -0
  30. underscorepy-0.6.2/_/supports/__init__.py +29 -0
  31. underscorepy-0.6.2/_/supports/nginx/__init__.py +58 -0
  32. underscorepy-0.6.2/_/supports/nginx/nginx.conf +59 -0
  33. underscorepy-0.6.2/_/supports/systemd/__init__.py +54 -0
  34. underscorepy-0.6.2/_/supports/systemd/systemd.service +12 -0
  35. underscorepy-0.6.2/_/utils.py +99 -0
  36. underscorepy-0.6.2/_/version.py +5 -0
  37. underscorepy-0.6.2/_/websockets.py +49 -0
  38. underscorepy-0.6.2/setup.py +29 -0
  39. underscorepy-0.6.2/underscorepy.egg-info/PKG-INFO +23 -0
  40. underscorepy-0.6.2/underscorepy.egg-info/SOURCES.txt +43 -0
  41. underscorepy-0.6.2/underscorepy.egg-info/not-zip-safe +1 -0
  42. underscorepy-0.6.2/underscorepy.egg-info/requires.txt +1 -0
  43. underscorepy-0.5.1/MANIFEST.in +0 -1
  44. underscorepy-0.5.1/PKG-INFO +0 -20
  45. underscorepy-0.5.1/README.rst +0 -3
  46. underscorepy-0.5.1/_/__init__.py +0 -22
  47. underscorepy-0.5.1/_/colors.py +0 -448
  48. underscorepy-0.5.1/_/components.py +0 -69
  49. underscorepy-0.5.1/_/decorators.py +0 -10
  50. underscorepy-0.5.1/_/io.py +0 -62
  51. underscorepy-0.5.1/_/piper.py +0 -152
  52. underscorepy-0.5.1/_/settings.py +0 -125
  53. underscorepy-0.5.1/_/shell.py +0 -371
  54. underscorepy-0.5.1/_/skel.py +0 -33
  55. underscorepy-0.5.1/_/storage/__init__.py +0 -16
  56. underscorepy-0.5.1/_/storage/engines/__init__.py +0 -41
  57. underscorepy-0.5.1/_/storage/engines/mongo.py +0 -63
  58. underscorepy-0.5.1/_/storage/engines/postgres.py +0 -151
  59. underscorepy-0.5.1/_/storage/engines/redis.py +0 -32
  60. underscorepy-0.5.1/_/storage/engines/sqlite.py +0 -98
  61. underscorepy-0.5.1/_/storage/records.py +0 -157
  62. underscorepy-0.5.1/_/version.py +0 -8
  63. underscorepy-0.5.1/_/web/__init__.py +0 -18
  64. underscorepy-0.5.1/_/web/application.py +0 -123
  65. underscorepy-0.5.1/_/web/auth/__init__.py +0 -15
  66. underscorepy-0.5.1/_/web/auth/basic.py +0 -24
  67. underscorepy-0.5.1/_/web/auth/facebook.py +0 -69
  68. underscorepy-0.5.1/_/web/auth/google.py +0 -54
  69. underscorepy-0.5.1/_/web/auth/logout.py +0 -8
  70. underscorepy-0.5.1/_/web/auth/slap.py +0 -46
  71. underscorepy-0.5.1/_/web/handlers/__init__.py +0 -14
  72. underscorepy-0.5.1/_/web/handlers/index.py +0 -46
  73. underscorepy-0.5.1/_/web/handlers/ws.py +0 -42
  74. underscorepy-0.5.1/_/web/util.py +0 -9
  75. underscorepy-0.5.1/setup.py +0 -40
  76. underscorepy-0.5.1/underscorepy.egg-info/PKG-INFO +0 -20
  77. underscorepy-0.5.1/underscorepy.egg-info/SOURCES.txt +0 -38
  78. underscorepy-0.5.1/underscorepy.egg-info/requires.txt +0 -2
  79. {underscorepy-0.5.1 → underscorepy-0.6.2}/setup.cfg +0 -0
  80. {underscorepy-0.5.1 → underscorepy-0.6.2}/underscorepy.egg-info/dependency_links.txt +0 -0
  81. {underscorepy-0.5.1 → underscorepy-0.6.2}/underscorepy.egg-info/top_level.txt +0 -0
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015-2023 Matthew Shaw
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
@@ -0,0 +1,2 @@
1
+ recursive-include _ *
2
+ include *.md
@@ -0,0 +1,23 @@
1
+ Metadata-Version: 2.1
2
+ Name: underscorepy
3
+ Version: 0.6.2
4
+ Summary: _.py library
5
+ Home-page: https://shaw.cx/_.py
6
+ Author: Matthew Shaw
7
+ Author-email: mshaw.cx@gmail.com
8
+ License: MIT
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Topic :: Software Development :: Libraries
13
+ Classifier: Programming Language :: Python :: 3.7
14
+ License-File: LICENSE
15
+ Requires-Dist: tornado
16
+
17
+
18
+ underscore.py
19
+ -------------
20
+
21
+ This is a framework for me that has morphed overtime. Some "features" are broken because I have not needed them for my own projects for quite some time. Eventually those bugs will be resolved.
22
+
23
+ For anyone confused by Python files like `_/databases/__databases__.py` I wrote a custom import loader that looks for `__dirname__.py` files before `__init__.py` because I find it easier to track the files that way. Very experimental, and probably fragile, but maybe I'll submit a PEP for it someday.
@@ -0,0 +1,7 @@
1
+
2
+ underscore.py
3
+ -------------
4
+
5
+ This is a framework for me that has morphed overtime. Some "features" are broken because I have not needed them for my own projects for quite some time. Eventually those bugs will be resolved.
6
+
7
+ For anyone confused by Python files like `_/databases/__databases__.py` I wrote a custom import loader that looks for `__dirname__.py` files before `__init__.py` because I find it easier to track the files that way. Very experimental, and probably fragile, but maybe I'll submit a PEP for it someday.
@@ -0,0 +1,32 @@
1
+ #
2
+ # (c) 2015-2023 Matthew Shaw
3
+ #
4
+ # Authors
5
+ # =======
6
+ # Matthew Shaw <mshaw.cx@gmail.com>
7
+ #
8
+
9
+ import importlib
10
+ import os
11
+ import sys
12
+
13
+ root = os.path.dirname(__file__)
14
+ root = os.path.join(root, '..')
15
+ root = os.path.abspath(root)
16
+
17
+ from .utils import *
18
+
19
+ from . import version
20
+ from . import settings
21
+ from . import auth
22
+ from . import handlers
23
+ from . import components
24
+
25
+ from .application import Application
26
+
27
+ # placeholder for comonents
28
+ caches = Container()
29
+ databases = Container()
30
+ logins = Container()
31
+ records = Container()
32
+ supports = Container()
@@ -0,0 +1,221 @@
1
+ #
2
+ # (c) 2015-2023 Matthew Shaw
3
+ #
4
+ # Authors
5
+ # =======
6
+ # Matthew Shaw <mshaw.cx@gmail.com>
7
+ #
8
+
9
+ import asyncio
10
+ import logging
11
+ import os
12
+ import signal
13
+ import socket
14
+ import sys
15
+ import traceback
16
+
17
+ import tornado.web
18
+
19
+ import _
20
+
21
+
22
+ class Application(tornado.web.Application):
23
+ @classmethod
24
+ def main(cls, ns=None):
25
+ _.application = self = cls()
26
+ try:
27
+ asyncio.run(self.__async_main(ns))
28
+ except _.error as e:
29
+ if _.args.debug:
30
+ traceback.print_tb(e.__traceback__)
31
+ logging.error('%s', e)
32
+
33
+ async def __async_main(self, ns):
34
+ self._records_patterns = []
35
+ self._login_patterns = []
36
+
37
+ self.patterns = []
38
+ self.settings = {}
39
+
40
+ self.loop = asyncio.get_event_loop()
41
+ self._stop_event = asyncio.Event()
42
+
43
+ signal.signal(signal.SIGINT, self.__signalHandler)
44
+ signal.signal(signal.SIGTERM, self.__signalHandler)
45
+
46
+ name = self.__class__.__name__.lower()
47
+ try:
48
+ await _.settings.load(ns=ns, name=name)
49
+ await self.logging()
50
+ except _.error as e:
51
+ if _.args.debug:
52
+ traceback.print_tb(e.__traceback__)
53
+ logging.error('%s', e)
54
+ self.stop()
55
+
56
+ if not self._stop_event.is_set():
57
+ for name,component in _.logins.items():
58
+ try:
59
+ await _.wait(component.args(name))
60
+ except _.error as e:
61
+ logging.error('%s', e)
62
+ self.stop()
63
+ break
64
+
65
+ if not self._stop_event.is_set():
66
+ try:
67
+ await self.__async_init()
68
+ except _.error as e:
69
+ logging.error('%s', e)
70
+
71
+ for name,component in _.caches.items():
72
+ await component.close()
73
+
74
+ for name,component in _.databases.items():
75
+ await component.close()
76
+
77
+ async def __async_init(self, **kwds):
78
+ # check if a sessions cache was specified
79
+ _.sessions = _.config.get(_.name, 'sessions', fallback=None)
80
+ if _.sessions:
81
+ logging.debug('Sessions cache: %s', _.sessions)
82
+ try:
83
+ _.sessions = _.caches[_.sessions]
84
+ except KeyError:
85
+ raise _.error('Unknown sessions cache instance: %s', _.sessions)
86
+
87
+ self.settings['static_path'] = _.paths('static')
88
+ self.settings['template_path'] = _.paths('templates')
89
+ self.settings['debug'] = _.args.debug
90
+
91
+ # call the underscore application's entry point
92
+ try:
93
+ await _.wait(self.initialize())
94
+ except NotImplementedError:
95
+ logging.warning('No "initialize" function defined')
96
+
97
+ if 'cookie_secret' not in self.settings:
98
+ self.settings['cookie_secret'] = await self.cookie_secret()
99
+
100
+ patterns = list(self._records_patterns)
101
+
102
+ if self._login_patterns:
103
+ self._login_patterns += [
104
+ ( r'/login', _.logins.LoginPage ),
105
+ ( r'/logout', _.logins.Logout ),
106
+ ]
107
+ self.settings['login_url'] = '/login'
108
+ patterns += self._login_patterns
109
+ patterns += self.patterns
110
+
111
+ patterns.append(
112
+ ( r'/(favicon.ico)', tornado.web.StaticFileHandler, {'path':''}),
113
+ )
114
+
115
+ if _.args.debug:
116
+ for (pattern,cls,*params) in patterns:
117
+ handler = f'{cls.__module__}.{cls.__name__}'
118
+ logging.debug('%-32s %s %s', pattern, handler, params[0] if params else '')
119
+
120
+ await self.__listen(patterns)
121
+ await self._stop_event.wait()
122
+ await _.wait(self.on_stop())
123
+
124
+ async def __listen(self, patterns, **kwds):
125
+ # call the Tornado Application init here to give children a chance
126
+ # to initialize patterns and settings
127
+ super().__init__(patterns, **self.settings)
128
+
129
+ if 'xheaders' not in kwds:
130
+ kwds['xheaders'] = True
131
+
132
+ try:
133
+ self.listen(_.args.port, _.args.address, **kwds)
134
+ except Exception as e:
135
+ raise _.error('%s', e) from None
136
+
137
+ logging.info('Listening on %s:%d', _.args.address, _.args.port)
138
+
139
+ def _record_handler(self, name, cls):
140
+ self._records_patterns.append(
141
+ (f'/{name}/{cls._name}(?:/(.*))?', cls)
142
+ )
143
+
144
+ def _login_handler(self, name, cls):
145
+ self._login_patterns.append(
146
+ (f'/{name}/{cls._name}', cls)
147
+ )
148
+
149
+ async def initialize(self):
150
+ 'underscore apps should override this function'
151
+ raise NotImplementedError
152
+
153
+ async def logging(self):
154
+ 'underscore apps can override or extend this function'
155
+
156
+ # add the handlers to the logger
157
+ if _.config.getboolean(_.name, 'logging', fallback=False):
158
+ full_path = _.paths(f'{_.name}.log')
159
+ file_logger = logging.FileHandler(full_path)
160
+ file_logger.setLevel(logging.DEBUG if _.args.debug else logging.INFO)
161
+ file_logger.setFormatter(
162
+ logging.Formatter(
163
+ fmt = '%(asctime)s %(levelname)-8s %(message)s',
164
+ datefmt = '%Y-%m-%d %H:%M:%S',
165
+ )
166
+ )
167
+ root_logger = logging.getLogger()
168
+ root_logger.addHandler(file_logger)
169
+
170
+ async def cookie_secret(self):
171
+ 'underscore apps can override this function'
172
+ if _.sessions is not None:
173
+ return await _.wait(_.sessions.cookie_secret())
174
+
175
+ async def on_login(self, handler, user, *args, **kwds):
176
+ 'underscore apps should override this function if a login is specified'
177
+ raise NotImplementedError
178
+
179
+ def task(self, fn, *args, **kwds):
180
+ async def _task():
181
+ await _.wait(fn(*args, **kwds))
182
+ return asyncio.create_task(_task())
183
+
184
+ def periodic(self, _timeout, fn, *args, **kwds):
185
+ 'run a function or coroutine on a recurring basis'
186
+ async def _periodic():
187
+ while True:
188
+ # bail if the stop event is set
189
+ # otherwise run the function after the timeout occurs
190
+ try:
191
+ await asyncio.wait_for(self._stop_event.wait(), timeout=_timeout)
192
+ break
193
+ except asyncio.TimeoutError as e:
194
+ pass
195
+ try:
196
+ result = await _.wait(fn(*args, **kwds))
197
+ except Exception as e:
198
+ logging.exception(e)
199
+ return asyncio.create_task(_periodic())
200
+
201
+ def stop(self):
202
+ logging.debug('Setting stop event')
203
+ self._stop_event.set()
204
+
205
+ def on_stop(self):
206
+ pass
207
+
208
+ def __signalHandler(self, signum, frame):
209
+ 'handle signals in a thread-safe way'
210
+ signame = signal.Signals(signum).name
211
+ handler = getattr(self, f'on_{signame.lower()}', None)
212
+ if handler:
213
+ if handler(signum, frame):
214
+ return
215
+ logging.info('Terminating %s on %s signal', _.name, signame)
216
+ self.loop.call_soon_threadsafe(self.stop)
217
+
218
+ # demonstrate a specific signal handler
219
+ # and print newline after ^C on terminals
220
+ def on_sigint(self, signum, frame):
221
+ print()
@@ -0,0 +1,105 @@
1
+ #
2
+ # (c) 2015-2023 Matthew Shaw
3
+ #
4
+ # Authors
5
+ # =======
6
+ # Matthew Shaw <mshaw.cx@gmail.com>
7
+ #
8
+
9
+ import base64
10
+ import binascii
11
+ import functools
12
+ import hashlib
13
+ import urllib.parse
14
+
15
+ import _
16
+
17
+
18
+ # based on tornado.web.authenticated
19
+ def _next(handler):
20
+ if handler.request.method in ("GET","HEAD"):
21
+ url = handler.get_login_url()
22
+ if "?" not in url:
23
+ if urllib.parse.urlsplit(url).scheme:
24
+ next_url = handler.request.full_url()
25
+ else:
26
+ next_url = handler.request.uri
27
+ url += "?" + urllib.parse.urlencode(dict(next=next_url))
28
+ handler.redirect(url)
29
+ else:
30
+ raise _.HTTPError(403)
31
+
32
+
33
+ # generic decorator to callback to a function
34
+ # return true to allow the method to be called
35
+ # return false to block the method
36
+ def filter(filter_func):
37
+ def _filter(method):
38
+ @functools.wraps(method)
39
+ def wrapper(self, *args, **kwds):
40
+ if filter_func(self):
41
+ return method(self, *args, **kwds)
42
+ _next(self)
43
+ return wrapper
44
+ return _filter
45
+
46
+
47
+ # same as above but pass the current_user session to the function
48
+ # blocks when no session is associated with the request
49
+ # return true to allow the method to be called
50
+ # return false to block the method
51
+ def filter_user(filter_func):
52
+ def _filter_user(method):
53
+ @functools.wraps(method)
54
+ def wrapper(self, *args, **kwds):
55
+ if self.current_user:
56
+ if filter_func(self.current_user):
57
+ return method(self, *args, **kwds)
58
+ _next(self)
59
+ return wrapper
60
+ return _filter_user
61
+
62
+
63
+ # inteded to be used by underscore apps to check that any user is logged in
64
+ current_user = filter_user(lambda current_user: True)
65
+
66
+ # intended to be over-ridden before components are loaded for more fine-grain
67
+ # control over access to component handlers
68
+ protected = filter_user(lambda current_user: True)
69
+
70
+
71
+ # implement old school user/name password prompt
72
+ # useful for command-line tools to pass as part of the URL
73
+ def basic(realm='Authentication'):
74
+ def basic_auth(method):
75
+ @functools.wraps(method)
76
+ async def wrapper(self, *args, **kwds):
77
+ auth = self.request.headers.get('Authorization', '')
78
+ if auth.startswith('Basic '):
79
+ auth = binascii.a2b_base64(auth[6:]).decode('utf-8')
80
+ username,password = auth.split(':', 1)
81
+ component = _.config.get(_.name, 'basic', fallback=None)
82
+ if not component:
83
+ raise _.HTTPError(500, 'No component specified for basic auth')
84
+ try:
85
+ login = _.logins[component]
86
+ except KeyError:
87
+ raise _.HTTPError(500, 'Invalid component specified for basic auth')
88
+ success = await login.check(username, password)
89
+ if success:
90
+ return method(self, *args, **kwds)
91
+ self.set_status(401)
92
+ self.set_header('WWW-Authenticate', f'Basic realm={realm}')
93
+ self.finish()
94
+ return wrapper
95
+ return basic_auth
96
+
97
+
98
+ def simple_hash(value):
99
+ value = value.encode('utf-8')
100
+ for i in range(99999):
101
+ value = hashlib.sha512(value).digest()
102
+ return base64.b64encode(value).decode('ascii')
103
+
104
+
105
+ __all__ = _.all()
@@ -0,0 +1,50 @@
1
+ #
2
+ # (c) 2015-2023 Matthew Shaw
3
+ #
4
+ # Authors
5
+ # =======
6
+ # Matthew Shaw <mshaw.cx@gmail.com>
7
+ #
8
+
9
+ import _
10
+
11
+
12
+ class Cache:
13
+ @classmethod
14
+ async def _(cls, name, **kwds):
15
+ # create a dynamic child class with kwds from the ini file
16
+ try:
17
+ members = dict(_.config['sessions'])
18
+ except KeyError:
19
+ members = {}
20
+
21
+ members['expires'] = int(members.get('expires', 24))
22
+ members['interval'] = int(members.get('interval', 60))
23
+ members.update(kwds)
24
+
25
+ # instantiate the derived class
26
+ self = type(cls.__name__, (cls,), _.prefix(members))()
27
+ _.caches[name] = self
28
+ await self.init(name, **kwds)
29
+
30
+ async def init(self, **kwds):
31
+ pass
32
+
33
+ async def close(self):
34
+ pass
35
+
36
+ async def cookie_secret(self):
37
+ raise NotImplementedError
38
+
39
+ def save_session(self, session):
40
+ try:
41
+ return session['session_id']
42
+ except KeyError:
43
+ raise _.error('No session_id defined in session')
44
+
45
+ async def load_session(self, session_id):
46
+ raise NotImplementedError
47
+
48
+
49
+ class Handler(_.handlers.Protected):
50
+ pass
@@ -0,0 +1,117 @@
1
+ #
2
+ # (c) 2015-2023 Matthew Shaw
3
+ #
4
+ # Authors
5
+ # =======
6
+ # Matthew Shaw <mshaw.cx@gmail.com>
7
+ #
8
+
9
+ import base64
10
+ import json
11
+ import logging
12
+ import os
13
+
14
+ import _
15
+
16
+
17
+ class DbCache(_.caches.Cache):
18
+ _config = 'config'
19
+ _key_col = 'key'
20
+ _key = 'cookie'
21
+ _val_col = 'value'
22
+
23
+ _table = 'sessions'
24
+ _session_id = 'session_id'
25
+
26
+ async def init(self, name, database=None, table=None, **kwds):
27
+ if not hasattr(_.application, 'is_session_expired'):
28
+ raise _.error('Application does not have is_session_expired function defined')
29
+
30
+ if database is None:
31
+ if 1 == len(_.databases):
32
+ database = list(_.databases.keys())[0]
33
+ else:
34
+ raise _.error('dbcache requires a database to be specified')
35
+ self.db = _.databases[database]
36
+
37
+ schema = self.db.schema('config')
38
+ table = schema.table(self._config)
39
+ table.column(self._key_col).primary_key()
40
+ table.column(self._val_col)
41
+ await schema.apply()
42
+
43
+ schema = self.db.schema('sessions')
44
+ table = schema.table(self._table)
45
+ table.column(self._session_id).primary_key()
46
+ for col,dbtype in kwds.items():
47
+ table.column(col).type(dbtype)
48
+ await schema.apply()
49
+
50
+ # interval comes from the [sessions] section of the ini
51
+ _.application.periodic(self._interval, self.clear_stale_sessions)
52
+
53
+ members = dict(
54
+ name = name,
55
+ db = self.db,
56
+ table = self._table,
57
+ session_id = self._session_id,
58
+ )
59
+ subclass = type(name, (DbCacheSessions,), _.prefix(members))
60
+ _.application._record_handler('sessions', subclass)
61
+
62
+ async def cookie_secret(self):
63
+ secret = await self.db.find_one(self._config, self._key_col, self._key)
64
+ if secret:
65
+ secret = secret['value']
66
+ else:
67
+ secret = base64.b64encode(os.urandom(32))
68
+ record = {
69
+ self._key_col : self._key,
70
+ self._val_col : secret,
71
+ }
72
+ await self.db.upsert(self._config, record)
73
+ return secret
74
+
75
+ async def save_session(self, session):
76
+ super().save_session(session)
77
+ await self.db.upsert(self._table, session)
78
+
79
+ async def load_session(self, session_id):
80
+ record = await self.db.find_one(self._table, self._session_id, session_id)
81
+ if not record:
82
+ return None
83
+ if await _.wait(_.application.is_session_expired(record, self._expires)):
84
+ return None
85
+ return record
86
+
87
+ async def clear_stale_sessions(self):
88
+ for record in await self.db.find(self._table):
89
+ if await _.wait(_.application.is_session_expired(record, self._expires)):
90
+ logging.debug('Removing expired session: %s', record[self._session_id])
91
+ await self.db.delete(self._table, self._session_id, record[self._session_id])
92
+
93
+
94
+ class DbCacheSessions(_.handlers.Protected):
95
+ @_.auth.protected
96
+ async def get(self, session_id=None):
97
+ if session_id:
98
+ record = await self._db.find_one(self._table, self._session_id, session_id)
99
+ self.write(record)
100
+ else:
101
+ records = await self._db.find(self._table)
102
+ data = []
103
+ for record in records:
104
+ data.append(dict(record))
105
+ self.write({'data':data})
106
+
107
+ @_.auth.protected
108
+ async def delete(self, session_id=None):
109
+ self.set_status(204)
110
+ if session_id:
111
+ await self._db.delete(self._table, self._session_id, session_id)
112
+
113
+ callback = getattr(_.application, f'on_{self._name}_delete', None)
114
+ if callback is None:
115
+ callback = getattr(_.application, 'on_dbcache_delete', None)
116
+ if callback:
117
+ await _.wait(callback(self._name, record))
@@ -0,0 +1,71 @@
1
+ #
2
+ # (c) 2015-2023 Matthew Shaw
3
+ #
4
+ # Authors
5
+ # =======
6
+ # Matthew Shaw <mshaw.cx@gmail.com>
7
+ #
8
+
9
+ import os
10
+ import json
11
+
12
+ import _
13
+
14
+
15
+ class Memory(_.caches.Cache):
16
+ async def init(self, name, **kwds):
17
+ if not hasattr(_.application, 'is_session_expired'):
18
+ raise _.error('Application does not have is_session_expired function defined')
19
+
20
+ self.mem = {}
21
+
22
+ members = dict(
23
+ name = name,
24
+ mem = self.mem,
25
+ )
26
+ subclass = type(name, (MemorySessions,), _.prefix(members))
27
+ _.application._record_handler('sessions', subclass)
28
+
29
+ def cookie_secret(self):
30
+ return os.urandom(32)
31
+
32
+ def save_session(self, session):
33
+ session_id = super().save_session(session)
34
+ self.mem[session_id] = json.dumps(session)
35
+
36
+ async def load_session(self, session_id):
37
+ session = self.mem.get(session_id, None)
38
+ if not session:
39
+ return None
40
+ if await _.wait(_.application.is_session_expired(session, self.expires)):
41
+ return None
42
+ return json.loads(session)
43
+
44
+
45
+
46
+ class MemorySessions(_.handlers.Protected):
47
+ @_.auth.protected
48
+ async def get(self, session_id=None):
49
+ if session_id:
50
+ session = self._mem[session_id]
51
+ if not session:
52
+ raise _.HTTPError(404)
53
+ session = json.loads(session)
54
+ self.write(session)
55
+ else:
56
+ data = []
57
+ for session_id in self._mem:
58
+ session = self._mem[session_id]
59
+ data.append(json.loads(session))
60
+ self.write({'data':data})
61
+
62
+ @_.auth.protected
63
+ async def delete(self, session_id=None):
64
+ self.set_status(204)
65
+ if session_id:
66
+ del self._mem[session_id]
67
+ callback = getattr(_.application, f'on_{self._name}_delete', None)
68
+ if callback is None:
69
+ callback = getattr(_.application, 'on_memory_delete', None)
70
+ if callback:
71
+ await _.wait(callback(self._name, session_id))