django-nativemojo 0.1.15__py3-none-any.whl → 0.1.16__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.16.dist-info}/METADATA +3 -1
- django_nativemojo-0.1.16.dist-info/RECORD +302 -0
- mojo/__init__.py +1 -1
- mojo/apps/account/management/commands/serializer_admin.py +121 -1
- mojo/apps/account/migrations/0006_add_device_tracking_models.py +72 -0
- mojo/apps/account/migrations/0007_delete_userdevicelocation.py +16 -0
- mojo/apps/account/migrations/0008_userdevicelocation.py +33 -0
- mojo/apps/account/migrations/0009_geolocatedip_subnet.py +18 -0
- mojo/apps/account/migrations/0010_group_avatar.py +20 -0
- mojo/apps/account/migrations/0011_user_org_registereddevice_pushconfig_and_more.py +118 -0
- mojo/apps/account/migrations/0012_remove_pushconfig_apns_key_file_and_more.py +21 -0
- mojo/apps/account/migrations/0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more.py +28 -0
- mojo/apps/account/migrations/0014_notificationdelivery_data_payload_and_more.py +48 -0
- mojo/apps/account/models/__init__.py +2 -0
- mojo/apps/account/models/device.py +281 -0
- mojo/apps/account/models/group.py +294 -8
- mojo/apps/account/models/member.py +14 -1
- mojo/apps/account/models/push/__init__.py +4 -0
- mojo/apps/account/models/push/config.py +112 -0
- mojo/apps/account/models/push/delivery.py +93 -0
- mojo/apps/account/models/push/device.py +66 -0
- mojo/apps/account/models/push/template.py +99 -0
- mojo/apps/account/models/user.py +190 -17
- mojo/apps/account/rest/__init__.py +2 -0
- mojo/apps/account/rest/device.py +39 -0
- mojo/apps/account/rest/group.py +8 -0
- mojo/apps/account/rest/push.py +187 -0
- mojo/apps/account/rest/user.py +95 -5
- mojo/apps/account/services/__init__.py +1 -0
- mojo/apps/account/services/push.py +363 -0
- mojo/apps/aws/migrations/0001_initial.py +206 -0
- mojo/apps/aws/migrations/0002_emaildomain_can_recv_emaildomain_can_send_and_more.py +28 -0
- mojo/apps/aws/migrations/0003_mailbox_is_domain_default_mailbox_is_system_default_and_more.py +31 -0
- mojo/apps/aws/migrations/0004_s3bucket.py +39 -0
- mojo/apps/aws/migrations/0005_alter_emaildomain_region_delete_s3bucket.py +21 -0
- mojo/apps/aws/models/__init__.py +19 -0
- mojo/apps/aws/models/email_attachment.py +99 -0
- mojo/apps/aws/models/email_domain.py +218 -0
- mojo/apps/aws/models/email_template.py +132 -0
- mojo/apps/aws/models/incoming_email.py +197 -0
- mojo/apps/aws/models/mailbox.py +288 -0
- mojo/apps/aws/models/sent_message.py +175 -0
- mojo/apps/aws/rest/__init__.py +6 -0
- mojo/apps/aws/rest/email.py +33 -0
- mojo/apps/aws/rest/email_ops.py +183 -0
- mojo/apps/aws/rest/messages.py +32 -0
- mojo/apps/aws/rest/send.py +101 -0
- mojo/apps/aws/rest/sns.py +403 -0
- mojo/apps/aws/rest/templates.py +19 -0
- mojo/apps/aws/services/__init__.py +32 -0
- mojo/apps/aws/services/email.py +390 -0
- mojo/apps/aws/services/email_ops.py +548 -0
- mojo/apps/docit/__init__.py +6 -0
- mojo/apps/docit/markdown_plugins/syntax_highlight.py +25 -0
- mojo/apps/docit/markdown_plugins/toc.py +12 -0
- mojo/apps/docit/migrations/0001_initial.py +113 -0
- mojo/apps/docit/migrations/0002_alter_book_modified_by_alter_page_modified_by.py +26 -0
- mojo/apps/docit/migrations/0003_alter_book_group.py +20 -0
- mojo/apps/docit/models/__init__.py +17 -0
- mojo/apps/docit/models/asset.py +231 -0
- mojo/apps/docit/models/book.py +227 -0
- mojo/apps/docit/models/page.py +319 -0
- mojo/apps/docit/models/page_revision.py +203 -0
- mojo/apps/docit/rest/__init__.py +10 -0
- mojo/apps/docit/rest/asset.py +17 -0
- mojo/apps/docit/rest/book.py +22 -0
- mojo/apps/docit/rest/page.py +22 -0
- mojo/apps/docit/rest/page_revision.py +17 -0
- mojo/apps/docit/services/__init__.py +11 -0
- mojo/apps/docit/services/docit.py +315 -0
- mojo/apps/docit/services/markdown.py +44 -0
- mojo/apps/fileman/backends/s3.py +209 -0
- mojo/apps/fileman/models/file.py +45 -9
- mojo/apps/fileman/models/manager.py +269 -3
- mojo/apps/incident/migrations/0007_event_uid.py +18 -0
- mojo/apps/incident/migrations/0008_ticket_ticketnote.py +55 -0
- mojo/apps/incident/migrations/0009_incident_status.py +18 -0
- mojo/apps/incident/migrations/0010_event_country_code.py +18 -0
- mojo/apps/incident/migrations/0011_incident_country_code.py +18 -0
- mojo/apps/incident/migrations/0012_alter_incident_status.py +18 -0
- mojo/apps/incident/models/__init__.py +1 -0
- mojo/apps/incident/models/event.py +35 -0
- mojo/apps/incident/models/incident.py +2 -0
- mojo/apps/incident/models/ticket.py +62 -0
- mojo/apps/incident/reporter.py +21 -3
- mojo/apps/incident/rest/__init__.py +1 -0
- mojo/apps/incident/rest/ticket.py +43 -0
- mojo/apps/jobs/__init__.py +489 -0
- mojo/apps/jobs/adapters.py +24 -0
- mojo/apps/jobs/cli.py +616 -0
- mojo/apps/jobs/daemon.py +370 -0
- mojo/apps/jobs/examples/sample_jobs.py +376 -0
- mojo/apps/jobs/examples/webhook_examples.py +203 -0
- mojo/apps/jobs/handlers/__init__.py +5 -0
- mojo/apps/jobs/handlers/webhook.py +317 -0
- mojo/apps/jobs/job_engine.py +734 -0
- mojo/apps/jobs/keys.py +203 -0
- mojo/apps/jobs/local_queue.py +363 -0
- mojo/apps/jobs/management/__init__.py +3 -0
- mojo/apps/jobs/management/commands/__init__.py +3 -0
- mojo/apps/jobs/manager.py +1327 -0
- mojo/apps/jobs/migrations/0001_initial.py +97 -0
- mojo/apps/jobs/migrations/0002_alter_job_max_retries_joblog.py +39 -0
- mojo/apps/jobs/models/__init__.py +6 -0
- mojo/apps/jobs/models/job.py +441 -0
- mojo/apps/jobs/rest/__init__.py +2 -0
- mojo/apps/jobs/rest/control.py +466 -0
- mojo/apps/jobs/rest/jobs.py +421 -0
- mojo/apps/jobs/scheduler.py +571 -0
- mojo/apps/jobs/services/__init__.py +6 -0
- mojo/apps/jobs/services/job_actions.py +465 -0
- mojo/apps/jobs/settings.py +209 -0
- mojo/apps/logit/models/log.py +3 -0
- mojo/apps/metrics/__init__.py +8 -1
- mojo/apps/metrics/redis_metrics.py +198 -0
- mojo/apps/metrics/rest/__init__.py +3 -0
- mojo/apps/metrics/rest/categories.py +266 -0
- mojo/apps/metrics/rest/helpers.py +48 -0
- mojo/apps/metrics/rest/permissions.py +99 -0
- mojo/apps/metrics/rest/values.py +277 -0
- mojo/apps/metrics/utils.py +17 -0
- mojo/decorators/http.py +40 -1
- mojo/helpers/aws/__init__.py +11 -7
- mojo/helpers/aws/inbound_email.py +309 -0
- mojo/helpers/aws/kms.py +413 -0
- mojo/helpers/aws/ses_domain.py +959 -0
- mojo/helpers/crypto/__init__.py +1 -1
- mojo/helpers/crypto/utils.py +15 -0
- mojo/helpers/location/__init__.py +2 -0
- mojo/helpers/location/countries.py +262 -0
- mojo/helpers/location/geolocation.py +196 -0
- mojo/helpers/logit.py +37 -0
- mojo/helpers/redis/__init__.py +2 -0
- mojo/helpers/redis/adapter.py +606 -0
- mojo/helpers/redis/client.py +48 -0
- mojo/helpers/redis/pool.py +225 -0
- mojo/helpers/request.py +8 -0
- mojo/helpers/response.py +8 -0
- mojo/middleware/auth.py +1 -1
- mojo/middleware/cors.py +40 -0
- mojo/middleware/logging.py +131 -12
- mojo/middleware/mojo.py +5 -0
- mojo/models/rest.py +271 -57
- mojo/models/secrets.py +86 -0
- mojo/serializers/__init__.py +16 -10
- mojo/serializers/core/__init__.py +90 -0
- mojo/serializers/core/cache/__init__.py +121 -0
- mojo/serializers/core/cache/backends.py +518 -0
- mojo/serializers/core/cache/base.py +102 -0
- mojo/serializers/core/cache/disabled.py +181 -0
- mojo/serializers/core/cache/memory.py +287 -0
- mojo/serializers/core/cache/redis.py +533 -0
- mojo/serializers/core/cache/utils.py +454 -0
- mojo/serializers/{manager.py → core/manager.py} +53 -4
- mojo/serializers/core/serializer.py +475 -0
- mojo/serializers/{advanced/formats → formats}/csv.py +116 -139
- mojo/serializers/suggested_improvements.md +388 -0
- testit/client.py +1 -1
- testit/helpers.py +14 -0
- testit/runner.py +23 -6
- django_nativemojo-0.1.15.dist-info/RECORD +0 -234
- mojo/apps/notify/README.md +0 -91
- mojo/apps/notify/README_NOTIFICATIONS.md +0 -566
- mojo/apps/notify/admin.py +0 -52
- mojo/apps/notify/handlers/example_handlers.py +0 -516
- mojo/apps/notify/handlers/ses/__init__.py +0 -25
- mojo/apps/notify/handlers/ses/complaint.py +0 -25
- mojo/apps/notify/handlers/ses/message.py +0 -86
- mojo/apps/notify/management/commands/__init__.py +0 -1
- mojo/apps/notify/management/commands/process_notifications.py +0 -370
- mojo/apps/notify/mod +0 -0
- mojo/apps/notify/models/__init__.py +0 -12
- mojo/apps/notify/models/account.py +0 -128
- mojo/apps/notify/models/attachment.py +0 -24
- mojo/apps/notify/models/bounce.py +0 -68
- mojo/apps/notify/models/complaint.py +0 -40
- mojo/apps/notify/models/inbox.py +0 -113
- mojo/apps/notify/models/inbox_message.py +0 -173
- mojo/apps/notify/models/outbox.py +0 -129
- mojo/apps/notify/models/outbox_message.py +0 -288
- mojo/apps/notify/models/template.py +0 -30
- mojo/apps/notify/providers/aws.py +0 -73
- mojo/apps/notify/rest/ses.py +0 -0
- mojo/apps/notify/utils/__init__.py +0 -2
- mojo/apps/notify/utils/notifications.py +0 -404
- mojo/apps/notify/utils/parsing.py +0 -202
- mojo/apps/notify/utils/render.py +0 -144
- mojo/apps/tasks/README.md +0 -118
- mojo/apps/tasks/__init__.py +0 -44
- mojo/apps/tasks/manager.py +0 -644
- mojo/apps/tasks/rest/__init__.py +0 -2
- mojo/apps/tasks/rest/hooks.py +0 -0
- mojo/apps/tasks/rest/tasks.py +0 -76
- mojo/apps/tasks/runner.py +0 -439
- mojo/apps/tasks/task.py +0 -99
- mojo/apps/tasks/tq_handlers.py +0 -132
- mojo/helpers/crypto/__pycache__/hash.cpython-310.pyc +0 -0
- mojo/helpers/crypto/__pycache__/sign.cpython-310.pyc +0 -0
- mojo/helpers/crypto/__pycache__/utils.cpython-310.pyc +0 -0
- mojo/helpers/redis.py +0 -10
- mojo/models/meta.py +0 -262
- mojo/serializers/advanced/README.md +0 -363
- mojo/serializers/advanced/__init__.py +0 -247
- mojo/serializers/advanced/formats/__init__.py +0 -28
- mojo/serializers/advanced/formats/excel.py +0 -516
- mojo/serializers/advanced/formats/json.py +0 -239
- mojo/serializers/advanced/formats/response.py +0 -485
- mojo/serializers/advanced/serializer.py +0 -568
- mojo/serializers/optimized.py +0 -618
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.16.dist-info}/LICENSE +0 -0
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.16.dist-info}/NOTICE +0 -0
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.16.dist-info}/WHEEL +0 -0
- /mojo/apps/{notify → aws/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/handlers → docit/markdown_plugins}/__init__.py +0 -0
- /mojo/apps/{notify/management → docit/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/providers → jobs/examples}/__init__.py +0 -0
- /mojo/apps/{notify/rest → jobs/migrations}/__init__.py +0 -0
- /mojo/{serializers → rest}/openapi.py +0 -0
- /mojo/serializers/{settings_example.py → examples/settings.py} +0 -0
- /mojo/{apps/notify/handlers/ses/bounce.py → serializers/formats/__init__.py} +0 -0
- /mojo/serializers/{advanced/formats → formats}/localizers.py +0 -0
@@ -0,0 +1,376 @@
|
|
1
|
+
"""
|
2
|
+
Example job functions showing the new Django-MOJO Jobs pattern.
|
3
|
+
|
4
|
+
No decorators or registration required - just plain functions that accept a Job model.
|
5
|
+
"""
|
6
|
+
from datetime import datetime, timezone
|
7
|
+
import time
|
8
|
+
import requests
|
9
|
+
from typing import Optional
|
10
|
+
from mojo.apps.jobs.models import Job
|
11
|
+
from turtledemo.chaos import f
|
12
|
+
|
13
|
+
|
14
|
+
def send_email(job: Job) -> str:
|
15
|
+
"""
|
16
|
+
Send email to recipients.
|
17
|
+
|
18
|
+
Expected payload:
|
19
|
+
recipients: List of email addresses
|
20
|
+
subject: Email subject
|
21
|
+
body: Email body
|
22
|
+
template: Optional template name
|
23
|
+
"""
|
24
|
+
recipients = job.payload.get('recipients', [])
|
25
|
+
subject = job.payload.get('subject', 'No Subject')
|
26
|
+
body = job.payload.get('body', '')
|
27
|
+
template = job.payload.get('template')
|
28
|
+
|
29
|
+
# Check for cancellation
|
30
|
+
if job.cancel_requested:
|
31
|
+
job.metadata['cancelled'] = True
|
32
|
+
job.metadata['cancelled_at'] = datetime.now(timezone.utc).isoformat()
|
33
|
+
return "cancelled"
|
34
|
+
|
35
|
+
sent_count = 0
|
36
|
+
failed_recipients = []
|
37
|
+
|
38
|
+
for recipient in recipients:
|
39
|
+
try:
|
40
|
+
job.add_log(f"sent to {recipient} successfully")
|
41
|
+
# Your actual email sending logic here
|
42
|
+
# send_mail(recipient, subject, body, template)
|
43
|
+
print(f"Sending email to {recipient}")
|
44
|
+
sent_count += 1
|
45
|
+
time.sleep(2)
|
46
|
+
|
47
|
+
# Check cancellation periodically for long lists
|
48
|
+
if sent_count % 10 == 0 and job.cancel_requested:
|
49
|
+
job.metadata['cancelled_at_recipient'] = sent_count
|
50
|
+
break
|
51
|
+
|
52
|
+
except Exception as e:
|
53
|
+
failed_recipients.append({'email': recipient, 'error': str(e)})
|
54
|
+
|
55
|
+
# Update metadata with results
|
56
|
+
job.metadata['sent_count'] = sent_count
|
57
|
+
job.metadata['failed_count'] = len(failed_recipients)
|
58
|
+
if failed_recipients:
|
59
|
+
job.metadata['failed_recipients'] = failed_recipients[:10] # Keep first 10 failures
|
60
|
+
job.metadata['completed_at'] = datetime.now(timezone.utc).isoformat()
|
61
|
+
|
62
|
+
return "completed"
|
63
|
+
|
64
|
+
|
65
|
+
def simulate_long_job(job: Job) -> str:
|
66
|
+
"""
|
67
|
+
Simulate a long-running job.
|
68
|
+
|
69
|
+
Expected payload:
|
70
|
+
duration: Duration in seconds
|
71
|
+
"""
|
72
|
+
duration = job.payload.get('duration', 10)
|
73
|
+
|
74
|
+
# Simulate long-running task
|
75
|
+
time.sleep(duration)
|
76
|
+
|
77
|
+
job.add_log("Job completed")
|
78
|
+
|
79
|
+
def process_file_upload(job: Job) -> str:
|
80
|
+
"""
|
81
|
+
Process an uploaded file in chunks.
|
82
|
+
|
83
|
+
Expected payload:
|
84
|
+
file_path: Path to uploaded file
|
85
|
+
processing_type: Type of processing to perform
|
86
|
+
options: Processing options dict
|
87
|
+
"""
|
88
|
+
file_path = job.payload['file_path']
|
89
|
+
processing_type = job.payload.get('processing_type', 'default')
|
90
|
+
options = job.payload.get('options', {})
|
91
|
+
|
92
|
+
# Initialize processing
|
93
|
+
job.metadata['started_at'] = datetime.now(timezone.utc).isoformat()
|
94
|
+
job.metadata['file_path'] = file_path
|
95
|
+
job.metadata['processing_type'] = processing_type
|
96
|
+
|
97
|
+
try:
|
98
|
+
# Simulate file processing
|
99
|
+
total_size = 1000 # In real code: os.path.getsize(file_path)
|
100
|
+
chunk_size = 100
|
101
|
+
processed = 0
|
102
|
+
|
103
|
+
while processed < total_size:
|
104
|
+
# Check for cancellation
|
105
|
+
if job.cancel_requested:
|
106
|
+
job.metadata['cancelled'] = True
|
107
|
+
job.metadata['processed_bytes'] = processed
|
108
|
+
job.metadata['cancelled_at'] = datetime.now(timezone.utc).isoformat()
|
109
|
+
return "cancelled"
|
110
|
+
|
111
|
+
# Process chunk (simulate work)
|
112
|
+
time.sleep(0.3) # Simulate processing time
|
113
|
+
processed += chunk_size
|
114
|
+
|
115
|
+
# Update progress
|
116
|
+
progress = min(100, (processed / total_size) * 100)
|
117
|
+
job.metadata['progress'] = f"{progress:.1f}%"
|
118
|
+
job.metadata['processed_bytes'] = processed
|
119
|
+
|
120
|
+
# Save progress periodically (optional - has DB overhead)
|
121
|
+
if processed % 500 == 0:
|
122
|
+
job.save(update_fields=['metadata'])
|
123
|
+
|
124
|
+
job.metadata['completed_at'] = datetime.now(timezone.utc).isoformat()
|
125
|
+
job.metadata['total_processed'] = processed
|
126
|
+
return "completed"
|
127
|
+
|
128
|
+
except Exception as e:
|
129
|
+
job.metadata['error'] = str(e)
|
130
|
+
job.metadata['failed_at'] = datetime.now(timezone.utc).isoformat()
|
131
|
+
job.log(f"Error processing job: {e}")
|
132
|
+
raise # Re-raise to trigger retry logic
|
133
|
+
|
134
|
+
|
135
|
+
def fetch_external_api(job: Job) -> str:
|
136
|
+
"""
|
137
|
+
Fetch data from an external API with retry logic.
|
138
|
+
|
139
|
+
Expected payload:
|
140
|
+
url: API endpoint URL
|
141
|
+
method: HTTP method (GET, POST, etc.)
|
142
|
+
headers: Optional headers dict
|
143
|
+
data: Optional request data
|
144
|
+
timeout: Request timeout in seconds
|
145
|
+
"""
|
146
|
+
url = job.payload['url']
|
147
|
+
method = job.payload.get('method', 'GET')
|
148
|
+
headers = job.payload.get('headers', {})
|
149
|
+
data = job.payload.get('data')
|
150
|
+
timeout = job.payload.get('timeout', 20)
|
151
|
+
|
152
|
+
job.metadata['request_started'] = datetime.now(timezone.utc).isoformat()
|
153
|
+
job.metadata['attempt'] = job.attempt
|
154
|
+
|
155
|
+
try:
|
156
|
+
response = requests.request(
|
157
|
+
method=method,
|
158
|
+
url=url,
|
159
|
+
headers=headers,
|
160
|
+
json=data if data else None,
|
161
|
+
timeout=timeout
|
162
|
+
)
|
163
|
+
|
164
|
+
# Check response
|
165
|
+
response.raise_for_status()
|
166
|
+
|
167
|
+
# Store response metadata
|
168
|
+
job.metadata['status_code'] = response.status_code
|
169
|
+
job.metadata['response_size'] = len(response.content)
|
170
|
+
job.metadata['response_headers'] = dict(response.headers)
|
171
|
+
job.metadata['completed_at'] = datetime.now(timezone.utc).isoformat()
|
172
|
+
|
173
|
+
# If response is JSON, store a sample
|
174
|
+
try:
|
175
|
+
response_data = response.json()
|
176
|
+
if isinstance(response_data, dict):
|
177
|
+
job.metadata['response_sample'] = {k: v for k, v in list(response_data.items())[:5]}
|
178
|
+
elif isinstance(response_data, list):
|
179
|
+
job.metadata['response_count'] = len(response_data)
|
180
|
+
except:
|
181
|
+
job.add_log("not a valid JSON response")
|
182
|
+
|
183
|
+
return "success"
|
184
|
+
|
185
|
+
except requests.exceptions.Timeout:
|
186
|
+
job.metadata['error'] = 'Request timed out'
|
187
|
+
job.metadata['timeout_seconds'] = timeout
|
188
|
+
raise # Will retry based on job.max_retries
|
189
|
+
|
190
|
+
except requests.exceptions.HTTPError as e:
|
191
|
+
job.metadata['error'] = f"HTTP {e.response.status_code}: {e.response.reason}"
|
192
|
+
job.metadata['status_code'] = e.response.status_code
|
193
|
+
|
194
|
+
# Only retry on specific status codes
|
195
|
+
if e.response.status_code in [408, 429, 502, 503, 504]:
|
196
|
+
raise # Will retry
|
197
|
+
else:
|
198
|
+
return "failed" # Don't retry client errors
|
199
|
+
|
200
|
+
except Exception as e:
|
201
|
+
job.metadata['error'] = str(e)
|
202
|
+
raise # Will retry
|
203
|
+
|
204
|
+
|
205
|
+
def cleanup_old_records(job: Job) -> str:
|
206
|
+
"""
|
207
|
+
Clean up old database records in batches.
|
208
|
+
|
209
|
+
Expected payload:
|
210
|
+
model_name: Name of model to clean
|
211
|
+
days_old: Delete records older than this many days
|
212
|
+
batch_size: Number of records to delete per batch
|
213
|
+
dry_run: If True, don't actually delete
|
214
|
+
"""
|
215
|
+
model_name = job.payload['model_name']
|
216
|
+
days_old = job.payload.get('days_old', 30)
|
217
|
+
batch_size = job.payload.get('batch_size', 100)
|
218
|
+
dry_run = job.payload.get('dry_run', False)
|
219
|
+
|
220
|
+
from django.utils import timezone
|
221
|
+
from datetime import timedelta
|
222
|
+
|
223
|
+
cutoff_date = timezone.now() - timedelta(days=days_old)
|
224
|
+
|
225
|
+
job.metadata['started_at'] = datetime.now(timezone.utc).isoformat()
|
226
|
+
job.metadata['cutoff_date'] = cutoff_date.isoformat()
|
227
|
+
job.metadata['dry_run'] = dry_run
|
228
|
+
|
229
|
+
deleted_count = 0
|
230
|
+
batch_count = 0
|
231
|
+
|
232
|
+
# This is a simplified example - in real code you'd import the actual model
|
233
|
+
# from myapp.models import MyModel
|
234
|
+
# queryset = MyModel.objects.filter(created__lt=cutoff_date)
|
235
|
+
|
236
|
+
while True:
|
237
|
+
# Check for cancellation
|
238
|
+
if job.check_cancel_requested():
|
239
|
+
job.metadata['cancelled'] = True
|
240
|
+
job.metadata['deleted_count'] = deleted_count
|
241
|
+
return "cancelled"
|
242
|
+
|
243
|
+
# Simulate batch deletion
|
244
|
+
# batch = queryset[:batch_size]
|
245
|
+
# if not batch.exists():
|
246
|
+
# break
|
247
|
+
|
248
|
+
# Simulate work
|
249
|
+
time.sleep(0.5)
|
250
|
+
batch_count += 1
|
251
|
+
|
252
|
+
if dry_run:
|
253
|
+
# Count but don't delete
|
254
|
+
# deleted_count += batch.count()
|
255
|
+
deleted_count += batch_size
|
256
|
+
else:
|
257
|
+
# Actually delete
|
258
|
+
# deleted_count += batch.delete()[0]
|
259
|
+
deleted_count += batch_size
|
260
|
+
|
261
|
+
# Update progress
|
262
|
+
job.metadata['deleted_count'] = deleted_count
|
263
|
+
job.metadata['batch_count'] = batch_count
|
264
|
+
|
265
|
+
# Stop after a few batches for this example
|
266
|
+
if batch_count >= 5:
|
267
|
+
break
|
268
|
+
|
269
|
+
job.metadata['completed_at'] = datetime.now(timezone.utc).isoformat()
|
270
|
+
job.metadata['total_deleted'] = deleted_count
|
271
|
+
|
272
|
+
return "completed"
|
273
|
+
|
274
|
+
|
275
|
+
def generate_report(job: Job) -> str:
|
276
|
+
"""
|
277
|
+
Generate a report with progress updates.
|
278
|
+
|
279
|
+
Expected payload:
|
280
|
+
report_type: Type of report to generate
|
281
|
+
start_date: Report start date
|
282
|
+
end_date: Report end date
|
283
|
+
format: Output format (pdf, csv, excel)
|
284
|
+
email_to: Optional email to send report to
|
285
|
+
"""
|
286
|
+
report_type = job.payload['report_type']
|
287
|
+
start_date = job.payload['start_date']
|
288
|
+
end_date = job.payload['end_date']
|
289
|
+
output_format = job.payload.get('format', 'pdf')
|
290
|
+
email_to = job.payload.get('email_to')
|
291
|
+
|
292
|
+
job.metadata['report_type'] = report_type
|
293
|
+
job.metadata['date_range'] = f"{start_date} to {end_date}"
|
294
|
+
|
295
|
+
# Simulate report generation steps
|
296
|
+
steps = [
|
297
|
+
'Fetching data',
|
298
|
+
'Processing records',
|
299
|
+
'Calculating metrics',
|
300
|
+
'Generating charts',
|
301
|
+
'Creating output file'
|
302
|
+
]
|
303
|
+
|
304
|
+
for i, step in enumerate(steps):
|
305
|
+
# Check cancellation
|
306
|
+
if job.check_cancel_requested():
|
307
|
+
job.metadata['cancelled_at_step'] = step
|
308
|
+
return "cancelled"
|
309
|
+
|
310
|
+
job.metadata['current_step'] = step
|
311
|
+
job.metadata['progress'] = f"{((i + 1) / len(steps)) * 100:.0f}%"
|
312
|
+
|
313
|
+
# Save progress (optional)
|
314
|
+
job.save(update_fields=['metadata'])
|
315
|
+
|
316
|
+
# Simulate work
|
317
|
+
time.sleep(1)
|
318
|
+
|
319
|
+
# Generate report file
|
320
|
+
report_file = f"/tmp/report_{job.id}.{output_format}"
|
321
|
+
job.metadata['report_file'] = report_file
|
322
|
+
|
323
|
+
# Send email if requested
|
324
|
+
if email_to:
|
325
|
+
# send_report_email(email_to, report_file)
|
326
|
+
job.metadata['email_sent_to'] = email_to
|
327
|
+
|
328
|
+
job.metadata['completed_at'] = datetime.now(timezone.utc).isoformat()
|
329
|
+
|
330
|
+
return "completed"
|
331
|
+
|
332
|
+
|
333
|
+
# Publishing examples (would be in your application code, not here):
|
334
|
+
"""
|
335
|
+
from mojo.apps.jobs import publish
|
336
|
+
|
337
|
+
# Publish by module path (no import needed)
|
338
|
+
job_id = publish(
|
339
|
+
"mojo.apps.jobs.examples.sample_jobs.send_email",
|
340
|
+
payload={
|
341
|
+
'recipients': ['user1@example.com', 'user2@example.com'],
|
342
|
+
'subject': 'Newsletter',
|
343
|
+
'body': 'Hello from our newsletter!'
|
344
|
+
},
|
345
|
+
channel='emails',
|
346
|
+
max_retries=3
|
347
|
+
)
|
348
|
+
|
349
|
+
# Or if you have the function imported
|
350
|
+
from mojo.apps.jobs.examples.sample_jobs import process_file_upload
|
351
|
+
|
352
|
+
job_id = publish(
|
353
|
+
process_file_upload, # Callable - will extract module path
|
354
|
+
payload={
|
355
|
+
'file_path': '/uploads/data.csv',
|
356
|
+
'processing_type': 'import',
|
357
|
+
'options': {'skip_duplicates': True}
|
358
|
+
},
|
359
|
+
channel='uploads'
|
360
|
+
)
|
361
|
+
|
362
|
+
# Schedule a cleanup job for later
|
363
|
+
from datetime import datetime, timedelta
|
364
|
+
|
365
|
+
job_id = publish(
|
366
|
+
"mojo.apps.jobs.examples.sample_jobs.cleanup_old_records",
|
367
|
+
payload={
|
368
|
+
'model_name': 'LogEntry',
|
369
|
+
'days_old': 90,
|
370
|
+
'batch_size': 500,
|
371
|
+
'dry_run': False
|
372
|
+
},
|
373
|
+
channel='maintenance',
|
374
|
+
run_at=datetime.now() + timedelta(hours=2) # Run in 2 hours
|
375
|
+
)
|
376
|
+
"""
|
@@ -0,0 +1,203 @@
|
|
1
|
+
"""
|
2
|
+
Webhook Examples for Django-MOJO Jobs System
|
3
|
+
|
4
|
+
Examples showing how to use the new publish_webhook() function for
|
5
|
+
sending HTTP POST webhooks with proper retry logic and error handling.
|
6
|
+
"""
|
7
|
+
from datetime import datetime, timedelta
|
8
|
+
from mojo.apps.jobs import publish_webhook
|
9
|
+
|
10
|
+
|
11
|
+
def example_basic_webhook():
|
12
|
+
"""Basic webhook example - send user signup data to external API."""
|
13
|
+
|
14
|
+
job_id = publish_webhook(
|
15
|
+
url="https://api.example.com/webhooks/user-signup",
|
16
|
+
data={
|
17
|
+
"user_id": 123,
|
18
|
+
"email": "user@example.com",
|
19
|
+
"event": "signup",
|
20
|
+
"timestamp": datetime.now().isoformat()
|
21
|
+
}
|
22
|
+
)
|
23
|
+
|
24
|
+
print(f"Webhook job {job_id} queued to webhooks channel")
|
25
|
+
return job_id
|
26
|
+
|
27
|
+
|
28
|
+
def example_webhook_with_auth():
|
29
|
+
"""Webhook with authentication headers."""
|
30
|
+
|
31
|
+
job_id = publish_webhook(
|
32
|
+
url="https://secure-api.example.com/webhooks/payment",
|
33
|
+
data={
|
34
|
+
"payment_id": "pay_123456",
|
35
|
+
"amount": 29.99,
|
36
|
+
"currency": "USD",
|
37
|
+
"status": "completed",
|
38
|
+
"customer_id": "cust_789"
|
39
|
+
},
|
40
|
+
headers={
|
41
|
+
"Authorization": "Bearer sk_live_abc123...",
|
42
|
+
"X-API-Version": "2023-01-01",
|
43
|
+
"X-Idempotency-Key": "payment_123456_completed"
|
44
|
+
},
|
45
|
+
webhook_id="payment_notification",
|
46
|
+
max_retries=3
|
47
|
+
)
|
48
|
+
|
49
|
+
print(f"Secure webhook job {job_id} queued")
|
50
|
+
return job_id
|
51
|
+
|
52
|
+
|
53
|
+
def example_delayed_webhook():
|
54
|
+
"""Webhook scheduled for future delivery."""
|
55
|
+
|
56
|
+
# Send reminder webhook 1 hour from now
|
57
|
+
job_id = publish_webhook(
|
58
|
+
url="https://notifications.example.com/webhooks/reminder",
|
59
|
+
data={
|
60
|
+
"user_id": 456,
|
61
|
+
"reminder_type": "trial_ending",
|
62
|
+
"trial_ends_at": (datetime.now() + timedelta(days=1)).isoformat(),
|
63
|
+
"message": "Your free trial ends tomorrow!"
|
64
|
+
},
|
65
|
+
delay=3600, # 1 hour delay
|
66
|
+
expires_in=86400, # Expire after 24 hours
|
67
|
+
webhook_id="trial_reminder_456"
|
68
|
+
)
|
69
|
+
|
70
|
+
print(f"Delayed webhook job {job_id} scheduled for 1 hour from now")
|
71
|
+
return job_id
|
72
|
+
|
73
|
+
|
74
|
+
def example_webhook_with_custom_retry():
|
75
|
+
"""Webhook with custom retry configuration for critical notifications."""
|
76
|
+
|
77
|
+
job_id = publish_webhook(
|
78
|
+
url="https://critical-alerts.example.com/webhooks/system-alert",
|
79
|
+
data={
|
80
|
+
"alert_id": "alert_789",
|
81
|
+
"severity": "critical",
|
82
|
+
"service": "payment_processor",
|
83
|
+
"message": "Payment processor is experiencing issues",
|
84
|
+
"timestamp": datetime.now().isoformat(),
|
85
|
+
"affected_users": 1250
|
86
|
+
},
|
87
|
+
headers={
|
88
|
+
"X-Alert-Priority": "critical",
|
89
|
+
"Content-Type": "application/json"
|
90
|
+
},
|
91
|
+
max_retries=10, # Retry up to 10 times for critical alerts
|
92
|
+
backoff_base=1.5, # Slower backoff (1.5^attempt)
|
93
|
+
backoff_max=3600, # Max 1 hour between retries
|
94
|
+
timeout=60, # Longer timeout for critical notifications
|
95
|
+
webhook_id="critical_alert_789"
|
96
|
+
)
|
97
|
+
|
98
|
+
print(f"Critical alert webhook job {job_id} queued with aggressive retry policy")
|
99
|
+
return job_id
|
100
|
+
|
101
|
+
|
102
|
+
def example_webhook_batch():
|
103
|
+
"""Send multiple webhooks for batch processing."""
|
104
|
+
|
105
|
+
job_ids = []
|
106
|
+
|
107
|
+
# Send order confirmations for multiple orders
|
108
|
+
orders = [
|
109
|
+
{"order_id": "ord_001", "customer": "Alice", "total": 45.99},
|
110
|
+
{"order_id": "ord_002", "customer": "Bob", "total": 123.45},
|
111
|
+
{"order_id": "ord_003", "customer": "Carol", "total": 67.89}
|
112
|
+
]
|
113
|
+
|
114
|
+
for order in orders:
|
115
|
+
job_id = publish_webhook(
|
116
|
+
url="https://fulfillment.example.com/webhooks/new-order",
|
117
|
+
data={
|
118
|
+
"event": "order_created",
|
119
|
+
"order": order,
|
120
|
+
"timestamp": datetime.now().isoformat()
|
121
|
+
},
|
122
|
+
headers={
|
123
|
+
"Authorization": "Bearer fulfillment_token_123"
|
124
|
+
},
|
125
|
+
webhook_id=f"order_confirmation_{order['order_id']}",
|
126
|
+
idempotency_key=f"order_{order['order_id']}_created" # Prevent duplicates
|
127
|
+
)
|
128
|
+
job_ids.append(job_id)
|
129
|
+
|
130
|
+
print(f"Queued {len(job_ids)} order confirmation webhooks")
|
131
|
+
return job_ids
|
132
|
+
|
133
|
+
|
134
|
+
def example_webhook_integration_test():
|
135
|
+
"""Example webhook for testing integration with external services."""
|
136
|
+
|
137
|
+
# Test webhook with httpbin.org (useful for debugging)
|
138
|
+
job_id = publish_webhook(
|
139
|
+
url="https://httpbin.org/post", # Echo service for testing
|
140
|
+
data={
|
141
|
+
"test": True,
|
142
|
+
"service": "django-mojo-jobs",
|
143
|
+
"timestamp": datetime.now().isoformat(),
|
144
|
+
"environment": "development"
|
145
|
+
},
|
146
|
+
headers={
|
147
|
+
"X-Test-Header": "webhook-test",
|
148
|
+
"X-Source": "django-mojo"
|
149
|
+
},
|
150
|
+
webhook_id="integration_test",
|
151
|
+
max_retries=1 # Only retry once for tests
|
152
|
+
)
|
153
|
+
|
154
|
+
print(f"Test webhook job {job_id} sent to httpbin.org")
|
155
|
+
print("Check the job metadata after completion to see the response")
|
156
|
+
return job_id
|
157
|
+
|
158
|
+
|
159
|
+
# Usage examples that would be in your application code:
|
160
|
+
|
161
|
+
def handle_user_signup(user_id, email):
|
162
|
+
"""Example: Send webhook when user signs up."""
|
163
|
+
return publish_webhook(
|
164
|
+
url="https://analytics.yoursite.com/webhooks/signup",
|
165
|
+
data={"user_id": user_id, "email": email, "event": "signup"}
|
166
|
+
)
|
167
|
+
|
168
|
+
|
169
|
+
def handle_payment_success(payment_id, amount, customer_id):
|
170
|
+
"""Example: Send webhook when payment succeeds."""
|
171
|
+
return publish_webhook(
|
172
|
+
url="https://fulfillment.yoursite.com/webhooks/payment",
|
173
|
+
data={
|
174
|
+
"payment_id": payment_id,
|
175
|
+
"amount": amount,
|
176
|
+
"customer_id": customer_id,
|
177
|
+
"status": "success"
|
178
|
+
},
|
179
|
+
headers={"Authorization": "Bearer your_webhook_secret"},
|
180
|
+
max_retries=5 # Important for payment notifications
|
181
|
+
)
|
182
|
+
|
183
|
+
|
184
|
+
def handle_system_alert(alert_data):
|
185
|
+
"""Example: Send critical system alerts."""
|
186
|
+
return publish_webhook(
|
187
|
+
url="https://alerts.yoursite.com/webhooks/system",
|
188
|
+
data=alert_data,
|
189
|
+
max_retries=10,
|
190
|
+
timeout=120, # Longer timeout for critical alerts
|
191
|
+
webhook_id=f"alert_{alert_data.get('alert_id')}"
|
192
|
+
)
|
193
|
+
|
194
|
+
|
195
|
+
if __name__ == "__main__":
|
196
|
+
# Run examples (uncomment to test)
|
197
|
+
# example_basic_webhook()
|
198
|
+
# example_webhook_with_auth()
|
199
|
+
# example_delayed_webhook()
|
200
|
+
# example_webhook_with_custom_retry()
|
201
|
+
# example_webhook_batch()
|
202
|
+
# example_webhook_integration_test()
|
203
|
+
pass
|