dj-queue 0.2.3__tar.gz → 0.2.4__tar.gz

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 (54) hide show
  1. {dj_queue-0.2.3 → dj_queue-0.2.4}/PKG-INFO +6 -3
  2. {dj_queue-0.2.3 → dj_queue-0.2.4}/README.md +5 -2
  3. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/admin.py +65 -9
  4. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/dashboard.py +18 -4
  5. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/operations/jobs.py +17 -0
  6. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/templates/admin/dj_queue/change_form.html +1 -1
  7. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/templates/admin/dj_queue/queue_jobs.html +2 -0
  8. {dj_queue-0.2.3 → dj_queue-0.2.4}/pyproject.toml +1 -1
  9. {dj_queue-0.2.3 → dj_queue-0.2.4}/LICENSE +0 -0
  10. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/__init__.py +0 -0
  11. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/api.py +0 -0
  12. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/apps.py +0 -0
  13. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/backend.py +0 -0
  14. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/config.py +0 -0
  15. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/contrib/__init__.py +0 -0
  16. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/contrib/asgi.py +0 -0
  17. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/contrib/gunicorn.py +0 -0
  18. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/db.py +0 -0
  19. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/exceptions.py +0 -0
  20. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/hooks.py +0 -0
  21. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/log.py +0 -0
  22. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/management/__init__.py +0 -0
  23. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/management/commands/__init__.py +0 -0
  24. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/management/commands/dj_queue.py +0 -0
  25. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/management/commands/dj_queue_health.py +0 -0
  26. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/management/commands/dj_queue_prune.py +0 -0
  27. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/migrations/0001_initial.py +0 -0
  28. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/migrations/0002_pause_semaphore.py +0 -0
  29. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/migrations/0003_recurringtask_recurringexecution.py +0 -0
  30. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/migrations/0004_dashboard.py +0 -0
  31. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/migrations/__init__.py +0 -0
  32. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/models/__init__.py +0 -0
  33. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/models/jobs.py +0 -0
  34. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/models/recurring.py +0 -0
  35. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/models/runtime.py +0 -0
  36. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/operations/__init__.py +0 -0
  37. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/operations/cleanup.py +0 -0
  38. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/operations/concurrency.py +0 -0
  39. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/operations/recurring.py +0 -0
  40. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/routers.py +0 -0
  41. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/runtime/__init__.py +0 -0
  42. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/runtime/base.py +0 -0
  43. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/runtime/dispatcher.py +0 -0
  44. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/runtime/errors.py +0 -0
  45. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/runtime/interruptible.py +0 -0
  46. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/runtime/notify.py +0 -0
  47. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/runtime/pidfile.py +0 -0
  48. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/runtime/pool.py +0 -0
  49. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/runtime/procline.py +0 -0
  50. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/runtime/scheduler.py +0 -0
  51. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/runtime/supervisor.py +0 -0
  52. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/runtime/worker.py +0 -0
  53. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/templates/admin/dj_queue/change_list.html +0 -0
  54. {dj_queue-0.2.3 → dj_queue-0.2.4}/dj_queue/templates/admin/dj_queue/dashboard.html +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dj-queue
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: Database-backed task queue backend for Django's django.tasks framework
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -164,6 +164,8 @@ If Django admin is installed, `dj_queue` adds an operator dashboard at
164
164
  - queue, process, recurring-task, and semaphore overview
165
165
  - backend-aware dashboard and raw changelists
166
166
  - queue controls: pause, resume, clear ready
167
+ - job detail action: enqueue a fresh copy of any stored job
168
+ - pause detail action: resume the paused queue from the raw pause row
167
169
  - failed-job actions: retry and discard from list and detail views
168
170
  - queue drill-down pages for state-specific inspection
169
171
 
@@ -430,8 +432,9 @@ python manage.py dj_queue_prune --task-path myapp.tasks.cleanup
430
432
  When a task raises, `dj_queue` keeps the job and its failed execution row in the
431
433
  queue database, including the exception class, message, and traceback.
432
434
 
433
- You can retry and discard failed jobs through Django admin, or call the same
434
- operations directly through the operations layer:
435
+ You can retry and discard failed jobs through Django admin, and any raw job
436
+ detail page can enqueue a fresh copy of that stored job. The failed-job actions
437
+ also stay available directly through the operations layer:
435
438
 
