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
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)