django-nativemojo 0.1.10__py3-none-any.whl → 0.1.15__py3-none-any.whl

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 (120) hide show
  1. django_nativemojo-0.1.15.dist-info/METADATA +136 -0
  2. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/RECORD +105 -65
  3. mojo/__init__.py +1 -1
  4. mojo/apps/account/management/__init__.py +5 -0
  5. mojo/apps/account/management/commands/__init__.py +6 -0
  6. mojo/apps/account/management/commands/serializer_admin.py +531 -0
  7. mojo/apps/account/migrations/0004_user_avatar.py +20 -0
  8. mojo/apps/account/migrations/0005_group_last_activity.py +18 -0
  9. mojo/apps/account/models/group.py +25 -7
  10. mojo/apps/account/models/member.py +15 -4
  11. mojo/apps/account/models/user.py +197 -20
  12. mojo/apps/account/rest/group.py +1 -0
  13. mojo/apps/account/rest/user.py +6 -2
  14. mojo/apps/aws/rest/__init__.py +1 -0
  15. mojo/apps/aws/rest/s3.py +64 -0
  16. mojo/apps/fileman/README.md +8 -8
  17. mojo/apps/fileman/backends/base.py +76 -70
  18. mojo/apps/fileman/backends/filesystem.py +86 -86
  19. mojo/apps/fileman/backends/s3.py +200 -108
  20. mojo/apps/fileman/migrations/0001_initial.py +106 -0
  21. mojo/apps/fileman/migrations/0002_filemanager_parent_alter_filemanager_max_file_size.py +24 -0
  22. mojo/apps/fileman/migrations/0003_remove_file_fileman_fil_upload__c4bc35_idx_and_more.py +25 -0
  23. mojo/apps/fileman/migrations/0004_remove_file_original_filename_and_more.py +39 -0
  24. mojo/apps/fileman/migrations/0005_alter_file_upload_token.py +18 -0
  25. mojo/apps/fileman/migrations/0006_file_download_url_filemanager_forever_urls.py +23 -0
  26. mojo/apps/fileman/migrations/0007_remove_filemanager_forever_urls_and_more.py +22 -0
  27. mojo/apps/fileman/migrations/0008_file_category.py +18 -0
  28. mojo/apps/fileman/migrations/0009_rename_file_path_file_storage_file_path.py +18 -0
  29. mojo/apps/fileman/migrations/0010_filerendition.py +33 -0
  30. mojo/apps/fileman/migrations/0011_alter_filerendition_original_file.py +19 -0
  31. mojo/apps/fileman/models/__init__.py +1 -5
  32. mojo/apps/fileman/models/file.py +204 -58
  33. mojo/apps/fileman/models/manager.py +161 -31
  34. mojo/apps/fileman/models/rendition.py +118 -0
  35. mojo/apps/fileman/renderer/__init__.py +111 -0
  36. mojo/apps/fileman/renderer/audio.py +403 -0
  37. mojo/apps/fileman/renderer/base.py +205 -0
  38. mojo/apps/fileman/renderer/document.py +404 -0
  39. mojo/apps/fileman/renderer/image.py +222 -0
  40. mojo/apps/fileman/renderer/utils.py +297 -0
  41. mojo/apps/fileman/renderer/video.py +304 -0
  42. mojo/apps/fileman/rest/__init__.py +1 -18
  43. mojo/apps/fileman/rest/upload.py +22 -32
  44. mojo/apps/fileman/signals.py +58 -0
  45. mojo/apps/fileman/tasks.py +254 -0
  46. mojo/apps/fileman/utils/__init__.py +40 -16
  47. mojo/apps/incident/migrations/0005_incidenthistory.py +39 -0
  48. mojo/apps/incident/migrations/0006_alter_incident_state.py +18 -0
  49. mojo/apps/incident/models/__init__.py +1 -0
  50. mojo/apps/incident/models/history.py +36 -0
  51. mojo/apps/incident/models/incident.py +1 -1
  52. mojo/apps/incident/reporter.py +3 -1
  53. mojo/apps/incident/rest/event.py +7 -1
  54. mojo/apps/logit/migrations/0004_alter_log_level.py +18 -0
  55. mojo/apps/logit/models/log.py +4 -1
  56. mojo/apps/metrics/utils.py +2 -2
  57. mojo/apps/notify/handlers/ses/message.py +1 -1
  58. mojo/apps/notify/providers/aws.py +2 -2
  59. mojo/apps/tasks/__init__.py +34 -1
  60. mojo/apps/tasks/manager.py +200 -45
  61. mojo/apps/tasks/rest/tasks.py +24 -10
  62. mojo/apps/tasks/runner.py +283 -18
  63. mojo/apps/tasks/task.py +99 -0
  64. mojo/apps/tasks/tq_handlers.py +118 -0
  65. mojo/decorators/auth.py +6 -1
  66. mojo/decorators/http.py +7 -2
  67. mojo/helpers/aws/__init__.py +41 -0
  68. mojo/helpers/aws/ec2.py +804 -0
  69. mojo/helpers/aws/iam.py +748 -0
  70. mojo/helpers/aws/s3.py +451 -11
  71. mojo/helpers/aws/ses.py +483 -0
  72. mojo/helpers/aws/sns.py +461 -0
  73. mojo/helpers/crypto/__pycache__/hash.cpython-310.pyc +0 -0
  74. mojo/helpers/crypto/__pycache__/sign.cpython-310.pyc +0 -0
  75. mojo/helpers/crypto/__pycache__/utils.cpython-310.pyc +0 -0
  76. mojo/helpers/dates.py +18 -0
  77. mojo/helpers/response.py +6 -2
  78. mojo/helpers/settings/__init__.py +2 -0
  79. mojo/helpers/{settings.py → settings/helper.py} +1 -37
  80. mojo/helpers/settings/parser.py +132 -0
  81. mojo/middleware/logging.py +1 -1
  82. mojo/middleware/mojo.py +5 -0
  83. mojo/models/rest.py +261 -46
  84. mojo/models/secrets.py +13 -4
  85. mojo/serializers/__init__.py +100 -0
  86. mojo/serializers/advanced/README.md +363 -0
  87. mojo/serializers/advanced/__init__.py +247 -0
  88. mojo/serializers/advanced/formats/__init__.py +28 -0
  89. mojo/serializers/advanced/formats/csv.py +416 -0
  90. mojo/serializers/advanced/formats/excel.py +516 -0
  91. mojo/serializers/advanced/formats/json.py +239 -0
  92. mojo/serializers/advanced/formats/localizers.py +509 -0
  93. mojo/serializers/advanced/formats/response.py +485 -0
  94. mojo/serializers/advanced/serializer.py +568 -0
  95. mojo/serializers/manager.py +501 -0
  96. mojo/serializers/optimized.py +618 -0
  97. mojo/serializers/settings_example.py +322 -0
  98. mojo/serializers/{models.py → simple.py} +38 -15
  99. testit/helpers.py +21 -4
  100. django_nativemojo-0.1.10.dist-info/METADATA +0 -96
  101. mojo/apps/metrics/rest/db.py +0 -0
  102. mojo/helpers/aws/setup_email.py +0 -0
  103. mojo/ws4redis/README.md +0 -174
  104. mojo/ws4redis/__init__.py +0 -2
  105. mojo/ws4redis/client.py +0 -283
  106. mojo/ws4redis/connection.py +0 -327
  107. mojo/ws4redis/exceptions.py +0 -32
  108. mojo/ws4redis/redis.py +0 -183
  109. mojo/ws4redis/servers/base.py +0 -86
  110. mojo/ws4redis/servers/django.py +0 -171
  111. mojo/ws4redis/servers/uwsgi.py +0 -63
  112. mojo/ws4redis/settings.py +0 -45
  113. mojo/ws4redis/utf8validator.py +0 -128
  114. mojo/ws4redis/websocket.py +0 -403
  115. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/LICENSE +0 -0
  116. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/NOTICE +0 -0
  117. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/WHEEL +0 -0
  118. /mojo/{ws4redis/servers → apps/aws}/__init__.py +0 -0
  119. /mojo/apps/{fileman/models/render.py → aws/models/__init__.py} +0 -0
  120. /mojo/apps/fileman/{rest/__init__ → migrations/__init__.py} +0 -0
