django-nativemojo 0.1.15__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.
Files changed (221) hide show
  1. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.16.dist-info}/METADATA +3 -1
  2. django_nativemojo-0.1.16.dist-info/RECORD +302 -0
  3. mojo/__init__.py +1 -1
  4. mojo/apps/account/management/commands/serializer_admin.py +121 -1
  5. mojo/apps/account/migrations/0006_add_device_tracking_models.py +72 -0
  6. mojo/apps/account/migrations/0007_delete_userdevicelocation.py +16 -0
  7. mojo/apps/account/migrations/0008_userdevicelocation.py +33 -0
  8. mojo/apps/account/migrations/0009_geolocatedip_subnet.py +18 -0
  9. mojo/apps/account/migrations/0010_group_avatar.py +20 -0
  10. mojo/apps/account/migrations/0011_user_org_registereddevice_pushconfig_and_more.py +118 -0
  11. mojo/apps/account/migrations/0012_remove_pushconfig_apns_key_file_and_more.py +21 -0
  12. mojo/apps/account/migrations/0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more.py +28 -0
  13. mojo/apps/account/migrations/0014_notificationdelivery_data_payload_and_more.py +48 -0
  14. mojo/apps/account/models/__init__.py +2 -0
  15. mojo/apps/account/models/device.py +281 -0
  16. mojo/apps/account/models/group.py +294 -8
  17. mojo/apps/account/models/member.py +14 -1
  18. mojo/apps/account/models/push/__init__.py +4 -0
  19. mojo/apps/account/models/push/config.py +112 -0
  20. mojo/apps/account/models/push/delivery.py +93 -0
  21. mojo/apps/account/models/push/device.py +66 -0
  22. mojo/apps/account/models/push/template.py +99 -0
  23. mojo/apps/account/models/user.py +190 -17
  24. mojo/apps/account/rest/__init__.py +2 -0
  25. mojo/apps/account/rest/device.py +39 -0
  26. mojo/apps/account/rest/group.py +8 -0
  27. mojo/apps/account/rest/push.py +187 -0
  28. mojo/apps/account/rest/user.py +95 -5
  29. mojo/apps/account/services/__init__.py +1 -0
  30. mojo/apps/account/services/push.py +363 -0
  31. mojo/apps/aws/migrations/0001_initial.py +206 -0
  32. mojo/apps/aws/migrations/0002_emaildomain_can_recv_emaildomain_can_send_and_more.py +28 -0
  33. mojo/apps/aws/migrations/0003_mailbox_is_domain_default_mailbox_is_system_default_and_more.py +31 -0
  34. mojo/apps/aws/migrations/0004_s3bucket.py +39 -0
  35. mojo/apps/aws/migrations/0005_alter_emaildomain_region_delete_s3bucket.py +21 -0
  36. mojo/apps/aws/models/__init__.py +19 -0
  37. mojo/apps/aws/models/email_attachment.py +99 -0
  38. mojo/apps/aws/models/email_domain.py +218 -0
  39. mojo/apps/aws/models/email_template.py +132 -0
  40. mojo/apps/aws/models/incoming_email.py +197 -0
  41. mojo/apps/aws/models/mailbox.py +288 -0
  42. mojo/apps/aws/models/sent_message.py +175 -0
  43. mojo/apps/aws/rest/__init__.py +6 -0
  44. mojo/apps/aws/rest/email.py +33 -0
  45. mojo/apps/aws/rest/email_ops.py +183 -0
  46. mojo/apps/aws/rest/messages.py +32 -0
  47. mojo/apps/aws/rest/send.py +101 -0
  48. mojo/apps/aws/rest/sns.py +403 -0
  49. mojo/apps/aws/rest/templates.py +19 -0
  50. mojo/apps/aws/services/__init__.py +32 -0
  51. mojo/apps/aws/services/email.py +390 -0
  52. mojo/apps/aws/services/email_ops.py +548 -0
  53. mojo/apps/docit/__init__.py +6 -0
  54. mojo/apps/docit/markdown_plugins/syntax_highlight.py +25 -0
  55. mojo/apps/docit/markdown_plugins/toc.py +12 -0
  56. mojo/apps/docit/migrations/0001_initial.py +113 -0
  57. mojo/apps/docit/migrations/0002_alter_book_modified_by_alter_page_modified_by.py +26 -0
  58. mojo/apps/docit/migrations/0003_alter_book_group.py +20 -0
  59. mojo/apps/docit/models/__init__.py +17 -0
  60. mojo/apps/docit/models/asset.py +231 -0
  61. mojo/apps/docit/models/book.py +227 -0
  62. mojo/apps/docit/models/page.py +319 -0
  63. mojo/apps/docit/models/page_revision.py +203 -0
  64. mojo/apps/docit/rest/__init__.py +10 -0
  65. mojo/apps/docit/rest/asset.py +17 -0
  66. mojo/apps/docit/rest/book.py +22 -0
  67. mojo/apps/docit/rest/page.py +22 -0
  68. mojo/apps/docit/rest/page_revision.py +17 -0
  69. mojo/apps/docit/services/__init__.py +11 -0
  70. mojo/apps/docit/services/docit.py +315 -0
  71. mojo/apps/docit/services/markdown.py +44 -0
  72. mojo/apps/fileman/backends/s3.py +209 -0
  73. mojo/apps/fileman/models/file.py +45 -9
  74. mojo/apps/fileman/models/manager.py +269 -3
  75. mojo/apps/incident/migrations/0007_event_uid.py +18 -0
  76. mojo/apps/incident/migrations/0008_ticket_ticketnote.py +55 -0
  77. mojo/apps/incident/migrations/0009_incident_status.py +18 -0
  78. mojo/apps/incident/migrations/0010_event_country_code.py +18 -0
  79. mojo/apps/incident/migrations/0011_incident_country_code.py +18 -0
  80. mojo/apps/incident/migrations/0012_alter_incident_status.py +18 -0
  81. mojo/apps/incident/models/__init__.py +1 -0
  82. mojo/apps/incident/models/event.py +35 -0
  83. mojo/apps/incident/models/incident.py +2 -0
  84. mojo/apps/incident/models/ticket.py +62 -0
  85. mojo/apps/incident/reporter.py +21 -3
  86. mojo/apps/incident/rest/__init__.py +1 -0
  87. mojo/apps/incident/rest/ticket.py +43 -0
  88. mojo/apps/jobs/__init__.py +489 -0
  89. mojo/apps/jobs/adapters.py +24 -0
  90. mojo/apps/jobs/cli.py +616 -0
  91. mojo/apps/jobs/daemon.py +370 -0
  92. mojo/apps/jobs/examples/sample_jobs.py +376 -0
  93. mojo/apps/jobs/examples/webhook_examples.py +203 -0
  94. mojo/apps/jobs/handlers/__init__.py +5 -0
  95. mojo/apps/jobs/handlers/webhook.py +317 -0
  96. mojo/apps/jobs/job_engine.py +734 -0
  97. mojo/apps/jobs/keys.py +203 -0
  98. mojo/apps/jobs/local_queue.py +363 -0
  99. mojo/apps/jobs/management/__init__.py +3 -0
  100. mojo/apps/jobs/management/commands/__init__.py +3 -0
  101. mojo/apps/jobs/manager.py +1327 -0
  102. mojo/apps/jobs/migrations/0001_initial.py +97 -0
  103. mojo/apps/jobs/migrations/0002_alter_job_max_retries_joblog.py +39 -0
  104. mojo/apps/jobs/models/__init__.py +6 -0
  105. mojo/apps/jobs/models/job.py +441 -0
  106. mojo/apps/jobs/rest/__init__.py +2 -0
  107. mojo/apps/jobs/rest/control.py +466 -0
  108. mojo/apps/jobs/rest/jobs.py +421 -0
  109. mojo/apps/jobs/scheduler.py +571 -0
  110. mojo/apps/jobs/services/__init__.py +6 -0
  111. mojo/apps/jobs/services/job_actions.py +465 -0
  112. mojo/apps/jobs/settings.py +209 -0
  113. mojo/apps/logit/models/log.py +3 -0
  114. mojo/apps/metrics/__init__.py +8 -1
  115. mojo/apps/metrics/redis_metrics.py +198 -0
  116. mojo/apps/metrics/rest/__init__.py +3 -0
  117. mojo/apps/metrics/rest/categories.py +266 -0
  118. mojo/apps/metrics/rest/helpers.py +48 -0
  119. mojo/apps/metrics/rest/permissions.py +99 -0
  120. mojo/apps/metrics/rest/values.py +277 -0
  121. mojo/apps/metrics/utils.py +17 -0
  122. mojo/decorators/http.py +40 -1
  123. mojo/helpers/aws/__init__.py +11 -7
  124. mojo/helpers/aws/inbound_email.py +309 -0
  125. mojo/helpers/aws/kms.py +413 -0
  126. mojo/helpers/aws/ses_domain.py +959 -0
  127. mojo/helpers/crypto/__init__.py +1 -1
  128. mojo/helpers/crypto/utils.py +15 -0
  129. mojo/helpers/location/__init__.py +2 -0
  130. mojo/helpers/location/countries.py +262 -0
  131. mojo/helpers/location/geolocation.py +196 -0
  132. mojo/helpers/logit.py +37 -0
  133. mojo/helpers/redis/__init__.py +2 -0
  134. mojo/helpers/redis/adapter.py +606 -0
  135. mojo/helpers/redis/client.py +48 -0
  136. mojo/helpers/redis/pool.py +225 -0
  137. mojo/helpers/request.py +8 -0
  138. mojo/helpers/response.py +8 -0
  139. mojo/middleware/auth.py +1 -1
  140. mojo/middleware/cors.py +40 -0
  141. mojo/middleware/logging.py +131 -12
  142. mojo/middleware/mojo.py +5 -0
  143. mojo/models/rest.py +271 -57
  144. mojo/models/secrets.py +86 -0
  145. mojo/serializers/__init__.py +16 -10
  146. mojo/serializers/core/__init__.py +90 -0
  147. mojo/serializers/core/cache/__init__.py +121 -0
  148. mojo/serializers/core/cache/backends.py +518 -0
  149. mojo/serializers/core/cache/base.py +102 -0
  150. mojo/serializers/core/cache/disabled.py +181 -0
  151. mojo/serializers/core/cache/memory.py +287 -0
  152. mojo/serializers/core/cache/redis.py +533 -0
  153. mojo/serializers/core/cache/utils.py +454 -0
  154. mojo/serializers/{manager.py → core/manager.py} +53 -4
  155. mojo/serializers/core/serializer.py +475 -0
  156. mojo/serializers/{advanced/formats → formats}/csv.py +116 -139
  157. mojo/serializers/suggested_improvements.md +388 -0
  158. testit/client.py +1 -1
  159. testit/helpers.py +14 -0
  160. testit/runner.py +23 -6
  161. django_nativemojo-0.1.15.dist-info/RECORD +0 -234
  162. mojo/apps/notify/README.md +0 -91
  163. mojo/apps/notify/README_NOTIFICATIONS.md +0 -566
  164. mojo/apps/notify/admin.py +0 -52
  165. mojo/apps/notify/handlers/example_handlers.py +0 -516
  166. mojo/apps/notify/handlers/ses/__init__.py +0 -25
  167. mojo/apps/notify/handlers/ses/complaint.py +0 -25
  168. mojo/apps/notify/handlers/ses/message.py +0 -86
  169. mojo/apps/notify/management/commands/__init__.py +0 -1
  170. mojo/apps/notify/management/commands/process_notifications.py +0 -370
  171. mojo/apps/notify/mod +0 -0
  172. mojo/apps/notify/models/__init__.py +0 -12
  173. mojo/apps/notify/models/account.py +0 -128
  174. mojo/apps/notify/models/attachment.py +0 -24
  175. mojo/apps/notify/models/bounce.py +0 -68
  176. mojo/apps/notify/models/complaint.py +0 -40
  177. mojo/apps/notify/models/inbox.py +0 -113
  178. mojo/apps/notify/models/inbox_message.py +0 -173
  179. mojo/apps/notify/models/outbox.py +0 -129
  180. mojo/apps/notify/models/outbox_message.py +0 -288
  181. mojo/apps/notify/models/template.py +0 -30
  182. mojo/apps/notify/providers/aws.py +0 -73
  183. mojo/apps/notify/rest/ses.py +0 -0
  184. mojo/apps/notify/utils/__init__.py +0 -2
  185. mojo/apps/notify/utils/notifications.py +0 -404
  186. mojo/apps/notify/utils/parsing.py +0 -202
  187. mojo/apps/notify/utils/render.py +0 -144
  188. mojo/apps/tasks/README.md +0 -118
  189. mojo/apps/tasks/__init__.py +0 -44
  190. mojo/apps/tasks/manager.py +0 -644
  191. mojo/apps/tasks/rest/__init__.py +0 -2
  192. mojo/apps/tasks/rest/hooks.py +0 -0
  193. mojo/apps/tasks/rest/tasks.py +0 -76
  194. mojo/apps/tasks/runner.py +0 -439
  195. mojo/apps/tasks/task.py +0 -99
  196. mojo/apps/tasks/tq_handlers.py +0 -132
  197. mojo/helpers/crypto/__pycache__/hash.cpython-310.pyc +0 -0
  198. mojo/helpers/crypto/__pycache__/sign.cpython-310.pyc +0 -0
  199. mojo/helpers/crypto/__pycache__/utils.cpython-310.pyc +0 -0
  200. mojo/helpers/redis.py +0 -10
  201. mojo/models/meta.py +0 -262
  202. mojo/serializers/advanced/README.md +0 -363
  203. mojo/serializers/advanced/__init__.py +0 -247
  204. mojo/serializers/advanced/formats/__init__.py +0 -28
  205. mojo/serializers/advanced/formats/excel.py +0 -516
  206. mojo/serializers/advanced/formats/json.py +0 -239
  207. mojo/serializers/advanced/formats/response.py +0 -485
  208. mojo/serializers/advanced/serializer.py +0 -568
  209. mojo/serializers/optimized.py +0 -618
  210. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.16.dist-info}/LICENSE +0 -0
  211. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.16.dist-info}/NOTICE +0 -0
  212. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.16.dist-info}/WHEEL +0 -0
  213. /mojo/apps/{notify → aws/migrations}/__init__.py +0 -0
  214. /mojo/apps/{notify/handlers → docit/markdown_plugins}/__init__.py +0 -0
  215. /mojo/apps/{notify/management → docit/migrations}/__init__.py +0 -0
  216. /mojo/apps/{notify/providers → jobs/examples}/__init__.py +0 -0
  217. /mojo/apps/{notify/rest → jobs/migrations}/__init__.py +0 -0
  218. /mojo/{serializers → rest}/openapi.py +0 -0
  219. /mojo/serializers/{settings_example.py → examples/settings.py} +0 -0
  220. /mojo/{apps/notify/handlers/ses/bounce.py → serializers/formats/__init__.py} +0 -0
  221. /mojo/serializers/{advanced/formats → formats}/localizers.py +0 -0
