underscorepy 0.6.0__tar.gz → 0.6.3__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 (98) hide show
  1. {underscorepy-0.6.0 → underscorepy-0.6.3}/MANIFEST.in +1 -1
  2. {underscorepy-0.6.0/underscorepy.egg-info → underscorepy-0.6.3}/PKG-INFO +4 -1
  3. underscorepy-0.6.3/README.md +7 -0
  4. {underscorepy-0.6.0 → underscorepy-0.6.3}/_/__init__.py +10 -13
  5. {underscorepy-0.6.0 → underscorepy-0.6.3}/_/application.py +66 -44
  6. underscorepy-0.6.3/_/auth.py +105 -0
  7. {underscorepy-0.6.0 → underscorepy-0.6.3}/_/caches/__init__.py +17 -5
  8. underscorepy-0.6.3/_/caches/dbcache.py +117 -0
  9. underscorepy-0.6.3/_/caches/memory.py +71 -0
  10. underscorepy-0.6.3/_/caches/redis.py +95 -0
  11. {underscorepy-0.6.0 → underscorepy-0.6.3}/_/components.py +41 -8
  12. underscorepy-0.6.3/_/databases/__init__.py +178 -0
  13. {underscorepy-0.6.0 → underscorepy-0.6.3}/_/databases/postgres.py +8 -15
  14. {underscorepy-0.6.0 → underscorepy-0.6.3}/_/databases/sqlite.py +61 -37
  15. underscorepy-0.6.0/_/handlers/protected.py → underscorepy-0.6.3/_/handlers.py +14 -5
  16. {underscorepy-0.6.0 → underscorepy-0.6.3}/_/logins/__init__.py +30 -8
  17. underscorepy-0.6.3/_/logins/dblogin.py +194 -0
  18. {underscorepy-0.6.0 → underscorepy-0.6.3}/_/logins/facebook.py +1 -1
  19. {underscorepy-0.6.0 → underscorepy-0.6.3}/_/logins/gitlab.py +13 -12
  20. {underscorepy-0.6.0 → underscorepy-0.6.3}/_/logins/google.py +6 -2
  21. {underscorepy-0.6.0 → underscorepy-0.6.3}/_/logins/htpasswd.py +1 -1
  22. {underscorepy-0.6.0 → underscorepy-0.6.3}/_/logins/oauth2.py +17 -3
  23. {underscorepy-0.6.0 → underscorepy-0.6.3}/_/logins/slap.py +1 -1
  24. underscorepy-0.6.3/_/records/Protobuf.proto +19 -0
  25. underscorepy-0.6.3/_/records/Protobuf_pb2.py +35 -0
  26. underscorepy-0.6.3/_/records/__init__.py +197 -0
  27. underscorepy-0.6.3/_/records/data.py +186 -0
  28. underscorepy-0.6.3/_/records/protobuf.py +149 -0
  29. {underscorepy-0.6.0 → underscorepy-0.6.3}/_/settings.py +32 -31
  30. {underscorepy-0.6.0 → underscorepy-0.6.3}/_/supports/__init__.py +2 -4
  31. underscorepy-0.6.0/_/supports/nginx/nginx.py → underscorepy-0.6.3/_/supports/nginx/__init__.py +3 -3
  32. {underscorepy-0.6.0 → underscorepy-0.6.3}/_/supports/nginx/nginx.conf +2 -2
  33. underscorepy-0.6.0/_/supports/systemd/systemd.py → underscorepy-0.6.3/_/supports/systemd/__init__.py +3 -3
  34. {underscorepy-0.6.0 → underscorepy-0.6.3}/_/supports/systemd/systemd.service +1 -1
  35. underscorepy-0.6.3/_/utils.py +99 -0
  36. {underscorepy-0.6.0 → underscorepy-0.6.3}/_/version.py +2 -2
  37. underscorepy-0.6.0/_/websockets/websocket.py → underscorepy-0.6.3/_/websockets.py +18 -1
  38. {underscorepy-0.6.0 → underscorepy-0.6.3}/setup.py +1 -1
  39. {underscorepy-0.6.0 → underscorepy-0.6.3/underscorepy.egg-info}/PKG-INFO +4 -1
  40. underscorepy-0.6.3/underscorepy.egg-info/SOURCES.txt +44 -0
  41. underscorepy-0.6.0/README.rst +0 -5
  42. underscorepy-0.6.0/_/__pycache__/__init__.cpython-311.pyc +0 -0
  43. underscorepy-0.6.0/_/__pycache__/application.cpython-311.pyc +0 -0
  44. underscorepy-0.6.0/_/__pycache__/components.cpython-311.pyc +0 -0
  45. underscorepy-0.6.0/_/__pycache__/settings.cpython-311.pyc +0 -0
  46. underscorepy-0.6.0/_/__pycache__/utils.cpython-311.pyc +0 -0
  47. underscorepy-0.6.0/_/__pycache__/version.cpython-311.pyc +0 -0
  48. underscorepy-0.6.0/_/__pycache__/websockets.cpython-311.pyc +0 -0
  49. underscorepy-0.6.0/_/auth/__init__.py +0 -3
  50. underscorepy-0.6.0/_/auth/__pycache__/__init__.cpython-311.pyc +0 -0
  51. underscorepy-0.6.0/_/auth/__pycache__/basic.cpython-311.pyc +0 -0
  52. underscorepy-0.6.0/_/auth/__pycache__/hash.cpython-311.pyc +0 -0
  53. underscorepy-0.6.0/_/auth/basic.py +0 -39
  54. underscorepy-0.6.0/_/auth/hash.py +0 -20
  55. underscorepy-0.6.0/_/caches/__pycache__/__init__.cpython-311.pyc +0 -0
  56. underscorepy-0.6.0/_/caches/__pycache__/dbcache.cpython-311.pyc +0 -0
  57. underscorepy-0.6.0/_/caches/__pycache__/memory.cpython-311.pyc +0 -0
  58. underscorepy-0.6.0/_/caches/__pycache__/redis.cpython-311.pyc +0 -0
  59. underscorepy-0.6.0/_/caches/dbcache.py +0 -47
  60. underscorepy-0.6.0/_/caches/memory.py +0 -30
  61. underscorepy-0.6.0/_/caches/redis.py +0 -58
  62. underscorepy-0.6.0/_/databases/__init__.py +0 -43
  63. underscorepy-0.6.0/_/databases/__pycache__/__init__.cpython-311.pyc +0 -0
  64. underscorepy-0.6.0/_/databases/__pycache__/sqlite.cpython-311.pyc +0 -0
  65. underscorepy-0.6.0/_/handlers/__init__.py +0 -4
  66. underscorepy-0.6.0/_/handlers/__pycache__/__init__.cpython-311.pyc +0 -0
  67. underscorepy-0.6.0/_/handlers/__pycache__/login.cpython-311.pyc +0 -0
  68. underscorepy-0.6.0/_/handlers/__pycache__/protected.cpython-311.pyc +0 -0
  69. underscorepy-0.6.0/_/handlers/__pycache__/template.cpython-311.pyc +0 -0
  70. underscorepy-0.6.0/_/handlers/login.py +0 -27
  71. underscorepy-0.6.0/_/handlers/template.py +0 -19
  72. underscorepy-0.6.0/_/logins/__pycache__/__init__.cpython-311.pyc +0 -0
  73. underscorepy-0.6.0/_/logins/__pycache__/dblogin.cpython-311.pyc +0 -0
  74. underscorepy-0.6.0/_/logins/__pycache__/gitlab.cpython-311.pyc +0 -0
  75. underscorepy-0.6.0/_/logins/__pycache__/oauth2.cpython-311.pyc +0 -0
  76. underscorepy-0.6.0/_/logins/dblogin.py +0 -99
  77. underscorepy-0.6.0/_/supports/__pycache__/__init__.cpython-311.pyc +0 -0
  78. underscorepy-0.6.0/_/supports/nginx/__init__.py +0 -1
  79. underscorepy-0.6.0/_/supports/nginx/__pycache__/__init__.cpython-311.pyc +0 -0
  80. underscorepy-0.6.0/_/supports/nginx/__pycache__/nginx.cpython-311.pyc +0 -0
  81. underscorepy-0.6.0/_/supports/systemd/__init__.py +0 -1
  82. underscorepy-0.6.0/_/supports/systemd/__pycache__/__init__.cpython-311.pyc +0 -0
  83. underscorepy-0.6.0/_/supports/systemd/__pycache__/systemd.cpython-311.pyc +0 -0
  84. underscorepy-0.6.0/_/utils.py +0 -34
  85. underscorepy-0.6.0/_/websockets/__init__.py +0 -4
  86. underscorepy-0.6.0/_/websockets/__pycache__/__init__.cpython-311.pyc +0 -0
  87. underscorepy-0.6.0/_/websockets/__pycache__/mixins.cpython-311.pyc +0 -0
  88. underscorepy-0.6.0/_/websockets/__pycache__/protected.cpython-311.pyc +0 -0
  89. underscorepy-0.6.0/_/websockets/__pycache__/websocket.cpython-311.pyc +0 -0
  90. underscorepy-0.6.0/_/websockets/mixins.py +0 -20
  91. underscorepy-0.6.0/_/websockets/protected.py +0 -22
  92. underscorepy-0.6.0/underscorepy.egg-info/SOURCES.txt +0 -82
  93. {underscorepy-0.6.0 → underscorepy-0.6.3}/LICENSE +0 -0
  94. {underscorepy-0.6.0 → underscorepy-0.6.3}/setup.cfg +0 -0
  95. {underscorepy-0.6.0 → underscorepy-0.6.3}/underscorepy.egg-info/dependency_links.txt +0 -0
  96. {underscorepy-0.6.0 → underscorepy-0.6.3}/underscorepy.egg-info/not-zip-safe +0 -0
  97. {underscorepy-0.6.0 → underscorepy-0.6.3}/underscorepy.egg-info/requires.txt +0 -0
  98. {underscorepy-0.6.0 → underscorepy-0.6.3}/underscorepy.egg-info/top_level.txt +0 -0
