django-admin-background-task 0.3.1__tar.gz → 0.4.0__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 (30) hide show
  1. {django_admin_background_task-0.3.1 → django_admin_background_task-0.4.0}/PKG-INFO +1 -1
  2. {django_admin_background_task-0.3.1 → django_admin_background_task-0.4.0}/bgtask/backends/thread_pool.py +2 -2
  3. {django_admin_background_task-0.3.1 → django_admin_background_task-0.4.0}/bgtask/migrations/0001_initial.py +1 -1
  4. django_admin_background_task-0.4.0/bgtask/migrations/0003_backgroundtask_queued_at.py +18 -0
  5. {django_admin_background_task-0.3.1 → django_admin_background_task-0.4.0}/bgtask/model_admin.py +23 -7
  6. {django_admin_background_task-0.3.1 → django_admin_background_task-0.4.0}/bgtask/models.py +44 -7
  7. {django_admin_background_task-0.3.1 → django_admin_background_task-0.4.0}/bgtask/static/bgtask/js/bgtask.js +18 -3
  8. {django_admin_background_task-0.3.1 → django_admin_background_task-0.4.0}/bgtask/templates/bgtask/admin/change_list.html +6 -3
  9. {django_admin_background_task-0.3.1 → django_admin_background_task-0.4.0}/pyproject.toml +3 -1
  10. django_admin_background_task-0.3.1/bgtask/stack_contexts.py +0 -11
  11. {django_admin_background_task-0.3.1 → django_admin_background_task-0.4.0}/LICENSE +0 -0
  12. {django_admin_background_task-0.3.1 → django_admin_background_task-0.4.0}/README.md +0 -0
  13. {django_admin_background_task-0.3.1 → django_admin_background_task-0.4.0}/bgtask/__init__.py +0 -0
  14. {django_admin_background_task-0.3.1 → django_admin_background_task-0.4.0}/bgtask/admin.py +0 -0
  15. {django_admin_background_task-0.3.1 → django_admin_background_task-0.4.0}/bgtask/apps.py +0 -0
  16. {django_admin_background_task-0.3.1 → django_admin_background_task-0.4.0}/bgtask/backends/__init__.py +0 -0
  17. {django_admin_background_task-0.3.1 → django_admin_background_task-0.4.0}/bgtask/decorators.py +0 -0
  18. {django_admin_background_task-0.3.1 → django_admin_background_task-0.4.0}/bgtask/migrations/0002_backgroundtask_namespace_alter_backgroundtask_name.py +0 -0
  19. {django_admin_background_task-0.3.1 → django_admin_background_task-0.4.0}/bgtask/migrations/__init__.py +0 -0
  20. {django_admin_background_task-0.3.1 → django_admin_background_task-0.4.0}/bgtask/static/bgtask/js/bgtask-once.js +0 -0
  21. {django_admin_background_task-0.3.1 → django_admin_background_task-0.4.0}/bgtask/templates/bgtask/bg_changelist_status_column.html +0 -0
  22. {django_admin_background_task-0.3.1 → django_admin_background_task-0.4.0}/bgtask/templates/bgtask/bg_completed_column.html +0 -0
  23. {django_admin_background_task-0.3.1 → django_admin_background_task-0.4.0}/bgtask/templates/bgtask/bgtask_templates.html +0 -0
  24. {django_admin_background_task-0.3.1 → django_admin_background_task-0.4.0}/bgtask/templates/bgtask/bgtask_view.html +0 -0
  25. {django_admin_background_task-0.3.1 → django_admin_background_task-0.4.0}/bgtask/templates/bgtask/progress.html +0 -0
  26. {django_admin_background_task-0.3.1 → django_admin_background_task-0.4.0}/bgtask/templatetags/__init__.py +0 -0
  27. {django_admin_background_task-0.3.1 → django_admin_background_task-0.4.0}/bgtask/templatetags/bgtask.py +0 -0
  28. {django_admin_background_task-0.3.1 → django_admin_background_task-0.4.0}/bgtask/tests/test_bgtask.py +0 -0
  29. {django_admin_background_task-0.3.1 → django_admin_background_task-0.4.0}/bgtask/urls.py +0 -0
  30. {django_admin_background_task-0.3.1 → django_admin_background_task-0.4.0}/bgtask/views.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-admin-background-task
