dj-queue 0.1.0__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 (48) hide show
  1. dj_queue/__init__.py +0 -0
  2. dj_queue/admin.py +90 -0
  3. dj_queue/api.py +122 -0
  4. dj_queue/apps.py +6 -0
  5. dj_queue/backend.py +161 -0
  6. dj_queue/config.py +456 -0
  7. dj_queue/contrib/__init__.py +1 -0
  8. dj_queue/contrib/asgi.py +32 -0
  9. dj_queue/contrib/gunicorn.py +25 -0
  10. dj_queue/db.py +68 -0
  11. dj_queue/exceptions.py +26 -0
  12. dj_queue/hooks.py +86 -0
  13. dj_queue/log.py +27 -0
  14. dj_queue/management/__init__.py +1 -0
  15. dj_queue/management/commands/__init__.py +1 -0
  16. dj_queue/management/commands/dj_queue.py +39 -0
  17. dj_queue/management/commands/dj_queue_health.py +32 -0
  18. dj_queue/management/commands/dj_queue_prune.py +22 -0
  19. dj_queue/migrations/0001_initial.py +262 -0
  20. dj_queue/migrations/0002_pause_semaphore.py +52 -0
  21. dj_queue/migrations/0003_recurringtask_recurringexecution.py +73 -0
  22. dj_queue/migrations/__init__.py +0 -0
  23. dj_queue/models/__init__.py +24 -0
  24. dj_queue/models/jobs.py +328 -0
  25. dj_queue/models/recurring.py +51 -0
  26. dj_queue/models/runtime.py +55 -0
  27. dj_queue/operations/__init__.py +1 -0
  28. dj_queue/operations/cleanup.py +37 -0
  29. dj_queue/operations/concurrency.py +176 -0
  30. dj_queue/operations/jobs.py +637 -0
  31. dj_queue/operations/recurring.py +81 -0
  32. dj_queue/routers.py +26 -0
  33. dj_queue/runtime/__init__.py +1 -0
  34. dj_queue/runtime/base.py +198 -0
  35. dj_queue/runtime/dispatcher.py +78 -0
  36. dj_queue/runtime/errors.py +39 -0
  37. dj_queue/runtime/interruptible.py +46 -0
  38. dj_queue/runtime/notify.py +119 -0
  39. dj_queue/runtime/pidfile.py +39 -0
  40. dj_queue/runtime/pool.py +62 -0
  41. dj_queue/runtime/procline.py +11 -0
  42. dj_queue/runtime/scheduler.py +128 -0
  43. dj_queue/runtime/supervisor.py +460 -0
  44. dj_queue/runtime/worker.py +116 -0
  45. dj_queue-0.1.0.dist-info/METADATA +613 -0
  46. dj_queue-0.1.0.dist-info/RECORD +48 -0
  47. dj_queue-0.1.0.dist-info/WHEEL +4 -0
  48. dj_queue-0.1.0.dist-info/licenses/LICENSE +21 -0
dj_queue/log.py ADDED
@@ -0,0 +1,27 @@
1
+ import logging
2
+ from typing import Any
3
+
4
+ from dj_queue.config import load_backend_config
5
+
6
+ logger = logging.getLogger("dj_queue")
7
+
8
+
9
+ def log_event(
10
+ event: str,
11
+ *,
12
+ level: int = logging.INFO,
13
+ backend_alias: str = "default",
14
+ polling: bool = False,
15
+ **fields: Any,
16
+ ):
17
+ if polling and load_backend_config(backend_alias).silence_polling:
18
+ return
19
+
20
+ logger.log(
21
+ level,
22
+ event,
23
+ extra={
24
+ "event": event,
25
+ "dj_queue": fields,
26
+ },
27
+ )
@@ -0,0 +1 @@
1
+ """Management package for dj_queue."""
@@ -0,0 +1 @@
1
+ """Management commands for dj_queue."""
@@ -0,0 +1,39 @@
1
+ from django.core.management.base import BaseCommand
2
+
3
+ from dj_queue.config import load_backend_config
4
+ from dj_queue.runtime.supervisor import AsyncSupervisor, ForkSupervisor
5
+
6
+
7
+ def build_supervisor(*, backend_alias, cli_overrides):
8
+ mode = load_backend_config(backend_alias, cli_overrides=cli_overrides).mode
9
+ supervisor_class = AsyncSupervisor if mode == "async" else ForkSupervisor
10
+ return supervisor_class.from_backend_config(
11
+ backend_alias=backend_alias,
12
+ cli_overrides=cli_overrides,
13
+ )
14
+
15
+
16
+ class Command(BaseCommand):
17
+ help = "Start the dj_queue supervisor"
18
+
19
+ def add_arguments(self, parser):
20
+ parser.add_argument("--backend", default="default")
21
+ parser.add_argument("--config")
22
+ parser.add_argument("--mode", choices=("fork", "async"))
23
+ parser.add_argument("--only-work", action="store_true")
24
+ parser.add_argument("--only-dispatch", action="store_true")
25
+ parser.add_argument("--skip-recurring", action="store_true")
26
+
27
+ def handle(self, *args, **options):
28
+ cli_overrides = {
29
+ "config": options["config"],
30
+ "mode": options["mode"],
31
+ "only_work": options["only_work"],
32
+ "only_dispatch": options["only_dispatch"],
33
+ "skip_recurring": options["skip_recurring"],
34
+ }
35
+ supervisor = build_supervisor(
36
+ backend_alias=options["backend"],
37
+ cli_overrides=cli_overrides,
38
+ )
39
+ supervisor.run()
@@ -0,0 +1,32 @@
1
+ from datetime import timedelta
2
+
3
+ from django.core.management.base import BaseCommand
4
+ from django.utils import timezone
5
+
6
+ from dj_queue.config import load_backend_config
7
+ from dj_queue.db import get_database_alias
8
+ from dj_queue.models import Process
9
+
10
+
11
+ class Command(BaseCommand):
12
+ help = "Check dj_queue process health"
13
+
14
+ def add_arguments(self, parser):
15
+ parser.add_argument("--backend", default="default")
16
+ parser.add_argument("--max-age", type=int)
17
+
18
+ def handle(self, *args, **options):
19
+ backend_alias = options["backend"]
20
+ config = load_backend_config(backend_alias)
21
+ max_age = options["max_age"]
22
+ if max_age is None:
23
+ max_age = config.process_alive_threshold
24
+
25
+ cutoff = timezone.now() - timedelta(seconds=max_age)
26
+ alias = get_database_alias(backend_alias)
27
+ healthy = Process.objects.using(alias).filter(last_heartbeat_at__gte=cutoff).exists()
28
+ if healthy:
29
+ self.stdout.write("healthy")
30
+ return
31
+
32
+ raise SystemExit(1)
@@ -0,0 +1,22 @@
1
+ from django.core.management.base import BaseCommand
2
+
3
+ from dj_queue.operations.cleanup import clear_finished_jobs
4
+
5
+
6
+ class Command(BaseCommand):
7
+ help = "Prune finished dj_queue jobs"
8
+
9
+ def add_arguments(self, parser):
10
+ parser.add_argument("--backend", default="default")
11
+ parser.add_argument("--older-than", type=int)
12
+ parser.add_argument("--batch-size", type=int, default=500)
13
+ parser.add_argument("--task-path")
14
+
15
+ def handle(self, *args, **options):
16
+ deleted = clear_finished_jobs(
17
+ older_than=options["older_than"],
18
+ task_path=options["task_path"],
19
+ batch_size=options["batch_size"],
20
+ backend_alias=options["backend"],
21
+ )
22
+ self.stdout.write(f"deleted {deleted} finished jobs")
@@ -0,0 +1,262 @@
1
+ # Generated by Django 6.0.3 on 2026-04-06 15:03
2
+
3
+ import django.db.models.deletion
4
+ import uuid
5
+ from django.db import migrations, models
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+ initial = True
10
+
11
+ dependencies = []
12
+
13
+ operations = [
14
+ migrations.CreateModel(
15
+ name="Job",
16
+ fields=[
17
+ (
18
+ "id",
19
+ models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False),
20
+ ),
21
+ ("task_path", models.TextField()),
22
+ ("queue_name", models.CharField(default="default", max_length=64)),
23
+ ("priority", models.SmallIntegerField(default=0)),
24
+ ("payload", models.JSONField(default=dict)),
25
+ ("backend_name", models.CharField(max_length=64)),
26
+ ("scheduled_at", models.DateTimeField(blank=True, null=True)),
27
+ ("concurrency_key", models.CharField(blank=True, max_length=255, null=True)),
28
+ ("finished_at", models.DateTimeField(blank=True, null=True)),
29
+ ("return_value", models.JSONField(blank=True, null=True)),
30
+ ("created_at", models.DateTimeField(auto_now_add=True)),
31
+ ("updated_at", models.DateTimeField(auto_now=True)),
32
+ ],
33
+ options={
34
+ "db_table": "dj_queue_jobs",
35
+ "indexes": [
36
+ models.Index(
37
+ fields=["queue_name", "finished_at"], name="dj_queue_jo_queue_n_b824ac_idx"
38
+ ),
39
+ models.Index(
40
+ fields=["scheduled_at", "finished_at"], name="dj_queue_jo_schedul_92b51a_idx"
41
+ ),
42
+ models.Index(fields=["finished_at"], name="dj_queue_jo_finishe_9acb9a_idx"),
43
+ ],
44
+ "constraints": [
45
+ models.CheckConstraint(
46
+ condition=models.Q(("priority__gte", -100), ("priority__lte", 100)),
47
+ name="dj_queue_jobs_priority_range",
48
+ )
49
+ ],
50
+ },
51
+ ),
52
+ migrations.CreateModel(
53
+ name="FailedExecution",
54
+ fields=[
55
+ (
56
+ "id",
57
+ models.BigAutoField(
58
+ auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
59
+ ),
60
+ ),
61
+ ("exception_class", models.CharField(max_length=255)),
62
+ ("message", models.TextField(default="")),
63
+ ("traceback", models.TextField(default="")),
64
+ ("created_at", models.DateTimeField(auto_now_add=True)),
65
+ (
66
+ "job",
67
+ models.OneToOneField(
68
+ on_delete=django.db.models.deletion.CASCADE,
69
+ related_name="failed_execution",
70
+ to="dj_queue.job",
71
+ ),
72
+ ),
73
+ ],
74
+ options={
75
+ "db_table": "dj_queue_failed_executions",
76
+ },
77
+ ),
78
+ migrations.CreateModel(
79
+ name="Process",
80
+ fields=[
81
+ (
82
+ "id",
83
+ models.BigAutoField(
84
+ auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
85
+ ),
86
+ ),
87
+ ("kind", models.CharField(max_length=32)),
88
+ ("pid", models.IntegerField()),
89
+ ("hostname", models.CharField(max_length=255)),
90
+ ("name", models.CharField(max_length=255)),
91
+ ("metadata", models.JSONField(default=dict)),
92
+ ("last_heartbeat_at", models.DateTimeField()),
93
+ ("created_at", models.DateTimeField(auto_now_add=True)),
94
+ (
95
+ "supervisor",
96
+ models.ForeignKey(
97
+ blank=True,
98
+ null=True,
99
+ on_delete=django.db.models.deletion.SET_NULL,
100
+ related_name="children",
101
+ to="dj_queue.process",
102
+ ),
103
+ ),
104
+ ],
105
+ options={
106
+ "db_table": "dj_queue_processes",
107
+ },
108
+ ),
109
+ migrations.CreateModel(
110
+ name="ClaimedExecution",
111
+ fields=[
112
+ (
113
+ "id",
114
+ models.BigAutoField(
115
+ auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
116
+ ),
117
+ ),
118
+ ("created_at", models.DateTimeField(auto_now_add=True)),
119
+ (
120
+ "job",
121
+ models.OneToOneField(
122
+ on_delete=django.db.models.deletion.CASCADE,
123
+ related_name="claimed_execution",
124
+ to="dj_queue.job",
125
+ ),
126
+ ),
127
+ (
128
+ "process",
129
+ models.ForeignKey(
130
+ blank=True,
131
+ null=True,
132
+ on_delete=django.db.models.deletion.SET_NULL,
133
+ related_name="claimed_executions",
134
+ to="dj_queue.process",
135
+ ),
136
+ ),
137
+ ],
138
+ options={
139
+ "db_table": "dj_queue_claimed_executions",
140
+ },
141
+ ),
142
+ migrations.CreateModel(
143
+ name="ReadyExecution",
144
+ fields=[
145
+ (
146
+ "id",
147
+ models.BigAutoField(
148
+ auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
149
+ ),
150
+ ),
151
+ ("queue_name", models.CharField(max_length=64)),
152
+ ("priority", models.SmallIntegerField()),
153
+ ("created_at", models.DateTimeField(auto_now_add=True)),
154
+ (
155
+ "job",
156
+ models.OneToOneField(
157
+ on_delete=django.db.models.deletion.CASCADE,
158
+ related_name="ready_execution",
159
+ to="dj_queue.job",
160
+ ),
161
+ ),
162
+ ],
163
+ options={
164
+ "db_table": "dj_queue_ready_executions",
165
+ },
166
+ ),
167
+ migrations.CreateModel(
168
+ name="ScheduledExecution",
169
+ fields=[
170
+ (
171
+ "id",
172
+ models.BigAutoField(
173
+ auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
174
+ ),
175
+ ),
176
+ ("queue_name", models.CharField(max_length=64)),
177
+ ("priority", models.SmallIntegerField()),
178
+ ("scheduled_at", models.DateTimeField()),
179
+ ("created_at", models.DateTimeField(auto_now_add=True)),
180
+ (
181
+ "job",
182
+ models.OneToOneField(
183
+ on_delete=django.db.models.deletion.CASCADE,
184
+ related_name="scheduled_execution",
185
+ to="dj_queue.job",
186
+ ),
187
+ ),
188
+ ],
189
+ options={
190
+ "db_table": "dj_queue_scheduled_executions",
191
+ },
192
+ ),
193
+ migrations.CreateModel(
194
+ name="BlockedExecution",
195
+ fields=[
196
+ (
197
+ "id",
198
+ models.BigAutoField(
199
+ auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
200
+ ),
201
+ ),
202
+ ("queue_name", models.CharField(max_length=64)),
203
+ ("priority", models.SmallIntegerField()),
204
+ ("concurrency_key", models.CharField(max_length=255)),
205
+ ("expires_at", models.DateTimeField()),
206
+ ("created_at", models.DateTimeField(auto_now_add=True)),
207
+ (
208
+ "job",
209
+ models.OneToOneField(
210
+ on_delete=django.db.models.deletion.CASCADE,
211
+ related_name="blocked_execution",
212
+ to="dj_queue.job",
213
+ ),
214
+ ),
215
+ ],
216
+ options={
217
+ "db_table": "dj_queue_blocked_executions",
218
+ "indexes": [
219
+ models.Index(
220
+ fields=["concurrency_key", "priority", "id"], name="dj_queue_bl_concurr_1ce730_idx"
221
+ ),
222
+ models.Index(
223
+ fields=["expires_at", "concurrency_key"], name="dj_queue_bl_expires_ea27f7_idx"
224
+ ),
225
+ ],
226
+ },
227
+ ),
228
+ migrations.AddIndex(
229
+ model_name="process",
230
+ index=models.Index(fields=["name", "supervisor"], name="dj_queue_pr_name_d374d8_idx"),
231
+ ),
232
+ migrations.AddIndex(
233
+ model_name="process",
234
+ index=models.Index(fields=["last_heartbeat_at"], name="dj_queue_pr_last_he_813530_idx"),
235
+ ),
236
+ migrations.AddConstraint(
237
+ model_name="process",
238
+ constraint=models.UniqueConstraint(
239
+ fields=("name", "supervisor"), name="dj_queue_processes_name_supervisor_unique"
240
+ ),
241
+ ),
242
+ migrations.AddIndex(
243
+ model_name="claimedexecution",
244
+ index=models.Index(fields=["process", "job"], name="dj_queue_cl_process_647400_idx"),
245
+ ),
246
+ migrations.AddIndex(
247
+ model_name="readyexecution",
248
+ index=models.Index(fields=["priority", "id"], name="dj_queue_re_priorit_bfc737_idx"),
249
+ ),
250
+ migrations.AddIndex(
251
+ model_name="readyexecution",
252
+ index=models.Index(
253
+ fields=["queue_name", "priority", "id"], name="dj_queue_re_queue_n_9931e4_idx"
254
+ ),
255
+ ),
256
+ migrations.AddIndex(
257
+ model_name="scheduledexecution",
258
+ index=models.Index(
259
+ fields=["scheduled_at", "priority", "id"], name="dj_queue_sc_schedul_7e8dd0_idx"
260
+ ),
261
+ ),
262
+ ]
@@ -0,0 +1,52 @@
1
+ # Generated by Django 6.0.3 on 2026-04-06 15:16
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+ dependencies = [
8
+ ("dj_queue", "0001_initial"),
9
+ ]
10
+
11
+ operations = [
12
+ migrations.CreateModel(
13
+ name="Pause",
14
+ fields=[
15
+ (
16
+ "id",
17
+ models.BigAutoField(
18
+ auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
19
+ ),
20
+ ),
21
+ ("queue_name", models.CharField(max_length=64, unique=True)),
22
+ ("created_at", models.DateTimeField(auto_now_add=True)),
23
+ ],
24
+ options={
25
+ "db_table": "dj_queue_pauses",
26
+ },
27
+ ),
28
+ migrations.CreateModel(
29
+ name="Semaphore",
30
+ fields=[
31
+ (
32
+ "id",
33
+ models.BigAutoField(
34
+ auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
35
+ ),
36
+ ),
37
+ ("key", models.CharField(max_length=255, unique=True)),
38
+ ("value", models.IntegerField()),
39
+ ("limit", models.IntegerField()),
40
+ ("expires_at", models.DateTimeField()),
41
+ ("created_at", models.DateTimeField(auto_now_add=True)),
42
+ ("updated_at", models.DateTimeField(auto_now=True)),
43
+ ],
44
+ options={
45
+ "db_table": "dj_queue_semaphores",
46
+ "indexes": [
47
+ models.Index(fields=["key", "value"], name="dj_queue_se_key_7a5af2_idx"),
48
+ models.Index(fields=["expires_at"], name="dj_queue_se_expires_811b4d_idx"),
49
+ ],
50
+ },
51
+ ),
52
+ ]
@@ -0,0 +1,73 @@
1
+ # Generated by Django 6.0.3 on 2026-04-06 15:34
2
+
3
+ import django.db.models.deletion
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+ dependencies = [
9
+ ("dj_queue", "0002_pause_semaphore"),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.CreateModel(
14
+ name="RecurringTask",
15
+ fields=[
16
+ (
17
+ "id",
18
+ models.BigAutoField(
19
+ auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
20
+ ),
21
+ ),
22
+ ("key", models.CharField(max_length=255, unique=True)),
23
+ ("task_path", models.CharField(max_length=255)),
24
+ ("payload", models.JSONField(blank=True, null=True)),
25
+ ("schedule", models.CharField(max_length=255)),
26
+ ("queue_name", models.CharField(default="default", max_length=64)),
27
+ ("priority", models.SmallIntegerField(default=0)),
28
+ ("description", models.TextField(blank=True, default="")),
29
+ ("static", models.BooleanField(default=False)),
30
+ ("created_at", models.DateTimeField(auto_now_add=True)),
31
+ ("updated_at", models.DateTimeField(auto_now=True)),
32
+ ],
33
+ options={
34
+ "db_table": "dj_queue_recurring_tasks",
35
+ },
36
+ ),
37
+ migrations.CreateModel(
38
+ name="RecurringExecution",
39
+ fields=[
40
+ (
41
+ "id",
42
+ models.BigAutoField(
43
+ auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
44
+ ),
45
+ ),
46
+ ("task_key", models.CharField(max_length=255)),
47
+ ("run_at", models.DateTimeField()),
48
+ ("created_at", models.DateTimeField(auto_now_add=True)),
49
+ (
50
+ "job",
51
+ models.OneToOneField(
52
+ blank=True,
53
+ null=True,
54
+ on_delete=django.db.models.deletion.CASCADE,
55
+ related_name="recurring_execution",
56
+ to="dj_queue.job",
57
+ ),
58
+ ),
59
+ ],
60
+ options={
61
+ "db_table": "dj_queue_recurring_executions",
62
+ "indexes": [
63
+ models.Index(fields=["task_key", "run_at"], name="dj_queue_re_task_ke_8e21b4_idx")
64
+ ],
65
+ "constraints": [
66
+ models.UniqueConstraint(
67
+ fields=("task_key", "run_at"),
68
+ name="dj_queue_recurring_executions_task_key_run_at_unique",
69
+ )
70
+ ],
71
+ },
72
+ ),
73
+ ]
File without changes
@@ -0,0 +1,24 @@
1
+ from dj_queue.models.jobs import (
2
+ BlockedExecution,
3
+ ClaimedExecution,
4
+ FailedExecution,
5
+ Job,
6
+ ReadyExecution,
7
+ ScheduledExecution,
8
+ )
9
+ from dj_queue.models.recurring import RecurringExecution, RecurringTask
10
+ from dj_queue.models.runtime import Pause, Process, Semaphore
11
+
12
+ __all__ = [
13
+ "BlockedExecution",
14
+ "ClaimedExecution",
15
+ "FailedExecution",
16
+ "Job",
17
+ "Pause",
18
+ "Process",
19
+ "ReadyExecution",
20
+ "RecurringExecution",
21
+ "RecurringTask",
22
+ "ScheduledExecution",
23
+ "Semaphore",
24
+ ]