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.
- django_nativemojo-0.1.16.dist-info/METADATA +138 -0
- django_nativemojo-0.1.16.dist-info/RECORD +302 -0
- mojo/__init__.py +1 -1
- mojo/apps/account/management/__init__.py +5 -0
- mojo/apps/account/management/commands/__init__.py +6 -0
- mojo/apps/account/management/commands/serializer_admin.py +651 -0
- mojo/apps/account/migrations/0004_user_avatar.py +20 -0
- mojo/apps/account/migrations/0005_group_last_activity.py +18 -0
- 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 +319 -15
- mojo/apps/account/models/member.py +29 -5
- 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 +369 -19
- mojo/apps/account/rest/__init__.py +2 -0
- mojo/apps/account/rest/device.py +39 -0
- mojo/apps/account/rest/group.py +9 -0
- mojo/apps/account/rest/push.py +187 -0
- mojo/apps/account/rest/user.py +100 -6
- 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 +7 -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/s3.py +64 -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/README.md +8 -8
- mojo/apps/fileman/backends/base.py +76 -70
- mojo/apps/fileman/backends/filesystem.py +86 -86
- mojo/apps/fileman/backends/s3.py +409 -108
- mojo/apps/fileman/migrations/0001_initial.py +106 -0
- mojo/apps/fileman/migrations/0002_filemanager_parent_alter_filemanager_max_file_size.py +24 -0
- mojo/apps/fileman/migrations/0003_remove_file_fileman_fil_upload__c4bc35_idx_and_more.py +25 -0
- mojo/apps/fileman/migrations/0004_remove_file_original_filename_and_more.py +39 -0
- mojo/apps/fileman/migrations/0005_alter_file_upload_token.py +18 -0
- mojo/apps/fileman/migrations/0006_file_download_url_filemanager_forever_urls.py +23 -0
- mojo/apps/fileman/migrations/0007_remove_filemanager_forever_urls_and_more.py +22 -0
- mojo/apps/fileman/migrations/0008_file_category.py +18 -0
- mojo/apps/fileman/migrations/0009_rename_file_path_file_storage_file_path.py +18 -0
- mojo/apps/fileman/migrations/0010_filerendition.py +33 -0
- mojo/apps/fileman/migrations/0011_alter_filerendition_original_file.py +19 -0
- mojo/apps/fileman/models/__init__.py +1 -5
- mojo/apps/fileman/models/file.py +240 -58
- mojo/apps/fileman/models/manager.py +427 -31
- mojo/apps/fileman/models/rendition.py +118 -0
- mojo/apps/fileman/renderer/__init__.py +111 -0
- mojo/apps/fileman/renderer/audio.py +403 -0
- mojo/apps/fileman/renderer/base.py +205 -0
- mojo/apps/fileman/renderer/document.py +404 -0
- mojo/apps/fileman/renderer/image.py +222 -0
- mojo/apps/fileman/renderer/utils.py +297 -0
- mojo/apps/fileman/renderer/video.py +304 -0
- mojo/apps/fileman/rest/__init__.py +1 -18
- mojo/apps/fileman/rest/upload.py +22 -32
- mojo/apps/fileman/signals.py +58 -0
- mojo/apps/fileman/tasks.py +254 -0
- mojo/apps/fileman/utils/__init__.py +40 -16
- mojo/apps/incident/migrations/0005_incidenthistory.py +39 -0
- mojo/apps/incident/migrations/0006_alter_incident_state.py +18 -0
- 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 +2 -0
- mojo/apps/incident/models/event.py +35 -0
- mojo/apps/incident/models/history.py +36 -0
- mojo/apps/incident/models/incident.py +3 -1
- mojo/apps/incident/models/ticket.py +62 -0
- mojo/apps/incident/reporter.py +21 -1
- mojo/apps/incident/rest/__init__.py +1 -0
- mojo/apps/incident/rest/event.py +7 -1
- 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/migrations/0004_alter_log_level.py +18 -0
- mojo/apps/logit/models/log.py +7 -1
- 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 +19 -2
- mojo/decorators/auth.py +6 -1
- mojo/decorators/http.py +47 -3
- mojo/helpers/aws/__init__.py +45 -0
- mojo/helpers/aws/ec2.py +804 -0
- mojo/helpers/aws/iam.py +748 -0
- mojo/helpers/aws/inbound_email.py +309 -0
- mojo/helpers/aws/kms.py +413 -0
- mojo/helpers/aws/s3.py +451 -11
- mojo/helpers/aws/ses.py +483 -0
- mojo/helpers/aws/ses_domain.py +959 -0
- mojo/helpers/aws/sns.py +461 -0
- mojo/helpers/crypto/__init__.py +1 -1
- mojo/helpers/crypto/utils.py +15 -0
- mojo/helpers/dates.py +18 -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 +14 -2
- mojo/helpers/settings/__init__.py +2 -0
- mojo/helpers/{settings.py → settings/helper.py} +1 -37
- mojo/helpers/settings/parser.py +132 -0
- mojo/middleware/auth.py +1 -1
- mojo/middleware/cors.py +40 -0
- mojo/middleware/logging.py +131 -12
- mojo/middleware/mojo.py +10 -0
- mojo/models/rest.py +494 -65
- mojo/models/secrets.py +98 -3
- mojo/serializers/__init__.py +106 -0
- 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/core/manager.py +550 -0
- mojo/serializers/core/serializer.py +475 -0
- mojo/serializers/examples/settings.py +322 -0
- mojo/serializers/formats/csv.py +393 -0
- mojo/serializers/formats/localizers.py +509 -0
- mojo/serializers/{models.py → simple.py} +38 -15
- mojo/serializers/suggested_improvements.md +388 -0
- testit/client.py +1 -1
- testit/helpers.py +35 -4
- testit/runner.py +23 -6
- django_nativemojo-0.1.10.dist-info/METADATA +0 -96
- django_nativemojo-0.1.10.dist-info/RECORD +0 -194
- mojo/apps/metrics/rest/db.py +0 -0
- 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/bounce.py +0 -0
- 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 -11
- mojo/apps/tasks/manager.py +0 -489
- mojo/apps/tasks/rest/__init__.py +0 -2
- mojo/apps/tasks/rest/hooks.py +0 -0
- mojo/apps/tasks/rest/tasks.py +0 -62
- mojo/apps/tasks/runner.py +0 -174
- mojo/apps/tasks/tq_handlers.py +0 -14
- mojo/helpers/aws/setup_email.py +0 -0
- mojo/helpers/redis.py +0 -10
- mojo/models/meta.py +0 -262
- mojo/ws4redis/README.md +0 -174
- mojo/ws4redis/__init__.py +0 -2
- mojo/ws4redis/client.py +0 -283
- mojo/ws4redis/connection.py +0 -327
- mojo/ws4redis/exceptions.py +0 -32
- mojo/ws4redis/redis.py +0 -183
- mojo/ws4redis/servers/base.py +0 -86
- mojo/ws4redis/servers/django.py +0 -171
- mojo/ws4redis/servers/uwsgi.py +0 -63
- mojo/ws4redis/settings.py +0 -45
- mojo/ws4redis/utf8validator.py +0 -128
- mojo/ws4redis/websocket.py +0 -403
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/LICENSE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/NOTICE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/WHEEL +0 -0
- /mojo/apps/{notify → aws}/__init__.py +0 -0
- /mojo/apps/{notify/handlers → aws/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/management → docit/markdown_plugins}/__init__.py +0 -0
- /mojo/apps/{notify/providers → docit/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/rest → fileman/migrations}/__init__.py +0 -0
- /mojo/{ws4redis/servers → apps/jobs/examples}/__init__.py +0 -0
- /mojo/apps/{fileman/models/render.py → jobs/migrations/__init__.py} +0 -0
- /mojo/{serializers → rest}/openapi.py +0 -0
- /mojo/{apps/fileman/rest/__init__ → serializers/formats/__init__.py} +0 -0
mojo/apps/jobs/keys.py
ADDED
@@ -0,0 +1,203 @@
|
|
1
|
+
"""
|
2
|
+
Redis key builder for jobs system.
|
3
|
+
Centralized, prefix-aware key management.
|
4
|
+
"""
|
5
|
+
from typing import Optional
|
6
|
+
from django.conf import settings
|
7
|
+
|
8
|
+
|
9
|
+
class JobKeys:
|
10
|
+
"""Centralized Redis key builder for the jobs system."""
|
11
|
+
|
12
|
+
def __init__(self, prefix: Optional[str] = None):
|
13
|
+
"""
|
14
|
+
Initialize with optional prefix override.
|
15
|
+
|
16
|
+
Args:
|
17
|
+
prefix: Override the default prefix from settings
|
18
|
+
"""
|
19
|
+
self.prefix = prefix or getattr(settings, 'JOBS_REDIS_PREFIX', 'mojo:jobs')
|
20
|
+
|
21
|
+
# ----------------------------
|
22
|
+
# Streams (legacy/compat only)
|
23
|
+
# ----------------------------
|
24
|
+
def stream(self, channel: str) -> str:
|
25
|
+
"""
|
26
|
+
Get the stream key for a channel.
|
27
|
+
|
28
|
+
Args:
|
29
|
+
channel: The channel name
|
30
|
+
|
31
|
+
Returns:
|
32
|
+
Redis key for the channel's main stream
|
33
|
+
"""
|
34
|
+
return f"{self.prefix}:stream:{channel}"
|
35
|
+
|
36
|
+
def stream_broadcast(self, channel: str) -> str:
|
37
|
+
"""
|
38
|
+
Get the broadcast stream key for a channel.
|
39
|
+
|
40
|
+
Args:
|
41
|
+
channel: The channel name
|
42
|
+
|
43
|
+
Returns:
|
44
|
+
Redis key for the channel's broadcast stream
|
45
|
+
"""
|
46
|
+
return f"{self.prefix}:stream:{channel}:broadcast"
|
47
|
+
|
48
|
+
def group_workers(self, channel: str) -> str:
|
49
|
+
"""
|
50
|
+
Get the consumer group key for workers on a channel.
|
51
|
+
|
52
|
+
Args:
|
53
|
+
channel: The channel name
|
54
|
+
|
55
|
+
Returns:
|
56
|
+
Redis consumer group name for workers
|
57
|
+
"""
|
58
|
+
return f"{self.prefix}:cg:{channel}:workers"
|
59
|
+
|
60
|
+
def group_runner(self, channel: str, runner_id: str) -> str:
|
61
|
+
"""
|
62
|
+
Get the consumer group key for a specific runner on broadcast stream.
|
63
|
+
|
64
|
+
Args:
|
65
|
+
channel: The channel name
|
66
|
+
runner_id: The runner's unique identifier
|
67
|
+
|
68
|
+
Returns:
|
69
|
+
Redis consumer group name for this runner
|
70
|
+
"""
|
71
|
+
return f"{self.prefix}:cg:{channel}:runner:{runner_id}"
|
72
|
+
|
73
|
+
# ----------------------------
|
74
|
+
# Plan B: List + ZSET keys
|
75
|
+
# ----------------------------
|
76
|
+
def queue(self, channel: str) -> str:
|
77
|
+
"""
|
78
|
+
Immediate jobs list (queue).
|
79
|
+
RPUSH to enqueue, BRPOP to claim.
|
80
|
+
"""
|
81
|
+
return f"{self.prefix}:queue:{channel}"
|
82
|
+
|
83
|
+
def processing(self, channel: str) -> str:
|
84
|
+
"""
|
85
|
+
In-flight tracking ZSET for visibility timeout.
|
86
|
+
ZADD on claim, ZREM on completion.
|
87
|
+
"""
|
88
|
+
return f"{self.prefix}:processing:{channel}"
|
89
|
+
|
90
|
+
def sched(self, channel: str) -> str:
|
91
|
+
"""
|
92
|
+
Get the scheduled jobs ZSET key for a channel.
|
93
|
+
|
94
|
+
Args:
|
95
|
+
channel: The channel name
|
96
|
+
|
97
|
+
Returns:
|
98
|
+
Redis ZSET key for scheduled/delayed jobs
|
99
|
+
"""
|
100
|
+
return f"{self.prefix}:sched:{channel}"
|
101
|
+
|
102
|
+
def sched_broadcast(self, channel: str) -> str:
|
103
|
+
"""
|
104
|
+
Scheduled jobs ZSET for broadcast (optional).
|
105
|
+
"""
|
106
|
+
return f"{self.prefix}:sched_broadcast:{channel}"
|
107
|
+
|
108
|
+
def reaper_lock(self, channel: str) -> str:
|
109
|
+
"""
|
110
|
+
Per-channel lock key for the reaper (to avoid races).
|
111
|
+
"""
|
112
|
+
return f"{self.prefix}:lock:reaper:{channel}"
|
113
|
+
|
114
|
+
def channel_pause(self, channel: str) -> str:
|
115
|
+
"""
|
116
|
+
Get the pause flag key for a channel.
|
117
|
+
"""
|
118
|
+
return f"{self.prefix}:channel:{channel}:paused"
|
119
|
+
|
120
|
+
# ----------------------------
|
121
|
+
# Job metadata / control
|
122
|
+
# ----------------------------
|
123
|
+
def job(self, job_id: str) -> str:
|
124
|
+
"""
|
125
|
+
Get the hash key for a specific job's metadata.
|
126
|
+
|
127
|
+
Args:
|
128
|
+
job_id: The job's unique identifier
|
129
|
+
|
130
|
+
Returns:
|
131
|
+
Redis hash key for job metadata
|
132
|
+
"""
|
133
|
+
return f"{self.prefix}:job:{job_id}"
|
134
|
+
|
135
|
+
def runner_ctl(self, runner_id: str) -> str:
|
136
|
+
"""
|
137
|
+
Get the control channel key for a runner.
|
138
|
+
|
139
|
+
Args:
|
140
|
+
runner_id: The runner's unique identifier
|
141
|
+
|
142
|
+
Returns:
|
143
|
+
Redis key for runner control messages
|
144
|
+
"""
|
145
|
+
return f"{self.prefix}:runner:{runner_id}:ctl"
|
146
|
+
|
147
|
+
def runner_hb(self, runner_id: str) -> str:
|
148
|
+
"""
|
149
|
+
Get the heartbeat key for a runner.
|
150
|
+
|
151
|
+
Args:
|
152
|
+
runner_id: The runner's unique identifier
|
153
|
+
|
154
|
+
Returns:
|
155
|
+
Redis key for runner heartbeat (with TTL)
|
156
|
+
"""
|
157
|
+
return f"{self.prefix}:runner:{runner_id}:hb"
|
158
|
+
|
159
|
+
def scheduler_lock(self) -> str:
|
160
|
+
"""
|
161
|
+
Get the scheduler leadership lock key.
|
162
|
+
|
163
|
+
Returns:
|
164
|
+
Redis key for scheduler lock
|
165
|
+
"""
|
166
|
+
return f"{self.prefix}:lock:scheduler"
|
167
|
+
|
168
|
+
def stats_counter(self, metric: str) -> str:
|
169
|
+
"""
|
170
|
+
Get a stats counter key.
|
171
|
+
|
172
|
+
Args:
|
173
|
+
metric: The metric name (e.g., 'published', 'completed')
|
174
|
+
|
175
|
+
Returns:
|
176
|
+
Redis key for the stats counter
|
177
|
+
"""
|
178
|
+
return f"{self.prefix}:stats:{metric}"
|
179
|
+
|
180
|
+
def registry_key(self) -> str:
|
181
|
+
"""
|
182
|
+
Get the job registry hash key.
|
183
|
+
|
184
|
+
Returns:
|
185
|
+
Redis key for the job function registry
|
186
|
+
"""
|
187
|
+
return f"{self.prefix}:registry"
|
188
|
+
|
189
|
+
def idempotency(self, key: str) -> str:
|
190
|
+
"""
|
191
|
+
Get the idempotency check key.
|
192
|
+
|
193
|
+
Args:
|
194
|
+
key: The idempotency key from the client
|
195
|
+
|
196
|
+
Returns:
|
197
|
+
Redis key for idempotency checking
|
198
|
+
"""
|
199
|
+
return f"{self.prefix}:idempotent:{key}"
|
200
|
+
|
201
|
+
|
202
|
+
# Default instance for module-level use
|
203
|
+
default_keys = JobKeys()
|
@@ -0,0 +1,363 @@
|
|
1
|
+
"""
|
2
|
+
Local in-process job queue for lightweight tasks.
|
3
|
+
|
4
|
+
No persistence, no retries, no distribution - just a simple
|
5
|
+
single worker thread with a queue for ultra-short work.
|
6
|
+
"""
|
7
|
+
import queue
|
8
|
+
import threading
|
9
|
+
import traceback
|
10
|
+
import time
|
11
|
+
from typing import Any, Callable, Optional
|
12
|
+
from dataclasses import dataclass
|
13
|
+
from datetime import datetime
|
14
|
+
|
15
|
+
from django.conf import settings
|
16
|
+
from django.utils import timezone
|
17
|
+
|
18
|
+
from mojo.helpers import logit
|
19
|
+
|
20
|
+
|
21
|
+
@dataclass
|
22
|
+
class LocalJob:
|
23
|
+
"""Container for a local job."""
|
24
|
+
job_id: str
|
25
|
+
func: Callable
|
26
|
+
args: tuple
|
27
|
+
kwargs: dict
|
28
|
+
delay_seconds: float = 0.0
|
29
|
+
|
30
|
+
|
31
|
+
class LocalQueue:
|
32
|
+
"""
|
33
|
+
Simple in-process job queue with single worker thread.
|
34
|
+
|
35
|
+
For ultra-lightweight tasks that don't need persistence,
|
36
|
+
retries, or distributed execution. Uses time.sleep() for delays.
|
37
|
+
"""
|
38
|
+
|
39
|
+
def __init__(self, maxsize: Optional[int] = None):
|
40
|
+
"""
|
41
|
+
Initialize the local queue.
|
42
|
+
|
43
|
+
Args:
|
44
|
+
maxsize: Maximum queue size (default from settings or 1000)
|
45
|
+
"""
|
46
|
+
if maxsize is None:
|
47
|
+
maxsize = getattr(settings, 'JOBS_LOCAL_QUEUE_MAXSIZE', 1000)
|
48
|
+
|
49
|
+
self.queue = queue.Queue(maxsize=maxsize)
|
50
|
+
self.worker_thread = None
|
51
|
+
self.stop_event = threading.Event()
|
52
|
+
self.started = False
|
53
|
+
self._lock = threading.RLock()
|
54
|
+
self._processed_count = 0
|
55
|
+
self._error_count = 0
|
56
|
+
|
57
|
+
def start(self):
|
58
|
+
"""Start the worker thread."""
|
59
|
+
with self._lock:
|
60
|
+
if self.started:
|
61
|
+
return
|
62
|
+
|
63
|
+
self.stop_event.clear()
|
64
|
+
self.worker_thread = threading.Thread(
|
65
|
+
target=self._worker,
|
66
|
+
name="LocalJobWorker",
|
67
|
+
daemon=True # Dies with main process
|
68
|
+
)
|
69
|
+
self.worker_thread.start()
|
70
|
+
self.started = True
|
71
|
+
logit.info("Local job queue worker started")
|
72
|
+
|
73
|
+
def stop(self, timeout: float = 5.0):
|
74
|
+
"""
|
75
|
+
Stop the worker thread gracefully.
|
76
|
+
|
77
|
+
Args:
|
78
|
+
timeout: Maximum time to wait for thread to stop
|
79
|
+
"""
|
80
|
+
with self._lock:
|
81
|
+
if not self.started:
|
82
|
+
return
|
83
|
+
|
84
|
+
logit.info("Stopping local job queue worker...")
|
85
|
+
self.stop_event.set()
|
86
|
+
|
87
|
+
# Put a sentinel to unblock the worker if waiting
|
88
|
+
try:
|
89
|
+
self.queue.put_nowait(None) # Sentinel value
|
90
|
+
except queue.Full:
|
91
|
+
pass
|
92
|
+
|
93
|
+
if self.worker_thread and self.worker_thread.is_alive():
|
94
|
+
self.worker_thread.join(timeout)
|
95
|
+
if self.worker_thread.is_alive():
|
96
|
+
logit.warn("Local job worker thread did not stop cleanly")
|
97
|
+
|
98
|
+
self.started = False
|
99
|
+
logit.info(f"Local job queue stopped (processed={self._processed_count}, "
|
100
|
+
f"errors={self._error_count})")
|
101
|
+
|
102
|
+
def put(self, func: Callable, args: tuple, kwargs: dict,
|
103
|
+
job_id: str, run_at: Optional[datetime] = None) -> bool:
|
104
|
+
"""
|
105
|
+
Add a job to the queue.
|
106
|
+
|
107
|
+
Args:
|
108
|
+
func: Function to execute
|
109
|
+
args: Positional arguments
|
110
|
+
kwargs: Keyword arguments
|
111
|
+
job_id: Job identifier
|
112
|
+
run_at: When to execute the job (None for immediate)
|
113
|
+
|
114
|
+
Returns:
|
115
|
+
True if queued, False if queue is full
|
116
|
+
"""
|
117
|
+
if not self.started:
|
118
|
+
self.start()
|
119
|
+
|
120
|
+
# Calculate delay in seconds
|
121
|
+
delay_seconds = 0.0
|
122
|
+
if run_at:
|
123
|
+
now = timezone.now()
|
124
|
+
if run_at > now:
|
125
|
+
delay_seconds = (run_at - now).total_seconds()
|
126
|
+
|
127
|
+
job = LocalJob(
|
128
|
+
job_id=job_id,
|
129
|
+
func=func,
|
130
|
+
args=args,
|
131
|
+
kwargs=kwargs,
|
132
|
+
delay_seconds=delay_seconds
|
133
|
+
)
|
134
|
+
|
135
|
+
try:
|
136
|
+
self.queue.put_nowait(job)
|
137
|
+
return True
|
138
|
+
except queue.Full:
|
139
|
+
logit.warn(f"Local job queue is full, rejecting job {job_id}")
|
140
|
+
return False
|
141
|
+
|
142
|
+
def size(self) -> int:
|
143
|
+
"""Get current queue size."""
|
144
|
+
return self.queue.qsize()
|
145
|
+
|
146
|
+
def is_empty(self) -> bool:
|
147
|
+
"""Check if queue is empty."""
|
148
|
+
return self.queue.empty()
|
149
|
+
|
150
|
+
def stats(self) -> dict:
|
151
|
+
"""
|
152
|
+
Get queue statistics.
|
153
|
+
|
154
|
+
Returns:
|
155
|
+
Dict with queue stats
|
156
|
+
"""
|
157
|
+
with self._lock:
|
158
|
+
return {
|
159
|
+
'size': self.size(),
|
160
|
+
'maxsize': self.queue.maxsize,
|
161
|
+
'processed': self._processed_count,
|
162
|
+
'errors': self._error_count,
|
163
|
+
'running': self.started,
|
164
|
+
'worker_alive': self.worker_thread.is_alive() if self.worker_thread else False
|
165
|
+
}
|
166
|
+
|
167
|
+
def _worker(self):
|
168
|
+
"""
|
169
|
+
Worker thread main loop.
|
170
|
+
|
171
|
+
Continuously processes jobs from the queue until stopped.
|
172
|
+
Simple approach: get job, sleep if needed, execute, repeat.
|
173
|
+
"""
|
174
|
+
logit.info("Local job worker thread started")
|
175
|
+
|
176
|
+
while not self.stop_event.is_set():
|
177
|
+
try:
|
178
|
+
# Get job from queue with timeout
|
179
|
+
try:
|
180
|
+
job = self.queue.get(timeout=1.0)
|
181
|
+
except queue.Empty:
|
182
|
+
continue
|
183
|
+
|
184
|
+
# Check for shutdown sentinel
|
185
|
+
if job is None:
|
186
|
+
logit.debug("Worker received shutdown sentinel")
|
187
|
+
break
|
188
|
+
|
189
|
+
# Sleep if job has a delay
|
190
|
+
if job.delay_seconds > 0:
|
191
|
+
logit.debug(f"Job {job.job_id} sleeping for {job.delay_seconds:.2f}s")
|
192
|
+
|
193
|
+
# Sleep in small chunks to allow for quick shutdown
|
194
|
+
sleep_remaining = job.delay_seconds
|
195
|
+
while sleep_remaining > 0 and not self.stop_event.is_set():
|
196
|
+
sleep_time = min(0.1, sleep_remaining) # Sleep max 100ms at a time
|
197
|
+
time.sleep(sleep_time)
|
198
|
+
sleep_remaining -= sleep_time
|
199
|
+
|
200
|
+
# If we were asked to stop during sleep, break
|
201
|
+
if self.stop_event.is_set():
|
202
|
+
break
|
203
|
+
|
204
|
+
# Execute the job
|
205
|
+
self._execute_job(job)
|
206
|
+
|
207
|
+
with self._lock:
|
208
|
+
self._processed_count += 1
|
209
|
+
|
210
|
+
# Mark task as done for queue.join() if anyone uses it
|
211
|
+
self.queue.task_done()
|
212
|
+
|
213
|
+
except Exception as e:
|
214
|
+
# This should never happen (caught in _execute_job)
|
215
|
+
# but just in case...
|
216
|
+
logit.error(f"Unexpected error in local job worker: {e}")
|
217
|
+
with self._lock:
|
218
|
+
self._error_count += 1
|
219
|
+
|
220
|
+
logit.info("Local job worker thread exiting")
|
221
|
+
|
222
|
+
def _execute_job(self, job: LocalJob):
|
223
|
+
"""
|
224
|
+
Execute a single job.
|
225
|
+
|
226
|
+
Args:
|
227
|
+
job: LocalJob to execute
|
228
|
+
"""
|
229
|
+
start_time = timezone.now()
|
230
|
+
|
231
|
+
try:
|
232
|
+
# Log execution start
|
233
|
+
logit.debug(f"Executing local job {job.job_id}")
|
234
|
+
|
235
|
+
# Close old database connections before execution
|
236
|
+
from django.db import close_old_connections
|
237
|
+
close_old_connections()
|
238
|
+
|
239
|
+
# Execute the function
|
240
|
+
result = job.func(*job.args, **job.kwargs)
|
241
|
+
|
242
|
+
# Close connections after execution
|
243
|
+
close_old_connections()
|
244
|
+
|
245
|
+
# Log success
|
246
|
+
duration = (timezone.now() - start_time).total_seconds()
|
247
|
+
logit.info(f"Local job {job.job_id} completed in {duration:.2f}s")
|
248
|
+
|
249
|
+
# Emit metric
|
250
|
+
try:
|
251
|
+
from mojo.apps import metrics
|
252
|
+
metrics.record(
|
253
|
+
slug="jobs.local.completed",
|
254
|
+
when=timezone.now(),
|
255
|
+
count=1,
|
256
|
+
category="jobs"
|
257
|
+
)
|
258
|
+
metrics.record(
|
259
|
+
slug="jobs.local.duration_ms",
|
260
|
+
when=timezone.now(),
|
261
|
+
count=int(duration * 1000),
|
262
|
+
category="jobs"
|
263
|
+
)
|
264
|
+
except Exception as e:
|
265
|
+
logit.debug(f"Failed to record local job metrics: {e}")
|
266
|
+
|
267
|
+
return result
|
268
|
+
|
269
|
+
except Exception as e:
|
270
|
+
# Log error
|
271
|
+
with self._lock:
|
272
|
+
self._error_count += 1
|
273
|
+
duration = (timezone.now() - start_time).total_seconds()
|
274
|
+
|
275
|
+
error_msg = str(e)
|
276
|
+
stack = traceback.format_exc()
|
277
|
+
|
278
|
+
logit.error(f"Local job {job.job_id} failed after {duration:.2f}s: {error_msg}")
|
279
|
+
logit.debug(f"Stack trace for {job.job_id}:\n{stack}")
|
280
|
+
|
281
|
+
# Emit error metric
|
282
|
+
try:
|
283
|
+
from mojo.apps import metrics
|
284
|
+
metrics.record(
|
285
|
+
slug="jobs.local.failed",
|
286
|
+
when=timezone.now(),
|
287
|
+
count=1,
|
288
|
+
category="jobs"
|
289
|
+
)
|
290
|
+
except Exception as me:
|
291
|
+
logit.debug(f"Failed to record local job error metrics: {me}")
|
292
|
+
|
293
|
+
# Local jobs don't retry - just log and move on
|
294
|
+
return None
|
295
|
+
|
296
|
+
|
297
|
+
class LocalQueueManager:
|
298
|
+
"""
|
299
|
+
Manager for local queue singleton.
|
300
|
+
|
301
|
+
Ensures only one queue instance exists per process.
|
302
|
+
"""
|
303
|
+
|
304
|
+
def __init__(self):
|
305
|
+
self._queue = None
|
306
|
+
self._lock = threading.RLock()
|
307
|
+
|
308
|
+
def get_queue(self) -> LocalQueue:
|
309
|
+
"""
|
310
|
+
Get or create the local queue instance.
|
311
|
+
|
312
|
+
Returns:
|
313
|
+
LocalQueue instance
|
314
|
+
"""
|
315
|
+
with self._lock:
|
316
|
+
if self._queue is None:
|
317
|
+
self._queue = LocalQueue()
|
318
|
+
return self._queue
|
319
|
+
|
320
|
+
def stop_queue(self, timeout: float = 5.0):
|
321
|
+
"""
|
322
|
+
Stop the local queue if running.
|
323
|
+
|
324
|
+
Args:
|
325
|
+
timeout: Maximum time to wait for stop
|
326
|
+
"""
|
327
|
+
with self._lock:
|
328
|
+
if self._queue:
|
329
|
+
self._queue.stop(timeout)
|
330
|
+
self._queue = None
|
331
|
+
|
332
|
+
def reset(self):
|
333
|
+
"""Reset the queue (useful for testing)."""
|
334
|
+
self.stop_queue()
|
335
|
+
|
336
|
+
|
337
|
+
# Global manager instance
|
338
|
+
_manager = LocalQueueManager()
|
339
|
+
|
340
|
+
|
341
|
+
def get_local_queue() -> LocalQueue:
|
342
|
+
"""
|
343
|
+
Get the local job queue instance.
|
344
|
+
|
345
|
+
Returns:
|
346
|
+
LocalQueue singleton
|
347
|
+
"""
|
348
|
+
return _manager.get_queue()
|
349
|
+
|
350
|
+
|
351
|
+
def stop_local_queue(timeout: float = 5.0):
|
352
|
+
"""
|
353
|
+
Stop the local job queue.
|
354
|
+
|
355
|
+
Args:
|
356
|
+
timeout: Maximum time to wait
|
357
|
+
"""
|
358
|
+
_manager.stop_queue(timeout)
|
359
|
+
|
360
|
+
|
361
|
+
def reset_local_queue():
|
362
|
+
"""Reset the local queue (useful for testing)."""
|
363
|
+
_manager.reset()
|