3
- Version: 0.3.1
3
+ Version: 0.4.0
4
4
  Summary: A set of tools for django apps to persist and monitor the status of background tasks
5
5
  Home-page: https://github.com/daphtdazz/django-bgtask
6
6
  License: Apache-2.0
@@ -4,9 +4,9 @@ from concurrent.futures import ThreadPoolExecutor
4
4
  SHARED_THREAD_POOL = None
5
5
 
6
6
 
7
- def dispatch(func, bg_task, *args, **kwargs):
7
+ def dispatch(func, *args, **kwargs):
8
8
  global SHARED_THREAD_POOL
9
9
  if SHARED_THREAD_POOL is None:
10
10
  SHARED_THREAD_POOL = ThreadPoolExecutor()
11
11
 
12
- SHARED_THREAD_POOL.submit(func, bg_task, *args, **kwargs)
12
+ SHARED_THREAD_POOL.submit(func, *args, **kwargs)
@@ -21,7 +21,7 @@ class Migration(migrations.Migration):
21
21
  ('updated', models.DateTimeField(auto_now=True)),
22
22
  ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
23
23
  ('name', models.CharField(max_length=1000)),
24
- ('state', models.CharField(choices=[('not_started', 'not_started'), ('running', 'running'), ('success', 'success'), ('partial_success', 'partial_success'), ('failed', 'failed')], default='not_started', max_length=16)),
24
+ ('state', models.CharField(choices=[('not_started', 'not_started'), ('queued', 'queued'), ('running', 'running'), ('success', 'success'), ('partial_success', 'partial_success'), ('failed', 'failed')], default='not_started', max_length=16)),
25
25
  ('steps_to_complete', models.PositiveIntegerField(blank=True, help_text='The number of steps in the task for it to be completed.', null=True)),
26
26
  ('steps_completed', models.PositiveIntegerField(blank=True, help_text='The number of steps completed so far by this task', null=True)),
27
27
  ('started_at', models.DateTimeField(blank=True, null=True)),
@@ -0,0 +1,18 @@
1
+ # Generated by Django 4.2.11 on 2025-02-07 21:57
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ("bgtask", "0002_backgroundtask_namespace_alter_backgroundtask_name"),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name="backgroundtask",
15
+ name="queued_at",
16
+ field=models.DateTimeField(blank=True, null=True),
17
+ ),
18
+ ]
@@ -13,9 +13,14 @@ from .models import BackgroundTask
13
13
  class BGTaskModelAdmin(admin.ModelAdmin):
14
14
  # This is not overridden to avoid messing with the implicit logic for finding change list
15
15
  # templates that ModelAdmin uses. So you either need to specify this yourself on your
16
- # subclass or you need to extend from this in your custom template.
16
+ # subclass or you need to extend from this in your custom template (which you can install at
17
+ # <your-app>/templates/admin/<your-app>/<your-model>/change_list.html)
17
18
  #
18
19
  # change_list_template = "bgtask/admin/change_list.html"
20
+ #
21
+ # Set this to tell the admin change list page which background tasks to show in the table.
22
+ #
23
+ # bgtask_names = ["task a", "task b"]
19
24
 
20
25
  # ----------------------------------------------------------------------------------------------
21
26
  # Class API
@@ -41,14 +46,15 @@ class BGTaskModelAdmin(admin.ModelAdmin):
41
46
  # API for subclasses
42
47
  # ----------------------------------------------------------------------------------------------
43
48
  def start_bgtask(self, name, **kwargs):
