django-admin-background-task 0.3.2__py3-none-any.whl → 0.4.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.
- bgtask/backends/thread_pool.py +2 -2
- bgtask/migrations/0001_initial.py +1 -1
- bgtask/migrations/0003_backgroundtask_queued_at.py +18 -0
- bgtask/model_admin.py +23 -7
- bgtask/models.py +44 -7
- bgtask/static/bgtask/js/bgtask.js +18 -3
- bgtask/templates/bgtask/admin/change_list.html +5 -2
- {django_admin_background_task-0.3.2.dist-info → django_admin_background_task-0.4.0.dist-info}/METADATA +1 -1
- {django_admin_background_task-0.3.2.dist-info → django_admin_background_task-0.4.0.dist-info}/RECORD +11 -11
- bgtask/stack_contexts.py +0 -11
- {django_admin_background_task-0.3.2.dist-info → django_admin_background_task-0.4.0.dist-info}/LICENSE +0 -0
- {django_admin_background_task-0.3.2.dist-info → django_admin_background_task-0.4.0.dist-info}/WHEEL +0 -0
bgtask/backends/thread_pool.py
CHANGED
@@ -4,9 +4,9 @@ from concurrent.futures import ThreadPoolExecutor
|
|
4
4
|
SHARED_THREAD_POOL = None
|
5
5
|
|
6
6
|
|
7
|
-
def dispatch(func,
|
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,
|
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
|
+
]
|
bgtask/model_admin.py
CHANGED
@@ -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 =
|
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]
|
bgtask/models.py
CHANGED
@@ -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
|
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,
|
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 {
|
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
|
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("
|
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.
|
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
|
463
|
+
if (!['running', 'queued'].includes(task.state)) {
|
449
464
|
this.stopMonitoringTask(task.id);
|
450
465
|
}
|
451
466
|
}
|
@@ -19,8 +19,11 @@
|
|
19
19
|
<tbody>
|
20
20
|
{% for bgt in admin_bg_tasks %}
|
21
21
|
{% with bgtask=bgt.task_dict %}
|
22
|
-
<tr class="bgtask-row"
|
23
|
-
|
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>
|
24
27
|
{% endwith %}
|
25
28
|
{% endfor %}
|
26
29
|
</tbody>
|
{django_admin_background_task-0.3.2.dist-info → django_admin_background_task-0.4.0.dist-info}/RECORD
RENAMED
@@ -2,17 +2,17 @@ bgtask/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
bgtask/admin.py,sha256=cOhbR__7WJ3rmjlbZjGKh_gJDpEz_c6Ll2zwHo7mpiM,965
|
3
3
|
bgtask/apps.py,sha256=zZHsNaW9nnJgK7jnpty0IjM0TTpsk33xhpsGTa-x3iE,144
|
4
4
|
bgtask/backends/__init__.py,sha256=zZlL7gHXL12heAhHD_7D7lEBihCE-LHYb07yIbk055s,57
|
5
|
-
bgtask/backends/thread_pool.py,sha256=
|
5
|
+
bgtask/backends/thread_pool.py,sha256=KD_RccLR1fSiPZGI_fbX4Rcic6mE21VUdrUkusuI9Zk,286
|
6
6
|
bgtask/decorators.py,sha256=UsiCBhqaWm1Yn4n4hmFbauGQvNezoeDm7Iqpjbvxbjk,1119
|
7
|
-
bgtask/migrations/0001_initial.py,sha256=
|
7
|
+
bgtask/migrations/0001_initial.py,sha256=TPuplGYJtqHJ7h4-A-nINMmvRD9HYvfhOY4bZyinz2c,2035
|
8
8
|
bgtask/migrations/0002_backgroundtask_namespace_alter_backgroundtask_name.py,sha256=XIG_gIvdA73tz0NRbNkGU_ieKde3ZtQltPeLdslD5Xo,968
|
9
|
+
bgtask/migrations/0003_backgroundtask_queued_at.py,sha256=Hp0WeQLKP0R5jNeEAgVlsx-7jgIzbnIkqkPUGsenK8o,439
|
9
10
|
bgtask/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
|
-
bgtask/model_admin.py,sha256=
|
11
|
-
bgtask/models.py,sha256=
|
12
|
-
bgtask/stack_contexts.py,sha256=2Hun6O5Mm949uAioC2E3i54Rg-_H8P7LsHdhJYc0w6E,231
|
11
|
+
bgtask/model_admin.py,sha256=loEh9uPUZbC19_daPoPfHNuLrI42oAA930J5IEH_1-w,5057
|
12
|
+
bgtask/models.py,sha256=meuAEcYP--YocGmQ8kVcvK7zCoom9vGSQX0EWiuSn3U,10566
|
13
13
|
bgtask/static/bgtask/js/bgtask-once.js,sha256=DBerpxPLP21IH--HpBxVFk3kN52FtBiuyJsDCjg7mCA,746
|
14
|
-
bgtask/static/bgtask/js/bgtask.js,sha256=
|
15
|
-
bgtask/templates/bgtask/admin/change_list.html,sha256=
|
14
|
+
bgtask/static/bgtask/js/bgtask.js,sha256=6fVepM6AY_a3-I5ZABfaCaf7_g8IkClOFq9ZxG9GsX4,15366
|
15
|
+
bgtask/templates/bgtask/admin/change_list.html,sha256=pxajrUXZvsBfSM4Lh3HLjdjGNNO7lJPaTkUwZ_zbWEc,1080
|
16
16
|
bgtask/templates/bgtask/bg_changelist_status_column.html,sha256=SA7EAL50oBrcRPGstXROXkFJ541vYA8RlHidM3srTPU,844
|
17
17
|
bgtask/templates/bgtask/bg_completed_column.html,sha256=Q4l3EQaOXHYVOzv0jpx3ww3Wa2Bb33RyZp3RSWI1zog,788
|
18
18
|
bgtask/templates/bgtask/bgtask_templates.html,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
@@ -23,7 +23,7 @@ bgtask/templatetags/bgtask.py,sha256=WUEWlQX5AarbXJBV6EGp-khT1zegVwGNigSKRF5mCTE
|
|
23
23
|
bgtask/tests/test_bgtask.py,sha256=9AsBppPphaIdphuB-pHuQjgLhYNp8ySWtmcWF2uQl3Q,1090
|
24
24
|
bgtask/urls.py,sha256=DXXjgj18LTSCq2-xZf23-pkgO4RmZglv67mgrp-fDyk,161
|
25
25
|
bgtask/views.py,sha256=tPblidBq8mIv9cbyAfzJfNlM-I-Z11XVoobdA3DZBVM,1765
|
26
|
-
django_admin_background_task-0.
|
27
|
-
django_admin_background_task-0.
|
28
|
-
django_admin_background_task-0.
|
29
|
-
django_admin_background_task-0.
|
26
|
+
django_admin_background_task-0.4.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
27
|
+
django_admin_background_task-0.4.0.dist-info/METADATA,sha256=8IjDh7pOq9dKgYCqPr_fbHgI4rZNmRDeUB2NApdmCT4,1375
|
28
|
+
django_admin_background_task-0.4.0.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
29
|
+
django_admin_background_task-0.4.0.dist-info/RECORD,,
|
bgtask/stack_contexts.py
DELETED
File without changes
|
{django_admin_background_task-0.3.2.dist-info → django_admin_background_task-0.4.0.dist-info}/WHEEL
RENAMED
File without changes
|