@@ -1,2 +1,2 @@
1
1
  recursive-include _ *
2
- include *.rst
2
+ include *.md
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: underscorepy
3
- Version: 0.6.0
3
+ Version: 0.6.3
4
4
  Summary: _.py library
5
5
  Home-page: https://shaw.cx/_.py
6
6
  Author: Matthew Shaw
@@ -12,9 +12,12 @@ Classifier: License :: OSI Approved :: MIT License
12
12
  Classifier: Topic :: Software Development :: Libraries
13
13
  Classifier: Programming Language :: Python :: 3.7
14
14
  License-File: LICENSE
15
+ Requires-Dist: tornado
15
16
 
16
17
 
17
18
  underscore.py
18
19
  -------------
19
20
 
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.
@@ -6,30 +6,27 @@
6
6
  # Matthew Shaw <mshaw.cx@gmail.com>
7
7
  #
8
8
 
9
- import asyncio
9
+ import importlib
10
10
  import os
11
+ import sys
11
12
 
12
13
  root = os.path.dirname(__file__)
13
14
  root = os.path.join(root, '..')
14
15
  root = os.path.abspath(root)
15
16
 
16
- stop = asyncio.Event()
17
-
18
- # a generic error class for throwing exceptions
19
- class error(Exception):
20
- def __init__(self, fmt, *args):
21
- self.message = fmt % args
22
-
23
- def __str__(self):
24
- return self.message
25
-
26
17
  from .utils import *