44
- bgtask = BackgroundTask.objects.create(
45
- name=name,
46
- namespace=self._bgtask_namespace,
47
- **kwargs,
48
- )
49
+ bgtask = self._create_bgtask(name=name, **kwargs)
49
50
  bgtask.start()
50
51
  return bgtask
51
52
 
53
+ def queue_bgtask(self, name, **kwargs):
54
+ bgtask = self._create_bgtask(name=name, **kwargs)
55
+ bgtask.queue()
56
+ return bgtask
57
+
52
58
  # ----------------------------------------------------------------------------------------------
53
59
  # Superclass overrides
54
60
  # ----------------------------------------------------------------------------------------------
@@ -78,6 +84,12 @@ class BGTaskModelAdmin(admin.ModelAdmin):
78
84
 
79
85
  next_action = next_action.__wrapped__
80
86
 
87
+ def _create_bgtask(self, **kwargs):
88
+ return BackgroundTask.objects.create(
89
+ namespace=self._bgtask_namespace,
90
+ **kwargs,
91
+ )
92
+
81
93
  def _admin_bg_tasks(self, request):
82
94
  task_name_to_desc = {}
83
95
  for action, action_name, action_description in self.get_actions(request).values():
@@ -100,12 +112,16 @@ class BGTaskModelAdmin(admin.ModelAdmin):
100
112
  Q(state=BackgroundTask.STATES.running)
101
113
  & Q(started_at__gt=timezone.now() - timedelta(days=1))
102
114
  )
115
+ | (
116
+ Q(state=BackgroundTask.STATES.queued)
117
+ & Q(queued_at__gt=timezone.now() - timedelta(days=1))
118
+ )
103
119
  | (
104
120
  ~Q(state=BackgroundTask.STATES.not_started)
105
121
  & Q(completed_at__gt=timezone.now() - timedelta(hours=2))
106
122
  )
107
123
  )
108
- .order_by("-started_at")
124
+ .order_by("-started_at", "-queued_at")
109
125
  )
110
126
  for bgt in bgts:
111
127
  bgt.admin_description = task_name_to_desc[bgt.name]
@@ -41,10 +41,11 @@ def locked(meth):
41
41
  def only_if_state(state):
42
42
  def only_if_state_decorator(meth):
43
43
  def only_if_state_wrapper(self, *args, **kwargs):
44
- if self.state != state:
44
+ states = (state,) if isinstance(state, str) else tuple(state)
45
+ if self.state not in states:
45
46
  raise RuntimeError(
46
- "%s cannot execute %s as in state %s not %s"
47
- % (self, meth.__name__, self.state, state)
47
+ "%s cannot execute %s as in state %s not one of %s"
48
+ % (self, meth.__name__, self.state, states)
48
49
  )
49
50
  return meth(self, *args, **kwargs)
50
51
 
@@ -72,7 +73,7 @@ class BackgroundTask(models.Model):
72
73
  ),
73
74
  )
74
75
 
75
- STATES = Choices("not_started", "running", "success", "partial_success", "failed")
76
+ STATES = Choices("not_started", "queued", "running", "success", "partial_success", "failed")
76
77
  state = models.CharField(max_length=16, default=STATES.not_started, choices=STATES)
77
78
  steps_to_complete = models.PositiveIntegerField(
78
79
  null=True, blank=True, help_text="The number of steps in the task for it to be completed."
@@ -81,6 +82,7 @@ class BackgroundTask(models.Model):
81
82
  null=True, blank=True, help_text="The number of steps completed so far by this task"
82
83
  )
83
84
 
85
+ queued_at = models.DateTimeField(null=True, blank=True)
84
86
  started_at = models.DateTimeField(null=True, blank=True)
85
87
  completed_at = models.DateTimeField(null=True, blank=True)
86
88
  result = models.JSONField(null=True, blank=True, help_text="The result(s) of the task, if any")
@@ -104,7 +106,12 @@ class BackgroundTask(models.Model):
104
106
  @property
105
107
  def task_dict(self):