@@ -1,327 +0,0 @@
1
- import time
2
- from objict import objict
3
- from django.core.handlers.wsgi import WSGIRequest
4
- from django.apps import apps
5
-
6
- from mojo.ws4redis import settings as private_settings
7
- from mojo.ws4redis.redis import RedisStore, RedisMessage
8
-
9
- from mojo.apps.account.utils.jwtoken import JWToken
10
- from mojo.helpers import request as rhelper
11
- from mojo.helpers.logit import get_logger
12
- logger = get_logger("async", filename="async.log")
13
-
14
- MODEL_CACHE = dict() # caching of app.Model for faster access
15
-
16
- ALLOW_ANY_FACILITY = not private_settings.WS4REDIS_FACILITIES
17
- DISCONNECT_AFTER_NO_CREDS = 30
18
-
19
-
20
-
21
- class WebsocketConnection():
22
- def __init__(self, server, environ, start_response):
23
- self.server = server
24
- self.request = WSGIRequest(environ)
25
- self.ip = rhelper.get_remote_ip(self.request)
26
- self.ua = rhelper.get_user_agent(self.request)
27
- self.facility = self.request.path_info.replace(private_settings.WEBSOCKET_URL, '', 1)
28
- self.credentials = objict()
29
- self.listening_fds = None
30
- self.redis = RedisStore(server._redis_connection)
31
- self.websocket = server.upgrade_websocket(environ, start_response)
32
- self.last_beat = time.time()
33
- self.conneted_time = time.time()
34
- self.last_msg = None
35
- self._heart_beat = private_settings.WS4REDIS_HEARTBEAT
36
- self.debug = private_settings.WS4REDIS_LOG_DEBUG
37
-
38
- @property
39
- def elapsed_time(self):
40
- return time.time() - self.conneted_time
41
-
42
- def refreshFDs(self):
43
- if len(self.listening_fds) == 1:
44
- sub_sd = self.redis.get_file_descriptor()
45
- if sub_sd:
46
- self.listening_fds.append(sub_sd)
47
-
48
- def on_auth(self, msg):
49
- if msg.kind == "jwt":
50
- self.on_auth_jwt(msg)
51
- return
52
- # check our other auth mechanisms
53
- auther = self.getAuthenticator(msg.kind)
54
- if auther is None or not hasattr(auther, "authWS4RedisConnection"):
55
- logger.error(f"{self.ip} invalid auth", msg)
56
- self.sendToWS(msg.channel, dict(error="invalid auth kind", code=500))
57
- return
58
-
59
- # logger.info(f"{self.ip} authenticating")
60
- self.credentials = auther.authWS4RedisConnection(msg)
61
- if self.credentials is None or self.credentials.pk is None:
62
- logger.error(f"{self.ip} invalid credentials for", msg, self.credentials)
63
- self.sendToWS(msg.channel, dict(error="invalid credentials", code=401))
64
- return
65
- self.on_authenticated()
66
-
67
- def on_auth_jwt(self, msg):
68
- token = JWToken()
69
- if token.payload is None:
70
- raise Exception("invalid auth token")
71
- User = apps.get_model("account", "User")
72
- user, error = User.validate_jwt(token)
73
- if error is not None:
74
- raise Exception(error)
75
- self.credentials = objict(kind="user", instance=user, pk=user.pk, uuid=user.username)
76
- self.on_authenticated()
77
-
78
- def on_authenticated(self):
79
- if self.debug:
80
- logger.info(F"authenticated {self.credentials.kind}: {self.credentials.uuid}")
81
- if self.credentials.kind == "user":
82
- channel_key = self.redis.subscribe("user", self.facility, self.credentials.uuid)
83
- self.forwardPending(channel_key)
84
- else:
85
- channel_key = self.redis.subscribe(self.credentials.kind, self.facility, self.credentials.uuid)
86
- self.forwardPending(channel_key)
87
-
88
- self.redis.publishModelOnline(self.credentials.kind, self.credentials.pk, only_one=self.credentials.only_one)
89
- self.refreshFDs()
90
- if self.credentials and self.credentials.instance:
91
- if hasattr(self.credentials.instance, "on_ws_online"):
92
- self.credentials.instance.on_ws_online()
93
-
94
- def forwardPending(self, channel_key):
95
- msg = self.redis.getPendingMessage(channel_key)
96
- if msg:
97
- if self.debug:
98
- logger.info("pending messages", repr(msg))
99
- channel, pk = self.parseChannel(channel_key)
100
- self.sendToWS(channel, msg, pk)
101
-
102
- def on_resubscribe(self, msg):
103
- for ch in msg.channels:
104
- self.redis.subscribe(ch.channel, self.facility, ch.pk)
105
-
106
- def getAppModel(self, app_model):
107
- if app_model not in MODEL_CACHE:
108
- app_label, model_name = app_model.split('.')
109
- MODEL_CACHE[app_model] = apps.get_model(app_label, model_name)
110
- return MODEL_CACHE[app_model]
111
-
112
- def getAuthenticator(self, channel):
113
- auther_path = private_settings.WS4REDIS_AUTHENTICATORS.get(channel, None)
114
- if auther_path is not None:
115
- return self.getAppModel(auther_path)
116
- return None
117
-
118
- def on_subscribe(self, msg):
119
- pks = self.canSubscribeTo(msg)
120
- if bool(pks):
121
- for pk in pks:
122
- channel_key = self.redis.subscribe(msg.channel, self.facility, pk)
123
- else:
124
- logger.warning("subscribe permission denied", msg)
125
- self.sendToWS(msg.channel, dict(error="subscribe permission denied"))
126
-
127
- def canSubscribeTo(self, msg):
128
- ch_model = private_settings.WS4REDIS_CHANNELS.get(msg.channel, None)
129
- if not ch_model:
130
- return None
131
- if ch_model == "any":
132
- # this allows anything to be sent
133
- return [msg.pk]
134
- Model = self.getAppModel(ch_model)
135
- if not hasattr(Model, "can_ws_subscribe_to"):
136
- return None
137
- return Model.can_ws_subscribe_to(self.credentials, msg)
138
-
139
- def on_unsubscribe(self, msg):
140
- self.redis.unsubscribe(msg.channel, self.facility, msg.pk)
141
-
142
- def on_publish(self, msg):
143
- if self.canPublishTo(msg):
144
- self.redis.publish(msg.message, channel=msg.channel, pk=msg.pk)
145
- else:
146
- logger.warning("publish permission denied", msg)
147
- self.sendToWS(msg.channel, dict(error="publish permission denied"))
148
-
149
- def canPublishTo(self, msg):
150
- ch_model = private_settings.WS4REDIS_CHANNELS.get(msg.channel, None)
151
- if not ch_model:
152
- # you can publish to any channels you may create
153
- return True
154
- Model = self.getAppModel(ch_model)
155
- if not hasattr(Model, "canPublishTo"):
156
- logger.warning("canPublishTo", "no canPublishTo")
157
- return False
158
- return Model.can_ws_publish_to(self.credentials, msg)
159
-
160
- def on_ws_msg(self, raw_data):
161
- if isinstance(raw_data, bytes):
162
- raw_data = raw_data.decode()
163
- if raw_data == self._heart_beat:
164
- # echo the heartbeat
165
- self.websocket.send(self._heart_beat)
166
- return
167
- if self.debug:
168
- logger.info("on_ws_msg", repr(raw_data))
169
- dmsg = objict.from_json(raw_data, ignore_errors=True)
170
- if dmsg.action:
171
- # this is a special message that we want to handle directly
172
- try:
173
- if dmsg.action == "auth":
174
- self.on_auth(dmsg)
175
- elif dmsg.action == "subscribe":
176
- self.on_subscribe(dmsg)
177
- elif dmsg.action == "unsubscribe":
178
- self.on_unsubscribe(dmsg)
179
- elif dmsg.action == "resubscribe":
180
- self.on_resubscribe(dmsg)
181
- elif dmsg.action == "publish":
182
- self.on_publish(dmsg)
183
- elif dmsg.action == "save":
184
- self.on_save(dmsg)
185
- elif dmsg.channel:
186
- self.on_channel_msg(dmsg)
187
- except Exception as err:
188
- self.sendToWS(dmsg.get("channel", "system"), dict(error=str(err)))
189
- logger.exception(self.ip)
190
-
191
- def on_save(self, msg):
192
- if self.credentials and self.credentials.instance:
193
- if hasattr(self.credentials.instance, "on_ws_save"):
194
- resp = objict(id=msg.id, name="save")
195
- if msg.echo:
196
- resp.data = msg.data
197
- resp.status = self.credentials.instance.on_ws_save(msg)
198
- self.sendToWS(self.credentials.kind, resp)
199
-
200
- def on_channel_msg(self, msg):
201
- if not self.canPublishTo(msg):
202
- logger.warning("on_channel_msg permission denied", msg, private_settings.WS4REDIS_CHANNELS)
203
- self.sendToWS(msg.channel, dict(error="cannot publish to channel"))
204
- return None
205
- ch_model = private_settings.WS4REDIS_CHANNELS.get(msg.channel, None)
206
- if ch_model is None:
207
- return None
208
- Model = self.getAppModel(ch_model)
209
- if not hasattr(Model, "on_ws_message"):
210
- logger.warning(f"{msg.channel} does not support on_ws_message")
211
- self.sendToWS(msg.channel, dict(error="channel does not support on_ws_message"))
212
- return None
213
- Model.on_ws_message(self.credentials, msg, self)
214
-
215
- def on_redis_pending(self):
216
- sub_resp = self.redis.getSubMessage()
217
- if sub_resp:
218
- if self.debug:
219
- logger.info("incoming redis msg", sub_resp)
220
- self.on_redis_msg(sub_resp)
221
-
222
- def sendToWS(self, channel, message, pk=None):
223
- msg = objict(channel=channel)
224
- if pk is not None:
225
- msg.pk = pk
226
- if isinstance(message, (str, bytes)):
227
- msg.message = objict.from_json(message, ignore_errors=True)
228
- if not msg.message:
229
- msg.message = message
230
- elif msg.message.name == "logout" and msg.message.pk == self.credentials.pk:
231
- raise Exception("websocket is being logged out")
232
- self.websocket.send(msg.toJSON(as_string=True))
233
-
234
- def on_redis_msg(self, sub_resp):
235
- if isinstance(sub_resp, list):
236
- if sub_resp[0] == b'subscribe':
237
- # this is succesfull subscriptions
238
- # notify ws
239
- channel, pk = self.parseChannel(sub_resp[1].decode())
240
- msg = objict(name="subscribed", channel=channel, status=sub_resp[2])
241
- if pk is not None:
242
- msg.pk = pk
243
- self.websocket.send(msg.toJSON(as_string=True))
244
- return
245
- elif sub_resp[0] == b'unsubscribe':
246
- # this is succesfull subscriptions
247
- # notify ws
248
- channel, pk = self.parseChannel(sub_resp[1].decode())
249
- msg = objict(name="unsubscribed", channel=channel, status=sub_resp[2])
250
- if pk is not None:
251
- msg.pk = pk
252
- self.websocket.send(msg.toJSON(as_string=True))
253
- return
254
- elif sub_resp[0] == b'message':
255
- channel, pk = self.parseChannel(sub_resp[1].decode())
256
- self.sendToWS(channel, sub_resp[2].decode(), pk)
257
- return
258
- sendmsg = RedisMessage(sub_resp)
259
- if self.debug:
260
- logger.info(sub_resp)
261
- if sendmsg:
262
- if self.debug:
263
- logger.info("pushing to websocket", sendmsg)
264
- self.websocket.send(sendmsg)
265
-
266
- def parseChannel(self, channel):
267
- fields = channel.split(":")
268
- fields.pop(0)
269
- fields.pop()
270
- if len(fields) == 1:
271
- return fields[0], None
272
- return fields[0], fields[1]
273
-
274
- def on_ws_pending(self):
275
- try:
276
- self.last_msg = self.websocket.receive()
277
- if bool(self.last_msg):
278
- self.on_ws_msg(self.last_msg)
279
- except Exception:
280
- # logger.exception()
281
- logger.error(f"{self.ip}: unable to recv on ws... flushing")
282
- self.websocket.flush()
283
-
284
- def checkHeartbeat(self):
285
- beat_delta = time.time() - self.last_beat
286
- if beat_delta > 30.0:
287
- self.last_beat = time.time()
288
- if private_settings.WS4REDIS_HEARTBEAT and not self.websocket.closed:
289
- # logger.info("send heartbeat")
290
- self.websocket.send(private_settings.WS4REDIS_HEARTBEAT)
291
-
292
- def release(self):
293
- self.redis.release()
294
- self.listening_fds = None
295
- if self.websocket:
296
- self.websocket.close(code=1001, message='Websocket Closed')
297
- if self.credentials and self.credentials.instance:
298
- if hasattr(self.credentials.instance, "on_ws_offline"):
299
- self.credentials.instance.on_ws_offline()
300
-
301
- def handleComs(self):
302
- # check if token is in url
303
- if not ALLOW_ANY_FACILITY and self.facility not in private_settings.WS4REDIS_FACILITIES:
304
- self.sendToWS("facility", dict(error=f"facility name not allowed: '{self.facility}'. Please check your connection URL."))
305
- return
306
-
307
- websocket_fd = self.websocket.get_file_descriptor()
308
- self.listening_fds = [websocket_fd]
309
- if callable(private_settings.URL_AUTHENTICATOR):
310
- private_settings.URL_AUTHENTICATOR(self)
311
-
312
- while self.websocket and not self.websocket.closed:
313
- ready = self.server.select(self.listening_fds, [], [], 4.0)[0]
314
- if not ready:
315
- self.websocket.flush()
316
- if not self.credentials:
317
- if self.elapsed_time > 8 and self.elapsed_time < 30:
318
- logger.error(f"{self.ip} has sent no credentials", self.ua)
319
- self.sendToWS("user", dict(error="no credentials received"))
320
- elif private_settings.WS4REDIS_NOAUTH_CLOSE and self.elapsed_time > 30:
321
- self.release()
322
-
323
- for fd in ready:
324
- if fd == websocket_fd:
325
- self.on_ws_pending()
326
- else:
327
- self.on_redis_pending()
@@ -1,32 +0,0 @@
1
- #-*- coding: utf-8 -*-
2
- from socket import error as socket_error
3
- from django.http import BadHeaderError
4
-
5
-
6
- class WebSocketError(socket_error):
7
- """
8
- Raised when an active websocket encounters a problem.
9
- """
10
-
11
-
12
- class FrameTooLargeException(WebSocketError):
13
- """
14
- Raised if a received frame is too large.
15
- """
16
-
17
-
18
- class HandshakeError(BadHeaderError):
19
- """
20
- Raised if an error occurs during protocol handshake.
21
- """
22
-
23
-
24
- class UpgradeRequiredError(HandshakeError):
25
- """
26
- Raised if protocol must be upgraded.
27
- """
28
-
29
- class SSLRequiredError(socket_error):
30
- """
31
- Raised if protocol must be upgraded.
32
- """
mojo/ws4redis/redis.py DELETED
@@ -1,183 +0,0 @@
1
- from redis import ConnectionPool, StrictRedis
2
-
3
- from mojo.ws4redis import settings
4
- import time
5
- from objict import objict
6
-
7
- from mojo.helpers.logit import get_logger
8
- logger = get_logger("async", filename="async.log")
9
-
10
-
11
- REDIS_CON_POOL = None
12
-
13
-
14
- def getRedisClient():
15
- global REDIS_CON_POOL
16
- if REDIS_CON_POOL is None:
17
- REDIS_CON_POOL = ConnectionPool(**settings.WS4REDIS_CONNECTION)
18
- return StrictRedis(connection_pool=REDIS_CON_POOL)
19
-
20
-
21
- # Function to check the number of connections in use and available
22
- def getPoolStatus():
23
- status = objict()
24
- if REDIS_CON_POOL is None:
25
- return status
26
- status.max_size = REDIS_CON_POOL.max_connections
27
- status.size = REDIS_CON_POOL._created_connections
28
- status.in_use = len(REDIS_CON_POOL._in_use_connections)
29
- status.available = len(REDIS_CON_POOL._available_connections)
30
- return status
31
-
32
-
33
- class RedisMessage(bytes):
34
- def __new__(cls, value):
35
- if isinstance(value, str):
36
- if value != settings.WS4REDIS_HEARTBEAT:
37
- return bytes(value, 'utf-8')
38
- elif isinstance(value, list):
39
- if len(value) >= 2 and value[0] == b'message':
40
- return value[2]
41
- elif isinstance(value, dict):
42
- if not hasattr(value, "toJSON"):
43
- value = objict(value)
44
- return bytes(value.toJSON(as_string=True), 'utf-8')
45
- elif isinstance(value, bytes):
46
- return value
47
- return None
48
-
49
-
50
- class RedisStore():
51
- def __init__(self, connection=None):
52
- self.connection = connection
53
- if self.connection is None:
54
- self.connection = getRedisClient()
55
- self.subscriptions = []
56
- self.pubsub = None
57
- self.online_pk = None
58
- self.online_channel = None
59
- self.only_one = False # don't count connections, only allows one instance
60
- self.expire = settings.WS4REDIS_EXPIRE
61
-
62
- def publish(self, message, channel, facility="events", pk=None, expire=None, prefix=settings.WS4REDIS_PREFIX):
63
- if expire is None:
64
- expire = self.expire
65
- if not isinstance(message, RedisMessage):
66
- message = RedisMessage(message)
67
-
68
- if not isinstance(message, bytes):
69
- raise ValueError('message is {} but should be bytes'.format(type(message)))
70
-
71
- if isinstance(pk, list):
72
- count = 0
73
- for spk in pk:
74
- count += self.publish(message, channel, facility, pk=spk, expire=expire, prefix=prefix)
75
- return count
76
-
77
- channel_key = self.channelToKey(channel, facility, pk, prefix)
78
- if settings.WS4REDIS_LOG_DEBUG:
79
- logger.info("publishing msg to: {0}".format(channel_key), message)
80
- count = self.connection.publish(channel_key, message)
81
- return count
82
-
83
- def getSubMessage(self):
84
- # get a message pending from subscription
85
- if self.pubsub:
86
- return self.pubsub.parse_response()
87
- return None
88
-
89
- def getPendingMessage(self, channel, facility="events", pk=None, prefix=settings.WS4REDIS_PREFIX):
90
- # get a message from the connection channel
91
- channel_key = self.channelToKey(channel, facility, pk, prefix)
92
- return self.connection.get(channel_key)
93
-
94
- def channelToKey(self, channel, facility="events", pk=None, prefix=settings.WS4REDIS_PREFIX):
95
- if not pk:
96
- key = F'{prefix}:{channel}:{facility}'
97
- else:
98
- key = F'{prefix}:{channel}:{pk}:{facility}'
99
- return key
100
-
101
- def subscribe(self, channel, facility="events", pk=None, prefix=settings.WS4REDIS_PREFIX):
102
- if self.pubsub is None:
103
- self.pubsub = self.connection.pubsub()
104
- key = self.channelToKey(channel, facility, pk, prefix)
105
- if key not in self.subscriptions:
106
- if settings.WS4REDIS_LOG_DEBUG:
107
- logger.info(F"subscribing to: {key}")
108
- self.subscriptions.append(key)
109
- self.pubsub.subscribe(key)
110
- return key
111
-
112
- def unsubscribe(self, channel, facility, pk=None, prefix=settings.WS4REDIS_PREFIX):
113
- key = self.channelToKey(channel, facility, pk, prefix)
114
- if key in self.subscriptions:
115
- if settings.WS4REDIS_LOG_DEBUG:
116
- logger.info(F"unsubscribing to: {key}")
117
- self.subscriptions.remove(key)
118
- self.pubsub.unsubscribe(key)
119
-
120
- def publishModelOnline(self, name, pk, only_one=False):
121
- if self.online_pk is None:
122
- self.online_pk = pk
123
- self.online_channel = name
124
- self.only_one = only_one
125
- if self.only_one:
126
- self.connection.sadd(F"{name}:online", pk)
127
- else:
128
- count = self.connection.hincrby(F"{name}:online:connections", pk, 1)
129
- if count == 1:
130
- self.connection.sadd(F"{name}:online", pk)
131
-
132
- def unpublishModelOnline(self):
133
- if self.online_pk:
134
- name = self.online_channel
135
- pk = self.online_pk
136
- self.online_channel = None
137
- self.online_pk = None
138
- if self.only_one:
139
- self.connection.srem(F"{name}:online", pk)
140
- else:
141
- count = self.connection.hincrby(F"{name}:online:connections", pk, -1)
142
- if count == 0:
143
- self.connection.srem(F"{name}:online", pk)
144
-
145
- def waitForMessage(self, muid=None, timeout=55):
146
- timeout_at = time.time() + timeout
147
- while time.time() < timeout_at:
148
- message = self.pubsub.get_message()
149
- if message is not None and message.get("type") == "message":
150
- imsg = objict.from_json(message.get("data"))
151
- if muid is not None and imsg.muid == muid:
152
- return imsg
153
- elif muid is None:
154
- return imsg
155
- time.sleep(1.0)
156
- return None
157
-
158
- def get_file_descriptor(self):
159
- """
160
- Returns the file descriptor used for passing to the select call when listening
161
- on the message queue.
162
- """
163
- if self.pubsub.connection:
164
- return self.pubsub.connection._sock.fileno()
165
- return None
166
-
167
- def release(self):
168
- """
169
- New implementation to free up Redis subscriptions when websockets close. This prevents
170
- memory sap when Redis Output Buffer and Output Lists build when websockets are abandoned.
171
- """
172
- self.unpublishModelOnline()
173
- if self.pubsub and self.pubsub.subscribed:
174
- self.pubsub.unsubscribe()
175
- self.pubsub.reset()
176
- self.connection = None
177
-
178
- def __enter__(self):
179
- return self
180
-
181
- def __exit__(self, exc_type, exc_value, traceback):
182
- self.release()
183
- return False
@@ -1,86 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- import sys
3
- from redis import StrictRedis
4
- from http import client as http_client
5
- from mojo.ws4redis import settings as private_settings
6
- from mojo.ws4redis.exceptions import WebSocketError, HandshakeError, UpgradeRequiredError, SSLRequiredError
7
- from mojo.ws4redis.connection import WebsocketConnection
8
-
9
- from django.core.exceptions import PermissionDenied
10
- from django.utils.encoding import force_str
11
- from django import http
12
-
13
- from mojo.helpers.logit import get_logger
14
- logger = get_logger("async", filename="async.log")
15
-
16
-
17
- class WebsocketServerBase(object):
18
- def __init__(self, redis_connection=None):
19
- """
20
- redis_connection can be overriden by a mock object.
21
- """
22
- self._websockets = set() # a list of currently active websockets
23
- self._redis_connection = redis_connection
24
- if redis_connection is None:
25
- self._redis_connection = StrictRedis(**private_settings.WS4REDIS_CONNECTION)
26
-
27
- # clear out all online user connections from redis
28
- self._redis_connection.delete("users:online:connections")
29
- self._redis_connection.delete("users:online")
30
-
31
- def assure_protocol_requirements(self, environ):
32
- if environ.get('REQUEST_METHOD') != 'GET':
33
- raise HandshakeError('HTTP method must be a GET')
34
-
35
- if environ.get('SERVER_PROTOCOL') != 'HTTP/1.1':
36
- raise HandshakeError('HTTP server protocol must be 1.1')
37
-
38
- if environ.get('HTTP_UPGRADE', '').lower() != 'websocket':
39
- raise HandshakeError('Client does not wish to upgrade to a websocket')
40
-
41
- @property
42
- def websockets(self):
43
- return self._websockets
44
-
45
- def __call__(self, environ, start_response):
46
- """ Hijack the main loop from the original thread and listen on events on Redis and Websockets"""
47
- connection = None
48
- try:
49
- self.assure_protocol_requirements(environ)
50
- connection = WebsocketConnection(self, environ, start_response)
51
- connection.handleComs()
52
- except WebSocketError:
53
- logger.exception()
54
- response = http.HttpResponse(status=1001, content='Websocket Closed')
55
- except UpgradeRequiredError as excpt:
56
- logger.exception()
57
- response = http.HttpResponseBadRequest(status=426, content=excpt)
58
- except HandshakeError as excpt:
59
- logger.exception()
60
- response = http.HttpResponseBadRequest(content=excpt)
61
- except PermissionDenied as excpt:
62
- logger.exception("PermissionDenied")
63
- logger.warning('PermissionDenied: {}'.format(excpt), exc_info=sys.exc_info())
64
- response = http.HttpResponseForbidden(content=excpt)
65
- except SSLRequiredError as excpt:
66
- logger.exception("SSLRequiredError")
67
- response = http.HttpResponseServerError(content=excpt)
68
- except Exception as excpt:
69
- logger.exception()
70
- response = http.HttpResponseServerError(content=excpt)
71
- else:
72
- response = http.HttpResponse()
73
- finally:
74
- logger.info("closing websocket")
75
- if connection:
76
- connection.release()
77
- else:
78
- logger.warning('Starting late response on websocket')
79
- status_text = http_client.responses.get(response.status_code, 'UNKNOWN STATUS CODE')
80
- status = '{0} {1}'.format(response.status_code, status_text)
81
- # headers = list(response._headers.values())
82
- # if six.PY3:
83
- # headers = list(headers)
84
- start_response(force_str(status), [])
85
- logger.info('Finish non-websocket response with status code: {}'.format(response.status_code))
86
- return response