27
18
 
28
19
  from . import version
29
20
  from . import settings
30
21
  from . import auth
31
- from . import components
32
22
  from . import handlers
33
- from . import websockets
23
+ from . import components
34
24
 
35
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()
@@ -22,7 +22,7 @@ import _
22
22
  class Application(tornado.web.Application):
23
23
  @classmethod
24
24
  def main(cls, ns=None):
25
- self = cls()
25
+ _.application = self = cls()
26
26
  try:
27
27
  asyncio.run(self.__async_main(ns))
28
28
  except _.error as e:
@@ -31,14 +31,21 @@ class Application(tornado.web.Application):
31
31
  logging.error('%s', e)
32
32
 
33
33
  async def __async_main(self, ns):
34
+ self._records_patterns = []
35
+ self._login_patterns = []
36
+
37
+ self.patterns = []
38
+ self.settings = {}
39
+
34
40
  self.loop = asyncio.get_event_loop()
41
+ self._stop_event = asyncio.Event()
35
42
 
36
43
  signal.signal(signal.SIGINT, self.__signalHandler)
37
44
  signal.signal(signal.SIGTERM, self.__signalHandler)
38
45
 
39
- app = self.__class__.__name__.lower()
46
+ name = self.__class__.__name__.lower()
40
47
  try:
41
- await _.settings.load(self, ns=ns, app=app)
48
+ await _.settings.load(ns=ns, name=name)
42
49
  await self.logging()
43
50
  except _.error as e:
44
51
  if _.args.debug:
@@ -46,17 +53,8 @@ class Application(tornado.web.Application):
46
53
  logging.error('%s', e)
47
54
  self.stop()
48
55
 
49
- if not _.stop.is_set():
50
- for name,component in _.login.items():
51
- try:
52
- await _.wait(component.args(name))
53
- except _.error as e:
54
- logging.error('%s', e)
55
- self.stop()
56
- break
57
-
58
- if not _.stop.is_set():
59
- for name,component in _.support.items():
56
+ if not self._stop_event.is_set():
57
+ for name,component in _.logins.items():
60
58
  try:
61
59
  await _.wait(component.args(name))
62
60
  except _.error as e:
@@ -64,32 +62,33 @@ class Application(tornado.web.Application):
64
62
  self.stop()
65
63
  break
66
64
 
67
- if not _.stop.is_set():
65
+ if not self._stop_event.is_set():
68
66
  try:
69
67
  await self.__async_init()
70
68
  except _.error as e:
71
69
  logging.error('%s', e)
72
70
 
73
- for name,component in _.database.items():
71
+ for name,component in _.caches.items():
74
72
  await component.close()
75
73
 
76
- for name,component in _.cache.items():
74
+ for name,component in _.databases.items():
77
75
  await component.close()
78
76
 
79
77
  async def __async_init(self, **kwds):
80
- self.patterns = []
81
- self.login_urls = []
82
-
83
78
  # check if a sessions cache was specified
84
- _.sessions = _.config.get(_.app, 'sessions', fallback=None)
79
+ _.sessions = _.config.get(_.name, 'sessions', fallback=None)
85
80
  if _.sessions:
86
81
  logging.debug('Sessions cache: %s', _.sessions)
87
82
  try:
88
- _.sessions = _.cache[_.sessions]
83
+ _.sessions = _.caches[_.sessions]
89
84
  except KeyError:
90
85
  raise _.error('Unknown sessions cache instance: %s', _.sessions)
91
86
 
92
- # call the child applications entry point
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
93
92
  try:
94
93
  await _.wait(self.initialize())
95
94
  except NotImplementedError:
@@ -98,28 +97,34 @@ class Application(tornado.web.Application):
98
97
  if 'cookie_secret' not in self.settings:
99
98
  self.settings['cookie_secret'] = await self.cookie_secret()
100
99
 
101
- for instance,cls in _.login.items():
102
- self.login_urls.append((f'/login/{instance}', cls))
100
+ patterns = list(self._records_patterns)
103
101
 
104
- if self.login_urls:
105
- self.patterns = self.login_urls + self.patterns
106
- self.patterns = [
107
- ( r'/login', _.handlers.LoginPage ),
108
- ( r'/logout', _.handlers.Logout ),
109
- ] + self.patterns
102
+ if self._login_patterns:
103
+ self._login_patterns += [
104
+ ( r'/login', _.logins.LoginPage ),
105
+ ( r'/logout', _.logins.Logout ),
106
+ ]
110
107
  self.settings['login_url'] = '/login'
108
+ patterns += self._login_patterns
109
+ patterns += self.patterns
111
110
 