106
108
  task_dict = model_to_dict(self)
107
- return {"id": str(self.id), "updated": self.updated.isoformat(), **task_dict}
109
+ return {
110
+ "id": str(self.id),
111
+ "updated": self.updated.isoformat(),
112
+ "position_in_queue": self.get_position_in_queue(),
113
+ **task_dict,
114
+ }
108
115
 
109
116
  @property
110
117
  def num_failed_steps(self):
@@ -114,6 +121,28 @@ class BackgroundTask(models.Model):
114
121
  def incomplete(self):
115
122
  return self.state in [self.STATES.not_started, self.STATES.running]
116
123
 
124
+ def get_position_in_queue(self):
125
+ if self.state != self.STATES.queued:
126
+ return None
127
+
128
+ if (
129
+ BackgroundTask.objects.filter(
130
+ queued_at__gt=self.queued_at,
131
+ )
132
+ .exclude(state=self.STATES.not_started)
133
+ .exclude(state=self.STATES.queued)
134
+ .exists()
135
+ ):
136
+ # More recent tasks have started, assume we're next (either that or we're stuck)
137
+ return 0
138
+
139
+ # Otherwise approximate queue position as the number of tasks that were queued before us
140
+ # and are still in the queue. So if there is nothing in front of us we are 0 in the queue.
141
+ assert self.queued_at
142
+ return BackgroundTask.objects.filter(
143
+ queued_at__lt=self.queued_at, state=self.STATES.queued
144
+ ).count()
145
+
117
146
  @contextmanager
118
147
  def runs_single_step(self):
119
148
  try:
@@ -134,8 +163,16 @@ class BackgroundTask(models.Model):
134
163
 
135
164
  @locked
136
165
  @only_if_state(STATES.not_started)
166
+ def queue(self):
167
+ log.info("Background Task queueing: %s", self.id)
168
+ self.state = self.STATES.queued
169
+ self.queued_at = timezone.now()
170
+ self.save()
171
+
172
+ @locked
173
+ @only_if_state((STATES.not_started, STATES.queued))
137
174
  def start(self):
138
- log.info("%s starting", self)
175
+ log.info("Background Task starting: %s", self.id)
139
176
  self.state = self.STATES.running
140
177
  self.started_at = timezone.now()
141
178
  self.save()
@@ -144,7 +181,7 @@ class BackgroundTask(models.Model):
144
181
  @only_if_state(STATES.running)
145
182
  def fail(self, exc):
146
183
  """Call to indicate a complete and final failure of the task"""
147
- log.info("%s failed: %s", self, exc)
184
+ log.info("Background Task failed: %s %s", self.id, exc)
148
185
  self.state = self.STATES.failed
149
186
  self.completed_at = timezone.now()
