django-admin-background-task 0.4.1__py3-none-any.whl → 0.4.4__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/model_admin.py +2 -1
- bgtask/models.py +63 -61
- bgtask/templates/bgtask/admin/change_list.html +1 -1
- bgtask/utils.py +56 -0
- bgtask/views.py +7 -12
- {django_admin_background_task-0.4.1.dist-info → django_admin_background_task-0.4.4.dist-info}/METADATA +1 -1
- {django_admin_background_task-0.4.1.dist-info → django_admin_background_task-0.4.4.dist-info}/RECORD +9 -8
- {django_admin_background_task-0.4.1.dist-info → django_admin_background_task-0.4.4.dist-info}/LICENSE +0 -0
- {django_admin_background_task-0.4.1.dist-info → django_admin_background_task-0.4.4.dist-info}/WHEEL +0 -0
bgtask/model_admin.py
CHANGED
@@ -103,7 +103,7 @@ class BGTaskModelAdmin(admin.ModelAdmin):
|
|
103
103
|
if not task_name_to_desc:
|
104
104
|
return BackgroundTask.objects.none()
|
105
105
|
|
106
|
-
bgts =
|
106
|
+
bgts = (
|
107
107
|
BackgroundTask.objects.filter(
|
108
108
|
name__in=task_name_to_desc, namespace=self._bgtask_namespace
|
109
109
|
)
|
@@ -123,6 +123,7 @@ class BGTaskModelAdmin(admin.ModelAdmin):
|
|
123
123
|
)
|
124
124
|
.order_by("-started_at", "-queued_at")
|
125
125
|
)
|
126
|
+
bgts.add_position_in_queue()
|
126
127
|
for bgt in bgts:
|
127
128
|
bgt.admin_description = task_name_to_desc[bgt.name]
|
128
129
|
|
bgtask/models.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
import
|
1
|
+
import collections
|
2
2
|
import logging
|
3
3
|
import os
|
4
4
|
import time
|
@@ -8,63 +8,52 @@ from contextlib import contextmanager
|
|
8
8
|
|
9
9
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
10
10
|
from django.contrib.contenttypes.models import ContentType
|
11
|
-
from django.db import models
|
11
|
+
from django.db import models
|
12
12
|
from django.forms.models import model_to_dict
|
13
13
|
from django.utils import timezone
|
14
14
|
|
15
15
|
from model_utils import Choices
|
16
16
|
|
17
|
-
|
18
|
-
log = logging.getLogger(__name__)
|
17
|
+
from .utils import locked, only_if_state, q_or
|
19
18
|
|
20
19
|
|
21
|
-
|
22
|
-
@functools.wraps(meth)
|
23
|
-
def _locked_meth(self, *args, **kwargs):
|
24
|
-
if getattr(self, "_locked", False):
|
25
|
-
return meth(self, *args, **kwargs)
|
20
|
+
log = logging.getLogger(__name__)
|
26
21
|
|
27
|
-
with transaction.atomic():
|
28
|
-
BackgroundTask.objects.filter(id=self.id).select_for_update().only("id").get()
|
29
|
-
self.refresh_from_db()
|
30
22
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
23
|
+
class BackgroundTaskQuerySet(models.QuerySet):
|
24
|
+
def add_position_in_queue(self):
|
25
|
+
"""This evaluates the queryset and adds position_in_queue to each one (which requires
|
26
|
+
more DB queries).
|
27
|
+
"""
|
28
|
+
recent_unqueued_task_by_nsn = {(task.namespace, task.name): None for task in self}
|
29
|
+
for ns, name in recent_unqueued_task_by_nsn:
|
30
|
+
recent_unqueued_task_by_nsn[(ns, name)] = BackgroundTask.most_recently_unqueued_task(
|
31
|
+
ns, name
|
32
|
+
)
|
37
33
|
|
38
|
-
|
34
|
+
queued_tasks_by_nsn = BackgroundTask.queued_tasks_in_order_by_nsn_like(self)
|
39
35
|
|
36
|
+
for task in self:
|
37
|
+
if task.state != task.STATES.queued:
|
38
|
+
continue
|
40
39
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
states = (state,) if isinstance(state, str) else tuple(state)
|
47
|
-
if self.state not in states:
|
48
|
-
raise RuntimeError(
|
49
|
-
"%s cannot execute %s as in state %s not one of %s"
|
50
|
-
% (self, meth.__name__, self.state, states)
|
51
|
-
)
|
52
|
-
return meth(self, *args, **kwargs)
|
40
|
+
unqueued_task = recent_unqueued_task_by_nsn[(task.namespace, task.name)]
|
41
|
+
if unqueued_task is not None and task.queued_at < unqueued_task.queued_at:
|
42
|
+
# More recently queued task has already been dispatched so we should be going...
|
43
|
+
task.position_in_queue = 0
|
44
|
+
continue
|
53
45
|
|
54
|
-
|
46
|
+
task.position_in_queue = 0
|
47
|
+
for queued_task in queued_tasks_by_nsn[(task.namespace, task.name)]:
|
48
|
+
if queued_task.queued_at >= task.queued_at:
|
49
|
+
break
|
50
|
+
task.position_in_queue += 1
|
55
51
|
|
56
|
-
|
52
|
+
return self
|
57
53
|
|
58
54
|
|
59
55
|
class BackgroundTask(models.Model):
|
60
56
|
id = models.UUIDField(primary_key=True, editable=False, default=uuid.uuid4)
|
61
|
-
name = models.CharField(
|
62
|
-
max_length=1000,
|
63
|
-
help_text=(
|
64
|
-
"Name (or type) of this task, is not unique "
|
65
|
-
"per task instance but generally per task functionality"
|
66
|
-
),
|
67
|
-
)
|
68
57
|
namespace = models.CharField(
|
69
58
|
max_length=1000,
|
70
59
|
default="",
|
@@ -74,6 +63,13 @@ class BackgroundTask(models.Model):
|
|
74
63
|
"entire codebase, allowing them to be shorter and human readable"
|
75
64
|
),
|
76
65
|
)
|
66
|
+
name = models.CharField(
|
67
|
+
max_length=1000,
|
68
|
+
help_text=(
|
69
|
+
"Name (or type) of this task, is not unique "
|
70
|
+
"per task instance but generally per task functionality"
|
71
|
+
),
|
72
|
+
)
|
77
73
|
|
78
74
|
STATES = Choices("not_started", "queued", "running", "success", "partial_success", "failed")
|
79
75
|
state = models.CharField(max_length=16, default=STATES.not_started, choices=STATES)
|
@@ -102,16 +98,22 @@ class BackgroundTask(models.Model):
|
|
102
98
|
created = models.DateTimeField(auto_now_add=True)
|
103
99
|
updated = models.DateTimeField(auto_now=True)
|
104
100
|
|
101
|
+
objects = models.Manager.from_queryset(BackgroundTaskQuerySet)()
|
102
|
+
|
105
103
|
class Meta:
|
106
104
|
ordering = ["created", "id"]
|
107
105
|
|
106
|
+
# This needs to be added dynamically to model instances, and is done by
|
107
|
+
# BackgroundTaskQuerySet.add_position_in_queue()
|
108
|
+
position_in_queue = None
|
109
|
+
|
108
110
|
@property
|
109
111
|
def task_dict(self):
|
110
112
|
task_dict = model_to_dict(self)
|
111
113
|
return {
|
112
114
|
"id": str(self.id),
|
113
115
|
"updated": self.updated.isoformat(),
|
114
|
-
"position_in_queue": self.
|
116
|
+
"position_in_queue": self.position_in_queue,
|
115
117
|
**task_dict,
|
116
118
|
}
|
117
119
|
|
@@ -123,27 +125,27 @@ class BackgroundTask(models.Model):
|
|
123
125
|
def incomplete(self):
|
124
126
|
return self.state in [self.STATES.not_started, self.STATES.running]
|
125
127
|
|
126
|
-
|
127
|
-
|
128
|
-
|
128
|
+
@classmethod
|
129
|
+
def most_recently_unqueued_task(cls, namespace, name):
|
130
|
+
return (
|
131
|
+
cls.objects.filter(queued_at__isnull=False, namespace=namespace, name=name)
|
132
|
+
.exclude(state=cls.STATES.not_started)
|
133
|
+
.exclude(state=cls.STATES.queued)
|
134
|
+
.order_by("-queued_at")
|
135
|
+
.first()
|
136
|
+
)
|
129
137
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
.
|
136
|
-
.
|
138
|
+
@classmethod
|
139
|
+
def queued_tasks_in_order_by_nsn_like(cls, tasks):
|
140
|
+
queued_by_nsn = collections.defaultdict(list)
|
141
|
+
nsns = {(task.namespace, task.name) for task in tasks}
|
142
|
+
for task in (
|
143
|
+
cls.objects.filter(queued_at__isnull=False, state=cls.STATES.queued)
|
144
|
+
.filter(q_or(models.Q(namespace=nsn[0], name=nsn[1]) for nsn in nsns))
|
145
|
+
.order_by("queued_at")
|
137
146
|
):
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
# Otherwise approximate queue position as the number of tasks that were queued before us
|
142
|
-
# and are still in the queue. So if there is nothing in front of us we are 0 in the queue.
|
143
|
-
assert self.queued_at
|
144
|
-
return BackgroundTask.objects.filter(
|
145
|
-
queued_at__lt=self.queued_at, state=self.STATES.queued
|
146
|
-
).count()
|
147
|
+
queued_by_nsn[(task.namespace, task.name)].append(task)
|
148
|
+
return queued_by_nsn
|
147
149
|
|
148
150
|
def set_steps_to_complete(self, steps_to_complete):
|
149
151
|
self.steps_to_complete = steps_to_complete
|
@@ -199,7 +201,7 @@ class BackgroundTask(models.Model):
|
|
199
201
|
self.save()
|
200
202
|
|
201
203
|
@locked
|
202
|
-
@only_if_state(STATES.running)
|
204
|
+
@only_if_state((STATES.queued, STATES.running))
|
203
205
|
def fail(self, exc):
|
204
206
|
"""Call to indicate a complete and final failure of the task"""
|
205
207
|
log.info("Background Task failed: %s %s", self.id, exc)
|
@@ -12,7 +12,7 @@
|
|
12
12
|
{% if not admin_bg_tasks|length %}
|
13
13
|
<p class="help" style="font-style: italic;">No recent background tasks</p>
|
14
14
|
{% else %}
|
15
|
-
<table>
|
15
|
+
<table style="max-height: 300px; display: block; overflow-y: scroll;">
|
16
16
|
<thead>
|
17
17
|
<tr><th>Task name</th><th>Started</th><th>Finished</th><th>Status</th></tr>
|
18
18
|
</thead>
|
bgtask/utils.py
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
import functools
|
2
|
+
import operator
|
3
|
+
from typing import Iterable
|
4
|
+
|
5
|
+
from django.db import models, transaction
|
6
|
+
|
7
|
+
|
8
|
+
# https://stackoverflow.com/questions/29900386/how-to-construct-django-q-object-matching-none
|
9
|
+
Q_NONE = models.Q(pk__in=[])
|
10
|
+
|
11
|
+
|
12
|
+
def q_or(q_objects: Iterable[models.Q]):
|
13
|
+
"""Chain an iterable of Q with OR.
|
14
|
+
|
15
|
+
The more Q()s are passed, the more things are filtered in, so it
|
16
|
+
makes sense if nothing is passed nothing is filtered in.
|
17
|
+
"""
|
18
|
+
return functools.reduce(operator.or_, q_objects, models.Q()) or Q_NONE
|
19
|
+
|
20
|
+
|
21
|
+
def locked(meth):
|
22
|
+
@functools.wraps(meth)
|
23
|
+
def _locked_meth(self, *args, **kwargs):
|
24
|
+
if getattr(self, "_locked", False):
|
25
|
+
return meth(self, *args, **kwargs)
|
26
|
+
|
27
|
+
with transaction.atomic():
|
28
|
+
type(self).objects.filter(id=self.id).select_for_update().only("id").get()
|
29
|
+
self.refresh_from_db()
|
30
|
+
|
31
|
+
# Mark as locked in case we are called recursively
|
32
|
+
self._locked = True
|
33
|
+
try:
|
34
|
+
return meth(self, *args, **kwargs)
|
35
|
+
finally:
|
36
|
+
self._locked = False
|
37
|
+
|
38
|
+
return _locked_meth
|
39
|
+
|
40
|
+
|
41
|
+
def only_if_state(state, no_op_states=frozenset()):
|
42
|
+
def only_if_state_decorator(meth):
|
43
|
+
def only_if_state_wrapper(self, *args, no_op_if_already_in_state=False, **kwargs):
|
44
|
+
if self.state in no_op_states:
|
45
|
+
return
|
46
|
+
states = (state,) if isinstance(state, str) else tuple(state)
|
47
|
+
if self.state not in states:
|
48
|
+
raise RuntimeError(
|
49
|
+
"%s cannot execute %s as in state %s not one of %s"
|
50
|
+
% (self, meth.__name__, self.state, states)
|
51
|
+
)
|
52
|
+
return meth(self, *args, **kwargs)
|
53
|
+
|
54
|
+
return only_if_state_wrapper
|
55
|
+
|
56
|
+
return only_if_state_decorator
|
bgtask/views.py
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
from django.core.exceptions import ValidationError
|
2
2
|
from django.db.models import Q
|
3
|
-
from django.forms.models import model_to_dict
|
4
3
|
from django.http import Http404, HttpResponseBadRequest, JsonResponse
|
5
4
|
from django.shortcuts import render
|
6
5
|
|
@@ -31,18 +30,12 @@ def background_tasks_view(request):
|
|
31
30
|
tasks = request.GET.get("tasks", "")
|
32
31
|
object_id = request.GET.get("object_id", None)
|
33
32
|
if tasks is None and object_id is None:
|
34
|
-
return HttpResponseBadRequest(
|
35
|
-
|
36
|
-
|
33
|
+
return HttpResponseBadRequest("Must pass 'tasks' or 'object_id' as a query parameter")
|
34
|
+
task_ids = tasks.split(",")
|
35
|
+
task_ids_q = Q(id__in=task_ids) if tasks else Q_NONE
|
36
|
+
object_id_q = Q(acted_on_object_id=object_id) if object_id is not None else Q_NONE
|
37
|
+
tasks = BackgroundTask.objects.filter(task_ids_q | object_id_q).order_by("-created")
|
37
38
|
try:
|
38
|
-
task_ids = tasks.split(",")
|
39
|
-
task_ids_q = Q(id__in=task_ids) if tasks else Q_NONE
|
40
|
-
object_id_q = (
|
41
|
-
Q(acted_on_object_id=object_id) if object_id is not None else Q_NONE
|
42
|
-
)
|
43
|
-
tasks = BackgroundTask.objects.filter(task_ids_q | object_id_q).order_by(
|
44
|
-
"-created"
|
45
|
-
)
|
46
39
|
if len(tasks) == 0:
|
47
40
|
raise ValidationError("Unfound tasks")
|
48
41
|
except ValidationError:
|
@@ -52,6 +45,8 @@ def background_tasks_view(request):
|
|
52
45
|
|
53
46
|
accepts = request.headers.get("Accept", "").split(",")
|
54
47
|
|
48
|
+
tasks.add_position_in_queue()
|
49
|
+
|
55
50
|
if "application/json" in accepts:
|
56
51
|
return background_tasks_view_json(tasks)
|
57
52
|
|
{django_admin_background_task-0.4.1.dist-info → django_admin_background_task-0.4.4.dist-info}/RECORD
RENAMED
@@ -8,11 +8,11 @@ bgtask/migrations/0001_initial.py,sha256=TPuplGYJtqHJ7h4-A-nINMmvRD9HYvfhOY4bZyi
|
|
8
8
|
bgtask/migrations/0002_backgroundtask_namespace_alter_backgroundtask_name.py,sha256=XIG_gIvdA73tz0NRbNkGU_ieKde3ZtQltPeLdslD5Xo,968
|
9
9
|
bgtask/migrations/0003_backgroundtask_queued_at.py,sha256=Hp0WeQLKP0R5jNeEAgVlsx-7jgIzbnIkqkPUGsenK8o,439
|
10
10
|
bgtask/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
11
|
-
bgtask/model_admin.py,sha256=
|
12
|
-
bgtask/models.py,sha256=
|
11
|
+
bgtask/model_admin.py,sha256=Of0Le1IV3482HurWyi-FB2TquuRXtDxDK0tEtc-OFtE,5090
|
12
|
+
bgtask/models.py,sha256=QYc3N3uiy396cX6RThL4yxhjvErbOtXe1smF4OmZw_o,11633
|
13
13
|
bgtask/static/bgtask/js/bgtask-once.js,sha256=DBerpxPLP21IH--HpBxVFk3kN52FtBiuyJsDCjg7mCA,746
|
14
14
|
bgtask/static/bgtask/js/bgtask.js,sha256=6fVepM6AY_a3-I5ZABfaCaf7_g8IkClOFq9ZxG9GsX4,15366
|
15
|
-
bgtask/templates/bgtask/admin/change_list.html,sha256=
|
15
|
+
bgtask/templates/bgtask/admin/change_list.html,sha256=cb5v5jkTsHgU7cZRQchxra0aXUXAvlBlxPZ0ZjLQPNc,1143
|
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
|
@@ -22,8 +22,9 @@ bgtask/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSu
|
|
22
22
|
bgtask/templatetags/bgtask.py,sha256=WUEWlQX5AarbXJBV6EGp-khT1zegVwGNigSKRF5mCTE,413
|
23
23
|
bgtask/tests/test_bgtask.py,sha256=pepgFBvRWNqmOdOFPkp4MC_ctWmYPYGpisb94OUUA9I,1882
|
24
24
|
bgtask/urls.py,sha256=DXXjgj18LTSCq2-xZf23-pkgO4RmZglv67mgrp-fDyk,161
|
25
|
-
bgtask/
|
26
|
-
|
27
|
-
django_admin_background_task-0.4.
|
28
|
-
django_admin_background_task-0.4.
|
29
|
-
django_admin_background_task-0.4.
|
25
|
+
bgtask/utils.py,sha256=lc3V276dEper-3d5xgwTdsfmsKOLv9KShrPkFCkPzlg,1789
|
26
|
+
bgtask/views.py,sha256=O-8J8VBLXnbBkbDseu-5y0hGt3aU5r-awsTq8Rdg7mc,1670
|
27
|
+
django_admin_background_task-0.4.4.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
28
|
+
django_admin_background_task-0.4.4.dist-info/METADATA,sha256=oEDjQzm3WcRtozEnQMTHqo8qVbdeJDrgDtY06UlCsNg,1379
|
29
|
+
django_admin_background_task-0.4.4.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
30
|
+
django_admin_background_task-0.4.4.dist-info/RECORD,,
|
File without changes
|
{django_admin_background_task-0.4.1.dist-info → django_admin_background_task-0.4.4.dist-info}/WHEEL
RENAMED
File without changes
|