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.
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.16.dist-info}/METADATA +3 -1
- django_nativemojo-0.1.16.dist-info/RECORD +302 -0
- mojo/__init__.py +1 -1
- mojo/apps/account/management/commands/serializer_admin.py +121 -1
- 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 +294 -8
- mojo/apps/account/models/member.py +14 -1
- 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 +190 -17
- mojo/apps/account/rest/__init__.py +2 -0
- mojo/apps/account/rest/device.py +39 -0
- mojo/apps/account/rest/group.py +8 -0
- mojo/apps/account/rest/push.py +187 -0
- mojo/apps/account/rest/user.py +95 -5
- 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 +6 -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/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/backends/s3.py +209 -0
- mojo/apps/fileman/models/file.py +45 -9
- mojo/apps/fileman/models/manager.py +269 -3
- 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 +1 -0
- mojo/apps/incident/models/event.py +35 -0
- mojo/apps/incident/models/incident.py +2 -0
- mojo/apps/incident/models/ticket.py +62 -0
- mojo/apps/incident/reporter.py +21 -3
- mojo/apps/incident/rest/__init__.py +1 -0
- 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/models/log.py +3 -0
- 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 +17 -0
- mojo/decorators/http.py +40 -1
- mojo/helpers/aws/__init__.py +11 -7
- mojo/helpers/aws/inbound_email.py +309 -0
- mojo/helpers/aws/kms.py +413 -0
- mojo/helpers/aws/ses_domain.py +959 -0
- mojo/helpers/crypto/__init__.py +1 -1
- mojo/helpers/crypto/utils.py +15 -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 +8 -0
- mojo/middleware/auth.py +1 -1
- mojo/middleware/cors.py +40 -0
- mojo/middleware/logging.py +131 -12
- mojo/middleware/mojo.py +5 -0
- mojo/models/rest.py +271 -57
- mojo/models/secrets.py +86 -0
- mojo/serializers/__init__.py +16 -10
- 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/{manager.py → core/manager.py} +53 -4
- mojo/serializers/core/serializer.py +475 -0
- mojo/serializers/{advanced/formats → formats}/csv.py +116 -139
- mojo/serializers/suggested_improvements.md +388 -0
- testit/client.py +1 -1
- testit/helpers.py +14 -0
- testit/runner.py +23 -6
- django_nativemojo-0.1.15.dist-info/RECORD +0 -234
- 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/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 -44
- mojo/apps/tasks/manager.py +0 -644
- mojo/apps/tasks/rest/__init__.py +0 -2
- mojo/apps/tasks/rest/hooks.py +0 -0
- mojo/apps/tasks/rest/tasks.py +0 -76
- mojo/apps/tasks/runner.py +0 -439
- mojo/apps/tasks/task.py +0 -99
- mojo/apps/tasks/tq_handlers.py +0 -132
- mojo/helpers/crypto/__pycache__/hash.cpython-310.pyc +0 -0
- mojo/helpers/crypto/__pycache__/sign.cpython-310.pyc +0 -0
- mojo/helpers/crypto/__pycache__/utils.cpython-310.pyc +0 -0
- mojo/helpers/redis.py +0 -10
- mojo/models/meta.py +0 -262
- mojo/serializers/advanced/README.md +0 -363
- mojo/serializers/advanced/__init__.py +0 -247
- mojo/serializers/advanced/formats/__init__.py +0 -28
- mojo/serializers/advanced/formats/excel.py +0 -516
- mojo/serializers/advanced/formats/json.py +0 -239
- mojo/serializers/advanced/formats/response.py +0 -485
- mojo/serializers/advanced/serializer.py +0 -568
- mojo/serializers/optimized.py +0 -618
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.16.dist-info}/LICENSE +0 -0
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.16.dist-info}/NOTICE +0 -0
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.16.dist-info}/WHEEL +0 -0
- /mojo/apps/{notify → aws/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/handlers → docit/markdown_plugins}/__init__.py +0 -0
- /mojo/apps/{notify/management → docit/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/providers → jobs/examples}/__init__.py +0 -0
- /mojo/apps/{notify/rest → jobs/migrations}/__init__.py +0 -0
- /mojo/{serializers → rest}/openapi.py +0 -0
- /mojo/serializers/{settings_example.py → examples/settings.py} +0 -0
- /mojo/{apps/notify/handlers/ses/bounce.py → serializers/formats/__init__.py} +0 -0
- /mojo/serializers/{advanced/formats → formats}/localizers.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)
|