@@ -1,76 +0,0 @@
1
- from mojo import decorators as md
2
- from mojo.helpers.response import JsonResponse
3
- # from django.http import JsonResponse
4
- from mojo.apps import tasks
5
-
6
- @md.GET('status')
7
- def api_status(request):
8
- tman = tasks.get_manager()
9
- return JsonResponse(dict(status=True, data=tman.get_status()))
10
-
11
-
12
- @md.GET('runners')
13
- def api_task_runners(request):
14
- tman = tasks.get_manager()
15
- runners = [r for r in tman.get_active_runners().values()]
16
- for r in runners:
17
- r['id'] = r['hostname']
18
- return JsonResponse(dict(status=True, data=runners, size=len(runners), count=len(runners)))
19
-
20
-
21
- @md.URL('pending')
22
- def api_pending(request):
23
- tman = tasks.get_manager()
24
- pending = tman.get_all_pending()
25
- size = len(pending)
26
- response = {
27
- 'status': True,
28
- 'count': size,
29
- 'page': 0,
30
- 'size': size,
31
- 'data': pending
32
- }
33
- return JsonResponse(response)
34
-
35
- @md.URL('completed')
36
- def api_completed(request):
37
- tman = tasks.get_manager()
38
- completed = tman.get_all_completed(include_data=True)
39
- size = len(completed)
40
- response = {
41
- 'status': True,
42
- 'count': size,
43
- 'page': 0,
44
- 'size': size,
45
- 'data': completed
46
- }
47
- return JsonResponse(response)
48
-
49
- @md.URL('running')
50
- def api_running(request):
51
- tman = tasks.get_manager()
52
- running = tman.get_all_running(include_data=True)
53
- size = len(running)
54
- response = {
55
- 'status': True,
56
- 'count': size,
57
- 'page': 0,
58
- 'size': size,
59
- 'data': running
60
- }
61
- return JsonResponse(response)
62
-
63
-
64
- @md.URL('errors')
65
- def api_errors(request):
66
- tman = tasks.get_manager()
67
- errors = tman.get_all_errors()
68
- size = len(errors)
69
- response = {
70
- 'status': True,
71
- 'count': size,
72
- 'page': 0,
73
- 'size': size,
74
- 'data': errors
75
- }
76
- return JsonResponse(response)
mojo/apps/tasks/runner.py DELETED
@@ -1,439 +0,0 @@
1
- from importlib import import_module
2
- from concurrent.futures import ThreadPoolExecutor
3
- from .manager import TaskManager
4
- from mojo.apps.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
- from mojo.apps import metrics
10
- import time
11
- import socket
12
- import threading
13
- import json
14
-
15
-
16
- class TaskEngine(daemon.Daemon):
17
- """
18
- The TaskEngine is responsible for managing and executing tasks across different channels.
19
- It leverages a thread pool to execute tasks concurrently and uses a task manager to maintain task states.
20
- """
21
- def __init__(self, channels=["broadcast"], max_workers=5):
22
- """
23
- Initialize the TaskEngine.
24
-
25
- Args:
26
- channels (list): A list of channel names where tasks are queued.
27
- max_workers (int, optional): The maximum number of threads available for task execution. Defaults to 5.
28
- """
29
- super().__init__("taskit", os.path.join(paths.VAR_ROOT, "taskit"))
30
- self.hostname = socket.gethostname()
31
- self.manager = manager.TaskManager(channels)
32
- self.channels = channels
33
- if "broadcast" not in self.channels:
34
- self.channels.append("broadcast")
35
-
36
- # Add hostname-specific channel for this runner
37
- self.runner_channel = f"runner_{self.hostname}"
38
- if self.runner_channel not in self.channels:
39
- self.channels.append(self.runner_channel)
40
-
41
- self.max_workers = max_workers
42
- self.executor = None
43
- self.logger = logit.get_logger("tasks", "tasks.log")
44
- self.ping_thread = None
45
- self.ping_interval = 30 # seconds
46
- self.started_at = time.time()
47
-
48
- def register_runner(self):
49
- """
50
- Register this runner as active in the system.
51
- """
52
- runner_data = {
53
- 'hostname': self.hostname,
54
- 'started_at': self.started_at,
55
- 'max_workers': self.max_workers,
56
- 'channels': self.channels,
57
- 'last_ping': time.time(),
58
- 'status': 'active'
59
- }
60
- self.manager.redis.hset(
61
- self.manager.get_runners_key(),
62
- self.hostname,
63
- json.dumps(runner_data)
64
- )
65
- self.logger.info(f"Registered runner {self.hostname}")
66
-
67
- def unregister_runner(self):
68
- """
69
- Unregister this runner from the active runners list.
70
- """
71
- self.manager.redis.hdel(self.manager.get_runners_key(), self.hostname)
72
- self.logger.info(f"Unregistered runner {self.hostname}")
73
-
74
- def update_runner_status(self, status_data=None):
75
- """
76
- Update the status of this runner.
77
- """
78
- if status_data is None:
79
- status_data = {}
80
-
81
- runner_data = {
82
- 'hostname': self.hostname,
83
- 'last_ping': time.time(),
84
- 'status': 'active',
85
- 'started_at': self.started_at,
86
- 'max_workers': self.max_workers,
87
- 'channels': self.channels,
88
- **status_data
89
- }
90
- self.manager.redis.hset(
91
- self.manager.get_runners_key(),
92
- self.hostname,
93
- json.dumps(runner_data)
94
- )
95
-
96
-
97
- def ping_runners(self):
98
- """
99
- Send ping messages to all active runners to check their status.
100
- """
101
- active_runners = self.manager.get_active_runners()
102
- for hostname in active_runners.keys():
103
- if hostname != self.hostname: # Don't ping ourselves
104
- ping_message = {
105
- 'type': 'ping',
106
- 'from': self.hostname,
107
- 'timestamp': time.time()
108
- }
109
- runner_channel = f"runner_{hostname}"
110
- self.manager.redis.publish(
111
- self.manager.get_channel_key(runner_channel),
112
- json.dumps(ping_message)
113
- )
114
-
115
- def handle_ping_request(self, message_data):
116
- """
117
- Handle incoming ping requests and send response.
118
- """
119
- ping_data = json.loads(message_data)
120
- response = {
121
- 'type': 'ping_response',
122
- 'from': self.hostname,
123
- 'to': ping_data['from'],
124
- 'timestamp': time.time(),
125
- 'status': self.get_runner_status()
126
- }
127
-
128
- # Send response to the requesting runner's channel
129
- requester_channel = f"runner_{ping_data['from']}"
130
- self.manager.redis.publish(
131
- self.manager.get_channel_key(requester_channel),
132
- json.dumps(response)
133
- )
134
-
135
- def handle_ping_response(self, message_data):
136
- """
137
- Handle ping responses from other runners.
138
- """
139
- response_data = json.loads(message_data)
140
- self.logger.info(f"Received ping response from {response_data['from']}")
141
- # Update the runner's status in our active runners list
142
- self.manager.redis.hset(
143
- self.manager.get_runners_key(),
144
- response_data['from'],
145
- json.dumps(response_data['status'])
146
- )
147
-
148
- def get_runner_status(self):
149
- """
150
- Get the current status of this runner.
151
-
152
- Returns:
153
- dict: Status information for this runner.
154
- """
155
- active_threads = 0
156
- if self.executor and hasattr(self.executor, '_threads'):
157
- active_threads = len([t for t in self.executor._threads if t.is_alive()])
158
-
159
- return {
160
- 'hostname': self.hostname,
161
- 'status': 'active',
162
- 'max_workers': self.max_workers,
163
- 'active_threads': active_threads,
164
- 'channels': self.channels,
165
- 'last_ping': time.time(),
166
- 'uptime': time.time() - getattr(self, 'start_time', time.time())
167
- }
168
-
169
- def start_ping_thread(self):
170
- """
171
- Start the background thread that periodically pings other runners.
172
- """
173
- def ping_loop():
174
- while self.running:
175
- try:
176
- self.ping_runners()
177
- self.update_runner_status()
178
- time.sleep(self.ping_interval)
179
- except Exception as e:
180
- self.logger.error(f"Error in ping loop: {e}")
181
- time.sleep(5)
182
-
183
- self.ping_thread = threading.Thread(target=ping_loop, daemon=True)
184
- self.ping_thread.start()
185
-
186
- def cleanup_stale_runners(self):
187
- """
188
- Remove runners that haven't been seen for a while.
189
- """
190
- cutoff_time = time.time() - (self.ping_interval * 3) # 3 missed pings
191
- active_runners = self.manager.get_active_runners()
192
-
193
- for hostname, runner_data in active_runners.items():
194
- last_ping = runner_data.get('last_ping', 0)
195
- if last_ping < cutoff_time:
196
- self.logger.info(f"Removing stale runner: {hostname}")
197
- self.manager.redis.hdel(self.manager.get_runners_key(), hostname)
198
-
199
- def reset_running_tasks(self):
200
- """
201
- Reset tasks that are stuck in a running state by moving them back to the pending state.
202
- """
203
- for channel in self.channels:
204
- for task_id in self.manager.get_running_ids(channel):
205
- self.logger.info(f"moving task {task_id} from running to pending")
206
- self.manager.remove_from_running(task_id, channel)
207
- self.manager.add_to_pending(task_id, channel)
208
-
209
- def queue_pending_tasks(self):
210
- """
211
- Queue all the pending tasks for execution.
212
- """
213
- for channel in self.channels:
214
- for task_id in self.manager.get_pending_ids(channel):
215
- self.queue_task(task_id)
216
-
217
- def handle_message(self, message):
218
- """
219
- Handle incoming messages from the channels, decoding task identifiers and queuing them for execution.
220
-
221
- Args:
222
- message (dict): A dictionary with message data containing task information.
223
- """
224
- message_data = message['data'].decode()
225
-
226
- # Check if this is a ping/status message
227
- try:
228
- parsed_message = json.loads(message_data)
229
- if isinstance(parsed_message, dict) and 'type' in parsed_message:
230
- if parsed_message['type'] == 'ping':
231
- self.handle_ping_request(message_data)
232
- return
233
- elif parsed_message['type'] == 'ping_response':
234
- self.handle_ping_response(message_data)
235
- return
236
- except (json.JSONDecodeError, TypeError):
237
- pass
238
-
239
- # If not a ping message, treat as a task
240
- self.queue_task(message_data)
241
-
242
- def on_run_task(self, task_id):
243
- """
244
- Execute a task based on its identifier by locating the relevant function and executing it.
245
-
246
- Args:
247
- task_id (str): The identifier of the task to be executed.
248
- """
249
- # this is a keep it thread safe with the redis connection
250
- tman = TaskManager([])
251
- task_data = tman.get_task(task_id)
252
- if not task_data:
253
- # this task has expired or no longer exists
254
- self.logger.info(f"Task {task_id} has expired or no longer exists")
255
- metrics.record("tasks_expired", category="tasks")
256
- # try and remove any pending dead tasks
257
- self.manager.channels = self.channels
258
- self.manager.take_out_the_dead(local=True)
259
- return
260
- self.logger.info(f"Executing task {task_id}")
261
- function_path = task_data.get('function')
262
- module_name, func_name = function_path.rsplit('.', 1)
263
- module = import_module(module_name)
264
- func = getattr(module, func_name)
265
- self.manager.remove_from_pending(task_id, task_data.channel)
266
- self.manager.add_to_running(task_id, task_data.channel)
267
-
268
- try:
269
- task_data.started_at = time.time()
270
- task_data._thread_id = threading.current_thread().ident
271
- tdata = task_data.get("data", {})
272
- if tdata and "args" in tdata and "kwargs" in tdata:
273
- args = tdata["args"]
274
- kwargs = tdata["kwargs"]
275
- # self.logger.info(f"Executing task {task_id} with args {args} and kwargs {kwargs}")
276
- func(*args, **kwargs)
277
- else:
278
- # self.logger.info(f"Executing task {task_id} with no arguments")
279
- func(task_data)
280
- task_data.completed_at = time.time()
281
- task_data.elapsed_time = task_data.completed_at - task_data.started_at
282
- if "_thread_id" in task_data:
283
- del task_data["_thread_id"]
284
- tman.save_task(task_data)
285
- tman.add_to_completed(task_data)
286
- metrics.record("tasks_completed", category="tasks")
287
- self.logger.info(f"Task {task_id} completed after {task_data.elapsed_time} seconds")
288
- except Exception as e:
289
- self.logger.exception(f"Error executing task {task_id}: {str(e)}")
290
- tman.add_to_errors(task_data, str(e))
291
- metrics.record("tasks_errors", category="tasks")
292
- finally:
293
- tman.remove_from_running(task_id, task_data.channel)
294
-
295
- def queue_task(self, task_id):
296
- """
297
- Submit a task for execution in the thread pool.
298
-
299
- Args:
300
- task_id (str): The identifier of the task to be queued.
301
- """
302
- self.logger.info(f"adding task {task_id}")
303
- self.executor.submit(self.on_run_task, task_id)
304
-
305
-
306
- def _clear_queued_tasks(self):
307
- import queue
308
- q = self.executor._work_queue
309
- removed = 0
310
- try:
311
- while True:
312
- q.get_nowait()
313
- removed += 1
314
- except queue.Empty:
315
- pass
316
- return removed
317
-
318
- def _wait_for_active_tasks(self, timeout=5.0):
319
- """
320
- Waits up to `timeout` seconds for active executor threads to finish.
321
- Returns True if all threads completed, False if timeout hit.
322
- """
323
- start_time = time.time()
324
- while time.time() - start_time < timeout:
325
- active = self.manager.get_all_running_ids(local=True)
326
- if len(active) == 0:
327
- return True
328
- time.sleep(0.01)
329
- return False
330
-
331
- def wait_for_all_tasks_to_complete(self, timeout=5):
332
- """
333
- Wait for all tasks submitted to the executor to complete with graceful degradation.
334
- """
335
- if not self.executor:
336
- return
337
-
338
- self.logger.info(f"Initiating graceful shutdown with {timeout}s timeout")
339
- self.executor.shutdown(wait=False)
340
- self._clear_queued_tasks()
341
- result = self._wait_for_active_tasks(timeout)
342
- if not result:
343
- self.logger.warning("Timeout reached while waiting for active tasks to complete")
344
- return result
345
-
346
- def start_listening(self):
347
- """
348
- Listen for messages on the subscribed channels and handle them as they arrive.
349
- """
350
- self.logger.info("starting with channels...", self.channels)
351
- self.start_time = time.time()
352
- self.register_runner()
353
- self.manager.take_out_the_dead(local=True)
354
- self.reset_running_tasks()
355
- self.queue_pending_tasks()
356
- self.start_ping_thread()
357
-
358
- pubsub = self.manager.redis.pubsub()
359
- channel_keys = {self.manager.get_channel_key(channel): self.handle_message for channel in self.channels}
360
- pubsub.subscribe(**channel_keys)
361
-
362
- for message in pubsub.listen():
363
- if not self.running:
364
- self.logger.info("shutting down, waiting for tasks to complete")
365
- self.wait_for_all_tasks_to_complete()
366
- self.unregister_runner()
367
- self.logger.info("shutdown complete")
368
- return
369
- if message['type'] != 'message':
370
- continue
371
- self.handle_message(message)
372
-
373
- def run(self):
374
- self.executor = ThreadPoolExecutor(max_workers=self.max_workers)
375
- self.start_listening()
376
-
377
-
378
- # HELPERS FOR RUNNING VIA CLI
379
- def get_args():
380
- """
381
- Setup the argument parser for command-line interface.
382
-
383
- Returns:
384
- Namespace: Parsed command-line arguments.
385
- """
386
- import argparse
387
- parser = argparse.ArgumentParser(description="TaskEngine Background Service")
388
- parser.add_argument("--start", action="store_true", help="Start the daemon")
389
- parser.add_argument("--stop", action="store_true", help="Stop the daemon")
390
- parser.add_argument("--foreground", "-f", action="store_true", help="Run in foreground mode")
391
- parser.add_argument("--status", action="store_true", help="Show status of all runners")
392
- parser.add_argument("-v", "--verbose", action="store_true",
393
- help="Enable verbose logging")
394
- return parser, parser.parse_args()
395
-
396
-
397
- def main():
398
- from mojo.helpers.settings import settings
399
- parser, args = get_args()
400
- daemon = TaskEngine(settings.TASK_CHANNELS)
401
-
402
- if args.status:
403
- runners = daemon.manager.get_active_runners()
404
- if runners:
405
- print("Active TaskEngine Runners:")
406
- for hostname, data in runners.items():
407
- print(f" {hostname}: {data.get('status', 'unknown')} "
408
- f"(last ping: {time.time() - data.get('last_ping', 0):.1f}s ago)")
409
- else:
410
- print("No active runners found")
411
- elif args.start:
412
- daemon.start()
413
- elif args.stop:
414
- daemon.stop()
415
- elif args.foreground:
416
- print("Running in foreground mode...")
417
- daemon.run()
418
- else:
419
- parser.print_help()
420
-
421
-
422
-
423
- def kill_thread(thread):
424
- import ctypes
425
- if not thread.is_alive():
426
- return False
427
-
428
- tid = thread.ident
429
- if tid is None:
430
- return False
431
-
432
- res = ctypes.pythonapi.PyThreadState_SetAsyncExc(
433
- ctypes.c_long(tid), ctypes.py_object(SystemExit)
434
- )
435
- if res > 1:
436
- # Undo if multiple threads were affected
437
- ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(tid), 0)
438
- return False
439
- return True
mojo/apps/tasks/task.py DELETED
@@ -1,99 +0,0 @@
1
- from objict import objict
2
- import time
3
-
4
-
5
- class Task(objict):
6
- """
7
- Task model for the Django Mojo task system.
8
-
9
- This class represents a task that can be queued, executed, and tracked
10
- through various states (pending, running, completed, error, cancelled).
11
- """
12
-
13
- def __init__(self, id=None, function=None, data=None, channel="default",
14
- expires=None, created=None, status="pending", error=None,
15
- completed_at=None, **kwargs):
16
- """
17
- Initialize a new Task instance.
18
-
19
- Args:
20
- id (str): Unique identifier for the task
21
- function (str): Function name to be executed
22
- data (dict): Data to be passed to the function
23
- channel (str): Channel name for task routing
24
- expires (float): Expiration timestamp
25
- created (float): Creation timestamp
26
- status (str): Current task status
27
- error (str): Error message if task failed
28
- completed_at (float): Completion timestamp
29
- **kwargs: Additional attributes
30
- """
31
- super().__init__(**kwargs)
32
-
33
- self.id = id
34
- self.function = function
35
- self.data = data or {}
36
- self.channel = channel
37
- self.expires = expires
38
- self.created = created or time.time()
39
- self.status = status
40
- self.error = error
41
- self.completed_at = completed_at
42
-
43
- def is_expired(self):
44
- """
45
- Check if the task has expired.
46
-
47
- Returns:
48
- bool: True if task has expired, False otherwise
49
- """
50
- if self.expires is None:
51
- return False
52
- return time.time() > self.expires
53
-
54
- def is_pending(self):
55
- """Check if task is in pending state."""
56
- return self.status == "pending"
57
-
58
- def is_running(self):
59
- """Check if task is in running state."""
60
- return self.status == "running"
61
-
62
- def is_completed(self):
63
- """Check if task is in completed state."""
64
- return self.status == "completed"
65
-
66
- def is_error(self):
67
- """Check if task is in error state."""
68
- return self.status == "error"
69
-
70
- def is_cancelled(self):
71
- """Check if task is in cancelled state."""
72
- return self.status == "cancelled"
73
-
74
- def mark_as_running(self):
75
- """Mark task as running."""
76
- self.status = "running"
77
-
78
- def mark_as_completed(self):
79
- """Mark task as completed."""
80
- self.status = "completed"
81
- self.completed_at = time.time()
82
-
83
- def mark_as_error(self, error_message):
84
- """Mark task as error with error message."""
85
- self.status = "error"
86
- self.error = error_message
87
-
88
- def mark_as_cancelled(self):
89
- """Mark task as cancelled."""
90
- self.status = "cancelled"
91
-
92
- def __str__(self):
93
- """String representation of the task."""
94
- return f"Task({self.id}, {self.function}, {self.status})"
95
-
96
- def __repr__(self):
97
- """Detailed string representation of the task."""
98
- return (f"Task(id='{self.id}', function='{self.function}', "
99
- f"status='{self.status}', channel='{self.channel}')")