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.
Files changed (221) hide show
  1. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.16.dist-info}/METADATA +3 -1
  2. django_nativemojo-0.1.16.dist-info/RECORD +302 -0
  3. mojo/__init__.py +1 -1
  4. mojo/apps/account/management/commands/serializer_admin.py +121 -1
  5. mojo/apps/account/migrations/0006_add_device_tracking_models.py +72 -0
  6. mojo/apps/account/migrations/0007_delete_userdevicelocation.py +16 -0
  7. mojo/apps/account/migrations/0008_userdevicelocation.py +33 -0
  8. mojo/apps/account/migrations/0009_geolocatedip_subnet.py +18 -0
  9. mojo/apps/account/migrations/0010_group_avatar.py +20 -0
  10. mojo/apps/account/migrations/0011_user_org_registereddevice_pushconfig_and_more.py +118 -0
  11. mojo/apps/account/migrations/0012_remove_pushconfig_apns_key_file_and_more.py +21 -0
  12. mojo/apps/account/migrations/0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more.py +28 -0
  13. mojo/apps/account/migrations/0014_notificationdelivery_data_payload_and_more.py +48 -0
  14. mojo/apps/account/models/__init__.py +2 -0
  15. mojo/apps/account/models/device.py +281 -0
  16. mojo/apps/account/models/group.py +294 -8
  17. mojo/apps/account/models/member.py +14 -1
  18. mojo/apps/account/models/push/__init__.py +4 -0
  19. mojo/apps/account/models/push/config.py +112 -0
  20. mojo/apps/account/models/push/delivery.py +93 -0
  21. mojo/apps/account/models/push/device.py +66 -0
  22. mojo/apps/account/models/push/template.py +99 -0
  23. mojo/apps/account/models/user.py +190 -17
  24. mojo/apps/account/rest/__init__.py +2 -0
  25. mojo/apps/account/rest/device.py +39 -0
  26. mojo/apps/account/rest/group.py +8 -0
  27. mojo/apps/account/rest/push.py +187 -0
  28. mojo/apps/account/rest/user.py +95 -5
  29. mojo/apps/account/services/__init__.py +1 -0
  30. mojo/apps/account/services/push.py +363 -0
  31. mojo/apps/aws/migrations/0001_initial.py +206 -0
  32. mojo/apps/aws/migrations/0002_emaildomain_can_recv_emaildomain_can_send_and_more.py +28 -0
  33. mojo/apps/aws/migrations/0003_mailbox_is_domain_default_mailbox_is_system_default_and_more.py +31 -0
  34. mojo/apps/aws/migrations/0004_s3bucket.py +39 -0
  35. mojo/apps/aws/migrations/0005_alter_emaildomain_region_delete_s3bucket.py +21 -0
  36. mojo/apps/aws/models/__init__.py +19 -0
  37. mojo/apps/aws/models/email_attachment.py +99 -0
  38. mojo/apps/aws/models/email_domain.py +218 -0
  39. mojo/apps/aws/models/email_template.py +132 -0
  40. mojo/apps/aws/models/incoming_email.py +197 -0
  41. mojo/apps/aws/models/mailbox.py +288 -0
  42. mojo/apps/aws/models/sent_message.py +175 -0
  43. mojo/apps/aws/rest/__init__.py +6 -0
  44. mojo/apps/aws/rest/email.py +33 -0
  45. mojo/apps/aws/rest/email_ops.py +183 -0
  46. mojo/apps/aws/rest/messages.py +32 -0
  47. mojo/apps/aws/rest/send.py +101 -0
  48. mojo/apps/aws/rest/sns.py +403 -0
  49. mojo/apps/aws/rest/templates.py +19 -0
  50. mojo/apps/aws/services/__init__.py +32 -0
  51. mojo/apps/aws/services/email.py +390 -0
  52. mojo/apps/aws/services/email_ops.py +548 -0
  53. mojo/apps/docit/__init__.py +6 -0
  54. mojo/apps/docit/markdown_plugins/syntax_highlight.py +25 -0
  55. mojo/apps/docit/markdown_plugins/toc.py +12 -0
  56. mojo/apps/docit/migrations/0001_initial.py +113 -0
  57. mojo/apps/docit/migrations/0002_alter_book_modified_by_alter_page_modified_by.py +26 -0
  58. mojo/apps/docit/migrations/0003_alter_book_group.py +20 -0
  59. mojo/apps/docit/models/__init__.py +17 -0
  60. mojo/apps/docit/models/asset.py +231 -0
  61. mojo/apps/docit/models/book.py +227 -0
  62. mojo/apps/docit/models/page.py +319 -0
  63. mojo/apps/docit/models/page_revision.py +203 -0
  64. mojo/apps/docit/rest/__init__.py +10 -0
  65. mojo/apps/docit/rest/asset.py +17 -0
  66. mojo/apps/docit/rest/book.py +22 -0
  67. mojo/apps/docit/rest/page.py +22 -0
  68. mojo/apps/docit/rest/page_revision.py +17 -0
  69. mojo/apps/docit/services/__init__.py +11 -0
  70. mojo/apps/docit/services/docit.py +315 -0
  71. mojo/apps/docit/services/markdown.py +44 -0
  72. mojo/apps/fileman/backends/s3.py +209 -0
  73. mojo/apps/fileman/models/file.py +45 -9
  74. mojo/apps/fileman/models/manager.py +269 -3
  75. mojo/apps/incident/migrations/0007_event_uid.py +18 -0
  76. mojo/apps/incident/migrations/0008_ticket_ticketnote.py +55 -0
  77. mojo/apps/incident/migrations/0009_incident_status.py +18 -0
  78. mojo/apps/incident/migrations/0010_event_country_code.py +18 -0
  79. mojo/apps/incident/migrations/0011_incident_country_code.py +18 -0
  80. mojo/apps/incident/migrations/0012_alter_incident_status.py +18 -0
  81. mojo/apps/incident/models/__init__.py +1 -0
  82. mojo/apps/incident/models/event.py +35 -0
  83. mojo/apps/incident/models/incident.py +2 -0
  84. mojo/apps/incident/models/ticket.py +62 -0
  85. mojo/apps/incident/reporter.py +21 -3
  86. mojo/apps/incident/rest/__init__.py +1 -0
  87. mojo/apps/incident/rest/ticket.py +43 -0
  88. mojo/apps/jobs/__init__.py +489 -0
  89. mojo/apps/jobs/adapters.py +24 -0
  90. mojo/apps/jobs/cli.py +616 -0
  91. mojo/apps/jobs/daemon.py +370 -0
  92. mojo/apps/jobs/examples/sample_jobs.py +376 -0
  93. mojo/apps/jobs/examples/webhook_examples.py +203 -0
  94. mojo/apps/jobs/handlers/__init__.py +5 -0
  95. mojo/apps/jobs/handlers/webhook.py +317 -0
  96. mojo/apps/jobs/job_engine.py +734 -0
  97. mojo/apps/jobs/keys.py +203 -0
  98. mojo/apps/jobs/local_queue.py +363 -0
  99. mojo/apps/jobs/management/__init__.py +3 -0
  100. mojo/apps/jobs/management/commands/__init__.py +3 -0
  101. mojo/apps/jobs/manager.py +1327 -0
  102. mojo/apps/jobs/migrations/0001_initial.py +97 -0
  103. mojo/apps/jobs/migrations/0002_alter_job_max_retries_joblog.py +39 -0
  104. mojo/apps/jobs/models/__init__.py +6 -0
  105. mojo/apps/jobs/models/job.py +441 -0
  106. mojo/apps/jobs/rest/__init__.py +2 -0
  107. mojo/apps/jobs/rest/control.py +466 -0
  108. mojo/apps/jobs/rest/jobs.py +421 -0
  109. mojo/apps/jobs/scheduler.py +571 -0
  110. mojo/apps/jobs/services/__init__.py +6 -0
  111. mojo/apps/jobs/services/job_actions.py +465 -0
  112. mojo/apps/jobs/settings.py +209 -0
  113. mojo/apps/logit/models/log.py +3 -0
  114. mojo/apps/metrics/__init__.py +8 -1
  115. mojo/apps/metrics/redis_metrics.py +198 -0
  116. mojo/apps/metrics/rest/__init__.py +3 -0
  117. mojo/apps/metrics/rest/categories.py +266 -0
  118. mojo/apps/metrics/rest/helpers.py +48 -0
  119. mojo/apps/metrics/rest/permissions.py +99 -0
  120. mojo/apps/metrics/rest/values.py +277 -0
  121. mojo/apps/metrics/utils.py +17 -0
  122. mojo/decorators/http.py +40 -1
  123. mojo/helpers/aws/__init__.py +11 -7
  124. mojo/helpers/aws/inbound_email.py +309 -0
  125. mojo/helpers/aws/kms.py +413 -0
  126. mojo/helpers/aws/ses_domain.py +959 -0
  127. mojo/helpers/crypto/__init__.py +1 -1
  128. mojo/helpers/crypto/utils.py +15 -0
  129. mojo/helpers/location/__init__.py +2 -0
  130. mojo/helpers/location/countries.py +262 -0
  131. mojo/helpers/location/geolocation.py +196 -0
  132. mojo/helpers/logit.py +37 -0
  133. mojo/helpers/redis/__init__.py +2 -0
  134. mojo/helpers/redis/adapter.py +606 -0
  135. mojo/helpers/redis/client.py +48 -0
  136. mojo/helpers/redis/pool.py +225 -0
  137. mojo/helpers/request.py +8 -0
  138. mojo/helpers/response.py +8 -0
  139. mojo/middleware/auth.py +1 -1
  140. mojo/middleware/cors.py +40 -0
  141. mojo/middleware/logging.py +131 -12
  142. mojo/middleware/mojo.py +5 -0
  143. mojo/models/rest.py +271 -57
  144. mojo/models/secrets.py +86 -0
  145. mojo/serializers/__init__.py +16 -10
  146. mojo/serializers/core/__init__.py +90 -0
  147. mojo/serializers/core/cache/__init__.py +121 -0
  148. mojo/serializers/core/cache/backends.py +518 -0
  149. mojo/serializers/core/cache/base.py +102 -0
  150. mojo/serializers/core/cache/disabled.py +181 -0
  151. mojo/serializers/core/cache/memory.py +287 -0
  152. mojo/serializers/core/cache/redis.py +533 -0
  153. mojo/serializers/core/cache/utils.py +454 -0
  154. mojo/serializers/{manager.py → core/manager.py} +53 -4
  155. mojo/serializers/core/serializer.py +475 -0
  156. mojo/serializers/{advanced/formats → formats}/csv.py +116 -139
  157. mojo/serializers/suggested_improvements.md +388 -0
  158. testit/client.py +1 -1
  159. testit/helpers.py +14 -0
  160. testit/runner.py +23 -6
  161. django_nativemojo-0.1.15.dist-info/RECORD +0 -234
  162. mojo/apps/notify/README.md +0 -91
  163. mojo/apps/notify/README_NOTIFICATIONS.md +0 -566
  164. mojo/apps/notify/admin.py +0 -52
  165. mojo/apps/notify/handlers/example_handlers.py +0 -516
  166. mojo/apps/notify/handlers/ses/__init__.py +0 -25
  167. mojo/apps/notify/handlers/ses/complaint.py +0 -25
  168. mojo/apps/notify/handlers/ses/message.py +0 -86
  169. mojo/apps/notify/management/commands/__init__.py +0 -1
  170. mojo/apps/notify/management/commands/process_notifications.py +0 -370
  171. mojo/apps/notify/mod +0 -0
  172. mojo/apps/notify/models/__init__.py +0 -12
  173. mojo/apps/notify/models/account.py +0 -128
  174. mojo/apps/notify/models/attachment.py +0 -24
  175. mojo/apps/notify/models/bounce.py +0 -68
  176. mojo/apps/notify/models/complaint.py +0 -40
  177. mojo/apps/notify/models/inbox.py +0 -113
  178. mojo/apps/notify/models/inbox_message.py +0 -173
  179. mojo/apps/notify/models/outbox.py +0 -129
  180. mojo/apps/notify/models/outbox_message.py +0 -288
  181. mojo/apps/notify/models/template.py +0 -30
  182. mojo/apps/notify/providers/aws.py +0 -73
  183. mojo/apps/notify/rest/ses.py +0 -0
  184. mojo/apps/notify/utils/__init__.py +0 -2
  185. mojo/apps/notify/utils/notifications.py +0 -404
  186. mojo/apps/notify/utils/parsing.py +0 -202
  187. mojo/apps/notify/utils/render.py +0 -144
  188. mojo/apps/tasks/README.md +0 -118
  189. mojo/apps/tasks/__init__.py +0 -44
  190. mojo/apps/tasks/manager.py +0 -644
  191. mojo/apps/tasks/rest/__init__.py +0 -2
  192. mojo/apps/tasks/rest/hooks.py +0 -0
  193. mojo/apps/tasks/rest/tasks.py +0 -76
  194. mojo/apps/tasks/runner.py +0 -439
  195. mojo/apps/tasks/task.py +0 -99
  196. mojo/apps/tasks/tq_handlers.py +0 -132
  197. mojo/helpers/crypto/__pycache__/hash.cpython-310.pyc +0 -0
  198. mojo/helpers/crypto/__pycache__/sign.cpython-310.pyc +0 -0
  199. mojo/helpers/crypto/__pycache__/utils.cpython-310.pyc +0 -0
  200. mojo/helpers/redis.py +0 -10
  201. mojo/models/meta.py +0 -262
  202. mojo/serializers/advanced/README.md +0 -363
  203. mojo/serializers/advanced/__init__.py +0 -247
  204. mojo/serializers/advanced/formats/__init__.py +0 -28
  205. mojo/serializers/advanced/formats/excel.py +0 -516
  206. mojo/serializers/advanced/formats/json.py +0 -239
  207. mojo/serializers/advanced/formats/response.py +0 -485
  208. mojo/serializers/advanced/serializer.py +0 -568
  209. mojo/serializers/optimized.py +0 -618
  210. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.16.dist-info}/LICENSE +0 -0
  211. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.16.dist-info}/NOTICE +0 -0
  212. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.16.dist-info}/WHEEL +0 -0
  213. /mojo/apps/{notify → aws/migrations}/__init__.py +0 -0
  214. /mojo/apps/{notify/handlers → docit/markdown_plugins}/__init__.py +0 -0
  215. /mojo/apps/{notify/management → docit/migrations}/__init__.py +0 -0
  216. /mojo/apps/{notify/providers → jobs/examples}/__init__.py +0 -0
  217. /mojo/apps/{notify/rest → jobs/migrations}/__init__.py +0 -0
  218. /mojo/{serializers → rest}/openapi.py +0 -0
  219. /mojo/serializers/{settings_example.py → examples/settings.py} +0 -0
  220. /mojo/{apps/notify/handlers/ses/bounce.py → serializers/formats/__init__.py} +0 -0
  221. /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
@@ -0,0 +1,5 @@
1
+ """
2
+ Job handlers for Django-MOJO Jobs System.
3
+
4
+ Specialized job functions for common tasks like webhooks, emails, etc.
5
+ """