django-nativemojo 0.1.10__py3-none-any.whl → 0.1.16__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.
- django_nativemojo-0.1.16.dist-info/METADATA +138 -0
- django_nativemojo-0.1.16.dist-info/RECORD +302 -0
- mojo/__init__.py +1 -1
- mojo/apps/account/management/__init__.py +5 -0
- mojo/apps/account/management/commands/__init__.py +6 -0
- mojo/apps/account/management/commands/serializer_admin.py +651 -0
- mojo/apps/account/migrations/0004_user_avatar.py +20 -0
- mojo/apps/account/migrations/0005_group_last_activity.py +18 -0
- mojo/apps/account/migrations/0006_add_device_tracking_models.py +72 -0
- mojo/apps/account/migrations/0007_delete_userdevicelocation.py +16 -0
- mojo/apps/account/migrations/0008_userdevicelocation.py +33 -0
- mojo/apps/account/migrations/0009_geolocatedip_subnet.py +18 -0
- mojo/apps/account/migrations/0010_group_avatar.py +20 -0
- mojo/apps/account/migrations/0011_user_org_registereddevice_pushconfig_and_more.py +118 -0
- mojo/apps/account/migrations/0012_remove_pushconfig_apns_key_file_and_more.py +21 -0
- mojo/apps/account/migrations/0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more.py +28 -0
- mojo/apps/account/migrations/0014_notificationdelivery_data_payload_and_more.py +48 -0
- mojo/apps/account/models/__init__.py +2 -0
- mojo/apps/account/models/device.py +281 -0
- mojo/apps/account/models/group.py +319 -15
- mojo/apps/account/models/member.py +29 -5
- mojo/apps/account/models/push/__init__.py +4 -0
- mojo/apps/account/models/push/config.py +112 -0
- mojo/apps/account/models/push/delivery.py +93 -0
- mojo/apps/account/models/push/device.py +66 -0
- mojo/apps/account/models/push/template.py +99 -0
- mojo/apps/account/models/user.py +369 -19
- mojo/apps/account/rest/__init__.py +2 -0
- mojo/apps/account/rest/device.py +39 -0
- mojo/apps/account/rest/group.py +9 -0
- mojo/apps/account/rest/push.py +187 -0
- mojo/apps/account/rest/user.py +100 -6
- mojo/apps/account/services/__init__.py +1 -0
- mojo/apps/account/services/push.py +363 -0
- mojo/apps/aws/migrations/0001_initial.py +206 -0
- mojo/apps/aws/migrations/0002_emaildomain_can_recv_emaildomain_can_send_and_more.py +28 -0
- mojo/apps/aws/migrations/0003_mailbox_is_domain_default_mailbox_is_system_default_and_more.py +31 -0
- mojo/apps/aws/migrations/0004_s3bucket.py +39 -0
- mojo/apps/aws/migrations/0005_alter_emaildomain_region_delete_s3bucket.py +21 -0
- mojo/apps/aws/models/__init__.py +19 -0
- mojo/apps/aws/models/email_attachment.py +99 -0
- mojo/apps/aws/models/email_domain.py +218 -0
- mojo/apps/aws/models/email_template.py +132 -0
- mojo/apps/aws/models/incoming_email.py +197 -0
- mojo/apps/aws/models/mailbox.py +288 -0
- mojo/apps/aws/models/sent_message.py +175 -0
- mojo/apps/aws/rest/__init__.py +7 -0
- mojo/apps/aws/rest/email.py +33 -0
- mojo/apps/aws/rest/email_ops.py +183 -0
- mojo/apps/aws/rest/messages.py +32 -0
- mojo/apps/aws/rest/s3.py +64 -0
- mojo/apps/aws/rest/send.py +101 -0
- mojo/apps/aws/rest/sns.py +403 -0
- mojo/apps/aws/rest/templates.py +19 -0
- mojo/apps/aws/services/__init__.py +32 -0
- mojo/apps/aws/services/email.py +390 -0
- mojo/apps/aws/services/email_ops.py +548 -0
- mojo/apps/docit/__init__.py +6 -0
- mojo/apps/docit/markdown_plugins/syntax_highlight.py +25 -0
- mojo/apps/docit/markdown_plugins/toc.py +12 -0
- mojo/apps/docit/migrations/0001_initial.py +113 -0
- mojo/apps/docit/migrations/0002_alter_book_modified_by_alter_page_modified_by.py +26 -0
- mojo/apps/docit/migrations/0003_alter_book_group.py +20 -0
- mojo/apps/docit/models/__init__.py +17 -0
- mojo/apps/docit/models/asset.py +231 -0
- mojo/apps/docit/models/book.py +227 -0
- mojo/apps/docit/models/page.py +319 -0
- mojo/apps/docit/models/page_revision.py +203 -0
- mojo/apps/docit/rest/__init__.py +10 -0
- mojo/apps/docit/rest/asset.py +17 -0
- mojo/apps/docit/rest/book.py +22 -0
- mojo/apps/docit/rest/page.py +22 -0
- mojo/apps/docit/rest/page_revision.py +17 -0
- mojo/apps/docit/services/__init__.py +11 -0
- mojo/apps/docit/services/docit.py +315 -0
- mojo/apps/docit/services/markdown.py +44 -0
- mojo/apps/fileman/README.md +8 -8
- mojo/apps/fileman/backends/base.py +76 -70
- mojo/apps/fileman/backends/filesystem.py +86 -86
- mojo/apps/fileman/backends/s3.py +409 -108
- mojo/apps/fileman/migrations/0001_initial.py +106 -0
- mojo/apps/fileman/migrations/0002_filemanager_parent_alter_filemanager_max_file_size.py +24 -0
- mojo/apps/fileman/migrations/0003_remove_file_fileman_fil_upload__c4bc35_idx_and_more.py +25 -0
- mojo/apps/fileman/migrations/0004_remove_file_original_filename_and_more.py +39 -0
- mojo/apps/fileman/migrations/0005_alter_file_upload_token.py +18 -0
- mojo/apps/fileman/migrations/0006_file_download_url_filemanager_forever_urls.py +23 -0
- mojo/apps/fileman/migrations/0007_remove_filemanager_forever_urls_and_more.py +22 -0
- mojo/apps/fileman/migrations/0008_file_category.py +18 -0
- mojo/apps/fileman/migrations/0009_rename_file_path_file_storage_file_path.py +18 -0
- mojo/apps/fileman/migrations/0010_filerendition.py +33 -0
- mojo/apps/fileman/migrations/0011_alter_filerendition_original_file.py +19 -0
- mojo/apps/fileman/models/__init__.py +1 -5
- mojo/apps/fileman/models/file.py +240 -58
- mojo/apps/fileman/models/manager.py +427 -31
- mojo/apps/fileman/models/rendition.py +118 -0
- mojo/apps/fileman/renderer/__init__.py +111 -0
- mojo/apps/fileman/renderer/audio.py +403 -0
- mojo/apps/fileman/renderer/base.py +205 -0
- mojo/apps/fileman/renderer/document.py +404 -0
- mojo/apps/fileman/renderer/image.py +222 -0
- mojo/apps/fileman/renderer/utils.py +297 -0
- mojo/apps/fileman/renderer/video.py +304 -0
- mojo/apps/fileman/rest/__init__.py +1 -18
- mojo/apps/fileman/rest/upload.py +22 -32
- mojo/apps/fileman/signals.py +58 -0
- mojo/apps/fileman/tasks.py +254 -0
- mojo/apps/fileman/utils/__init__.py +40 -16
- mojo/apps/incident/migrations/0005_incidenthistory.py +39 -0
- mojo/apps/incident/migrations/0006_alter_incident_state.py +18 -0
- mojo/apps/incident/migrations/0007_event_uid.py +18 -0
- mojo/apps/incident/migrations/0008_ticket_ticketnote.py +55 -0
- mojo/apps/incident/migrations/0009_incident_status.py +18 -0
- mojo/apps/incident/migrations/0010_event_country_code.py +18 -0
- mojo/apps/incident/migrations/0011_incident_country_code.py +18 -0
- mojo/apps/incident/migrations/0012_alter_incident_status.py +18 -0
- mojo/apps/incident/models/__init__.py +2 -0
- mojo/apps/incident/models/event.py +35 -0
- mojo/apps/incident/models/history.py +36 -0
- mojo/apps/incident/models/incident.py +3 -1
- mojo/apps/incident/models/ticket.py +62 -0
- mojo/apps/incident/reporter.py +21 -1
- mojo/apps/incident/rest/__init__.py +1 -0
- mojo/apps/incident/rest/event.py +7 -1
- mojo/apps/incident/rest/ticket.py +43 -0
- mojo/apps/jobs/__init__.py +489 -0
- mojo/apps/jobs/adapters.py +24 -0
- mojo/apps/jobs/cli.py +616 -0
- mojo/apps/jobs/daemon.py +370 -0
- mojo/apps/jobs/examples/sample_jobs.py +376 -0
- mojo/apps/jobs/examples/webhook_examples.py +203 -0
- mojo/apps/jobs/handlers/__init__.py +5 -0
- mojo/apps/jobs/handlers/webhook.py +317 -0
- mojo/apps/jobs/job_engine.py +734 -0
- mojo/apps/jobs/keys.py +203 -0
- mojo/apps/jobs/local_queue.py +363 -0
- mojo/apps/jobs/management/__init__.py +3 -0
- mojo/apps/jobs/management/commands/__init__.py +3 -0
- mojo/apps/jobs/manager.py +1327 -0
- mojo/apps/jobs/migrations/0001_initial.py +97 -0
- mojo/apps/jobs/migrations/0002_alter_job_max_retries_joblog.py +39 -0
- mojo/apps/jobs/models/__init__.py +6 -0
- mojo/apps/jobs/models/job.py +441 -0
- mojo/apps/jobs/rest/__init__.py +2 -0
- mojo/apps/jobs/rest/control.py +466 -0
- mojo/apps/jobs/rest/jobs.py +421 -0
- mojo/apps/jobs/scheduler.py +571 -0
- mojo/apps/jobs/services/__init__.py +6 -0
- mojo/apps/jobs/services/job_actions.py +465 -0
- mojo/apps/jobs/settings.py +209 -0
- mojo/apps/logit/migrations/0004_alter_log_level.py +18 -0
- mojo/apps/logit/models/log.py +7 -1
- mojo/apps/metrics/__init__.py +8 -1
- mojo/apps/metrics/redis_metrics.py +198 -0
- mojo/apps/metrics/rest/__init__.py +3 -0
- mojo/apps/metrics/rest/categories.py +266 -0
- mojo/apps/metrics/rest/helpers.py +48 -0
- mojo/apps/metrics/rest/permissions.py +99 -0
- mojo/apps/metrics/rest/values.py +277 -0
- mojo/apps/metrics/utils.py +19 -2
- mojo/decorators/auth.py +6 -1
- mojo/decorators/http.py +47 -3
- mojo/helpers/aws/__init__.py +45 -0
- mojo/helpers/aws/ec2.py +804 -0
- mojo/helpers/aws/iam.py +748 -0
- mojo/helpers/aws/inbound_email.py +309 -0
- mojo/helpers/aws/kms.py +413 -0
- mojo/helpers/aws/s3.py +451 -11
- mojo/helpers/aws/ses.py +483 -0
- mojo/helpers/aws/ses_domain.py +959 -0
- mojo/helpers/aws/sns.py +461 -0
- mojo/helpers/crypto/__init__.py +1 -1
- mojo/helpers/crypto/utils.py +15 -0
- mojo/helpers/dates.py +18 -0
- mojo/helpers/location/__init__.py +2 -0
- mojo/helpers/location/countries.py +262 -0
- mojo/helpers/location/geolocation.py +196 -0
- mojo/helpers/logit.py +37 -0
- mojo/helpers/redis/__init__.py +2 -0
- mojo/helpers/redis/adapter.py +606 -0
- mojo/helpers/redis/client.py +48 -0
- mojo/helpers/redis/pool.py +225 -0
- mojo/helpers/request.py +8 -0
- mojo/helpers/response.py +14 -2
- mojo/helpers/settings/__init__.py +2 -0
- mojo/helpers/{settings.py → settings/helper.py} +1 -37
- mojo/helpers/settings/parser.py +132 -0
- mojo/middleware/auth.py +1 -1
- mojo/middleware/cors.py +40 -0
- mojo/middleware/logging.py +131 -12
- mojo/middleware/mojo.py +10 -0
- mojo/models/rest.py +494 -65
- mojo/models/secrets.py +98 -3
- mojo/serializers/__init__.py +106 -0
- mojo/serializers/core/__init__.py +90 -0
- mojo/serializers/core/cache/__init__.py +121 -0
- mojo/serializers/core/cache/backends.py +518 -0
- mojo/serializers/core/cache/base.py +102 -0
- mojo/serializers/core/cache/disabled.py +181 -0
- mojo/serializers/core/cache/memory.py +287 -0
- mojo/serializers/core/cache/redis.py +533 -0
- mojo/serializers/core/cache/utils.py +454 -0
- mojo/serializers/core/manager.py +550 -0
- mojo/serializers/core/serializer.py +475 -0
- mojo/serializers/examples/settings.py +322 -0
- mojo/serializers/formats/csv.py +393 -0
- mojo/serializers/formats/localizers.py +509 -0
- mojo/serializers/{models.py → simple.py} +38 -15
- mojo/serializers/suggested_improvements.md +388 -0
- testit/client.py +1 -1
- testit/helpers.py +35 -4
- testit/runner.py +23 -6
- django_nativemojo-0.1.10.dist-info/METADATA +0 -96
- django_nativemojo-0.1.10.dist-info/RECORD +0 -194
- mojo/apps/metrics/rest/db.py +0 -0
- mojo/apps/notify/README.md +0 -91
- mojo/apps/notify/README_NOTIFICATIONS.md +0 -566
- mojo/apps/notify/admin.py +0 -52
- mojo/apps/notify/handlers/example_handlers.py +0 -516
- mojo/apps/notify/handlers/ses/__init__.py +0 -25
- mojo/apps/notify/handlers/ses/bounce.py +0 -0
- mojo/apps/notify/handlers/ses/complaint.py +0 -25
- mojo/apps/notify/handlers/ses/message.py +0 -86
- mojo/apps/notify/management/commands/__init__.py +0 -1
- mojo/apps/notify/management/commands/process_notifications.py +0 -370
- mojo/apps/notify/mod +0 -0
- mojo/apps/notify/models/__init__.py +0 -12
- mojo/apps/notify/models/account.py +0 -128
- mojo/apps/notify/models/attachment.py +0 -24
- mojo/apps/notify/models/bounce.py +0 -68
- mojo/apps/notify/models/complaint.py +0 -40
- mojo/apps/notify/models/inbox.py +0 -113
- mojo/apps/notify/models/inbox_message.py +0 -173
- mojo/apps/notify/models/outbox.py +0 -129
- mojo/apps/notify/models/outbox_message.py +0 -288
- mojo/apps/notify/models/template.py +0 -30
- mojo/apps/notify/providers/aws.py +0 -73
- mojo/apps/notify/rest/ses.py +0 -0
- mojo/apps/notify/utils/__init__.py +0 -2
- mojo/apps/notify/utils/notifications.py +0 -404
- mojo/apps/notify/utils/parsing.py +0 -202
- mojo/apps/notify/utils/render.py +0 -144
- mojo/apps/tasks/README.md +0 -118
- mojo/apps/tasks/__init__.py +0 -11
- mojo/apps/tasks/manager.py +0 -489
- mojo/apps/tasks/rest/__init__.py +0 -2
- mojo/apps/tasks/rest/hooks.py +0 -0
- mojo/apps/tasks/rest/tasks.py +0 -62
- mojo/apps/tasks/runner.py +0 -174
- mojo/apps/tasks/tq_handlers.py +0 -14
- mojo/helpers/aws/setup_email.py +0 -0
- mojo/helpers/redis.py +0 -10
- mojo/models/meta.py +0 -262
- mojo/ws4redis/README.md +0 -174
- mojo/ws4redis/__init__.py +0 -2
- mojo/ws4redis/client.py +0 -283
- mojo/ws4redis/connection.py +0 -327
- mojo/ws4redis/exceptions.py +0 -32
- mojo/ws4redis/redis.py +0 -183
- mojo/ws4redis/servers/base.py +0 -86
- mojo/ws4redis/servers/django.py +0 -171
- mojo/ws4redis/servers/uwsgi.py +0 -63
- mojo/ws4redis/settings.py +0 -45
- mojo/ws4redis/utf8validator.py +0 -128
- mojo/ws4redis/websocket.py +0 -403
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/LICENSE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/NOTICE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/WHEEL +0 -0
- /mojo/apps/{notify → aws}/__init__.py +0 -0
- /mojo/apps/{notify/handlers → aws/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/management → docit/markdown_plugins}/__init__.py +0 -0
- /mojo/apps/{notify/providers → docit/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/rest → fileman/migrations}/__init__.py +0 -0
- /mojo/{ws4redis/servers → apps/jobs/examples}/__init__.py +0 -0
- /mojo/apps/{fileman/models/render.py → jobs/migrations/__init__.py} +0 -0
- /mojo/{serializers → rest}/openapi.py +0 -0
- /mojo/{apps/fileman/rest/__init__ → serializers/formats/__init__.py} +0 -0
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
|
mojo/ws4redis/servers/base.py
DELETED
@@ -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
|
mojo/ws4redis/servers/django.py
DELETED
@@ -1,171 +0,0 @@
|
|
1
|
-
#-*- coding: utf-8 -*-
|
2
|
-
import base64
|
3
|
-
import select
|
4
|
-
from hashlib import sha1
|
5
|
-
from wsgiref import util
|
6
|
-
from django.core.handlers import wsgi as django_wsgi
|
7
|
-
from django.core.wsgi import get_wsgi_application
|
8
|
-
from django.core.servers.basehttp import WSGIServer, WSGIRequestHandler, ServerHandler
|
9
|
-
|
10
|
-
from django.conf import settings
|
11
|
-
from django.core.management.commands import runserver
|
12
|
-
import socketserver
|
13
|
-
from django.utils.encoding import force_str
|
14
|
-
from mojo.ws4redis.websocket import WebSocket
|
15
|
-
from mojo.ws4redis.servers.base import WebsocketServerBase, HandshakeError, UpgradeRequiredError
|
16
|
-
|
17
|
-
from io import IOBase
|
18
|
-
|
19
|
-
|
20
|
-
from mojo.helpers.logit import get_logger
|
21
|
-
logger = get_logger("async", filename="async.log")
|
22
|
-
|
23
|
-
logger.info("YES")
|
24
|
-
|
25
|
-
util._hoppish = {}.__contains__
|
26
|
-
|
27
|
-
|
28
|
-
class LimitedStreamPatched(IOBase):
|
29
|
-
"""
|
30
|
-
Wrap another stream to disallow reading it past a number of bytes.
|
31
|
-
|
32
|
-
Based on the implementation from werkzeug.wsgi.LimitedStream
|
33
|
-
See https://github.com/pallets/werkzeug/blob/dbf78f67/src/werkzeug/wsgi.py#L828
|
34
|
-
"""
|
35
|
-
|
36
|
-
def __init__(self, stream, limit):
|
37
|
-
self.stream = stream
|
38
|
-
self._read = stream.read
|
39
|
-
self._readline = stream.readline
|
40
|
-
self._pos = 0
|
41
|
-
self.limit = limit
|
42
|
-
|
43
|
-
def read(self, size=-1, /):
|
44
|
-
_pos = self._pos
|
45
|
-
limit = self.limit
|
46
|
-
if _pos >= limit:
|
47
|
-
return b""
|
48
|
-
if size == -1 or size is None:
|
49
|
-
size = limit - _pos
|
50
|
-
else:
|
51
|
-
size = min(size, limit - _pos)
|
52
|
-
data = self._read(size)
|
53
|
-
self._pos += len(data)
|
54
|
-
return data
|
55
|
-
|
56
|
-
def readline(self, size=-1, /):
|
57
|
-
_pos = self._pos
|
58
|
-
limit = self.limit
|
59
|
-
if _pos >= limit:
|
60
|
-
return b""
|
61
|
-
if size == -1 or size is None:
|
62
|
-
size = limit - _pos
|
63
|
-
else:
|
64
|
-
size = min(size, limit - _pos)
|
65
|
-
line = self._readline(size)
|
66
|
-
self._pos += len(line)
|
67
|
-
return line
|
68
|
-
|
69
|
-
|
70
|
-
def patchLimitedStream():
|
71
|
-
django_wsgi.LimitedStream = LimitedStreamPatched
|
72
|
-
|
73
|
-
|
74
|
-
class WSGIRequestHandlerRunServer(WSGIRequestHandler):
|
75
|
-
def handle(self):
|
76
|
-
self.raw_requestline = self.rfile.readline(65537)
|
77
|
-
if len(self.raw_requestline) > 65536:
|
78
|
-
self.requestline = ''
|
79
|
-
self.request_version = ''
|
80
|
-
self.command = ''
|
81
|
-
self.send_error(414)
|
82
|
-
return
|
83
|
-
|
84
|
-
if not self.parse_request(): # An error code has been sent, just exit
|
85
|
-
return
|
86
|
-
|
87
|
-
handler = ServerHandler(
|
88
|
-
self.rfile, self.wfile, self.get_stderr(), self.get_environ()
|
89
|
-
)
|
90
|
-
handler.request_handler = self # backpointer for logging
|
91
|
-
handler.http_version = '1.1'
|
92
|
-
handler.run(self.server.get_app())
|
93
|
-
|
94
|
-
|
95
|
-
class WebsocketRunServer(WebsocketServerBase):
|
96
|
-
WS_GUID = b'258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
|
97
|
-
WS_VERSIONS = ('13', '8', '7')
|
98
|
-
|
99
|
-
def upgrade_websocket(self, environ, start_response):
|
100
|
-
"""
|
101
|
-
Attempt to upgrade the socket environ['wsgi.input'] into a websocket enabled connection.
|
102
|
-
"""
|
103
|
-
websocket_version = environ.get('HTTP_SEC_WEBSOCKET_VERSION', '')
|
104
|
-
if not websocket_version:
|
105
|
-
raise UpgradeRequiredError
|
106
|
-
elif websocket_version not in self.WS_VERSIONS:
|
107
|
-
raise HandshakeError('Unsupported WebSocket Version: {0}'.format(websocket_version))
|
108
|
-
|
109
|
-
key = environ.get('HTTP_SEC_WEBSOCKET_KEY', '').strip()
|
110
|
-
if not key:
|
111
|
-
raise HandshakeError('Sec-WebSocket-Key header is missing/empty')
|
112
|
-
try:
|
113
|
-
key_len = len(base64.b64decode(key))
|
114
|
-
except TypeError:
|
115
|
-
raise HandshakeError('Invalid key: {0}'.format(key))
|
116
|
-
if key_len != 16:
|
117
|
-
# 5.2.1 (3)
|
118
|
-
raise HandshakeError('Invalid key: {0}'.format(key))
|
119
|
-
|
120
|
-
sec_ws_accept = base64.b64encode(sha1(key.encode('utf-8') + self.WS_GUID).digest())
|
121
|
-
sec_ws_accept = sec_ws_accept.decode('ascii')
|
122
|
-
headers = [
|
123
|
-
('Upgrade', 'websocket'),
|
124
|
-
('Connection', 'Upgrade'),
|
125
|
-
('Sec-WebSocket-Accept', sec_ws_accept),
|
126
|
-
('Sec-WebSocket-Version', str(websocket_version)),
|
127
|
-
]
|
128
|
-
if environ.get('HTTP_SEC_WEBSOCKET_PROTOCOL') is not None:
|
129
|
-
headers.append(('Sec-WebSocket-Protocol', environ.get('HTTP_SEC_WEBSOCKET_PROTOCOL')))
|
130
|
-
|
131
|
-
logger.debug('WebSocket request accepted, switching protocols')
|
132
|
-
start_response(force_str('101 Switching Protocols'), headers)
|
133
|
-
start_response.__self__.finish_content()
|
134
|
-
winput = environ['wsgi.input']
|
135
|
-
if not hasattr(winput, "stream"):
|
136
|
-
# hack to get the stream back in later DJANGO
|
137
|
-
winput.stream = winput._read.__self__
|
138
|
-
return WebSocket(winput.stream)
|
139
|
-
|
140
|
-
def select(self, rlist, wlist, xlist, timeout=None):
|
141
|
-
return select.select(rlist, wlist, xlist, timeout)
|
142
|
-
|
143
|
-
|
144
|
-
def run(addr, port, wsgi_handler, ipv6=False, threading=False, server_cls=None, **kwargs):
|
145
|
-
"""
|
146
|
-
Function to monkey patch the internal Django command: manage.py runserver
|
147
|
-
"""
|
148
|
-
server_address = (addr, port)
|
149
|
-
logger.info('Websocket support is enabled', server_address)
|
150
|
-
if not threading:
|
151
|
-
raise Exception("Django's Websocket server must run with threading enabled")
|
152
|
-
httpd_cls = type('WSGIServer', (socketserver.ThreadingMixIn, WSGIServer), {'daemon_threads': True})
|
153
|
-
httpd = httpd_cls(server_address, WSGIRequestHandlerRunServer, ipv6=ipv6)
|
154
|
-
httpd.set_app(wsgi_handler)
|
155
|
-
httpd.serve_forever()
|
156
|
-
|
157
|
-
|
158
|
-
patchLimitedStream()
|
159
|
-
|
160
|
-
runserver.run = run
|
161
|
-
|
162
|
-
_django_app = get_wsgi_application()
|
163
|
-
_websocket_app = WebsocketRunServer()
|
164
|
-
_websocket_url = getattr(settings, 'WEBSOCKET_URL')
|
165
|
-
|
166
|
-
|
167
|
-
def application(environ, start_response):
|
168
|
-
if _websocket_url and environ.get('PATH_INFO').startswith(_websocket_url):
|
169
|
-
# logger.info("environ", environ)
|
170
|
-
return _websocket_app(environ, start_response)
|
171
|
-
return _django_app(environ, start_response)
|
mojo/ws4redis/servers/uwsgi.py
DELETED
@@ -1,63 +0,0 @@
|
|
1
|
-
# -*- coding: utf-8 -*-
|
2
|
-
import uwsgi
|
3
|
-
import gevent.select
|
4
|
-
|
5
|
-
import django
|
6
|
-
django.setup()
|
7
|
-
|
8
|
-
from mojo.ws4redis.exceptions import WebSocketError, SSLRequiredError
|
9
|
-
from mojo.ws4redis.servers.base import WebsocketServerBase
|
10
|
-
|
11
|
-
|
12
|
-
class uWSGIWebsocket(object):
|
13
|
-
def __init__(self):
|
14
|
-
self._closed = False
|
15
|
-
|
16
|
-
def get_file_descriptor(self):
|
17
|
-
"""Return the file descriptor for the given websocket"""
|
18
|
-
try:
|
19
|
-
return uwsgi.connection_fd()
|
20
|
-
except IOError as e:
|
21
|
-
self.close()
|
22
|
-
raise WebSocketError(e)
|
23
|
-
|
24
|
-
@property
|
25
|
-
def closed(self):
|
26
|
-
return self._closed
|
27
|
-
|
28
|
-
def receive(self):
|
29
|
-
if self._closed:
|
30
|
-
raise WebSocketError("Connection is already closed")
|
31
|
-
try:
|
32
|
-
return uwsgi.websocket_recv_nb()
|
33
|
-
except IOError as e:
|
34
|
-
self.close()
|
35
|
-
raise WebSocketError(e)
|
36
|
-
|
37
|
-
def flush(self):
|
38
|
-
try:
|
39
|
-
uwsgi.websocket_recv_nb()
|
40
|
-
except IOError:
|
41
|
-
self.close()
|
42
|
-
|
43
|
-
def send(self, message, binary=None):
|
44
|
-
try:
|
45
|
-
uwsgi.websocket_send(message)
|
46
|
-
except IOError as e:
|
47
|
-
self.close()
|
48
|
-
raise WebSocketError(e)
|
49
|
-
|
50
|
-
def close(self, code=1000, message=''):
|
51
|
-
self._closed = True
|
52
|
-
|
53
|
-
|
54
|
-
class uWSGIWebsocketServer(WebsocketServerBase):
|
55
|
-
def upgrade_websocket(self, environ, start_response):
|
56
|
-
try:
|
57
|
-
uwsgi.websocket_handshake(environ['HTTP_SEC_WEBSOCKET_KEY'], environ.get('HTTP_ORIGIN', ''))
|
58
|
-
except Exception:
|
59
|
-
raise SSLRequiredError("UWSGI REQUIRES SSL SUPPORT!")
|
60
|
-
return uWSGIWebsocket()
|
61
|
-
|
62
|
-
def select(self, rlist, wlist, xlist, timeout=None):
|
63
|
-
return gevent.select.select(rlist, wlist, xlist, timeout)
|
mojo/ws4redis/settings.py
DELETED
@@ -1,45 +0,0 @@
|
|
1
|
-
# -*- coding: utf-8 -*-
|
2
|
-
from django.conf import settings
|
3
|
-
|
4
|
-
WEBSOCKET_URL = getattr(settings, 'WEBSOCKET_URL', '/ws/')
|
5
|
-
|
6
|
-
WS4REDIS_CONNECTION = getattr(settings, 'WS4REDIS_CONNECTION', {
|
7
|
-
'host': 'localhost',
|
8
|
-
'port': 6379,
|
9
|
-
'db': 0,
|
10
|
-
'password': None,
|
11
|
-
})
|
12
|
-
|
13
|
-
"""
|
14
|
-
A string to prefix elements in the Redis datastore, to avoid naming conflicts with other services.
|
15
|
-
"""
|
16
|
-
WS4REDIS_PREFIX = getattr(settings, 'WS4REDIS_PREFIX', None)
|
17
|
-
|
18
|
-
"""
|
19
|
-
The time in seconds, items shall be persisted by the Redis datastore.
|
20
|
-
"""
|
21
|
-
WS4REDIS_EXPIRE = getattr(settings, 'WS4REDIS_EXPIRE', 3600)
|
22
|
-
|
23
|
-
"""
|
24
|
-
Replace the subscriber class by a customized version.
|
25
|
-
"""
|
26
|
-
WS4REDIS_SUBSCRIBER = getattr(settings, 'WS4REDIS_SUBSCRIBER', 'ws4redis.subscriber.RedisSubscriber')
|
27
|
-
|
28
|
-
"""
|
29
|
-
This set the magic string to recognize heartbeat messages. If set, this message string is ignored
|
30
|
-
by the server and also shall be ignored on the client.
|
31
|
-
|
32
|
-
If WS4REDIS_HEARTBEAT is not None, the server sends at least every 4 seconds a heartbeat message.
|
33
|
-
It is then up to the client to decide, what to do with these messages.
|
34
|
-
"""
|
35
|
-
WS4REDIS_HEARTBEAT = getattr(settings, 'WS4REDIS_HEARTBEAT', None)
|
36
|
-
|
37
|
-
# by default we only allow the "events" facillity
|
38
|
-
WS4REDIS_FACILITIES = getattr(settings, "WS4REDIS_FACILITIES", ["events"])
|
39
|
-
WS4REDIS_CHANNELS = getattr(settings, "WS4REDIS_CHANNELS", {})
|
40
|
-
WS4REDIS_AUTHENTICATORS = getattr(settings, "WS4REDIS_AUTHENTICATORS", {})
|
41
|
-
|
42
|
-
URL_AUTHENTICATOR = getattr(settings, "URL_AUTHENTICATOR", None)
|
43
|
-
|
44
|
-
WS4REDIS_LOG_DEBUG = getattr(settings, 'WS4REDIS_LOG_DEBUG', False)
|
45
|
-
WS4REDIS_NOAUTH_CLOSE = getattr(settings, 'WS4REDIS_NOAUTH_CLOSE', False)
|
mojo/ws4redis/utf8validator.py
DELETED
@@ -1,128 +0,0 @@
|
|
1
|
-
###############################################################################
|
2
|
-
##
|
3
|
-
## Copyright 2011-2013 Tavendo GmbH
|
4
|
-
##
|
5
|
-
## Note:
|
6
|
-
##
|
7
|
-
## This code is a Python implementation of the algorithm
|
8
|
-
##
|
9
|
-
## "Flexible and Economical UTF-8 Decoder"
|
10
|
-
##
|
11
|
-
## by Bjoern Hoehrmann
|
12
|
-
##
|
13
|
-
## bjoern@hoehrmann.de
|
14
|
-
## http://bjoern.hoehrmann.de/utf-8/decoder/dfa/
|
15
|
-
##
|
16
|
-
## Licensed under the Apache License, Version 2.0 (the "License");
|
17
|
-
## you may not use this file except in compliance with the License.
|
18
|
-
## You may obtain a copy of the License at
|
19
|
-
##
|
20
|
-
## http://www.apache.org/licenses/LICENSE-2.0
|
21
|
-
##
|
22
|
-
## Unless required by applicable law or agreed to in writing, software
|
23
|
-
## distributed under the License is distributed on an "AS IS" BASIS,
|
24
|
-
## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
25
|
-
## See the License for the specific language governing permissions and
|
26
|
-
## limitations under the License.
|
27
|
-
##
|
28
|
-
###############################################################################
|
29
|
-
|
30
|
-
|
31
|
-
## use Cython implementation of UTF8 validator if available
|
32
|
-
##
|
33
|
-
try:
|
34
|
-
from wsaccel.utf8validator import Utf8Validator
|
35
|
-
except:
|
36
|
-
## fallback to pure Python implementation
|
37
|
-
|
38
|
-
class Utf8Validator:
|
39
|
-
"""
|
40
|
-
Incremental UTF-8 validator with constant memory consumption (minimal
|
41
|
-
state).
|
42
|
-
|
43
|
-
Implements the algorithm "Flexible and Economical UTF-8 Decoder" by
|
44
|
-
Bjoern Hoehrmann (http://bjoern.hoehrmann.de/utf-8/decoder/dfa/).
|
45
|
-
"""
|
46
|
-
|
47
|
-
## DFA transitions
|
48
|
-
UTF8VALIDATOR_DFA = [
|
49
|
-
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, # 00..1f
|
50
|
-
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, # 20..3f
|
51
|
-
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, # 40..5f
|
52
|
-
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, # 60..7f
|
53
|
-
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, # 80..9f
|
54
|
-
7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, # a0..bf
|
55
|
-
8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, # c0..df
|
56
|
-
0xa,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x4,0x3,0x3, # e0..ef
|
57
|
-
0xb,0x6,0x6,0x6,0x5,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8, # f0..ff
|
58
|
-
0x0,0x1,0x2,0x3,0x5,0x8,0x7,0x1,0x1,0x1,0x4,0x6,0x1,0x1,0x1,0x1, # s0..s0
|
59
|
-
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,0,1,0,1,1,1,1,1,1, # s1..s2
|
60
|
-
1,2,1,1,1,1,1,2,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1, # s3..s4
|
61
|
-
1,2,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,3,1,1,1,1,1,1, # s5..s6
|
62
|
-
1,3,1,1,1,1,1,3,1,3,1,1,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1, # s7..s8
|
63
|
-
]
|
64
|
-
|
65
|
-
UTF8_ACCEPT = 0
|
66
|
-
UTF8_REJECT = 1
|
67
|
-
|
68
|
-
def __init__(self):
|
69
|
-
self.reset()
|
70
|
-
|
71
|
-
def decode(self, b):
|
72
|
-
"""
|
73
|
-
Eat one UTF-8 octet, and validate on the fly.
|
74
|
-
|
75
|
-
Returns UTF8_ACCEPT when enough octets have been consumed, in which case
|
76
|
-
self.codepoint contains the decoded Unicode code point.
|
77
|
-
|
78
|
-
Returns UTF8_REJECT when invalid UTF-8 was encountered.
|
79
|
-
|
80
|
-
Returns some other positive integer when more octets need to be eaten.
|
81
|
-
"""
|
82
|
-
type = Utf8Validator.UTF8VALIDATOR_DFA[b]
|
83
|
-
|
84
|
-
if self.state != Utf8Validator.UTF8_ACCEPT:
|
85
|
-
self.codepoint = (b & 0x3f) | (self.codepoint << 6)
|
86
|
-
else:
|
87
|
-
self.codepoint = (0xff >> type) & b
|
88
|
-
|
89
|
-
self.state = Utf8Validator.UTF8VALIDATOR_DFA[256 + self.state * 16 + type]
|
90
|
-
|
91
|
-
return self.state
|
92
|
-
|
93
|
-
def reset(self):
|
94
|
-
"""
|
95
|
-
Reset validator to start new incremental UTF-8 decode/validation.
|
96
|
-
"""
|
97
|
-
self.state = Utf8Validator.UTF8_ACCEPT
|
98
|
-
self.codepoint = 0
|
99
|
-
self.i = 0
|
100
|
-
|
101
|
-
def validate(self, ba):
|
102
|
-
"""
|
103
|
-
Incrementally validate a chunk of bytes provided as string.
|
104
|
-
|
105
|
-
Will return a quad (valid?, endsOnCodePoint?, currentIndex, totalIndex).
|
106
|
-
|
107
|
-
As soon as an octet is encountered which renders the octet sequence
|
108
|
-
invalid, a quad with valid? == False is returned. currentIndex returns
|
109
|
-
the index within the currently consumed chunk, and totalIndex the
|
110
|
-
index within the total consumed sequence that was the point of bail out.
|
111
|
-
When valid? == True, currentIndex will be len(ba) and totalIndex the
|
112
|
-
total amount of consumed bytes.
|
113
|
-
"""
|
114
|
-
|
115
|
-
l = len(ba)
|
116
|
-
|
117
|
-
for i in range(l):
|
118
|
-
## optimized version of decode(), since we are not interested in actual code points
|
119
|
-
|
120
|
-
self.state = Utf8Validator.UTF8VALIDATOR_DFA[256 + (self.state << 4) + Utf8Validator.UTF8VALIDATOR_DFA[ord(ba[i])]]
|
121
|
-
|
122
|
-
if self.state == Utf8Validator.UTF8_REJECT:
|
123
|
-
self.i += i
|
124
|
-
return False, False, i, self.i
|
125
|
-
|
126
|
-
self.i += l
|
127
|
-
|
128
|
-
return True, self.state == Utf8Validator.UTF8_ACCEPT, l, self.i
|