436
439
  ```python
437
440
  from dj_queue.operations.jobs import discard_failed_job, retry_failed_job
@@ -138,6 +138,8 @@ If Django admin is installed, `dj_queue` adds an operator dashboard at
138
138
  - queue, process, recurring-task, and semaphore overview
139
139
  - backend-aware dashboard and raw changelists
140
140
  - queue controls: pause, resume, clear ready
141
+ - job detail action: enqueue a fresh copy of any stored job
142
+ - pause detail action: resume the paused queue from the raw pause row
141
143
  - failed-job actions: retry and discard from list and detail views
142
144
  - queue drill-down pages for state-specific inspection
143
145
 
@@ -404,8 +406,9 @@ python manage.py dj_queue_prune --task-path myapp.tasks.cleanup
404
406
  When a task raises, `dj_queue` keeps the job and its failed execution row in the
405
407
  queue database, including the exception class, message, and traceback.
406
408
 
407
- You can retry and discard failed jobs through Django admin, or call the same
408
- operations directly through the operations layer:
409
+ You can retry and discard failed jobs through Django admin, and any raw job
410
+ detail page can enqueue a fresh copy of that stored job. The failed-job actions
411
+ also stay available directly through the operations layer:
409
412
 
410
413
  ```python
411
414
  from dj_queue.operations.jobs import discard_failed_job, retry_failed_job
@@ -15,6 +15,7 @@ from django.utils import timezone
15
15
 
16
16
  from dj_queue.config import load_backend_config
17
17
  from dj_queue import dashboard
18
+ from dj_queue.api import QueueInfo
18
19
  from dj_queue.db import get_database_alias
