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
@@ -0,0 +1,97 @@
|
|
1
|
+
# Generated by Django 4.2.23 on 2025-09-03 20:22
|
2
|
+
|
3
|
+
from django.db import migrations, models
|
4
|
+
import django.db.models.deletion
|
5
|
+
import mojo.models.rest
|
6
|
+
|
7
|
+
|
8
|
+
class Migration(migrations.Migration):
|
9
|
+
|
10
|
+
initial = True
|
11
|
+
|
12
|
+
dependencies = [
|
13
|
+
]
|
14
|
+
|
15
|
+
operations = [
|
16
|
+
migrations.CreateModel(
|
17
|
+
name='Job',
|
18
|
+
fields=[
|
19
|
+
('id', models.CharField(editable=False, max_length=32, primary_key=True, serialize=False)),
|
20
|
+
('channel', models.CharField(db_index=True, help_text='Logical queue/channel name', max_length=100)),
|
21
|
+
('func', models.CharField(db_index=True, help_text='Registry key for the job function', max_length=255)),
|
22
|
+
('payload', models.JSONField(blank=True, default=dict, help_text='Job arguments/data (keep small)')),
|
23
|
+
('status', models.CharField(choices=[('pending', 'Pending'), ('running', 'Running'), ('completed', 'Completed'), ('failed', 'Failed'), ('canceled', 'Canceled'), ('expired', 'Expired')], db_index=True, default='pending', help_text='Current job status', max_length=16)),
|
24
|
+
('run_at', models.DateTimeField(blank=True, db_index=True, help_text='When to run this job (null = immediate)', null=True)),
|
25
|
+
('expires_at', models.DateTimeField(blank=True, db_index=True, help_text='Job expires if not run by this time', null=True)),
|
26
|
+
('attempt', models.IntegerField(default=0, help_text='Current attempt number')),
|
27
|
+
('max_retries', models.IntegerField(default=3, help_text='Maximum retry attempts')),
|
28
|
+
('backoff_base', models.FloatField(default=2.0, help_text='Base for exponential backoff')),
|
29
|
+
('backoff_max_sec', models.IntegerField(default=3600, help_text='Maximum backoff in seconds')),
|
30
|
+
('broadcast', models.BooleanField(db_index=True, default=False, help_text='If true, all runners execute this job')),
|
31
|
+
('cancel_requested', models.BooleanField(default=False, help_text='Cooperative cancel flag')),
|
32
|
+
('max_exec_seconds', models.IntegerField(blank=True, help_text='Hard execution time limit', null=True)),
|
33
|
+
('runner_id', models.CharField(blank=True, db_index=True, help_text='ID of runner currently executing', max_length=64, null=True)),
|
34
|
+
('last_error', models.TextField(blank=True, default='', help_text='Latest error message')),
|
35
|
+
('stack_trace', models.TextField(blank=True, default='', help_text='Latest stack trace')),
|
36
|
+
('metadata', models.JSONField(blank=True, default=dict, help_text='Custom metadata from job execution')),
|
37
|
+
('created', models.DateTimeField(auto_now_add=True, db_index=True)),
|
38
|
+
('modified', models.DateTimeField(auto_now=True, db_index=True)),
|
39
|
+
('started_at', models.DateTimeField(blank=True, help_text='When job execution started', null=True)),
|
40
|
+
('finished_at', models.DateTimeField(blank=True, help_text='When job execution finished', null=True)),
|
41
|
+
('idempotency_key', models.CharField(blank=True, help_text='Optional key for exactly-once semantics', max_length=64, null=True, unique=True)),
|
42
|
+
],
|
43
|
+
options={
|
44
|
+
'db_table': 'jobs_job',
|
45
|
+
'ordering': ['-created'],
|
46
|
+
},
|
47
|
+
bases=(models.Model, mojo.models.rest.MojoModel),
|
48
|
+
),
|
49
|
+
migrations.CreateModel(
|
50
|
+
name='JobEvent',
|
51
|
+
fields=[
|
52
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
53
|
+
('channel', models.CharField(db_index=True, max_length=100)),
|
54
|
+
('event', models.CharField(choices=[('created', 'Created'), ('queued', 'Queued'), ('scheduled', 'Scheduled'), ('running', 'Running'), ('retry', 'Retry'), ('canceled', 'Canceled'), ('completed', 'Completed'), ('failed', 'Failed'), ('expired', 'Expired'), ('claimed', 'Claimed'), ('released', 'Released')], db_index=True, help_text='Event type', max_length=24)),
|
55
|
+
('at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
56
|
+
('runner_id', models.CharField(blank=True, db_index=True, help_text='Runner that generated this event', max_length=64, null=True)),
|
57
|
+
('attempt', models.IntegerField(default=0, help_text='Attempt number at time of event')),
|
58
|
+
('details', models.JSONField(blank=True, default=dict, help_text='Event-specific details (keep minimal)')),
|
59
|
+
('created', models.DateTimeField(auto_now_add=True, db_index=True)),
|
60
|
+
('modified', models.DateTimeField(auto_now=True, db_index=True)),
|
61
|
+
('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='events', to='jobs.job')),
|
62
|
+
],
|
63
|
+
options={
|
64
|
+
'db_table': 'jobs_jobevent',
|
65
|
+
'ordering': ['-at'],
|
66
|
+
},
|
67
|
+
bases=(models.Model, mojo.models.rest.MojoModel),
|
68
|
+
),
|
69
|
+
migrations.AddIndex(
|
70
|
+
model_name='job',
|
71
|
+
index=models.Index(fields=['channel', 'status'], name='jobs_job_channel_b6258d_idx'),
|
72
|
+
),
|
73
|
+
migrations.AddIndex(
|
74
|
+
model_name='job',
|
75
|
+
index=models.Index(fields=['status', 'run_at'], name='jobs_job_status_f5c023_idx'),
|
76
|
+
),
|
77
|
+
migrations.AddIndex(
|
78
|
+
model_name='job',
|
79
|
+
index=models.Index(fields=['runner_id', 'status'], name='jobs_job_runner__068502_idx'),
|
80
|
+
),
|
81
|
+
migrations.AddIndex(
|
82
|
+
model_name='jobevent',
|
83
|
+
index=models.Index(fields=['job', '-at'], name='jobs_jobeve_job_id_4bb0aa_idx'),
|
84
|
+
),
|
85
|
+
migrations.AddIndex(
|
86
|
+
model_name='jobevent',
|
87
|
+
index=models.Index(fields=['channel', 'event', '-at'], name='jobs_jobeve_channel_65ad24_idx'),
|
88
|
+
),
|
89
|
+
migrations.AddIndex(
|
90
|
+
model_name='jobevent',
|
91
|
+
index=models.Index(fields=['runner_id', '-at'], name='jobs_jobeve_runner__8d884e_idx'),
|
92
|
+
),
|
93
|
+
migrations.AddIndex(
|
94
|
+
model_name='jobevent',
|
95
|
+
index=models.Index(fields=['-at'], name='jobs_jobeve_at_67fd08_idx'),
|
96
|
+
),
|
97
|
+
]
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# Generated by Django 4.2.23 on 2025-09-04 19:21
|
2
|
+
|
3
|
+
from django.db import migrations, models
|
4
|
+
import django.db.models.deletion
|
5
|
+
import mojo.models.rest
|
6
|
+
|
7
|
+
|
8
|
+
class Migration(migrations.Migration):
|
9
|
+
|
10
|
+
dependencies = [
|
11
|
+
('jobs', '0001_initial'),
|
12
|
+
]
|
13
|
+
|
14
|
+
operations = [
|
15
|
+
migrations.AlterField(
|
16
|
+
model_name='job',
|
17
|
+
name='max_retries',
|
18
|
+
field=models.IntegerField(default=0, help_text='Maximum retry attempts'),
|
19
|
+
),
|
20
|
+
migrations.CreateModel(
|
21
|
+
name='JobLog',
|
22
|
+
fields=[
|
23
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
24
|
+
('channel', models.CharField(db_index=True, max_length=100)),
|
25
|
+
('kind', models.CharField(choices=[('debug', 'Debug'), ('info', 'Info'), ('warn', 'Warn'), ('error', 'Error')], db_index=True, default='info', help_text='Log level/kind', max_length=16)),
|
26
|
+
('message', models.TextField(help_text='Log message')),
|
27
|
+
('meta', models.JSONField(blank=True, default=dict, help_text='Optional structured context')),
|
28
|
+
('created', models.DateTimeField(auto_now_add=True, db_index=True)),
|
29
|
+
('modified', models.DateTimeField(auto_now=True, db_index=True)),
|
30
|
+
('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='logs', to='jobs.job')),
|
31
|
+
],
|
32
|
+
options={
|
33
|
+
'db_table': 'jobs_joblog',
|
34
|
+
'ordering': ['-created'],
|
35
|
+
'indexes': [models.Index(fields=['job', '-created'], name='jobs_joblog_job_id_e9a555_idx'), models.Index(fields=['channel', 'kind', '-created'], name='jobs_joblog_channel_245a26_idx'), models.Index(fields=['-created'], name='jobs_joblog_created_41aac8_idx')],
|
36
|
+
},
|
37
|
+
bases=(models.Model, mojo.models.rest.MojoModel),
|
38
|
+
),
|
39
|
+
]
|
@@ -0,0 +1,441 @@
|
|
1
|
+
"""
|
2
|
+
Job and JobEvent models for the jobs system.
|
3
|
+
"""
|
4
|
+
from django.db import models
|
5
|
+
from mojo.models import MojoModel
|
6
|
+
from mojo.helpers import dates
|
7
|
+
from typing import Optional, Dict, Any
|
8
|
+
|
9
|
+
|
10
|
+
class Job(models.Model, MojoModel):
|
11
|
+
"""
|
12
|
+
Represents a background job in the system.
|
13
|
+
Stores current state and metadata for job execution.
|
14
|
+
"""
|
15
|
+
|
16
|
+
# Primary identifier - UUID without dashes
|
17
|
+
id = models.CharField(primary_key=True, max_length=32, editable=False)
|
18
|
+
|
19
|
+
# Job targeting
|
20
|
+
channel = models.CharField(max_length=100, db_index=True,
|
21
|
+
help_text="Logical queue/channel name")
|
22
|
+
func = models.CharField(max_length=255, db_index=True,
|
23
|
+
help_text="Registry key for the job function")
|
24
|
+
payload = models.JSONField(default=dict, blank=True,
|
25
|
+
help_text="Job arguments/data (keep small)")
|
26
|
+
|
27
|
+
# Current status
|
28
|
+
status = models.CharField(
|
29
|
+
max_length=16,
|
30
|
+
db_index=True,
|
31
|
+
choices=[
|
32
|
+
('pending', 'Pending'),
|
33
|
+
('running', 'Running'),
|
34
|
+
('completed', 'Completed'),
|
35
|
+
('failed', 'Failed'),
|
36
|
+
('canceled', 'Canceled'),
|
37
|
+
('expired', 'Expired')
|
38
|
+
],
|
39
|
+
default='pending',
|
40
|
+
help_text="Current job status"
|
41
|
+
)
|
42
|
+
|
43
|
+
# Scheduling & timing
|
44
|
+
run_at = models.DateTimeField(null=True, blank=True, db_index=True,
|
45
|
+
help_text="When to run this job (null = immediate)")
|
46
|
+
expires_at = models.DateTimeField(null=True, blank=True, db_index=True,
|
47
|
+
help_text="Job expires if not run by this time")
|
48
|
+
|
49
|
+
# Retry configuration
|
50
|
+
attempt = models.IntegerField(default=0,
|
51
|
+
help_text="Current attempt number")
|
52
|
+
max_retries = models.IntegerField(default=0,
|
53
|
+
help_text="Maximum retry attempts")
|
54
|
+
backoff_base = models.FloatField(default=2.0,
|
55
|
+
help_text="Base for exponential backoff")
|
56
|
+
backoff_max_sec = models.IntegerField(default=3600,
|
57
|
+
help_text="Maximum backoff in seconds")
|
58
|
+
|
59
|
+
# Behavior flags
|
60
|
+
broadcast = models.BooleanField(default=False, db_index=True,
|
61
|
+
help_text="If true, all runners execute this job")
|
62
|
+
cancel_requested = models.BooleanField(default=False,
|
63
|
+
help_text="Cooperative cancel flag")
|
64
|
+
max_exec_seconds = models.IntegerField(null=True, blank=True,
|
65
|
+
help_text="Hard execution time limit")
|
66
|
+
|
67
|
+
# Runner tracking
|
68
|
+
runner_id = models.CharField(max_length=64, null=True, blank=True, db_index=True,
|
69
|
+
help_text="ID of runner currently executing")
|
70
|
+
|
71
|
+
# Error diagnostics (latest only)
|
72
|
+
last_error = models.TextField(blank=True, default="",
|
73
|
+
help_text="Latest error message")
|
74
|
+
stack_trace = models.TextField(blank=True, default="",
|
75
|
+
help_text="Latest stack trace")
|
76
|
+
|
77
|
+
# Additional metadata
|
78
|
+
metadata = models.JSONField(default=dict, blank=True,
|
79
|
+
help_text="Custom metadata from job execution")
|
80
|
+
|
81
|
+
# Timestamps
|
82
|
+
created = models.DateTimeField(auto_now_add=True, editable=False, db_index=True)
|
83
|
+
modified = models.DateTimeField(auto_now=True, db_index=True)
|
84
|
+
started_at = models.DateTimeField(null=True, blank=True,
|
85
|
+
help_text="When job execution started")
|
86
|
+
finished_at = models.DateTimeField(null=True, blank=True,
|
87
|
+
help_text="When job execution finished")
|
88
|
+
|
89
|
+
# Idempotency support
|
90
|
+
idempotency_key = models.CharField(max_length=64, null=True, blank=True,
|
91
|
+
unique=True,
|
92
|
+
help_text="Optional key for exactly-once semantics")
|
93
|
+
|
94
|
+
class Meta:
|
95
|
+
db_table = 'jobs_job'
|
96
|
+
indexes = [
|
97
|
+
models.Index(fields=['channel', 'status']),
|
98
|
+
models.Index(fields=['status', 'run_at']),
|
99
|
+
models.Index(fields=['runner_id', 'status']),
|
100
|
+
]
|
101
|
+
ordering = ['-created']
|
102
|
+
|
103
|
+
class RestMeta:
|
104
|
+
# Permissions - jobs system specific permissions
|
105
|
+
VIEW_PERMS = ['view_jobs', 'manage_jobs']
|
106
|
+
SAVE_PERMS = ['manage_jobs']
|
107
|
+
DELETE_PERMS = ['manage_jobs']
|
108
|
+
POST_SAVE_ACTIONS = ["cancel_request", "retry_request", "get_status", "publish_job"]
|
109
|
+
|
110
|
+
# Graphs for different use cases
|
111
|
+
GRAPHS = {
|
112
|
+
'default': {
|
113
|
+
'extra': ['duration_ms'],
|
114
|
+
'fields': [
|
115
|
+
'id', 'channel', 'func', 'status',
|
116
|
+
'created', 'modified', 'attempt',
|
117
|
+
'started_at', 'finished_at', 'run_at'
|
118
|
+
]
|
119
|
+
},
|
120
|
+
'detail': {
|
121
|
+
'extra': ['duration_ms'],
|
122
|
+
'fields': [
|
123
|
+
'id', 'channel', 'func', 'payload', 'status',
|
124
|
+
'run_at', 'expires_at', 'attempt', 'max_retries',
|
125
|
+
'broadcast', 'cancel_requested', 'max_exec_seconds',
|
126
|
+
'runner_id', 'last_error', 'metadata'
|
127
|
+
'created', 'modified', 'started_at', 'finished_at'
|
128
|
+
]
|
129
|
+
},
|
130
|
+
'status': {
|
131
|
+
'fields': [
|
132
|
+
'id', 'status', 'runner_id', 'attempt',
|
133
|
+
'started_at', 'finished_at', 'last_error'
|
134
|
+
]
|
135
|
+
},
|
136
|
+
'admin': {
|
137
|
+
'fields': '__all__',
|
138
|
+
'exclude': ['stack_trace'] # Stack traces can be large
|
139
|
+
}
|
140
|
+
}
|
141
|
+
|
142
|
+
def __str__(self):
|
143
|
+
return f"Job {self.id} ({self.func}@{self.channel}): {self.status}"
|
144
|
+
|
145
|
+
@property
|
146
|
+
def is_terminal(self) -> bool:
|
147
|
+
"""Check if job is in a terminal state."""
|
148
|
+
return self.status in ('completed', 'failed', 'canceled', 'expired')
|
149
|
+
|
150
|
+
@property
|
151
|
+
def is_retriable(self) -> bool:
|
152
|
+
"""Check if job can be retried."""
|
153
|
+
return self.status == 'failed' and self.attempt < self.max_retries
|
154
|
+
|
155
|
+
@property
|
156
|
+
def duration_ms(self) -> int:
|
157
|
+
"""Calculate job execution duration in milliseconds."""
|
158
|
+
if self.started_at and self.finished_at:
|
159
|
+
delta = self.finished_at - self.started_at
|
160
|
+
return int(delta.total_seconds() * 1000)
|
161
|
+
return 0
|
162
|
+
|
163
|
+
@property
|
164
|
+
def is_expired(self) -> bool:
|
165
|
+
"""Check if job has expired."""
|
166
|
+
return self.expires_at and dates.utcnow() > self.expires_at
|
167
|
+
|
168
|
+
def check_cancel_requested(self) -> bool:
|
169
|
+
"""
|
170
|
+
Sync the cancel_requested field from the database and return updated value.
|
171
|
+
|
172
|
+
This method refreshes the cancel_requested field from the database to get
|
173
|
+
the most current cancellation status, useful for long-running jobs that
|
174
|
+
need to check for cancellation requests during execution.
|
175
|
+
|
176
|
+
Returns:
|
177
|
+
bool: Current cancel_requested value from database
|
178
|
+
"""
|
179
|
+
self.refresh_from_db(fields=['cancel_requested'])
|
180
|
+
return self.cancel_requested
|
181
|
+
|
182
|
+
def on_action_cancel_request(self, value):
|
183
|
+
"""
|
184
|
+
Cancel this job via REST API action.
|
185
|
+
|
186
|
+
Args:
|
187
|
+
value: Boolean indicating if cancellation is requested
|
188
|
+
|
189
|
+
Returns:
|
190
|
+
dict: Response indicating success/failure
|
191
|
+
"""
|
192
|
+
if not value:
|
193
|
+
return {'status': False, 'error': 'cancel_request must be true'}
|
194
|
+
|
195
|
+
from mojo.apps.jobs.services import JobActionsService
|
196
|
+
return JobActionsService.cancel_job(self)
|
197
|
+
|
198
|
+
def on_action_retry_request(self, value):
|
199
|
+
"""
|
200
|
+
Retry this failed/cancelled job via REST API action.
|
201
|
+
|
202
|
+
Args:
|
203
|
+
value: Can be boolean True or dict with 'delay' key for delayed retry
|
204
|
+
|
205
|
+
Returns:
|
206
|
+
dict: Response indicating success/failure with new job_id
|
207
|
+
"""
|
208
|
+
# Parse value - can be boolean or dict with delay
|
209
|
+
delay = None
|
210
|
+
if isinstance(value, dict):
|
211
|
+
if not value.get('retry'):
|
212
|
+
return {'status': False, 'error': 'retry_request must be true or {retry: true, delay: N}'}
|
213
|
+
delay = value.get('delay')
|
214
|
+
elif not value:
|
215
|
+
return {'status': False, 'error': 'retry_request must be true or {retry: true, delay: N}'}
|
216
|
+
|
217
|
+
from mojo.apps.jobs.services import JobActionsService
|
218
|
+
return JobActionsService.retry_job(self, delay=delay)
|
219
|
+
|
220
|
+
def on_action_get_status(self, value):
|
221
|
+
"""
|
222
|
+
Get detailed status of this job via REST API action.
|
223
|
+
|
224
|
+
Args:
|
225
|
+
value: Boolean (should be true)
|
226
|
+
|
227
|
+
Returns:
|
228
|
+
dict: Detailed job status information
|
229
|
+
"""
|
230
|
+
if not value:
|
231
|
+
return {'status': False, 'error': 'get_status must be true'}
|
232
|
+
|
233
|
+
from mojo.apps.jobs.services import JobActionsService
|
234
|
+
return JobActionsService.get_job_status(self)
|
235
|
+
|
236
|
+
def on_action_publish_job(self, value):
|
237
|
+
"""
|
238
|
+
Publish a new job using this job as a template via REST API action.
|
239
|
+
|
240
|
+
Args:
|
241
|
+
value: Dict with optional overrides for the new job:
|
242
|
+
- func: Override function path
|
243
|
+
- payload: Override payload
|
244
|
+
- channel: Override channel
|
245
|
+
- delay: Delay in seconds
|
246
|
+
- run_at: Specific run time
|
247
|
+
- max_retries: Override max retries
|
248
|
+
- broadcast: Override broadcast flag
|
249
|
+
|
250
|
+
Returns:
|
251
|
+
dict: Response with new job ID
|
252
|
+
"""
|
253
|
+
if not isinstance(value, dict):
|
254
|
+
return {'status': False, 'error': 'publish_job must be a dict with job parameters'}
|
255
|
+
|
256
|
+
from mojo.apps.jobs.services import JobActionsService
|
257
|
+
return JobActionsService.publish_job_from_template(self, value)
|
258
|
+
|
259
|
+
def add_log(self, message: str, kind: str = 'info', meta: Optional[dict] = None):
|
260
|
+
"""
|
261
|
+
Append a log entry for this job.
|
262
|
+
|
263
|
+
Args:
|
264
|
+
message: Log message text
|
265
|
+
kind: One of 'debug','info','warn','error' (default: 'info')
|
266
|
+
meta: Optional small dict for structured context
|
267
|
+
"""
|
268
|
+
# Normalize kind to known values
|
269
|
+
kind_norm = (kind or 'info').lower()
|
270
|
+
if kind_norm not in ('debug', 'info', 'warn', 'error'):
|
271
|
+
kind_norm = 'info'
|
272
|
+
|
273
|
+
# Persist log entry
|
274
|
+
JobLog.objects.create(
|
275
|
+
job=self,
|
276
|
+
channel=self.channel,
|
277
|
+
kind=kind_norm,
|
278
|
+
message=str(message),
|
279
|
+
meta=meta or {}
|
280
|
+
)
|
281
|
+
|
282
|
+
# Touch modified for easier tracking
|
283
|
+
self.save(update_fields=['modified'])
|
284
|
+
|
285
|
+
return True
|
286
|
+
|
287
|
+
|
288
|
+
class JobEvent(models.Model, MojoModel):
|
289
|
+
"""
|
290
|
+
Append-only audit log for job state transitions and events.
|
291
|
+
Kept minimal for efficient storage and querying.
|
292
|
+
"""
|
293
|
+
|
294
|
+
# Link to parent job
|
295
|
+
job = models.ForeignKey(Job, on_delete=models.CASCADE, related_name='events')
|
296
|
+
|
297
|
+
# Denormalized for efficient queries
|
298
|
+
channel = models.CharField(max_length=100, db_index=True)
|
299
|
+
|
300
|
+
# Event type
|
301
|
+
event = models.CharField(
|
302
|
+
max_length=24,
|
303
|
+
db_index=True,
|
304
|
+
choices=[
|
305
|
+
('created', 'Created'),
|
306
|
+
('queued', 'Queued'),
|
307
|
+
('scheduled', 'Scheduled'),
|
308
|
+
('running', 'Running'),
|
309
|
+
('retry', 'Retry'),
|
310
|
+
('canceled', 'Canceled'),
|
311
|
+
('completed', 'Completed'),
|
312
|
+
('failed', 'Failed'),
|
313
|
+
('expired', 'Expired'),
|
314
|
+
('claimed', 'Claimed'),
|
315
|
+
('released', 'Released')
|
316
|
+
],
|
317
|
+
help_text="Event type"
|
318
|
+
)
|
319
|
+
|
320
|
+
# When it happened
|
321
|
+
at = models.DateTimeField(auto_now_add=True, db_index=True)
|
322
|
+
|
323
|
+
# Who/what triggered it
|
324
|
+
runner_id = models.CharField(max_length=64, null=True, blank=True, db_index=True,
|
325
|
+
help_text="Runner that generated this event")
|
326
|
+
|
327
|
+
# Context
|
328
|
+
attempt = models.IntegerField(default=0,
|
329
|
+
help_text="Attempt number at time of event")
|
330
|
+
|
331
|
+
# Small details only - avoid large payloads
|
332
|
+
details = models.JSONField(default=dict, blank=True,
|
333
|
+
help_text="Event-specific details (keep minimal)")
|
334
|
+
|
335
|
+
# Standard timestamps
|
336
|
+
created = models.DateTimeField(auto_now_add=True, editable=False, db_index=True)
|
337
|
+
modified = models.DateTimeField(auto_now=True, db_index=True)
|
338
|
+
|
339
|
+
class Meta:
|
340
|
+
db_table = 'jobs_jobevent'
|
341
|
+
indexes = [
|
342
|
+
models.Index(fields=['job', '-at']),
|
343
|
+
models.Index(fields=['channel', 'event', '-at']),
|
344
|
+
models.Index(fields=['runner_id', '-at']),
|
345
|
+
models.Index(fields=['-at']), # For retention queries
|
346
|
+
]
|
347
|
+
ordering = ['-at']
|
348
|
+
|
349
|
+
class RestMeta:
|
350
|
+
# Permissions - restricted to system users only
|
351
|
+
VIEW_PERMS = ['manage_jobs', 'view_jobs']
|
352
|
+
SAVE_PERMS = [] # Events are system-created only
|
353
|
+
DELETE_PERMS = ['manage_jobs']
|
354
|
+
|
355
|
+
# Graphs
|
356
|
+
GRAPHS = {
|
357
|
+
'default': {
|
358
|
+
'fields': [
|
359
|
+
'id', 'event', 'at', 'runner_id', 'attempt', 'details'
|
360
|
+
]
|
361
|
+
},
|
362
|
+
'detail': {
|
363
|
+
'fields': [
|
364
|
+
'id', 'job_id', 'channel', 'event', 'at',
|
365
|
+
'runner_id', 'attempt', 'details'
|
366
|
+
]
|
367
|
+
},
|
368
|
+
'timeline': {
|
369
|
+
'fields': [
|
370
|
+
'event', 'at', 'runner_id', 'details'
|
371
|
+
]
|
372
|
+
}
|
373
|
+
}
|
374
|
+
|
375
|
+
def __str__(self):
|
376
|
+
return f"JobEvent {self.event} for {self.job_id} at {self.at}"
|
377
|
+
|
378
|
+
|
379
|
+
class JobLog(models.Model, MojoModel):
|
380
|
+
"""
|
381
|
+
Append-only log entries for individual jobs with optional structured context.
|
382
|
+
Useful for partial failures (e.g., per-recipient send outcomes).
|
383
|
+
"""
|
384
|
+
|
385
|
+
# Link to parent job
|
386
|
+
job = models.ForeignKey(Job, on_delete=models.CASCADE, related_name='logs')
|
387
|
+
|
388
|
+
# Denormalized channel for efficient filtering
|
389
|
+
channel = models.CharField(max_length=100, db_index=True)
|
390
|
+
|
391
|
+
# When it happened
|
392
|
+
created = models.DateTimeField(auto_now_add=True, db_index=True)
|
393
|
+
|
394
|
+
# Log kind/severity
|
395
|
+
kind = models.CharField(
|
396
|
+
max_length=16,
|
397
|
+
db_index=True,
|
398
|
+
choices=[
|
399
|
+
('debug', 'Debug'),
|
400
|
+
('info', 'Info'),
|
401
|
+
('warn', 'Warn'),
|
402
|
+
('error', 'Error'),
|
403
|
+
],
|
404
|
+
default='info',
|
405
|
+
help_text="Log level/kind"
|
406
|
+
)
|
407
|
+
|
408
|
+
# Message content
|
409
|
+
message = models.TextField(help_text="Log message")
|
410
|
+
|
411
|
+
# Optional structured metadata (keep small)
|
412
|
+
meta = models.JSONField(default=dict, blank=True, help_text="Optional structured context")
|
413
|
+
|
414
|
+
# Standard timestamps
|
415
|
+
created = models.DateTimeField(auto_now_add=True, editable=False, db_index=True)
|
416
|
+
modified = models.DateTimeField(auto_now=True, db_index=True)
|
417
|
+
|
418
|
+
class Meta:
|
419
|
+
db_table = 'jobs_joblog'
|
420
|
+
ordering = ['-created']
|
421
|
+
indexes = [
|
422
|
+
models.Index(fields=['job', '-created']),
|
423
|
+
models.Index(fields=['channel', 'kind', '-created']),
|
424
|
+
models.Index(fields=['-created']),
|
425
|
+
]
|
426
|
+
|
427
|
+
class RestMeta:
|
428
|
+
VIEW_PERMS = ['manage_jobs', 'view_jobs']
|
429
|
+
SAVE_PERMS = [] # Logs should be written via add_log / system actions
|
430
|
+
DELETE_PERMS = ['manage_jobs']
|
431
|
+
GRAPHS = {
|
432
|
+
'default': {
|
433
|
+
'fields': ['id', 'job_id', 'created', 'kind', 'message']
|
434
|
+
},
|
435
|
+
'detail': {
|
436
|
+
'fields': ['id', 'job_id', 'channel', 'created', 'kind', 'message', 'meta']
|
437
|
+
}
|
438
|
+
}
|
439
|
+
|
440
|
+
def __str__(self):
|
441
|
+
return f"JobLog {self.kind} for {self.job_id} at {self.created}"
|