150
187
  self.errors.append(
@@ -2,6 +2,7 @@ window.BG_TASK_ADDED = true;
2
2
 
3
3
  const STATE_TO_EMOJI = {
4
4
  "not_started": "⏱️",
5
+ "queued": "⋯",
5
6
  "running": "🏃🏽‍♀️",
6
7
  "success": "✅",
7
8
  "partial_success": "⚠️",
@@ -201,10 +202,14 @@ class TaskProgressDiv {
201
202
  this._showProgress();
202
203
  this._hideState();
203
204
  break;
205
+ case "queued":
206
+ this._hideProgress();
207
+ this._showState();
208
+ break;
204
209
  }
205
210
 
206
211
  const isOutOfDate = (
207
- !["success", "partial_success", "not_started", "failed"].includes(task.state)
212
+ !["success", "queued", "partial_success", "not_started", "failed"].includes(task.state)
208
213
  && (new Date() - task.updated) > OUT_OF_DATE_PERIOD_S * 1000
209
214
  );
210
215
 
@@ -212,11 +217,21 @@ class TaskProgressDiv {
212
217
  this._addTitle("This task has not been updated for a while");
213
218
  }
214
219
 
215
- this.stateEle.innerHTML = `${STATE_TO_EMOJI[task.state] || "❓"}`;
220
+ this._setStateEmoji(task);
216
221
 
217
222
  this.pgstate.update({ max: task.steps_to_complete, value: task.steps_completed, isOutOfDate });
218
223
  }
219
224
 
225
+ _setStateEmoji(task) {
226
+ this.stateEle.innerHTML = `${STATE_TO_EMOJI[task.state] || "❓"}`;
227
+
228
+ switch (task.state) {
229
+ case "queued":
230
+ this.stateEle.innerHTML += ` (${task.position_in_queue})`;
231
+ break;
232
+ }
233
+ }
234
+
220
235
  _addTitle(title) {
221
236
  if (!this.div.title) {
222
237
  this.div.title = title;
@@ -445,7 +460,7 @@ class BGTaskPoller {
445
460
  for (const cbk of (this.taskCallbacks[task.id] || [])) {
446
461
  cbk(task);
447
462
  }
448
- if (task.state !== "running") {
463
+ if (!['running', 'queued'].includes(task.state)) {
449
464
  this.stopMonitoringTask(task.id);
450
465
  }
451
466
  }
@@ -7,7 +7,6 @@
7
7
  {% endblock %}
8
8
 
9
9
  {% block object-tools %}
10
- {{ block.super }}
11
10
  <div style="margin-bottom: 10px;">
12
11
  <h3>Action background tasks</h3>
13
12
  {% if not admin_bg_tasks|length %}
@@ -20,12 +19,16 @@
20
19
  <tbody>
21
20
  {% for bgt in admin_bg_tasks %}
22
21
  {% with bgtask=bgt.task_dict %}
23
- <tr class="bgtask-row"><td><a href="{% url 'admin:bgtask_backgroundtask_change' bgtask.id %}">{{ bgt.admin_description }}</a></td><td>{{ bgt.started_at|timesince }} ago</td><td>{% include "bgtask/bg_completed_column.html" %}</td><td>
24
- {% include "bgtask/bg_changelist_status_column.html" %}</td></tr>
22
+ <tr class="bgtask-row">
23
+ <td><a href="{% url 'admin:bgtask_backgroundtask_change' bgtask.id %}">{{ bgt.admin_description }}</a></td>
24
+ <td>{{ bgt.started_at|timesince }} ago</td><td>{% include "bgtask/bg_completed_column.html" %}</td>
25
+ <td>{% include "bgtask/bg_changelist_status_column.html" %}</td>
26
+ </tr>
25
27
  {% endwith %}
26
28
  {% endfor %}
27
29
  </tbody>
28
30
  </table>
29
31
  {% endif %}
30
32
  </div>
33
+ {{ block.super }}
31
34
  {% endblock %}
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "django-admin-background-task"
7
- version = "0.3.1"
7
+ version = "0.4.0"
8
8
  description = "A set of tools for django apps to persist and monitor the status of background tasks"
9
9
  authors = [
10
10
  "David Park <david@greenparksoftware.co.uk>",
@@ -36,6 +36,8 @@ black = ">=24.2.0"
36
36
  # Sphinx dependencies also in docs/requirements.txt for read the docs to pick up.
37
37
  sphinx = ">=7.1.2"
38
38
  sphinx-rtd-theme = ">=1.3.0"
39
+ psycopg2 = ">=2.9.10"
40
+ colorlog = ">=6.9.0"
39
41
 
40
42
  [tool.poetry.extras]
41
43
  # ℹ️ if you update any of these, be sure to update "all = [...]" as well, so that it's easy to get
@@ -1,11 +0,0 @@
1
- from utils.stack_context import stack_context
2
-
3
- from .models import BackgroundTask
4
-
5
-
6
- __all__ = ["bgtask_context"]
7
-
8
-
9
- @stack_context(logging_param_name="bgtask_id")
10
- def bgtask_context(id):
11
- return BackgroundTask.objects.get(id=id)