19
20
  from dj_queue.models import (
20
21
  BlockedExecution,
@@ -26,6 +27,7 @@ from dj_queue.models import (
26
27
  RecurringTask,
27
28
  Semaphore,
28
29
  )
30
+ from dj_queue.operations.jobs import enqueue_job_again
29
31
 
30
32
 
31
33
  class DjQueueFirstAdminSite(admin.AdminSite):
@@ -214,6 +216,7 @@ class HiddenSidebarAdminMixin:
214
216
  extra_context = {
215
217
  **(extra_context or {}),
216
218
  "dashboard_url": self._dashboard_url(request),
219
+ "changelist_url": self._changelist_url(backend_alias=self._backend_alias(request)),
217
220
  "change_actions": self.get_change_actions(request, obj),
218
221
  }
219
222
  return super().changeform_view(
@@ -474,18 +477,49 @@ class JobAdmin(HiddenSidebarAdminMixin, admin.ModelAdmin):
474
477
  return format_html('<a href="{}">{}</a>', url, obj.queue_name)
475
478
 
476
479
  def get_change_actions(self, request, obj):
477
- if obj is None or obj.status != "failed":
480
+ if obj is None:
478
481
  return ()
479
- return (
480
- {"name": "retry", "label": "Retry failed job", "css_class": "djq-object-action-retry"},
481
- {
482
- "name": "discard",
483
- "label": "Discard failed job",
484
- "css_class": "djq-object-action-discard",
485
- },
486
- )
482
+ actions = [{"name": "enqueue", "label": "Enqueue job", "css_class": "djq-object-action-retry"}]
483
+ if obj.status == "failed":
484
+ actions.extend(
485
+ (
486
+ {
487
+ "name": "retry",
488
+ "label": "Retry failed job",
489
+ "css_class": "djq-object-action-retry",
490
+ },
491
+ {
492
+ "name": "discard",
493
+ "label": "Discard failed job",
494
+ "css_class": "djq-object-action-discard",
495
+ },
496
+ )
497
+ )
498
+ return tuple(actions)
487
499
 
488
500
  def handle_change_action(self, request, obj, action):
501
+ if action == "enqueue":
502
+ try:
503
+ new_job = enqueue_job_again(obj.pk, backend_alias=obj.backend_name)
504
+ except Exception as exc:
505
+ self.message_user(request, f"Could not enqueue job: {exc}", level=messages.ERROR)
506
+ return HttpResponseRedirect(
507
+ self._change_url(object_id=obj.pk, backend_alias=obj.backend_name)
508
+ )
509
+
510
+ self.message_user(
511
+ request,
512
+ format_html(
513
+ 'Enqueued job <a href="{}">{}</a>.',
514
+ self._change_url(object_id=new_job.pk, backend_alias=new_job.backend_name),
515
+ new_job.pk,
516
+ ),
517
+ level=messages.SUCCESS,
518
+ )
519
+ return HttpResponseRedirect(
520
+ self._change_url(object_id=obj.pk, backend_alias=obj.backend_name)
521
+ )
522
+
489
523
  if obj.status != "failed":
490
524
  self.message_user(request, "This job is not failed", level=messages.ERROR)
491
525
  return HttpResponseRedirect(
@@ -633,6 +667,28 @@ class PauseAdmin(HiddenSidebarAdminMixin, admin.ModelAdmin):
633
667
  readonly_fields = ("queue_name", "created_at")
634
668
  search_fields = ("queue_name",)
635
669
 
670
+ def get_change_actions(self, request, obj):
671
+ if obj is None:
672
+ return ()
673
+ return ({"name": "resume", "label": "Resume queue", "css_class": "djq-object-action-retry"},)
674
+
675
+ def handle_change_action(self, request, obj, action):
676
+ backend_alias = self._backend_alias(request)
677
+ if action == "resume":
678
+ QueueInfo(obj.queue_name, backend_alias=backend_alias).resume()
679
+ self.message_user(
680
+ request,
681
+ format_html(
682
+ 'Resumed queue <a href="{}">{}</a>',
683
+ f"{reverse('admin:dj_queue_dashboard_queue', args=[obj.queue_name])}?{urlencode({'backend': backend_alias})}",
684
+ obj.queue_name,
685
+ ),
686
+ level=messages.SUCCESS,
687
+ )
688
+ return HttpResponseRedirect(self._changelist_url(backend_alias=backend_alias))
689
+
690
+ return HttpResponseRedirect(self._change_url(object_id=obj.pk, backend_alias=backend_alias))
691
+
636
692
 
637
693
  @admin.register(Semaphore)
638
694
  class SemaphoreAdmin(HiddenSidebarAdminMixin, admin.ModelAdmin):
@@ -385,7 +385,14 @@ def queue_page_context(*, backend_alias, queue_name, state, page_number, query_p
385
385
  paginator = Paginator(jobs, PAGE_SIZE)
386
386
  page_obj = paginator.get_page(query_params.get("page", page_number))
387
387
  queue_info = QueueInfo(queue_name, backend_alias=backend_alias)
388
+ queue_paused = queue_info.paused
389
+ queue_latency_seconds = None if queue_paused else queue_info.latency
388
390
  state_counts = _queue_state_counts(backend_alias=backend_alias, queue_name=queue_name)
391
+ live_workers = [
392
+ process
393
+ for process in Process.objects.using(alias).filter(kind="Worker")
394
+ if process.last_heartbeat_at >= process_cutoff
395
+ ]
389
396
  state_tabs = [
390
397
  {
391
398
  "name": state_name,
@@ -425,7 +432,13 @@ def queue_page_context(*, backend_alias, queue_name, state, page_number, query_p
425
432
  "queue_database_alias": alias,
426
433
  "queue_name": queue_name,
427
434
  "queue_info": queue_info,
428
- "queue_paused": queue_info.paused,
435
+ "queue_paused": queue_paused,
436
+ "queue_latency_seconds": queue_latency_seconds,
437
+ "queue_worker_count": sum(
438
+ 1
439
+ for worker in live_workers
440
+ if _queue_matches_selectors(queue_name, worker.metadata.get("queues", []))
441
+ ),
429
442
  "state": state,
430
443
  "state_label": QUEUE_STATE_LABELS[state],
431
444
  "state_tabs": state_tabs,
@@ -1224,9 +1237,10 @@ def _queue_rows(*, backend_alias, now, process_cutoff):
1224
1237
 
1225
1238
  rows = []
1226
1239
  for queue_name in sorted(queue_names):
1240
+ paused = queue_name in paused_queues
1227
1241
  oldest_ready_at = oldest_ready.get(queue_name)
1228
1242
  latency_seconds = None
1229
- if oldest_ready_at is not None:
1243
+ if oldest_ready_at is not None and paused is False:
1230
1244
  latency_seconds = max((now - oldest_ready_at).total_seconds(), 0.0)
1231
1245
 
1232
1246
  ready_count = ready_counts.get(queue_name, 0)
@@ -1236,7 +1250,7 @@ def _queue_rows(*, backend_alias, now, process_cutoff):
1236
1250
  failed_count = failed_counts.get(queue_name, 0)
1237
1251
  finished_count = finished_counts.get(queue_name, 0)
1238
1252
  shared_sources = []
1239
- if queue_name in paused_queues:
1253
+ if paused:
1240
1254
  shared_sources.append("pause")
1241
1255
  if queue_name in recurring_queues:
1242
1256
  shared_sources.append("recurring task")
@@ -1250,7 +1264,7 @@ def _queue_rows(*, backend_alias, now, process_cutoff):
1250
1264
  "blocked_count": blocked_count,
1251
1265
  "failed_count": failed_count,
1252
1266
  "finished_count": finished_count,
1253
- "paused": queue_name in paused_queues,
1267
+ "paused": paused,
1254
1268
  "latency_seconds": latency_seconds,
1255
1269
  "oldest_scheduled_at": oldest_scheduled.get(queue_name),
1256
1270
  "oldest_blocked_at": oldest_blocked.get(queue_name),
@@ -354,6 +354,23 @@ def retry_failed_job(job_id, *, backend_alias="default"):
354
354
  return job
355
355
 
356
356
 
357
+ def enqueue_job_again(job_id, *, backend_alias="default"):
358
+ alias = get_database_alias(backend_alias)
359
+ source_job = Job.objects.using(alias).get(pk=job_id)
360
+ task = import_string(source_job.task_path)
361
+ if hasattr(task, "using"):
362
+ task = task.using(
363
+ priority=source_job.priority,
364
+ queue_name=source_job.queue_name,
365
+ run_after=source_job.scheduled_at,
366
+ backend=source_job.backend_name,
367
+ )
368
+ args = list(source_job.payload.get("args", []))
369
+ kwargs = dict(source_job.payload.get("kwargs", {}))
370
+ job, _ = enqueue_job_with_dispatch(task, args, kwargs, backend_alias=source_job.backend_name)
371
+ return job
372
+
373
+
357
374
  def discard_failed_job(job_id, *, backend_alias="default"):
358
375
  alias = get_database_alias(backend_alias)
359
376
  queryset = Job.objects.using(alias).filter(pk=job_id, failed_execution__isnull=False)
@@ -46,7 +46,7 @@
46
46
  <div class="breadcrumbs">
47
47
  <a href='{% url "admin:index" %}'>{% translate "Home" %}</a>
48
48
  › <a href="{{ dashboard_url }}">dj_queue</a>
49
- › {% if has_view_permission %}<a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>{% else %}{{ opts.verbose_name_plural|capfirst }}{% endif %}
49
+ › {% if has_view_permission %}<a href="{{ changelist_url }}">{{ opts.verbose_name_plural|capfirst }}</a>{% else %}{{ opts.verbose_name_plural|capfirst }}{% endif %}
50
50
  › {% if add %}{% blocktranslate with name=opts.verbose_name %}Add {{ name }}{% endblocktranslate %}{% else %}{{ original|truncatewords:"18" }}{% endif %}
51
51
  </div>
52
52
  {% endblock breadcrumbs %}
@@ -216,6 +216,8 @@
216
216
  <div class="queue-toolbar-meta">
217
217
  <span><strong>Backend:</strong> {{ backend_alias }}</span>
218
218
  <span><strong>Database:</strong> {{ queue_database_alias }}</span>
219
+ <span><strong>Workers:</strong> {{ queue_worker_count }}</span>
220
+ <span><strong>Latency:</strong> {% if queue_latency_seconds != None %}{{ queue_latency_seconds|floatformat:1 }}s{% else %}-{% endif %}</span>
219
221
  <span><strong>Paused:</strong> {{ queue_paused|yesno:"yes,no" }}</span>
220
222
  </div>
221
223
  <div class="queue-toolbar-actions">
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "dj-queue"
7
- version = "0.2.3"
7
+ version = "0.2.4"
8
8
  description = "Database-backed task queue backend for Django's django.tasks framework"
9
9
  readme = "README.md"
10
10
  license = "MIT"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes