django-nativemojo 0.1.15__py3-none-any.whl → 0.1.17__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.17.dist-info}/METADATA +3 -2
- django_nativemojo-0.1.17.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 +279 -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.17.dist-info}/LICENSE +0 -0
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/NOTICE +0 -0
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.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
mojo/apps/jobs/cli.py
ADDED
@@ -0,0 +1,616 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
CLI interface for Django-MOJO Jobs System
|
4
|
+
|
5
|
+
Simple, clean interface for managing job engine and scheduler.
|
6
|
+
|
7
|
+
Usage:
|
8
|
+
python -m mojo.apps.jobs.cli [options] [command]
|
9
|
+
|
10
|
+
Global Commands:
|
11
|
+
status Check status of all daemons
|
12
|
+
stop Stop all running daemons
|
13
|
+
# start (deprecated): Use component commands instead
|
14
|
+
|
15
|
+
Component Commands:
|
16
|
+
engine start Start just the engine as daemon
|
17
|
+
engine foreground Start just the engine in foreground
|
18
|
+
engine stop Stop just the engine
|
19
|
+
scheduler start Start just the scheduler as daemon
|
20
|
+
scheduler foreground Start just the scheduler in foreground
|
21
|
+
scheduler stop Stop just the scheduler
|
22
|
+
|
23
|
+
Examples:
|
24
|
+
# Global control
|
25
|
+
python -m mojo.apps.jobs.cli status
|
26
|
+
python -m mojo.apps.jobs.cli stop
|
27
|
+
|
28
|
+
# Component control
|
29
|
+
python -m mojo.apps.jobs.cli engine start
|
30
|
+
python -m mojo.apps.jobs.cli engine stop
|
31
|
+
python -m mojo.apps.jobs.cli scheduler foreground
|
32
|
+
python -m mojo.apps.jobs.cli scheduler stop
|
33
|
+
|
34
|
+
# Verbose output
|
35
|
+
python -m mojo.apps.jobs.cli -v status
|
36
|
+
"""
|
37
|
+
import os
|
38
|
+
import sys
|
39
|
+
import argparse
|
40
|
+
import signal
|
41
|
+
import time
|
42
|
+
import subprocess
|
43
|
+
from pathlib import Path
|
44
|
+
from typing import Optional, List
|
45
|
+
|
46
|
+
from mojo.helpers import logit
|
47
|
+
|
48
|
+
|
49
|
+
def is_engine_running():
|
50
|
+
"""Check if any job engine is currently running."""
|
51
|
+
from mojo.apps.jobs.daemon import DaemonRunner
|
52
|
+
|
53
|
+
for pid_file in Path('/tmp').glob('job-engine-*.pid'):
|
54
|
+
runner = DaemonRunner("JobEngine", lambda: None, pidfile=str(pid_file))
|
55
|
+
if runner.status():
|
56
|
+
return True
|
57
|
+
return False
|
58
|
+
|
59
|
+
|
60
|
+
def is_scheduler_running():
|
61
|
+
"""Check if any job scheduler is currently running."""
|
62
|
+
from mojo.apps.jobs.daemon import DaemonRunner
|
63
|
+
|
64
|
+
for pid_file in Path('/tmp').glob('job-scheduler-*.pid'):
|
65
|
+
runner = DaemonRunner("Scheduler", lambda: None, pidfile=str(pid_file))
|
66
|
+
if runner.status():
|
67
|
+
return True
|
68
|
+
return False
|
69
|
+
|
70
|
+
|
71
|
+
def validate_environment(verbose=False):
|
72
|
+
"""Validate that all required services are available."""
|
73
|
+
errors = []
|
74
|
+
|
75
|
+
# Check Redis connection
|
76
|
+
try:
|
77
|
+
from mojo.apps.jobs.adapters import get_adapter
|
78
|
+
redis = get_adapter()
|
79
|
+
redis.ping()
|
80
|
+
if verbose:
|
81
|
+
print("✓ Redis connection successful")
|
82
|
+
except Exception as e:
|
83
|
+
errors.append(f"Redis connection failed: {e}")
|
84
|
+
|
85
|
+
# Check database connection
|
86
|
+
try:
|
87
|
+
from django.db import connection
|
88
|
+
with connection.cursor() as cursor:
|
89
|
+
cursor.execute("SELECT 1")
|
90
|
+
if verbose:
|
91
|
+
print("✓ Database connection successful")
|
92
|
+
except Exception as e:
|
93
|
+
errors.append(f"Database connection failed: {e}")
|
94
|
+
|
95
|
+
# Check job models
|
96
|
+
try:
|
97
|
+
from mojo.apps.jobs.models import Job
|
98
|
+
Job.objects.count() # Simple query to test model access
|
99
|
+
if verbose:
|
100
|
+
print("✓ Job models accessible")
|
101
|
+
except Exception as e:
|
102
|
+
errors.append(f"Job models not accessible: {e}")
|
103
|
+
|
104
|
+
if errors:
|
105
|
+
if verbose:
|
106
|
+
print("\n❌ Environment validation failed:")
|
107
|
+
for error in errors:
|
108
|
+
print(f" • {error}")
|
109
|
+
print("\nPlease fix these issues before running the job system.")
|
110
|
+
return False
|
111
|
+
else:
|
112
|
+
if verbose:
|
113
|
+
print("✓ Environment validation passed")
|
114
|
+
return True
|
115
|
+
|
116
|
+
|
117
|
+
def setup_signal_handlers(engine=None, scheduler=None):
|
118
|
+
"""Setup signal handlers for graceful shutdown."""
|
119
|
+
def signal_handler(signum, frame):
|
120
|
+
logit.info(f"Received signal {signum}, initiating graceful shutdown...")
|
121
|
+
if engine:
|
122
|
+
engine.stop()
|
123
|
+
if scheduler:
|
124
|
+
scheduler.stop()
|
125
|
+
sys.exit(0)
|
126
|
+
|
127
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
128
|
+
signal.signal(signal.SIGINT, signal_handler)
|
129
|
+
|
130
|
+
|
131
|
+
def start_engine_daemon(verbose=False, logfile_override: Optional[str] = None):
|
132
|
+
"""Start engine as daemon process."""
|
133
|
+
if is_engine_running():
|
134
|
+
if verbose:
|
135
|
+
print("✓ Engine already running, skipping")
|
136
|
+
return True
|
137
|
+
|
138
|
+
from mojo.apps.jobs.job_engine import JobEngine
|
139
|
+
from mojo.apps.jobs.daemon import DaemonRunner
|
140
|
+
from mojo.helpers import paths
|
141
|
+
|
142
|
+
# Get channels from settings
|
143
|
+
try:
|
144
|
+
from django.conf import settings
|
145
|
+
channels = getattr(settings, 'JOBS_CHANNELS', ['default'])
|
146
|
+
if isinstance(channels, str):
|
147
|
+
channels = [channels]
|
148
|
+
except:
|
149
|
+
channels = ['default']
|
150
|
+
|
151
|
+
# Create engine
|
152
|
+
engine = JobEngine(channels=channels)
|
153
|
+
|
154
|
+
# Auto-generate pidfile
|
155
|
+
pidfile = f"/tmp/job-engine-{engine.runner_id}.pid"
|
156
|
+
|
157
|
+
# Get logfile from settings
|
158
|
+
# logfile = logfile_override if logfile_override is not None else getattr(settings, 'JOBS_ENGINE_LOGFILE', None)
|
159
|
+
logfile = paths.LOG_ROOT / 'job_engine.log'
|
160
|
+
|
161
|
+
# Setup daemon runner
|
162
|
+
runner = DaemonRunner(
|
163
|
+
name="JobEngine",
|
164
|
+
run_func=engine.start,
|
165
|
+
stop_func=engine.stop,
|
166
|
+
pidfile=pidfile,
|
167
|
+
logfile=logfile,
|
168
|
+
daemon=True
|
169
|
+
)
|
170
|
+
|
171
|
+
try:
|
172
|
+
if verbose:
|
173
|
+
print(f"🚀 Starting engine daemon (PID file: {pidfile})")
|
174
|
+
runner.start()
|
175
|
+
return True
|
176
|
+
except Exception as e:
|
177
|
+
if verbose:
|
178
|
+
print(f"❌ Failed to start engine: {e}")
|
179
|
+
return False
|
180
|
+
|
181
|
+
|
182
|
+
def start_scheduler_daemon(verbose=False):
|
183
|
+
"""Start scheduler as daemon process."""
|
184
|
+
if is_scheduler_running():
|
185
|
+
if verbose:
|
186
|
+
print("✓ Scheduler already running, skipping")
|
187
|
+
return True
|
188
|
+
|
189
|
+
from mojo.apps.jobs.scheduler import Scheduler
|
190
|
+
from mojo.apps.jobs.daemon import DaemonRunner
|
191
|
+
from mojo.helpers import paths
|
192
|
+
|
193
|
+
# Get channels from settings
|
194
|
+
try:
|
195
|
+
from django.conf import settings
|
196
|
+
channels = getattr(settings, 'JOBS_CHANNELS', ['default'])
|
197
|
+
if isinstance(channels, str):
|
198
|
+
channels = [channels]
|
199
|
+
except:
|
200
|
+
channels = ['default']
|
201
|
+
|
202
|
+
# Create scheduler
|
203
|
+
scheduler = Scheduler(channels=channels)
|
204
|
+
|
205
|
+
# Auto-generate pidfile
|
206
|
+
pidfile = f"/tmp/job-scheduler-{scheduler.scheduler_id}.pid"
|
207
|
+
|
208
|
+
# Get logfile from settings
|
209
|
+
# logfile = getattr(settings, 'JOBS_SCHEDULER_LOGFILE', None)
|
210
|
+
logfile = paths.LOG_ROOT / 'job_scheduler.log'
|
211
|
+
|
212
|
+
# Setup daemon runner
|
213
|
+
runner = DaemonRunner(
|
214
|
+
name="Scheduler",
|
215
|
+
run_func=scheduler.start,
|
216
|
+
stop_func=scheduler.stop,
|
217
|
+
pidfile=pidfile,
|
218
|
+
logfile=logfile,
|
219
|
+
daemon=True
|
220
|
+
)
|
221
|
+
|
222
|
+
try:
|
223
|
+
if verbose:
|
224
|
+
print(f"🚀 Starting scheduler daemon (PID file: {pidfile})")
|
225
|
+
runner.start()
|
226
|
+
return True
|
227
|
+
except Exception as e:
|
228
|
+
if verbose:
|
229
|
+
print(f"❌ Failed to start scheduler: {e}")
|
230
|
+
return False
|
231
|
+
|
232
|
+
|
233
|
+
def start_engine_foreground(verbose=False):
|
234
|
+
"""Start engine in foreground mode."""
|
235
|
+
from mojo.apps.jobs.job_engine import JobEngine
|
236
|
+
|
237
|
+
# Get channels from settings
|
238
|
+
try:
|
239
|
+
from django.conf import settings
|
240
|
+
channels = getattr(settings, 'JOBS_CHANNELS', ['default'])
|
241
|
+
if isinstance(channels, str):
|
242
|
+
channels = [channels]
|
243
|
+
except:
|
244
|
+
channels = ['default']
|
245
|
+
|
246
|
+
# Create engine
|
247
|
+
engine = JobEngine(channels=channels)
|
248
|
+
|
249
|
+
if verbose:
|
250
|
+
print(f"🚀 Starting engine in foreground mode")
|
251
|
+
print(f" Channels: {channels}")
|
252
|
+
print(f" Runner ID: {engine.runner_id}")
|
253
|
+
print(f" Press Ctrl+C to stop")
|
254
|
+
print()
|
255
|
+
|
256
|
+
# Setup signal handlers
|
257
|
+
setup_signal_handlers(engine)
|
258
|
+
|
259
|
+
try:
|
260
|
+
engine.start()
|
261
|
+
return True
|
262
|
+
except KeyboardInterrupt:
|
263
|
+
if verbose:
|
264
|
+
print("\n👋 Engine interrupted by user")
|
265
|
+
engine.stop()
|
266
|
+
return True
|
267
|
+
except Exception as e:
|
268
|
+
if verbose:
|
269
|
+
print(f"❌ Engine failed: {e}")
|
270
|
+
logit.error(f"Engine failed: {e}")
|
271
|
+
engine.stop()
|
272
|
+
return False
|
273
|
+
|
274
|
+
|
275
|
+
def start_scheduler_foreground(verbose=False):
|
276
|
+
"""Start scheduler in foreground mode."""
|
277
|
+
from mojo.apps.jobs.scheduler import Scheduler
|
278
|
+
|
279
|
+
# Get channels from settings
|
280
|
+
try:
|
281
|
+
from django.conf import settings
|
282
|
+
channels = getattr(settings, 'JOBS_CHANNELS', ['default'])
|
283
|
+
if isinstance(channels, str):
|
284
|
+
channels = [channels]
|
285
|
+
except:
|
286
|
+
channels = ['default']
|
287
|
+
|
288
|
+
# Create scheduler
|
289
|
+
scheduler = Scheduler(channels=channels)
|
290
|
+
|
291
|
+
if verbose:
|
292
|
+
print(f"🚀 Starting scheduler in foreground mode")
|
293
|
+
print(f" Channels: {channels}")
|
294
|
+
print(f" Scheduler ID: {scheduler.scheduler_id}")
|
295
|
+
print(f" Press Ctrl+C to stop")
|
296
|
+
print()
|
297
|
+
print("⚠️ Note: Only one scheduler should be active cluster-wide.")
|
298
|
+
print(" This instance will attempt to acquire leadership lock.")
|
299
|
+
print()
|
300
|
+
|
301
|
+
# Setup signal handlers
|
302
|
+
setup_signal_handlers(scheduler=scheduler)
|
303
|
+
|
304
|
+
try:
|
305
|
+
scheduler.start()
|
306
|
+
return True
|
307
|
+
except KeyboardInterrupt:
|
308
|
+
if verbose:
|
309
|
+
print("\n👋 Scheduler interrupted by user")
|
310
|
+
scheduler.stop()
|
311
|
+
return True
|
312
|
+
except Exception as e:
|
313
|
+
if verbose:
|
314
|
+
print(f"❌ Scheduler failed: {e}")
|
315
|
+
logit.error(f"Scheduler failed: {e}")
|
316
|
+
scheduler.stop()
|
317
|
+
return False
|
318
|
+
|
319
|
+
|
320
|
+
def status_command(verbose=False):
|
321
|
+
"""Check status of all running daemons."""
|
322
|
+
from mojo.apps.jobs.daemon import DaemonRunner
|
323
|
+
|
324
|
+
results = []
|
325
|
+
|
326
|
+
# Check for engine PIDs
|
327
|
+
for pid_file in Path('/tmp').glob('job-engine-*.pid'):
|
328
|
+
runner = DaemonRunner("JobEngine", lambda: None, pidfile=str(pid_file))
|
329
|
+
if runner.status():
|
330
|
+
results.append(f"✓ Engine running (PID file: {pid_file})")
|
331
|
+
else:
|
332
|
+
results.append(f"❌ Engine not running (stale PID file: {pid_file})")
|
333
|
+
|
334
|
+
# Check for scheduler PIDs
|
335
|
+
for pid_file in Path('/tmp').glob('job-scheduler-*.pid'):
|
336
|
+
runner = DaemonRunner("Scheduler", lambda: None, pidfile=str(pid_file))
|
337
|
+
if runner.status():
|
338
|
+
results.append(f"✓ Scheduler running (PID file: {pid_file})")
|
339
|
+
else:
|
340
|
+
results.append(f"❌ Scheduler not running (stale PID file: {pid_file})")
|
341
|
+
|
342
|
+
if results:
|
343
|
+
for result in results:
|
344
|
+
print(result)
|
345
|
+
else:
|
346
|
+
print("No job system daemons running")
|
347
|
+
|
348
|
+
return len(results) > 0
|
349
|
+
|
350
|
+
|
351
|
+
def stop_command(verbose=False):
|
352
|
+
"""Stop all running daemons."""
|
353
|
+
from mojo.apps.jobs.daemon import DaemonRunner
|
354
|
+
|
355
|
+
stopped = 0
|
356
|
+
failed = 0
|
357
|
+
|
358
|
+
# Stop all engines
|
359
|
+
for pid_file in Path('/tmp').glob('job-engine-*.pid'):
|
360
|
+
runner = DaemonRunner("JobEngine", lambda: None, pidfile=str(pid_file))
|
361
|
+
if runner.stop():
|
362
|
+
if verbose:
|
363
|
+
print(f"✓ Stopped engine (PID file: {pid_file})")
|
364
|
+
stopped += 1
|
365
|
+
else:
|
366
|
+
if verbose:
|
367
|
+
print(f"❌ Failed to stop engine (PID file: {pid_file})")
|
368
|
+
failed += 1
|
369
|
+
|
370
|
+
# Stop all schedulers
|
371
|
+
for pid_file in Path('/tmp').glob('job-scheduler-*.pid'):
|
372
|
+
runner = DaemonRunner("Scheduler", lambda: None, pidfile=str(pid_file))
|
373
|
+
if runner.stop():
|
374
|
+
if verbose:
|
375
|
+
print(f"✓ Stopped scheduler (PID file: {pid_file})")
|
376
|
+
stopped += 1
|
377
|
+
else:
|
378
|
+
if verbose:
|
379
|
+
print(f"❌ Failed to stop scheduler (PID file: {pid_file})")
|
380
|
+
failed += 1
|
381
|
+
|
382
|
+
if verbose or (stopped > 0 or failed > 0):
|
383
|
+
print(f"Stopped: {stopped}, Failed: {failed}")
|
384
|
+
|
385
|
+
return failed == 0
|
386
|
+
|
387
|
+
|
388
|
+
def stop_engine_daemon(verbose=False):
|
389
|
+
"""Stop just the engine daemon."""
|
390
|
+
from mojo.apps.jobs.daemon import DaemonRunner
|
391
|
+
|
392
|
+
stopped = 0
|
393
|
+
failed = 0
|
394
|
+
|
395
|
+
# Stop all engine instances
|
396
|
+
for pid_file in Path('/tmp').glob('job-engine-*.pid'):
|
397
|
+
runner = DaemonRunner("JobEngine", lambda: None, pidfile=str(pid_file))
|
398
|
+
if runner.stop():
|
399
|
+
if verbose:
|
400
|
+
print(f"✓ Stopped engine (PID file: {pid_file})")
|
401
|
+
stopped += 1
|
402
|
+
else:
|
403
|
+
if verbose:
|
404
|
+
print(f"❌ Failed to stop engine (PID file: {pid_file})")
|
405
|
+
failed += 1
|
406
|
+
|
407
|
+
if stopped == 0 and failed == 0:
|
408
|
+
if verbose:
|
409
|
+
print("No engine daemons found to stop")
|
410
|
+
return True
|
411
|
+
|
412
|
+
if verbose:
|
413
|
+
print(f"Engine stop: {stopped} stopped, {failed} failed")
|
414
|
+
|
415
|
+
return failed == 0
|
416
|
+
|
417
|
+
|
418
|
+
def stop_scheduler_daemon(verbose=False):
|
419
|
+
"""Stop just the scheduler daemon."""
|
420
|
+
from mojo.apps.jobs.daemon import DaemonRunner
|
421
|
+
|
422
|
+
stopped = 0
|
423
|
+
failed = 0
|
424
|
+
|
425
|
+
# Stop all scheduler instances
|
426
|
+
for pid_file in Path('/tmp').glob('job-scheduler-*.pid'):
|
427
|
+
runner = DaemonRunner("Scheduler", lambda: None, pidfile=str(pid_file))
|
428
|
+
if runner.stop():
|
429
|
+
if verbose:
|
430
|
+
print(f"✓ Stopped scheduler (PID file: {pid_file})")
|
431
|
+
stopped += 1
|
432
|
+
else:
|
433
|
+
if verbose:
|
434
|
+
print(f"❌ Failed to stop scheduler (PID file: {pid_file})")
|
435
|
+
failed += 1
|
436
|
+
|
437
|
+
if stopped == 0 and failed == 0:
|
438
|
+
if verbose:
|
439
|
+
print("No scheduler daemons found to stop")
|
440
|
+
return True
|
441
|
+
|
442
|
+
if verbose:
|
443
|
+
print(f"Scheduler stop: {stopped} stopped, {failed} failed")
|
444
|
+
|
445
|
+
return failed == 0
|
446
|
+
|
447
|
+
|
448
|
+
def start_command(verbose=False):
|
449
|
+
"""Start both engine and scheduler as daemons (separate processes)."""
|
450
|
+
if verbose:
|
451
|
+
print("🚀 Starting both engine and scheduler as daemons...")
|
452
|
+
|
453
|
+
args_common = ["-v"] if verbose else []
|
454
|
+
python = sys.executable
|
455
|
+
module = "mojo.apps.jobs.cli"
|
456
|
+
|
457
|
+
# Launch engine in separate process
|
458
|
+
engine_result = subprocess.run(
|
459
|
+
[python, "-m", module, "engine", "start"] + args_common,
|
460
|
+
stdout=None if verbose else subprocess.DEVNULL,
|
461
|
+
stderr=None if verbose else subprocess.DEVNULL,
|
462
|
+
)
|
463
|
+
|
464
|
+
# Launch scheduler in separate process
|
465
|
+
scheduler_result = subprocess.run(
|
466
|
+
[python, "-m", module, "scheduler", "start"] + args_common,
|
467
|
+
stdout=None if verbose else subprocess.DEVNULL,
|
468
|
+
stderr=None if verbose else subprocess.DEVNULL,
|
469
|
+
)
|
470
|
+
|
471
|
+
engine_success = (engine_result.returncode == 0)
|
472
|
+
scheduler_success = (scheduler_result.returncode == 0)
|
473
|
+
|
474
|
+
if engine_success and scheduler_success:
|
475
|
+
if verbose:
|
476
|
+
print("✅ Both components started successfully")
|
477
|
+
return True
|
478
|
+
else:
|
479
|
+
if verbose:
|
480
|
+
print(f"❌ Failed to start one or more components "
|
481
|
+
f"(engine_rc={engine_result.returncode}, scheduler_rc={scheduler_result.returncode})")
|
482
|
+
return False
|
483
|
+
|
484
|
+
|
485
|
+
def main(args=None):
|
486
|
+
"""Main CLI entry point."""
|
487
|
+
parser = argparse.ArgumentParser(
|
488
|
+
description="Django-MOJO Jobs System CLI",
|
489
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
490
|
+
epilog="""
|
491
|
+
Global Commands:
|
492
|
+
status Check status of all daemons
|
493
|
+
stop Stop all running daemons
|
494
|
+
start Start both engine and scheduler as daemons
|
495
|
+
|
496
|
+
Component Commands:
|
497
|
+
engine start Start just the engine as daemon
|
498
|
+
engine foreground Start just the engine in foreground
|
499
|
+
engine stop Stop just the engine
|
500
|
+
scheduler start Start just the scheduler as daemon
|
501
|
+
scheduler foreground Start just the scheduler in foreground
|
502
|
+
scheduler stop Stop just the scheduler
|
503
|
+
|
504
|
+
Examples:
|
505
|
+
%(prog)s status # Check what's running
|
506
|
+
%(prog)s start # Start everything
|
507
|
+
%(prog)s stop # Stop everything
|
508
|
+
%(prog)s engine start # Start just engine
|
509
|
+
%(prog)s engine stop # Stop just engine
|
510
|
+
%(prog)s scheduler foreground # Run scheduler in foreground
|
511
|
+
%(prog)s scheduler stop # Stop just scheduler
|
512
|
+
%(prog)s -v status # Verbose status
|
513
|
+
"""
|
514
|
+
)
|
515
|
+
|
516
|
+
# Global options
|
517
|
+
parser.add_argument(
|
518
|
+
'-v', '--verbose',
|
519
|
+
action='store_true',
|
520
|
+
help='Enable verbose output (default is quiet mode)'
|
521
|
+
)
|
522
|
+
parser.add_argument(
|
523
|
+
'--validate',
|
524
|
+
action='store_true',
|
525
|
+
help='Validate environment and exit'
|
526
|
+
)
|
527
|
+
|
528
|
+
# Positional arguments for commands
|
529
|
+
parser.add_argument(
|
530
|
+
'command',
|
531
|
+
nargs='?',
|
532
|
+
choices=['status', 'stop', 'start', 'engine', 'scheduler'],
|
533
|
+
help='Command to execute'
|
534
|
+
)
|
535
|
+
# Engine-only options
|
536
|
+
parser.add_argument(
|
537
|
+
'--logfile',
|
538
|
+
type=str,
|
539
|
+
default=None,
|
540
|
+
help='Log file path for engine daemon mode (overrides settings)'
|
541
|
+
)
|
542
|
+
parser.add_argument(
|
543
|
+
'action',
|
544
|
+
nargs='?',
|
545
|
+
choices=['start', 'foreground', 'stop'],
|
546
|
+
help='Action for component commands (engine/scheduler only)'
|
547
|
+
)
|
548
|
+
|
549
|
+
# Parse arguments
|
550
|
+
parsed_args = parser.parse_args(args)
|
551
|
+
verbose = parsed_args.verbose
|
552
|
+
|
553
|
+
# Handle validation-only mode
|
554
|
+
if parsed_args.validate:
|
555
|
+
if validate_environment(verbose=True):
|
556
|
+
print("✅ Environment is ready for job system.")
|
557
|
+
return True
|
558
|
+
else:
|
559
|
+
return False
|
560
|
+
|
561
|
+
# Validate environment
|
562
|
+
if not validate_environment(verbose=verbose):
|
563
|
+
return False
|
564
|
+
|
565
|
+
# Handle commands
|
566
|
+
command = parsed_args.command
|
567
|
+
action = parsed_args.action
|
568
|
+
|
569
|
+
if not command:
|
570
|
+
parser.print_help()
|
571
|
+
return False
|
572
|
+
|
573
|
+
try:
|
574
|
+
if command == 'status':
|
575
|
+
return status_command(verbose)
|
576
|
+
elif command == 'stop':
|
577
|
+
return stop_command(verbose)
|
578
|
+
elif command == 'start':
|
579
|
+
# Deprecated global start
|
580
|
+
if verbose:
|
581
|
+
print("⚠️ 'start' is deprecated. Use 'engine start' and 'scheduler start' instead.")
|
582
|
+
return False
|
583
|
+
elif command == 'engine':
|
584
|
+
if action == 'start':
|
585
|
+
return start_engine_daemon(verbose, logfile_override=parsed_args.logfile)
|
586
|
+
elif action == 'foreground':
|
587
|
+
return start_engine_foreground(verbose)
|
588
|
+
elif action == 'stop':
|
589
|
+
return stop_engine_daemon(verbose)
|
590
|
+
else:
|
591
|
+
print("Engine command requires 'start', 'foreground', or 'stop' action")
|
592
|
+
return False
|
593
|
+
elif command == 'scheduler':
|
594
|
+
if action == 'start':
|
595
|
+
return start_scheduler_daemon(verbose)
|
596
|
+
elif action == 'foreground':
|
597
|
+
return start_scheduler_foreground(verbose)
|
598
|
+
elif action == 'stop':
|
599
|
+
return stop_scheduler_daemon(verbose)
|
600
|
+
else:
|
601
|
+
print("Scheduler command requires 'start', 'foreground', or 'stop' action")
|
602
|
+
return False
|
603
|
+
else:
|
604
|
+
parser.print_help()
|
605
|
+
return False
|
606
|
+
|
607
|
+
except Exception as e:
|
608
|
+
if verbose:
|
609
|
+
print(f"❌ Command failed: {e}")
|
610
|
+
logit.error(f"CLI command failed: {e}")
|
611
|
+
return False
|
612
|
+
|
613
|
+
|
614
|
+
if __name__ == '__main__':
|
615
|
+
success = main()
|
616
|
+
sys.exit(0 if success else 1)
|