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/apps/tasks/runner.py
DELETED
@@ -1,174 +0,0 @@
|
|
1
|
-
from importlib import import_module
|
2
|
-
from concurrent.futures import ThreadPoolExecutor
|
3
|
-
from .manager import TaskManager
|
4
|
-
from mojo.tasks import manager
|
5
|
-
import os
|
6
|
-
from mojo.helpers import logit
|
7
|
-
from mojo.helpers import daemon
|
8
|
-
from mojo.helpers import paths
|
9
|
-
import time
|
10
|
-
|
11
|
-
|
12
|
-
class TaskEngine(daemon.Daemon):
|
13
|
-
"""
|
14
|
-
The TaskEngine is responsible for managing and executing tasks across different channels.
|
15
|
-
It leverages a thread pool to execute tasks concurrently and uses a task manager to maintain task states.
|
16
|
-
"""
|
17
|
-
def __init__(self, channels=["broadcast"], max_workers=5):
|
18
|
-
"""
|
19
|
-
Initialize the TaskEngine.
|
20
|
-
|
21
|
-
Args:
|
22
|
-
channels (list): A list of channel names where tasks are queued.
|
23
|
-
max_workers (int, optional): The maximum number of threads available for task execution. Defaults to 5.
|
24
|
-
"""
|
25
|
-
super().__init__("taskit", os.path.join(paths.VAR_ROOT, "taskit"))
|
26
|
-
self.manager = manager.TaskManager(channels)
|
27
|
-
self.channels = channels
|
28
|
-
if "broadcast" not in self.channels:
|
29
|
-
self.channels.append("broadcast")
|
30
|
-
self.max_workers = max_workers
|
31
|
-
self.executor = None
|
32
|
-
self.logger = logit.get_logger("taskit", "taskit.log")
|
33
|
-
|
34
|
-
def reset_running_tasks(self):
|
35
|
-
"""
|
36
|
-
Reset tasks that are stuck in a running state by moving them back to the pending state.
|
37
|
-
"""
|
38
|
-
for channel in self.channels:
|
39
|
-
for task_id in self.manager.get_running_ids(channel):
|
40
|
-
self.logger.info(f"moving task {task_id} from running to pending")
|
41
|
-
self.manager.remove_from_running(channel, task_id)
|
42
|
-
self.manager.add_to_pending(channel, task_id)
|
43
|
-
|
44
|
-
def queue_pending_tasks(self):
|
45
|
-
"""
|
46
|
-
Queue all the pending tasks for execution.
|
47
|
-
"""
|
48
|
-
for channel in self.channels:
|
49
|
-
for task_id in self.manager.get_pending_ids(channel):
|
50
|
-
self.queue_task(task_id)
|
51
|
-
|
52
|
-
def handle_message(self, message):
|
53
|
-
"""
|
54
|
-
Handle incoming messages from the channels, decoding task identifiers and queuing them for execution.
|
55
|
-
|
56
|
-
Args:
|
57
|
-
message (dict): A dictionary with message data containing task information.
|
58
|
-
"""
|
59
|
-
self.queue_task(message['data'].decode())
|
60
|
-
|
61
|
-
def on_run_task(self, task_id):
|
62
|
-
"""
|
63
|
-
Execute a task based on its identifier by locating the relevant function and executing it.
|
64
|
-
|
65
|
-
Args:
|
66
|
-
task_id (str): The identifier of the task to be executed.
|
67
|
-
"""
|
68
|
-
# this is a keep it thread safe with the redis connection
|
69
|
-
tman = TaskManager([])
|
70
|
-
task_data = tman.get_task(task_id)
|
71
|
-
if not task_data:
|
72
|
-
# this task has expired or no longer exists
|
73
|
-
self.logger.info(f"Task {task_id} has expired or no longer exists")
|
74
|
-
tman.remove_from_pending(task_id)
|
75
|
-
return
|
76
|
-
self.logger.info(f"Executing task {task_id}")
|
77
|
-
function_path = task_data.get('function')
|
78
|
-
module_name, func_name = function_path.rsplit('.', 1)
|
79
|
-
module = import_module(module_name)
|
80
|
-
func = getattr(module, func_name)
|
81
|
-
self.manager.remove_from_pending(task_id, task_data.channel)
|
82
|
-
self.manager.add_to_running(task_id, task_data.channel)
|
83
|
-
|
84
|
-
try:
|
85
|
-
task_data.started_at = time.time()
|
86
|
-
func(task_data)
|
87
|
-
task_data.completed_at = time.time()
|
88
|
-
task_data.elapsed_time = task_data.completed_at - task_data.started_at
|
89
|
-
tman.save_task(task_data)
|
90
|
-
tman.add_to_completed(task_data)
|
91
|
-
self.logger.info(f"Task {task_id} completed after {task_data.elapsed_time} seconds")
|
92
|
-
except Exception as e:
|
93
|
-
self.logger.error(f"Error executing task {task_id}: {str(e)}")
|
94
|
-
tman.add_to_errors(task_data, str(e))
|
95
|
-
finally:
|
96
|
-
tman.remove_from_running(task_id, task_data.channel)
|
97
|
-
|
98
|
-
def queue_task(self, task_id):
|
99
|
-
"""
|
100
|
-
Submit a task for execution in the thread pool.
|
101
|
-
|
102
|
-
Args:
|
103
|
-
task_id (str): The identifier of the task to be queued.
|
104
|
-
"""
|
105
|
-
self.logger.info(f"adding task {task_id}")
|
106
|
-
self.executor.submit(self.on_run_task, task_id)
|
107
|
-
|
108
|
-
def wait_for_all_tasks_to_complete(self, timeout=30):
|
109
|
-
"""
|
110
|
-
Wait for all tasks submitted to the executor to complete.
|
111
|
-
"""
|
112
|
-
self.executor.shutdown(wait=True, timeout=timeout)
|
113
|
-
# Check if there are still active threads
|
114
|
-
active_threads = [thread for thread in self.executor._threads if thread.is_alive()]
|
115
|
-
if active_threads:
|
116
|
-
self.logger.warning(f"shutdown issue, {len(active_threads)} tasks exceeded timeout")
|
117
|
-
self.executor.shutdown(wait=False) # Stop accepting new tasks
|
118
|
-
|
119
|
-
def start_listening(self):
|
120
|
-
"""
|
121
|
-
Listen for messages on the subscribed channels and handle them as they arrive.
|
122
|
-
"""
|
123
|
-
self.logger.info("starting with channels...", self.channels)
|
124
|
-
self.reset_running_tasks()
|
125
|
-
self.queue_pending_tasks()
|
126
|
-
pubsub = self.manager.redis.pubsub()
|
127
|
-
channel_keys = {self.manager.get_channel_key(channel): self.handle_message for channel in self.channels}
|
128
|
-
pubsub.subscribe(**channel_keys)
|
129
|
-
for message in pubsub.listen():
|
130
|
-
if not self.running:
|
131
|
-
self.logger.info("shutting down, waiting for tasks to complete")
|
132
|
-
self.wait_for_all_tasks_to_complete()
|
133
|
-
self.logger.info("shutdown complete")
|
134
|
-
return
|
135
|
-
if message['type'] != 'message':
|
136
|
-
continue
|
137
|
-
self.handle_message(message)
|
138
|
-
|
139
|
-
def run(self):
|
140
|
-
self.executor = ThreadPoolExecutor(max_workers=self.max_workers)
|
141
|
-
self.start_listening()
|
142
|
-
|
143
|
-
|
144
|
-
# HELPERS FOR RUNNING VIA CLI
|
145
|
-
def get_args():
|
146
|
-
"""
|
147
|
-
Setup the argument parser for command-line interface.
|
148
|
-
|
149
|
-
Returns:
|
150
|
-
Namespace: Parsed command-line arguments.
|
151
|
-
"""
|
152
|
-
import argparse
|
153
|
-
parser = argparse.ArgumentParser(description="TaskEngine Background Service")
|
154
|
-
parser.add_argument("--start", action="store_true", help="Start the daemon")
|
155
|
-
parser.add_argument("--stop", action="store_true", help="Stop the daemon")
|
156
|
-
parser.add_argument("--foreground", "-f", action="store_true", help="Run in foreground mode")
|
157
|
-
parser.add_argument("-v", "--verbose", action="store_true",
|
158
|
-
help="Enable verbose logging")
|
159
|
-
return parser, parser.parse_args()
|
160
|
-
|
161
|
-
|
162
|
-
def main():
|
163
|
-
from mojo.helpers.settings import settings
|
164
|
-
parser, args = get_args()
|
165
|
-
daemon = TaskEngine(settings.TASKIT_CHANNELS)
|
166
|
-
if args.start:
|
167
|
-
daemon.start()
|
168
|
-
elif args.stop:
|
169
|
-
daemon.stop()
|
170
|
-
elif args.foreground:
|
171
|
-
print("Running in foreground mode...")
|
172
|
-
daemon.run()
|
173
|
-
else:
|
174
|
-
parser.print_help()
|
mojo/apps/tasks/tq_handlers.py
DELETED
@@ -1,14 +0,0 @@
|
|
1
|
-
from mojo.helpers import logit
|
2
|
-
import time
|
3
|
-
|
4
|
-
logger = logit.get_logger("ti_example", "ti_example.log")
|
5
|
-
|
6
|
-
def run_example_task(task):
|
7
|
-
logger.info("Running example task with data", task)
|
8
|
-
time.sleep(task.data.get("duration", 5))
|
9
|
-
|
10
|
-
|
11
|
-
def run_error_task(task):
|
12
|
-
logger.info("Running error task with data", task)
|
13
|
-
time.sleep(2)
|
14
|
-
raise Exception("Example error")
|
mojo/helpers/aws/setup_email.py
DELETED
File without changes
|
mojo/helpers/redis.py
DELETED
@@ -1,10 +0,0 @@
|
|
1
|
-
from redis import ConnectionPool, StrictRedis
|
2
|
-
from mojo.helpers.settings import settings
|
3
|
-
REDIS_POOL = None
|
4
|
-
|
5
|
-
|
6
|
-
def get_connection():
|
7
|
-
global REDIS_POOL
|
8
|
-
if REDIS_POOL is None:
|
9
|
-
REDIS_POOL = ConnectionPool(**settings.REDIS_DB)
|
10
|
-
return StrictRedis(connection_pool=REDIS_POOL)
|
mojo/models/meta.py
DELETED
@@ -1,262 +0,0 @@
|
|
1
|
-
from objict import objict
|
2
|
-
from django.db import models as dm
|
3
|
-
import string
|
4
|
-
from rest.encryption import ENCRYPTER, DECRYPTER
|
5
|
-
from datetime import datetime, date
|
6
|
-
|
7
|
-
class MetaDataBase(dm.Model):
|
8
|
-
class Meta:
|
9
|
-
abstract = True
|
10
|
-
|
11
|
-
category = dm.CharField(db_index=True, max_length=32, default=None, null=True, blank=True)
|
12
|
-
key = dm.CharField(db_index=True, max_length=80)
|
13
|
-
value_format = dm.CharField(max_length=16)
|
14
|
-
value = dm.TextField()
|
15
|
-
int_value = dm.IntegerField(default=None, null=True, blank=True)
|
16
|
-
float_value = dm.FloatField(default=None, null=True, blank=True)
|
17
|
-
|
18
|
-
def set_value(self, value):
|
19
|
-
self.value = str(value)
|
20
|
-
value_type = type(value)
|
21
|
-
|
22
|
-
if value_type is int or self.value in ["0", "1"]:
|
23
|
-
if value_type is int and value > 2147483647:
|
24
|
-
self.value_format = "S"
|
25
|
-
return
|
26
|
-
self.value_format = "I"
|
27
|
-
self.int_value = value
|
28
|
-
elif value_type is float:
|
29
|
-
self.value_format = "F"
|
30
|
-
self.float_value = value
|
31
|
-
elif isinstance(value, list):
|
32
|
-
self.value_format = "L"
|
33
|
-
elif isinstance(value, dict):
|
34
|
-
self.value_format = "O"
|
35
|
-
elif isinstance(value, str) and len(value) < 9 and value.isdigit():
|
36
|
-
self.value_format = "I"
|
37
|
-
self.int_value = int(value)
|
38
|
-
elif value in ["True", "true", "False", "false"]:
|
39
|
-
self.value_format = "I"
|
40
|
-
self.int_value = 1 if value.lower() == "true" else 0
|
41
|
-
elif isinstance(value, bool):
|
42
|
-
self.value_format = "I"
|
43
|
-
self.int_value = 1 if value else 0
|
44
|
-
else:
|
45
|
-
self.value_format = "S"
|
46
|
-
|
47
|
-
def get_strict_type(self, field_type):
|
48
|
-
try:
|
49
|
-
return field_type(self.value)
|
50
|
-
except (ValueError, TypeError):
|
51
|
-
if field_type is bool:
|
52
|
-
return self.int_value != 0 if self.value_format == 'I' else self.value.lower() in ['true', '1', 'y', 'yes']
|
53
|
-
elif field_type in [date, datetime]:
|
54
|
-
return rh.parseDate(self.value)
|
55
|
-
return self.value
|
56
|
-
|
57
|
-
def get_value(self, field_type=None):
|
58
|
-
if field_type:
|
59
|
-
return self.get_strict_type(field_type)
|
60
|
-
if self.value_format == 'I':
|
61
|
-
return self.int_value
|
62
|
-
elif self.value_format == 'F':
|
63
|
-
return self.float_value
|
64
|
-
elif self.value_format in ["L", "O"] and self.value:
|
65
|
-
try:
|
66
|
-
return eval(self.value)
|
67
|
-
except Exception:
|
68
|
-
pass
|
69
|
-
return self.value
|
70
|
-
|
71
|
-
def __str__(self):
|
72
|
-
return f"{self.category}.{self.key}={self.value}" if self.category else f"{self.key}={self.value}"
|
73
|
-
|
74
|
-
class MetaDataModel:
|
75
|
-
def set_metadata(self, request, values=None):
|
76
|
-
if not self.id:
|
77
|
-
self.save()
|
78
|
-
|
79
|
-
values = values or request
|
80
|
-
if isinstance(values, list):
|
81
|
-
values = objict({k: v for item in values if isinstance(item, dict) for k, v in item.items()})
|
82
|
-
|
83
|
-
if not isinstance(values, dict):
|
84
|
-
raise ValueError(f"invalid metadata: {values}")
|
85
|
-
|
86
|
-
for key, value in values.items():
|
87
|
-
cat, key = key.split('.', 1) if '.' in key else (None, key)
|
88
|
-
self.set_property(key, value, cat, request=request)
|
89
|
-
|
90
|
-
def metadata(self):
|
91
|
-
return self.get_properties()
|
92
|
-
|
93
|
-
def remove_properties(self, category=None):
|
94
|
-
self.properties.filter(category=category).delete()
|
95
|
-
|
96
|
-
def get_properties(self, category=None):
|
97
|
-
result = {}
|
98
|
-
for prop in self.properties.all():
|
99
|
-
category_ = prop.category
|
100
|
-
key = prop.key
|
101
|
-
|
102
|
-
if not category_:
|
103
|
-
self._add_property_to_result(result, prop)
|
104
|
-
continue
|
105
|
-
|
106
|
-
props = self.get_field_props(category_)
|
107
|
-
if props.hidden:
|
108
|
-
continue
|
109
|
-
|
110
|
-
if category_ not in result:
|
111
|
-
result[category_] = {}
|
112
|
-
|
113
|
-
if category_ == "secrets":
|
114
|
-
masked_value = "*" * prop.int_value if prop.int_value else "******"
|
115
|
-
result[category_][key] = masked_value
|
116
|
-
else:
|
117
|
-
self._add_property_to_result(result[category_], prop)
|
118
|
-
|
119
|
-
return result.get(category, {}) if category else result
|
120
|
-
|
121
|
-
def _add_property_to_result(self, result_dict, prop):
|
122
|
-
props = self.get_field_props(prop.key)
|
123
|
-
if not props.hidden:
|
124
|
-
result_dict[prop.key] = prop.get_value()
|
125
|
-
|
126
|
-
def get_field_props(self, key):
|
127
|
-
self._init_field_props()
|
128
|
-
category, key = key.split('.', 1) if '.' in key else (None, key)
|
129
|
-
props = objict()
|
130
|
-
|
131
|
-
if self.__field_props:
|
132
|
-
cat_props = self.__field_props.get(category, {})
|
133
|
-
self._update_props_with_category(props, cat_props)
|
134
|
-
|
135
|
-
field_props = self.__field_props.get(key, {})
|
136
|
-
self._update_props_with_field(props, field_props)
|
137
|
-
|
138
|
-
return props
|
139
|
-
|
140
|
-
def _update_props_with_category(self, props, cat_props):
|
141
|
-
if cat_props:
|
142
|
-
props.notify = cat_props.get("notify")
|
143
|
-
props.requires = cat_props.get("requires")
|
144
|
-
props.hidden = cat_props.get("hidden", False)
|
145
|
-
on_change_name = cat_props.get("on_change")
|
146
|
-
if on_change_name:
|
147
|
-
props.on_change = getattr(self, on_change_name, None)
|
148
|
-
|
149
|
-
def _update_props_with_field(self, props, field_props):
|
150
|
-
props.notify = field_props.get("notify", props.notify)
|
151
|
-
props.requires = field_props.get("requires", props.requires)
|
152
|
-
props.hidden = field_props.get("hidden", props.hidden)
|
153
|
-
on_change_name = field_props.get("on_change")
|
154
|
-
if on_change_name:
|
155
|
-
props.on_change = getattr(self, on_change_name, None)
|
156
|
-
|
157
|
-
def check_field_perms(self, full_key, props, request=None):
|
158
|
-
if not props.requires:
|
159
|
-
return True
|
160
|
-
if not request or not request.member:
|
161
|
-
return False
|
162
|
-
if request.member.hasPermission(props.requires) or request.user.is_superuser:
|
163
|
-
return True
|
164
|
-
|
165
|
-
if props.notify and request.member:
|
166
|
-
subject = f"permission denied changing protected '{full_key}' field"
|
167
|
-
msg = f"permission denied changing protected field '{full_key}'\nby user: {request.user.username}\nfor: {self}"
|
168
|
-
request.member.notifyWithPermission(props.notify, subject, msg, email_only=True)
|
169
|
-
raise re.PermissionDeniedException(subject, 481)
|
170
|
-
|
171
|
-
def set_properties(self, data, category=None, request=None, using=None):
|
172
|
-
for k, v in data.items():
|
173
|
-
self.set_property(k, v, category, request=request, using=using)
|
174
|
-
|
175
|
-
def set_property(self, key, value, category=None, request=None, using=None, ascii_only=False, encrypted=False):
|
176
|
-
if ascii_only and isinstance(value, str):
|
177
|
-
value = ''.join(filter(lambda x: x in string.printable, value))
|
178
|
-
|
179
|
-
if using is None:
|
180
|
-
using = getattr(self.RestMeta, "DATABASE", None)
|
181
|
-
|
182
|
-
if request is None:
|
183
|
-
request = rh.getActiveRequest()
|
184
|
-
|
185
|
-
self._init_field_props()
|
186
|
-
|
187
|
-
if '.' in key:
|
188
|
-
category, key = key.split('.', 1)
|
189
|
-
|
190
|
-
full_key = f"{category}.{key}" if category else key
|
191
|
-
field_props = self.get_field_props(full_key)
|
192
|
-
|
193
|
-
if not self.check_field_perms(full_key, field_props, request):
|
194
|
-
return False
|
195
|
-
|
196
|
-
prop = self.properties.filter(category=category, key=key).last()
|
197
|
-
if not prop and (value is None or value == ""):
|
198
|
-
return False
|
199
|
-
|
200
|
-
has_changed, old_value = self._update_or_create_property(prop, category, key, value, encrypted, using)
|
201
|
-
|
202
|
-
if has_changed and field_props.on_change:
|
203
|
-
field_props.on_change(key, value, old_value, category)
|
204
|
-
|
205
|
-
self._notify_change_if_required(field_props, full_key, value, request)
|
206
|
-
|
207
|
-
if hasattr(self, "_recordRestChange"):
|
208
|
-
self._recordRestChange(f"metadata.{full_key}", old_value)
|
209
|
-
|
210
|
-
return has_changed
|
211
|
-
|
212
|
-
def _update_or_create_property(self, prop, category, key, value, encrypted, using):
|
213
|
-
has_changed = False
|
214
|
-
old_value = None
|
215
|
-
|
216
|
-
value_len = len(value) if encrypted else 0
|
217
|
-
if encrypted:
|
218
|
-
value = ENCRYPTER.encrypt(value)
|
219
|
-
|
220
|
-
if prop:
|
221
|
-
old_value = prop.get_value()
|
222
|
-
if value is None or value == "":
|
223
|
-
self.properties.filter(category=category, key=key).delete()
|
224
|
-
has_changed = True
|
225
|
-
else:
|
226
|
-
has_changed = str(value) != prop.value
|
227
|
-
if has_changed:
|
228
|
-
prop.set_value(value)
|
229
|
-
if encrypted:
|
230
|
-
prop.int_value = value_len
|
231
|
-
prop.save(using=using)
|
232
|
-
else:
|
233
|
-
has_changed = True
|
234
|
-
PropClass = self.get_fk_model("properties")
|
235
|
-
prop = PropClass(parent=self, key=key, category=category)
|
236
|
-
prop.set_value(value)
|
237
|
-
prop.save(using=using)
|
238
|
-
|
239
|
-
return has_changed, old_value
|
240
|
-
|
241
|
-
def _notify_change_if_required(self, field_props, full_key, value, request):
|
242
|
-
if field_props.notify and request and request.member:
|
243
|
-
username = request.member.username if request and request.member else "root"
|
244
|
-
truncated_value = "***" if value and len(str(value)) > 5 else value
|
245
|
-
msg = (f"protected field '{full_key}' changed to '{truncated_value}'\n"
|
246
|
-
f"by user: {username}\nfor: {self}")
|
247
|
-
request.member.notifyWithPermission(field_props.notify, f"protected '{full_key}' field changed", msg, email_only=True)
|
248
|
-
|
249
|
-
def get_property(self, key, default=None, category=None, field_type=None, decrypted=False):
|
250
|
-
category, key = key.split('.', 1) if '.' in key else (category, key)
|
251
|
-
|
252
|
-
try:
|
253
|
-
prop_value = self.properties.get(category=category, key=key).get_value(field_type)
|
254
|
-
return DECRYPTER.decrypt(prop_value) if decrypted and prop_value else prop_value
|
255
|
-
except Exception:
|
256
|
-
return default
|
257
|
-
|
258
|
-
def set_secret_property(self, key, value):
|
259
|
-
return self.set_property(key, value, category="secrets", encrypted=True)
|
260
|
-
|
261
|
-
def get_secret_property(self, key, default=None):
|
262
|
-
return self.get_property(key, default, "secrets", decrypted=True)
|
mojo/ws4redis/README.md
DELETED
@@ -1,174 +0,0 @@
|
|
1
|
-
# Websocket HOWTO
|
2
|
-
|
3
|
-
## Authentication
|
4
|
-
|
5
|
-
### JWT
|
6
|
-
|
7
|
-
Requires an existing JWT token that has gone through authentication process via rest
|
8
|
-
|
9
|
-
```json
|
10
|
-
{
|
11
|
-
"action": "auth",
|
12
|
-
"kind": "jwt",
|
13
|
-
"token": "..."
|
14
|
-
}
|
15
|
-
```
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
### Model Authentication
|
20
|
-
|
21
|
-
You can implement custom authentication flows via a model by using the WS4REDIS_AUTHENTICATORS in your django settings.py.
|
22
|
-
|
23
|
-
##### WS4REDIS_AUTHENTICATORS
|
24
|
-
|
25
|
-
```python
|
26
|
-
WS4REDIS_AUTHENTICATORS = {
|
27
|
-
"mymodel": "myapp.MyModel"
|
28
|
-
}
|
29
|
-
```
|
30
|
-
|
31
|
-
In your Model you will need to add the following class methods.
|
32
|
-
|
33
|
-
This method is used by the async/websocket service to authenticate.
|
34
|
-
If the model can authenticate the connection it should return dict with kind and pk of the model that is authenticaed.
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
##### authWS4RedisConnection
|
39
|
-
|
40
|
-
This method will authenticate the model, or return None if authentication failed.
|
41
|
-
|
42
|
-
```python
|
43
|
-
@classmethod
|
44
|
-
def authWS4RedisConnection(cls, auth_data):
|
45
|
-
if auth_data and auth_data.token:
|
46
|
-
terminal = cls.objects.filter(token=auth_data.token).last()
|
47
|
-
if terminal is not None:
|
48
|
-
# we now return the terminal credentials to the framework
|
49
|
-
return UberDict(
|
50
|
-
kind="terminal",
|
51
|
-
pk=terminal.id,
|
52
|
-
uuid=terminal.tid,
|
53
|
-
token=auth_data.token,
|
54
|
-
only_one=True, # only allows one connection at a time
|
55
|
-
instance=terminal)
|
56
|
-
return None
|
57
|
-
```
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
##### canPublishTo
|
62
|
-
|
63
|
-
Add this to your Model to validate messages from this connection to be sent to this channel.
|
64
|
-
|
65
|
-
```python
|
66
|
-
@classmethod
|
67
|
-
def canPublishTo(cls, credentials, msg):
|
68
|
-
if credentials:
|
69
|
-
return True
|
70
|
-
return False
|
71
|
-
```
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
##### WS4REDIS_CHANNELS
|
76
|
-
|
77
|
-
Map channels to models
|
78
|
-
|
79
|
-
```python
|
80
|
-
WS4REDIS_CHANNELS = {
|
81
|
-
"group": "account.Group",
|
82
|
-
"chat": "chat.Room",
|
83
|
-
}
|
84
|
-
```
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
##### onWS4RedisMessage
|
89
|
-
|
90
|
-
Add this to your Model to allow for handling of messages sent to this channel.
|
91
|
-
|
92
|
-
```python
|
93
|
-
@classmethod
|
94
|
-
def onWS4RedisMessage(cls, credentials, msg):
|
95
|
-
if msg.action == "status":
|
96
|
-
cls.createStatusRecord(msg)
|
97
|
-
|
98
|
-
```
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
### URL Params
|
103
|
-
|
104
|
-
You can also use params in the url of the websocket.
|
105
|
-
|
106
|
-
**THIS IS NOT RECOMMENDED as the url params are not encrypted and can be easily snooped.**
|
107
|
-
|
108
|
-
Include something like the follow in your django settings.py:
|
109
|
-
|
110
|
-
```python
|
111
|
-
def URL_AUTHENTICATOR(ws_con):
|
112
|
-
from objict import objict
|
113
|
-
token = ws_con.request.GET.get("token", None)
|
114
|
-
session_key = ws_con.request.GET.get("session_key", None)
|
115
|
-
if token is not None:
|
116
|
-
# this example assume the token is used for terminal auth
|
117
|
-
# you will still need to implement the Custom Auth flows to handle this
|
118
|
-
ws_con.on_auth(objict(kind="terminal", token=token))
|
119
|
-
elif session_key is not None:
|
120
|
-
# or alternative is a session
|
121
|
-
ws_con.on_auth(objict(kind="session", token=session_key))
|
122
|
-
|
123
|
-
```
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
## Subscribe
|
128
|
-
|
129
|
-
```json
|
130
|
-
{
|
131
|
-
"action": "subscribe",
|
132
|
-
"channel": "group",
|
133
|
-
"pk": 3,
|
134
|
-
}
|
135
|
-
```
|
136
|
-
|
137
|
-
### Security
|
138
|
-
|
139
|
-
In settins WS4REDIS_CHANNELS, map your channel to a model.
|
140
|
-
The model should have a classmethod for canSubscribeTo that returns a list of pk they can subscribe to.
|
141
|
-
|
142
|
-
|
143
|
-
## UnSubscribe
|
144
|
-
|
145
|
-
```json
|
146
|
-
{
|
147
|
-
"action": "unsubscribe",
|
148
|
-
"channel": "group",
|
149
|
-
"pk": 3,
|
150
|
-
}
|
151
|
-
```
|
152
|
-
|
153
|
-
|
154
|
-
## Publish / Send To
|
155
|
-
|
156
|
-
```json
|
157
|
-
{
|
158
|
-
"action": "publish",
|
159
|
-
"channel": "group",
|
160
|
-
"pk": 3,
|
161
|
-
"message": "..."
|
162
|
-
}
|
163
|
-
```
|
164
|
-
|
165
|
-
### Security
|
166
|
-
|
167
|
-
In settins WS4REDIS_CHANNELS, map your channel to a model.
|
168
|
-
The model should have a classmethod for canPublishTo that returns a list of pk they can publish to.
|
169
|
-
|
170
|
-
|
171
|
-
## Custom Messages
|
172
|
-
|
173
|
-
If an unknown action is sent with a channel then the framework will call onWS4RedisMessage on the channel model.
|
174
|
-
|
mojo/ws4redis/__init__.py
DELETED