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.
Files changed (276) hide show
  1. django_nativemojo-0.1.16.dist-info/METADATA +138 -0
  2. django_nativemojo-0.1.16.dist-info/RECORD +302 -0
  3. mojo/__init__.py +1 -1
  4. mojo/apps/account/management/__init__.py +5 -0
  5. mojo/apps/account/management/commands/__init__.py +6 -0
  6. mojo/apps/account/management/commands/serializer_admin.py +651 -0
  7. mojo/apps/account/migrations/0004_user_avatar.py +20 -0
  8. mojo/apps/account/migrations/0005_group_last_activity.py +18 -0
  9. mojo/apps/account/migrations/0006_add_device_tracking_models.py +72 -0
  10. mojo/apps/account/migrations/0007_delete_userdevicelocation.py +16 -0
  11. mojo/apps/account/migrations/0008_userdevicelocation.py +33 -0
  12. mojo/apps/account/migrations/0009_geolocatedip_subnet.py +18 -0
  13. mojo/apps/account/migrations/0010_group_avatar.py +20 -0
  14. mojo/apps/account/migrations/0011_user_org_registereddevice_pushconfig_and_more.py +118 -0
  15. mojo/apps/account/migrations/0012_remove_pushconfig_apns_key_file_and_more.py +21 -0
  16. mojo/apps/account/migrations/0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more.py +28 -0
  17. mojo/apps/account/migrations/0014_notificationdelivery_data_payload_and_more.py +48 -0
  18. mojo/apps/account/models/__init__.py +2 -0
  19. mojo/apps/account/models/device.py +281 -0
  20. mojo/apps/account/models/group.py +319 -15
  21. mojo/apps/account/models/member.py +29 -5
  22. mojo/apps/account/models/push/__init__.py +4 -0
  23. mojo/apps/account/models/push/config.py +112 -0
  24. mojo/apps/account/models/push/delivery.py +93 -0
  25. mojo/apps/account/models/push/device.py +66 -0
  26. mojo/apps/account/models/push/template.py +99 -0
  27. mojo/apps/account/models/user.py +369 -19
  28. mojo/apps/account/rest/__init__.py +2 -0
  29. mojo/apps/account/rest/device.py +39 -0
  30. mojo/apps/account/rest/group.py +9 -0
  31. mojo/apps/account/rest/push.py +187 -0
  32. mojo/apps/account/rest/user.py +100 -6
  33. mojo/apps/account/services/__init__.py +1 -0
  34. mojo/apps/account/services/push.py +363 -0
  35. mojo/apps/aws/migrations/0001_initial.py +206 -0
  36. mojo/apps/aws/migrations/0002_emaildomain_can_recv_emaildomain_can_send_and_more.py +28 -0
  37. mojo/apps/aws/migrations/0003_mailbox_is_domain_default_mailbox_is_system_default_and_more.py +31 -0
  38. mojo/apps/aws/migrations/0004_s3bucket.py +39 -0
  39. mojo/apps/aws/migrations/0005_alter_emaildomain_region_delete_s3bucket.py +21 -0
  40. mojo/apps/aws/models/__init__.py +19 -0
  41. mojo/apps/aws/models/email_attachment.py +99 -0
  42. mojo/apps/aws/models/email_domain.py +218 -0
  43. mojo/apps/aws/models/email_template.py +132 -0
  44. mojo/apps/aws/models/incoming_email.py +197 -0
  45. mojo/apps/aws/models/mailbox.py +288 -0
  46. mojo/apps/aws/models/sent_message.py +175 -0
  47. mojo/apps/aws/rest/__init__.py +7 -0
  48. mojo/apps/aws/rest/email.py +33 -0
  49. mojo/apps/aws/rest/email_ops.py +183 -0
  50. mojo/apps/aws/rest/messages.py +32 -0
  51. mojo/apps/aws/rest/s3.py +64 -0
  52. mojo/apps/aws/rest/send.py +101 -0
  53. mojo/apps/aws/rest/sns.py +403 -0
  54. mojo/apps/aws/rest/templates.py +19 -0
  55. mojo/apps/aws/services/__init__.py +32 -0
  56. mojo/apps/aws/services/email.py +390 -0
  57. mojo/apps/aws/services/email_ops.py +548 -0
  58. mojo/apps/docit/__init__.py +6 -0
  59. mojo/apps/docit/markdown_plugins/syntax_highlight.py +25 -0
  60. mojo/apps/docit/markdown_plugins/toc.py +12 -0
  61. mojo/apps/docit/migrations/0001_initial.py +113 -0
  62. mojo/apps/docit/migrations/0002_alter_book_modified_by_alter_page_modified_by.py +26 -0
  63. mojo/apps/docit/migrations/0003_alter_book_group.py +20 -0
  64. mojo/apps/docit/models/__init__.py +17 -0
  65. mojo/apps/docit/models/asset.py +231 -0
  66. mojo/apps/docit/models/book.py +227 -0
  67. mojo/apps/docit/models/page.py +319 -0
  68. mojo/apps/docit/models/page_revision.py +203 -0
  69. mojo/apps/docit/rest/__init__.py +10 -0
  70. mojo/apps/docit/rest/asset.py +17 -0
  71. mojo/apps/docit/rest/book.py +22 -0
  72. mojo/apps/docit/rest/page.py +22 -0
  73. mojo/apps/docit/rest/page_revision.py +17 -0
  74. mojo/apps/docit/services/__init__.py +11 -0
  75. mojo/apps/docit/services/docit.py +315 -0
  76. mojo/apps/docit/services/markdown.py +44 -0
  77. mojo/apps/fileman/README.md +8 -8
  78. mojo/apps/fileman/backends/base.py +76 -70
  79. mojo/apps/fileman/backends/filesystem.py +86 -86
  80. mojo/apps/fileman/backends/s3.py +409 -108
  81. mojo/apps/fileman/migrations/0001_initial.py +106 -0
  82. mojo/apps/fileman/migrations/0002_filemanager_parent_alter_filemanager_max_file_size.py +24 -0
  83. mojo/apps/fileman/migrations/0003_remove_file_fileman_fil_upload__c4bc35_idx_and_more.py +25 -0
  84. mojo/apps/fileman/migrations/0004_remove_file_original_filename_and_more.py +39 -0
  85. mojo/apps/fileman/migrations/0005_alter_file_upload_token.py +18 -0
  86. mojo/apps/fileman/migrations/0006_file_download_url_filemanager_forever_urls.py +23 -0
  87. mojo/apps/fileman/migrations/0007_remove_filemanager_forever_urls_and_more.py +22 -0
  88. mojo/apps/fileman/migrations/0008_file_category.py +18 -0
  89. mojo/apps/fileman/migrations/0009_rename_file_path_file_storage_file_path.py +18 -0
  90. mojo/apps/fileman/migrations/0010_filerendition.py +33 -0
  91. mojo/apps/fileman/migrations/0011_alter_filerendition_original_file.py +19 -0
  92. mojo/apps/fileman/models/__init__.py +1 -5
  93. mojo/apps/fileman/models/file.py +240 -58
  94. mojo/apps/fileman/models/manager.py +427 -31
  95. mojo/apps/fileman/models/rendition.py +118 -0
  96. mojo/apps/fileman/renderer/__init__.py +111 -0
  97. mojo/apps/fileman/renderer/audio.py +403 -0
  98. mojo/apps/fileman/renderer/base.py +205 -0
  99. mojo/apps/fileman/renderer/document.py +404 -0
  100. mojo/apps/fileman/renderer/image.py +222 -0
  101. mojo/apps/fileman/renderer/utils.py +297 -0
  102. mojo/apps/fileman/renderer/video.py +304 -0
  103. mojo/apps/fileman/rest/__init__.py +1 -18
  104. mojo/apps/fileman/rest/upload.py +22 -32
  105. mojo/apps/fileman/signals.py +58 -0
  106. mojo/apps/fileman/tasks.py +254 -0
  107. mojo/apps/fileman/utils/__init__.py +40 -16
  108. mojo/apps/incident/migrations/0005_incidenthistory.py +39 -0
  109. mojo/apps/incident/migrations/0006_alter_incident_state.py +18 -0
  110. mojo/apps/incident/migrations/0007_event_uid.py +18 -0
  111. mojo/apps/incident/migrations/0008_ticket_ticketnote.py +55 -0
  112. mojo/apps/incident/migrations/0009_incident_status.py +18 -0
  113. mojo/apps/incident/migrations/0010_event_country_code.py +18 -0
  114. mojo/apps/incident/migrations/0011_incident_country_code.py +18 -0
  115. mojo/apps/incident/migrations/0012_alter_incident_status.py +18 -0
  116. mojo/apps/incident/models/__init__.py +2 -0
  117. mojo/apps/incident/models/event.py +35 -0
  118. mojo/apps/incident/models/history.py +36 -0
  119. mojo/apps/incident/models/incident.py +3 -1
  120. mojo/apps/incident/models/ticket.py +62 -0
  121. mojo/apps/incident/reporter.py +21 -1
  122. mojo/apps/incident/rest/__init__.py +1 -0
  123. mojo/apps/incident/rest/event.py +7 -1
  124. mojo/apps/incident/rest/ticket.py +43 -0
  125. mojo/apps/jobs/__init__.py +489 -0
  126. mojo/apps/jobs/adapters.py +24 -0
  127. mojo/apps/jobs/cli.py +616 -0
  128. mojo/apps/jobs/daemon.py +370 -0
  129. mojo/apps/jobs/examples/sample_jobs.py +376 -0
  130. mojo/apps/jobs/examples/webhook_examples.py +203 -0
  131. mojo/apps/jobs/handlers/__init__.py +5 -0
  132. mojo/apps/jobs/handlers/webhook.py +317 -0
  133. mojo/apps/jobs/job_engine.py +734 -0
  134. mojo/apps/jobs/keys.py +203 -0
  135. mojo/apps/jobs/local_queue.py +363 -0
  136. mojo/apps/jobs/management/__init__.py +3 -0
  137. mojo/apps/jobs/management/commands/__init__.py +3 -0
  138. mojo/apps/jobs/manager.py +1327 -0
  139. mojo/apps/jobs/migrations/0001_initial.py +97 -0
  140. mojo/apps/jobs/migrations/0002_alter_job_max_retries_joblog.py +39 -0
  141. mojo/apps/jobs/models/__init__.py +6 -0
  142. mojo/apps/jobs/models/job.py +441 -0
  143. mojo/apps/jobs/rest/__init__.py +2 -0
  144. mojo/apps/jobs/rest/control.py +466 -0
  145. mojo/apps/jobs/rest/jobs.py +421 -0
  146. mojo/apps/jobs/scheduler.py +571 -0
  147. mojo/apps/jobs/services/__init__.py +6 -0
  148. mojo/apps/jobs/services/job_actions.py +465 -0
  149. mojo/apps/jobs/settings.py +209 -0
  150. mojo/apps/logit/migrations/0004_alter_log_level.py +18 -0
  151. mojo/apps/logit/models/log.py +7 -1
  152. mojo/apps/metrics/__init__.py +8 -1
  153. mojo/apps/metrics/redis_metrics.py +198 -0
  154. mojo/apps/metrics/rest/__init__.py +3 -0
  155. mojo/apps/metrics/rest/categories.py +266 -0
  156. mojo/apps/metrics/rest/helpers.py +48 -0
  157. mojo/apps/metrics/rest/permissions.py +99 -0
  158. mojo/apps/metrics/rest/values.py +277 -0
  159. mojo/apps/metrics/utils.py +19 -2
  160. mojo/decorators/auth.py +6 -1
  161. mojo/decorators/http.py +47 -3
  162. mojo/helpers/aws/__init__.py +45 -0
  163. mojo/helpers/aws/ec2.py +804 -0
  164. mojo/helpers/aws/iam.py +748 -0
  165. mojo/helpers/aws/inbound_email.py +309 -0
  166. mojo/helpers/aws/kms.py +413 -0
  167. mojo/helpers/aws/s3.py +451 -11
  168. mojo/helpers/aws/ses.py +483 -0
  169. mojo/helpers/aws/ses_domain.py +959 -0
  170. mojo/helpers/aws/sns.py +461 -0
  171. mojo/helpers/crypto/__init__.py +1 -1
  172. mojo/helpers/crypto/utils.py +15 -0
  173. mojo/helpers/dates.py +18 -0
  174. mojo/helpers/location/__init__.py +2 -0
  175. mojo/helpers/location/countries.py +262 -0
  176. mojo/helpers/location/geolocation.py +196 -0
  177. mojo/helpers/logit.py +37 -0
  178. mojo/helpers/redis/__init__.py +2 -0
  179. mojo/helpers/redis/adapter.py +606 -0
  180. mojo/helpers/redis/client.py +48 -0
  181. mojo/helpers/redis/pool.py +225 -0
  182. mojo/helpers/request.py +8 -0
  183. mojo/helpers/response.py +14 -2
  184. mojo/helpers/settings/__init__.py +2 -0
  185. mojo/helpers/{settings.py → settings/helper.py} +1 -37
  186. mojo/helpers/settings/parser.py +132 -0
  187. mojo/middleware/auth.py +1 -1
  188. mojo/middleware/cors.py +40 -0
  189. mojo/middleware/logging.py +131 -12
  190. mojo/middleware/mojo.py +10 -0
  191. mojo/models/rest.py +494 -65
  192. mojo/models/secrets.py +98 -3
  193. mojo/serializers/__init__.py +106 -0
  194. mojo/serializers/core/__init__.py +90 -0
  195. mojo/serializers/core/cache/__init__.py +121 -0
  196. mojo/serializers/core/cache/backends.py +518 -0
  197. mojo/serializers/core/cache/base.py +102 -0
  198. mojo/serializers/core/cache/disabled.py +181 -0
  199. mojo/serializers/core/cache/memory.py +287 -0
  200. mojo/serializers/core/cache/redis.py +533 -0
  201. mojo/serializers/core/cache/utils.py +454 -0
  202. mojo/serializers/core/manager.py +550 -0
  203. mojo/serializers/core/serializer.py +475 -0
  204. mojo/serializers/examples/settings.py +322 -0
  205. mojo/serializers/formats/csv.py +393 -0
  206. mojo/serializers/formats/localizers.py +509 -0
  207. mojo/serializers/{models.py → simple.py} +38 -15
  208. mojo/serializers/suggested_improvements.md +388 -0
  209. testit/client.py +1 -1
  210. testit/helpers.py +35 -4
  211. testit/runner.py +23 -6
  212. django_nativemojo-0.1.10.dist-info/METADATA +0 -96
  213. django_nativemojo-0.1.10.dist-info/RECORD +0 -194
  214. mojo/apps/metrics/rest/db.py +0 -0
  215. mojo/apps/notify/README.md +0 -91
  216. mojo/apps/notify/README_NOTIFICATIONS.md +0 -566
  217. mojo/apps/notify/admin.py +0 -52
  218. mojo/apps/notify/handlers/example_handlers.py +0 -516
  219. mojo/apps/notify/handlers/ses/__init__.py +0 -25
  220. mojo/apps/notify/handlers/ses/bounce.py +0 -0
  221. mojo/apps/notify/handlers/ses/complaint.py +0 -25
  222. mojo/apps/notify/handlers/ses/message.py +0 -86
  223. mojo/apps/notify/management/commands/__init__.py +0 -1
  224. mojo/apps/notify/management/commands/process_notifications.py +0 -370
  225. mojo/apps/notify/mod +0 -0
  226. mojo/apps/notify/models/__init__.py +0 -12
  227. mojo/apps/notify/models/account.py +0 -128
  228. mojo/apps/notify/models/attachment.py +0 -24
  229. mojo/apps/notify/models/bounce.py +0 -68
  230. mojo/apps/notify/models/complaint.py +0 -40
  231. mojo/apps/notify/models/inbox.py +0 -113
  232. mojo/apps/notify/models/inbox_message.py +0 -173
  233. mojo/apps/notify/models/outbox.py +0 -129
  234. mojo/apps/notify/models/outbox_message.py +0 -288
  235. mojo/apps/notify/models/template.py +0 -30
  236. mojo/apps/notify/providers/aws.py +0 -73
  237. mojo/apps/notify/rest/ses.py +0 -0
  238. mojo/apps/notify/utils/__init__.py +0 -2
  239. mojo/apps/notify/utils/notifications.py +0 -404
  240. mojo/apps/notify/utils/parsing.py +0 -202
  241. mojo/apps/notify/utils/render.py +0 -144
  242. mojo/apps/tasks/README.md +0 -118
  243. mojo/apps/tasks/__init__.py +0 -11
  244. mojo/apps/tasks/manager.py +0 -489
  245. mojo/apps/tasks/rest/__init__.py +0 -2
  246. mojo/apps/tasks/rest/hooks.py +0 -0
  247. mojo/apps/tasks/rest/tasks.py +0 -62
  248. mojo/apps/tasks/runner.py +0 -174
  249. mojo/apps/tasks/tq_handlers.py +0 -14
  250. mojo/helpers/aws/setup_email.py +0 -0
  251. mojo/helpers/redis.py +0 -10
  252. mojo/models/meta.py +0 -262
  253. mojo/ws4redis/README.md +0 -174
  254. mojo/ws4redis/__init__.py +0 -2
  255. mojo/ws4redis/client.py +0 -283
  256. mojo/ws4redis/connection.py +0 -327
  257. mojo/ws4redis/exceptions.py +0 -32
  258. mojo/ws4redis/redis.py +0 -183
  259. mojo/ws4redis/servers/base.py +0 -86
  260. mojo/ws4redis/servers/django.py +0 -171
  261. mojo/ws4redis/servers/uwsgi.py +0 -63
  262. mojo/ws4redis/settings.py +0 -45
  263. mojo/ws4redis/utf8validator.py +0 -128
  264. mojo/ws4redis/websocket.py +0 -403
  265. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/LICENSE +0 -0
  266. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/NOTICE +0 -0
  267. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/WHEEL +0 -0
  268. /mojo/apps/{notify → aws}/__init__.py +0 -0
  269. /mojo/apps/{notify/handlers → aws/migrations}/__init__.py +0 -0
  270. /mojo/apps/{notify/management → docit/markdown_plugins}/__init__.py +0 -0
  271. /mojo/apps/{notify/providers → docit/migrations}/__init__.py +0 -0
  272. /mojo/apps/{notify/rest → fileman/migrations}/__init__.py +0 -0
  273. /mojo/{ws4redis/servers → apps/jobs/examples}/__init__.py +0 -0
  274. /mojo/apps/{fileman/models/render.py → jobs/migrations/__init__.py} +0 -0
  275. /mojo/{serializers → rest}/openapi.py +0 -0
  276. /mojo/{apps/fileman/rest/__init__ → serializers/formats/__init__.py} +0 -0
@@ -0,0 +1,466 @@
1
+ from mojo import decorators as md
2
+ from mojo.helpers.response import JsonResponse
3
+ from mojo.apps.jobs.models import Job
4
+ from mojo.apps.jobs.manager import get_manager
5
+ from django.utils import timezone
6
+ from django.db.models import Q
7
+ from mojo.apps.jobs.adapters import get_adapter
8
+ from mojo.apps.jobs.keys import JobKeys
9
+
10
+ from datetime import datetime
11
+
12
+
13
+ # Get runtime configuration
14
+ @md.GET('control/config')
15
+ @md.requires_perms('manage_jobs')
16
+ def on_get_config(request):
17
+ """Get current jobs system configuration."""
18
+ from django.conf import settings
19
+
20
+ config = {
21
+ 'redis_url': getattr(settings, 'JOBS_REDIS_URL', 'redis://localhost:6379/0'),
22
+ 'redis_prefix': getattr(settings, 'JOBS_REDIS_PREFIX', 'mojo:jobs'),
23
+ 'engine': {
24
+ 'max_workers': getattr(settings, 'JOBS_ENGINE_MAX_WORKERS', 10),
25
+ 'claim_buffer': getattr(settings, 'JOBS_ENGINE_CLAIM_BUFFER', 2),
26
+ 'claim_batch': getattr(settings, 'JOBS_ENGINE_CLAIM_BATCH', 5),
27
+ 'read_timeout': getattr(settings, 'JOBS_ENGINE_READ_TIMEOUT', 100),
28
+ },
29
+ 'defaults': {
30
+ 'channel': getattr(settings, 'JOBS_DEFAULT_CHANNEL', 'default'),
31
+ 'expires_sec': getattr(settings, 'JOBS_DEFAULT_EXPIRES_SEC', 900),
32
+ 'max_retries': getattr(settings, 'JOBS_DEFAULT_MAX_RETRIES', 3),
33
+ 'backoff_base': getattr(settings, 'JOBS_DEFAULT_BACKOFF_BASE', 2.0),
34
+ 'backoff_max': getattr(settings, 'JOBS_DEFAULT_BACKOFF_MAX', 3600),
35
+ },
36
+ 'limits': {
37
+ 'payload_max_bytes': getattr(settings, 'JOBS_PAYLOAD_MAX_BYTES', 1048576),
38
+ 'stream_maxlen': getattr(settings, 'JOBS_STREAM_MAXLEN', 100000),
39
+ 'local_queue_maxsize': getattr(settings, 'JOBS_LOCAL_QUEUE_MAXSIZE', 1000),
40
+ },
41
+ 'timeouts': {
42
+ 'idle_timeout_ms': getattr(settings, 'JOBS_IDLE_TIMEOUT_MS', 60000),
43
+ 'xpending_idle_ms': getattr(settings, 'JOBS_XPENDING_IDLE_MS', 60000),
44
+ 'runner_heartbeat_sec': getattr(settings, 'JOBS_RUNNER_HEARTBEAT_SEC', 5),
45
+ 'scheduler_lock_ttl_ms': getattr(settings, 'JOBS_SCHEDULER_LOCK_TTL_MS', 5000),
46
+ },
47
+ 'channels': getattr(settings, 'JOBS_CHANNELS', ['default'])
48
+ }
49
+
50
+ return JsonResponse({
51
+ 'status': True,
52
+ 'data': config
53
+ })
54
+
55
+
56
+ # Clear stuck jobs
57
+ @md.POST('control/clear-stuck')
58
+ @md.requires_perms('manage_jobs')
59
+ @md.requires_params('channel')
60
+ def on_clear_stuck_jobs(request):
61
+ """
62
+ Clear stuck jobs from a channel using JobManager methods.
63
+
64
+ Params:
65
+ channel: Channel to clear stuck jobs from
66
+ idle_threshold_ms: Consider stuck if idle longer than this (default: 60000)
67
+ """
68
+ try:
69
+ channel = request.DATA['channel']
70
+ idle_threshold_ms = int(request.DATA.get('idle_threshold_ms', 60000))
71
+
72
+ manager = get_manager()
73
+ result = manager.clear_stuck_jobs(channel, idle_threshold_ms=idle_threshold_ms)
74
+
75
+ return JsonResponse({
76
+ 'status': True,
77
+ 'message': result.get('message', f'Cleared {result.get("cleared", 0)} stuck jobs from {channel}'),
78
+ 'data': result
79
+ })
80
+
81
+ except Exception as e:
82
+ return JsonResponse({
83
+ 'status': False,
84
+ 'error': str(e)
85
+ }, status=400)
86
+
87
+
88
+ # Add a simpler manual reclaim endpoint
89
+ @md.POST('jobs/control/manual-reclaim')
90
+ @md.requires_perms('manage_jobs')
91
+ @md.requires_params('channel')
92
+ def on_manual_reclaim_jobs(request):
93
+ """
94
+ Manually reclaim all pending jobs in a channel.
95
+ Uses the clear_stuck_jobs method from JobManager.
96
+ """
97
+ try:
98
+ channel = request.DATA['channel']
99
+
100
+ manager = get_manager()
101
+ result = manager.clear_stuck_jobs(channel, idle_threshold_ms=0) # Clear all pending jobs
102
+
103
+ return JsonResponse({
104
+ 'status': True,
105
+ 'message': result.get('message', f'Manually reclaimed {result.get("cleared", 0)} jobs from {channel}'),
106
+ 'data': result
107
+ })
108
+
109
+ except Exception as e:
110
+ return JsonResponse({
111
+ 'status': False,
112
+ 'error': str(e)
113
+ }, status=400)
114
+
115
+
116
+ # Purge old job data
117
+ @md.POST('control/purge')
118
+ @md.requires_perms('manage_jobs')
119
+ @md.requires_params('days_old')
120
+ def on_purge_old_jobs(request):
121
+ """
122
+ Purge old job data via JobManager.
123
+ """
124
+ try:
125
+ days_old = int(request.DATA['days_old'])
126
+ status_filter = request.DATA.get('status')
127
+ dry_run = bool(request.DATA.get('dry_run', False))
128
+
129
+ manager = get_manager()
130
+ result = manager.purge_old_jobs(days_old=days_old, status=status_filter, dry_run=dry_run)
131
+
132
+ if result.get('status'):
133
+ return JsonResponse({
134
+ 'status': True,
135
+ 'data': result
136
+ })
137
+ else:
138
+ return JsonResponse({
139
+ 'status': False,
140
+ 'error': result.get('error', 'Unknown error')
141
+ }, status=400)
142
+
143
+ except Exception as e:
144
+ return JsonResponse({
145
+ 'status': False,
146
+ 'error': str(e)
147
+ }, status=400)
148
+
149
+
150
+ # Reset failed jobs
151
+ @md.POST('control/reset-failed')
152
+ @md.requires_perms('manage_jobs')
153
+ def on_reset_failed_jobs(request):
154
+ """
155
+ Reset failed jobs to pending status for retry and requeue via JobManager.
156
+
157
+ Params:
158
+ channel: Optional channel filter
159
+ since: Optional datetime filter (ISO format)
160
+ limit: Maximum number to reset (default: 100)
161
+ """
162
+ try:
163
+ channel = request.DATA.get('channel')
164
+ since = request.DATA.get('since')
165
+ limit = int(request.DATA.get('limit', 100))
166
+
167
+ # Build query
168
+ query = Q(status='failed')
169
+ if channel:
170
+ query &= Q(channel=channel)
171
+ if since:
172
+ since_dt = datetime.fromisoformat(since)
173
+ query &= Q(created__gte=since_dt)
174
+
175
+ # Capture affected channels for requeue
176
+ affected_channels = list(
177
+ Job.objects.filter(query).values_list('channel', flat=True).distinct()
178
+ )
179
+
180
+ # Reset to pending in bulk (select IDs first, then update)
181
+ reset_ids = list(
182
+ Job.objects.filter(query)
183
+ .order_by('-created')
184
+ .values_list('id', flat=True)[:limit]
185
+ )
186
+ reset_count = 0
187
+ if reset_ids:
188
+ reset_count = Job.objects.filter(id__in=reset_ids).update(
189
+ status='pending',
190
+ attempt=0,
191
+ last_error='',
192
+ stack_trace='',
193
+ run_at=None
194
+ )
195
+
196
+ # Requeue using JobManager
197
+ manager = get_manager()
198
+ requeue_results = []
199
+
200
+ if channel:
201
+ requeue_results.append(manager.requeue_db_pending(channel, limit=reset_count))
202
+ else:
203
+ for ch in affected_channels:
204
+ requeue_results.append(manager.requeue_db_pending(ch, limit=None))
205
+
206
+ return JsonResponse({
207
+ 'status': True,
208
+ 'message': f'Reset {reset_count} failed jobs to pending',
209
+ 'reset_count': reset_count,
210
+ 'requeue': requeue_results
211
+ })
212
+
213
+ except Exception as e:
214
+ return JsonResponse({
215
+ 'status': False,
216
+ 'error': str(e)
217
+ }, status=400)
218
+
219
+
220
+ # Clear Redis queues
221
+ @md.POST('control/clear-queue')
222
+ @md.requires_perms('manage_jobs')
223
+ @md.requires_params('channel')
224
+ def on_clear_queue(request):
225
+ """
226
+ Clear all messages from a channel's Redis queue.
227
+ WARNING: This will delete all pending jobs!
228
+
229
+ Params:
230
+ channel: Channel to clear
231
+ confirm: Must be "yes" to confirm deletion
232
+ """
233
+ try:
234
+ channel = request.DATA['channel']
235
+ confirm = request.DATA.get('confirm')
236
+
237
+ if confirm != 'yes':
238
+ return JsonResponse({
239
+ 'status': False,
240
+ 'error': 'Must confirm with confirm="yes"'
241
+ }, status=400)
242
+
243
+ manager = get_manager()
244
+ result = manager.clear_channel(channel, cancel_db_pending=True)
245
+
246
+ return JsonResponse({
247
+ 'status': result.get('status', True),
248
+ 'message': f'Cleared queue for channel {channel}',
249
+ 'data': result
250
+ })
251
+
252
+ except Exception as e:
253
+ return JsonResponse({
254
+ 'status': False,
255
+ 'error': str(e)
256
+ }, status=400)
257
+
258
+
259
+ # Get queue sizes
260
+ @md.GET('control/queue-sizes')
261
+ @md.requires_perms('view_jobs', 'manage_jobs')
262
+ def on_get_queue_sizes(request):
263
+ """Get current queue sizes for all channels via JobManager."""
264
+ try:
265
+ manager = get_manager()
266
+ result = manager.get_queue_sizes()
267
+ if result.get('status'):
268
+ return JsonResponse({
269
+ 'status': True,
270
+ 'data': result.get('data', {})
271
+ })
272
+ else:
273
+ return JsonResponse({
274
+ 'status': False,
275
+ 'error': result.get('error', 'Unknown error')
276
+ }, status=400)
277
+ except Exception as e:
278
+ return JsonResponse({
279
+ 'status': False,
280
+ 'error': str(e)
281
+ }, status=400)
282
+
283
+
284
+ # Rebuild scheduled ZSETs from DB truth
285
+ @md.POST('control/rebuild-scheduled')
286
+ @md.requires_perms('manage_jobs')
287
+ def on_rebuild_scheduled(request):
288
+ """
289
+ Rebuild Redis scheduled ZSETs from DB pending jobs with future run_at.
290
+
291
+ Params:
292
+ channel: Optional channel to restrict rebuild
293
+ limit: Optional max number of jobs per channel
294
+ """
295
+ try:
296
+ manager = get_manager()
297
+ channel = request.DATA.get('channel')
298
+ limit = request.DATA.get('limit')
299
+ limit_val = int(limit) if limit is not None else None
300
+
301
+ result = manager.rebuild_scheduled(channel=channel, limit=limit_val)
302
+
303
+ if result.get('status', True):
304
+ return JsonResponse({
305
+ 'status': True,
306
+ 'data': result
307
+ })
308
+ else:
309
+ return JsonResponse({
310
+ 'status': False,
311
+ 'error': "; ".join(result.get('errors', [])) or 'Unknown error',
312
+ 'data': result
313
+ }, status=400)
314
+ except Exception as e:
315
+ return JsonResponse({
316
+ 'status': False,
317
+ 'error': str(e)
318
+ }, status=400)
319
+
320
+
321
+ # Cleanup consumer groups and stale consumers
322
+ @md.POST('control/cleanup-consumers')
323
+ @md.requires_perms('manage_jobs')
324
+ def on_cleanup_consumers(request):
325
+ """
326
+ Cleanup Redis Stream consumer groups and consumers.
327
+
328
+ Optional params:
329
+ channel: If provided, only clean this channel
330
+ destroy_empty_groups: If true, destroys empty groups after cleanup (default: true)
331
+ """
332
+ try:
333
+ manager = get_manager()
334
+ channel = request.DATA.get('channel')
335
+ destroy = request.DATA.get('destroy_empty_groups', True)
336
+ destroy = bool(destroy) if isinstance(destroy, bool) else str(destroy).lower() in ('1', 'true', 'yes', 'on')
337
+ result = manager.cleanup_consumer_groups(channel=channel, destroy_empty_groups=destroy)
338
+ if result.get('status', True):
339
+ return JsonResponse({
340
+ 'status': True,
341
+ 'data': result
342
+ })
343
+ else:
344
+ return JsonResponse({
345
+ 'status': False,
346
+ 'error': "; ".join(result.get('errors', [])) or 'Unknown error',
347
+ 'data': result
348
+ }, status=400)
349
+ except Exception as e:
350
+ return JsonResponse({
351
+ 'status': False,
352
+ 'error': str(e)
353
+ }, status=400)
354
+
355
+
356
+ # List discovered channels (from registered streams)
357
+ @md.GET('control/channels')
358
+ @md.requires_perms('manage_jobs', 'view_jobs')
359
+ def on_get_channels(request):
360
+ """
361
+ Discover channels by scanning Redis for registered streams.
362
+ """
363
+ try:
364
+ manager = get_manager()
365
+ channels = manager.get_registered_channels()
366
+ return JsonResponse({
367
+ 'status': True,
368
+ 'data': channels
369
+ })
370
+ except Exception as e:
371
+ return JsonResponse({
372
+ 'status': False,
373
+ 'error': str(e)
374
+ }, status=400)
375
+
376
+
377
+ # Force scheduler leadership
378
+ @md.POST('control/force-scheduler-lead')
379
+ @md.requires_perms('manage_jobs')
380
+ def on_force_scheduler_lead(request):
381
+ """
382
+ Force release scheduler lock to allow a new leader.
383
+ WARNING: Only use if scheduler is stuck!
384
+ """
385
+ try:
386
+ redis = get_adapter()
387
+ keys = JobKeys()
388
+
389
+ lock_key = keys.scheduler_lock()
390
+
391
+ # Check current lock
392
+ current = redis.get(lock_key)
393
+
394
+ if not current:
395
+ return JsonResponse({
396
+ 'status': True,
397
+ 'message': 'No scheduler lock exists',
398
+ 'previous_holder': None
399
+ })
400
+
401
+ # Delete the lock
402
+ redis.delete(lock_key)
403
+
404
+ return JsonResponse({
405
+ 'status': True,
406
+ 'message': 'Scheduler lock released',
407
+ 'previous_holder': current
408
+ })
409
+
410
+ except Exception as e:
411
+ return JsonResponse({
412
+ 'status': False,
413
+ 'error': str(e)
414
+ }, status=400)
415
+
416
+
417
+ # Test job execution
418
+ @md.POST('control/test')
419
+ @md.requires_perms('manage_jobs')
420
+ def on_test_job(request):
421
+ """
422
+ Publish a test job to verify the system is working.
423
+
424
+ Params:
425
+ channel: Channel to test (default: "default")
426
+ delay: Optional delay in seconds
427
+ """
428
+ try:
429
+ from mojo.apps.jobs import publish
430
+
431
+ channel = request.DATA.get('channel', 'default')
432
+ delay = request.DATA.get('delay')
433
+
434
+ # Define a simple test function module path
435
+ # This assumes you have a test job function available
436
+ test_func = 'mojo.apps.jobs.examples.sample_jobs.generate_report'
437
+
438
+ # Publish test job
439
+ job_id = publish(
440
+ func=test_func,
441
+ payload={
442
+ 'test': True,
443
+ 'timestamp': timezone.now().isoformat(),
444
+ 'channel': channel,
445
+ 'report_type': 'test',
446
+ 'start_date': timezone.now().date().isoformat(),
447
+ 'end_date': timezone.now().date().isoformat(),
448
+ 'format': 'pdf'
449
+ },
450
+ channel=channel,
451
+ delay=int(delay) if delay else None
452
+ )
453
+
454
+ return JsonResponse({
455
+ 'status': True,
456
+ 'message': 'Test job published',
457
+ 'job_id': job_id,
458
+ 'channel': channel,
459
+ 'delayed': bool(delay)
460
+ })
461
+
462
+ except Exception as e:
463
+ return JsonResponse({
464
+ 'status': False,
465
+ 'error': str(e)
466
+ }, status=400)