112
- self.patterns.append(
111
+ patterns.append(
113
112
  ( r'/(favicon.ico)', tornado.web.StaticFileHandler, {'path':''}),
114
113
  )
115
114
 
116
- await self.__listen()
117
- await _.stop.wait()
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())
118
123
 
119
- async def __listen(self, **kwds):
124
+ async def __listen(self, patterns, **kwds):
120
125
  # call the Tornado Application init here to give children a chance
121
126
  # to initialize patterns and settings
122
- super(Application, self).__init__(self.patterns, **self.settings)
127
+ super().__init__(patterns, **self.settings)
123
128
 
124
129
  if 'xheaders' not in kwds:
125
130
  kwds['xheaders'] = True
@@ -131,6 +136,16 @@ class Application(tornado.web.Application):
131
136
 
132
137
  logging.info('Listening on %s:%d', _.args.address, _.args.port)
133
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
+
134
149
  async def initialize(self):
135
150
  'underscore apps should override this function'
136
151
  raise NotImplementedError
@@ -139,8 +154,8 @@ class Application(tornado.web.Application):
139
154
  'underscore apps can override or extend this function'
140
155
 
141
156
  # add the handlers to the logger
142
- if _.config.getboolean(_.app, 'logging', fallback=False):
143
- full_path = _.paths(f'{_.app}.log')
157
+ if _.config.getboolean(_.name, 'logging', fallback=False):
158
+ full_path = _.paths(f'{_.name}.log')
144
159
  file_logger = logging.FileHandler(full_path)
145
160
  file_logger.setLevel(logging.DEBUG if _.args.debug else logging.INFO)
146
161
  file_logger.setFormatter(
@@ -158,9 +173,14 @@ class Application(tornado.web.Application):
158
173
  return await _.wait(_.sessions.cookie_secret())
159
174
 
160
175
  async def on_login(self, handler, user, *args, **kwds):
161
- 'underscore apps should override this function'
176
+ 'underscore apps should override this function if a login is specified'
162
177
  raise NotImplementedError
163
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
+
164
184
  def periodic(self, _timeout, fn, *args, **kwds):
165
185
  'run a function or coroutine on a recurring basis'
166
186
  async def _periodic():
@@ -168,7 +188,7 @@ class Application(tornado.web.Application):
168
188
  # bail if the stop event is set
169
189
  # otherwise run the function after the timeout occurs
170
190
  try:
171
- await asyncio.wait_for(_.stop.wait(), timeout=_timeout)
191
+ await asyncio.wait_for(self._stop_event.wait(), timeout=_timeout)
172
192
  break
173
193
  except asyncio.TimeoutError as e:
174
194
  pass
@@ -180,17 +200,19 @@ class Application(tornado.web.Application):
180
200
 
181
201
  def stop(self):
182
202
  logging.debug('Setting stop event')
183
- _.stop.set()
203
+ self._stop_event.set()
204
+
205
+ def on_stop(self):
206
+ pass
184
207
 
185
208
  def __signalHandler(self, signum, frame):
186
209
  'handle signals in a thread-safe way'
187
210
  signame = signal.Signals(signum).name
188
211
  handler = getattr(self, f'on_{signame.lower()}', None)
189
212
  if handler:
190
- stop = handler(signum, frame)
191
- if stop:
213
+ if handler(signum, frame):
192
214
  return
193
- logging.info('Terminating %s on %s signal', _.app, signame)
215
+ logging.info('Terminating %s on %s signal', _.name, signame)
194
216
  self.loop.call_soon_threadsafe(self.stop)
195
217
 
196
218
  # demonstrate a specific signal handler
@@ -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()
@@ -13,11 +13,19 @@ class Cache:
13
13
  @classmethod
14
14
  async def _(cls, name, **kwds):
15
15
  # create a dynamic child class with kwds from the ini file
16
- subclass = type(cls.__name__, (cls,), kwds)
17
- #await subclass.init(name)
18
- self = subclass()
19
- await self.init(**kwds)
20
- _.cache[name] = self
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)
21
29
 
22
30
  async def init(self, **kwds):
23
31
  pass
@@ -36,3 +44,7 @@ class Cache:
36
44
 
37
45
  async def load_session(self, session_id):
38
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))