django-admin-background-task 0.4.0__tar.gz → 0.4.3__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.
- {django_admin_background_task-0.4.0 → django_admin_background_task-0.4.3}/PKG-INFO +2 -1
 - {django_admin_background_task-0.4.0 → django_admin_background_task-0.4.3}/README.md +1 -0
 - {django_admin_background_task-0.4.0 → django_admin_background_task-0.4.3}/bgtask/model_admin.py +2 -1
 - {django_admin_background_task-0.4.0 → django_admin_background_task-0.4.3}/bgtask/models.py +81 -58
 - {django_admin_background_task-0.4.0 → django_admin_background_task-0.4.3}/bgtask/templates/bgtask/admin/change_list.html +1 -1
 - {django_admin_background_task-0.4.0 → django_admin_background_task-0.4.3}/bgtask/tests/test_bgtask.py +37 -2
 - django_admin_background_task-0.4.3/bgtask/utils.py +56 -0
 - {django_admin_background_task-0.4.0 → django_admin_background_task-0.4.3}/bgtask/views.py +7 -12
 - {django_admin_background_task-0.4.0 → django_admin_background_task-0.4.3}/pyproject.toml +2 -1
 - {django_admin_background_task-0.4.0 → django_admin_background_task-0.4.3}/LICENSE +0 -0
 - {django_admin_background_task-0.4.0 → django_admin_background_task-0.4.3}/bgtask/__init__.py +0 -0
 - {django_admin_background_task-0.4.0 → django_admin_background_task-0.4.3}/bgtask/admin.py +0 -0
 - {django_admin_background_task-0.4.0 → django_admin_background_task-0.4.3}/bgtask/apps.py +0 -0
 - {django_admin_background_task-0.4.0 → django_admin_background_task-0.4.3}/bgtask/backends/__init__.py +0 -0
 - {django_admin_background_task-0.4.0 → django_admin_background_task-0.4.3}/bgtask/backends/thread_pool.py +0 -0
 - {django_admin_background_task-0.4.0 → django_admin_background_task-0.4.3}/bgtask/decorators.py +0 -0
 - {django_admin_background_task-0.4.0 → django_admin_background_task-0.4.3}/bgtask/migrations/0001_initial.py +0 -0
 - {django_admin_background_task-0.4.0 → django_admin_background_task-0.4.3}/bgtask/migrations/0002_backgroundtask_namespace_alter_backgroundtask_name.py +0 -0
 - {django_admin_background_task-0.4.0 → django_admin_background_task-0.4.3}/bgtask/migrations/0003_backgroundtask_queued_at.py +0 -0
 - {django_admin_background_task-0.4.0 → django_admin_background_task-0.4.3}/bgtask/migrations/__init__.py +0 -0
 - {django_admin_background_task-0.4.0 → django_admin_background_task-0.4.3}/bgtask/static/bgtask/js/bgtask-once.js +0 -0
 - {django_admin_background_task-0.4.0 → django_admin_background_task-0.4.3}/bgtask/static/bgtask/js/bgtask.js +0 -0
 - {django_admin_background_task-0.4.0 → django_admin_background_task-0.4.3}/bgtask/templates/bgtask/bg_changelist_status_column.html +0 -0
 - {django_admin_background_task-0.4.0 → django_admin_background_task-0.4.3}/bgtask/templates/bgtask/bg_completed_column.html +0 -0
 - {django_admin_background_task-0.4.0 → django_admin_background_task-0.4.3}/bgtask/templates/bgtask/bgtask_templates.html +0 -0
 - {django_admin_background_task-0.4.0 → django_admin_background_task-0.4.3}/bgtask/templates/bgtask/bgtask_view.html +0 -0
 - {django_admin_background_task-0.4.0 → django_admin_background_task-0.4.3}/bgtask/templates/bgtask/progress.html +0 -0
 - {django_admin_background_task-0.4.0 → django_admin_background_task-0.4.3}/bgtask/templatetags/__init__.py +0 -0
 - {django_admin_background_task-0.4.0 → django_admin_background_task-0.4.3}/bgtask/templatetags/bgtask.py +0 -0
 - {django_admin_background_task-0.4.0 → django_admin_background_task-0.4.3}/bgtask/urls.py +0 -0
 
| 
         @@ -1,6 +1,6 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            Metadata-Version: 2.1
         
     | 
| 
       2 
2 
     | 
    
         
             
            Name: django-admin-background-task
         
     | 
| 
       3 
     | 
    
         
            -
            Version: 0.4. 
     | 
| 
      
 3 
     | 
    
         
            +
            Version: 0.4.3
         
     | 
| 
       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
         
     | 
| 
         @@ -58,4 +58,5 @@ urlpatterns = [ 
     | 
|
| 
       58 
58 
     | 
    
         
             
            from bgtask.models import BackgroundTask
         
     | 
| 
       59 
59 
     | 
    
         | 
| 
       60 
60 
     | 
    
         
             
            BackgroundTask.new(name)
         
     | 
| 
      
 61 
     | 
    
         
            +
            ```
         
     | 
| 
       61 
62 
     | 
    
         | 
    
        {django_admin_background_task-0.4.0 → django_admin_background_task-0.4.3}/bgtask/model_admin.py
    RENAMED
    
    | 
         @@ -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 
     | 
    
         | 
| 
         @@ -1,4 +1,4 @@ 
     | 
|
| 
       1 
     | 
    
         
            -
            import  
     | 
| 
      
 1 
     | 
    
         
            +
            import collections
         
     | 
| 
       2 
2 
     | 
    
         
             
            import logging
         
     | 
| 
       3 
3 
     | 
    
         
             
            import os
         
     | 
| 
       4 
4 
     | 
    
         
             
            import time
         
     | 
| 
         @@ -8,61 +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 
     | 
    
         
            -
                            raise RuntimeError(
         
     | 
| 
       47 
     | 
    
         
            -
                                "%s cannot execute %s as in state %s not one of %s"
         
     | 
| 
       48 
     | 
    
         
            -
                                % (self, meth.__name__, self.state, states)
         
     | 
| 
       49 
     | 
    
         
            -
                            )
         
     | 
| 
       50 
     | 
    
         
            -
                        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
         
     | 
| 
       51 
45 
     | 
    
         | 
| 
       52 
     | 
    
         
            -
             
     | 
| 
      
 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
         
     | 
| 
       53 
51 
     | 
    
         | 
| 
       54 
     | 
    
         
            -
             
     | 
| 
      
 52 
     | 
    
         
            +
                    return self
         
     | 
| 
       55 
53 
     | 
    
         | 
| 
       56 
54 
     | 
    
         | 
| 
       57 
55 
     | 
    
         
             
            class BackgroundTask(models.Model):
         
     | 
| 
       58 
56 
     | 
    
         
             
                id = models.UUIDField(primary_key=True, editable=False, default=uuid.uuid4)
         
     | 
| 
       59 
     | 
    
         
            -
                name = models.CharField(
         
     | 
| 
       60 
     | 
    
         
            -
                    max_length=1000,
         
     | 
| 
       61 
     | 
    
         
            -
                    help_text=(
         
     | 
| 
       62 
     | 
    
         
            -
                        "Name (or type) of this task, is not unique "
         
     | 
| 
       63 
     | 
    
         
            -
                        "per task instance but generally per task functionality"
         
     | 
| 
       64 
     | 
    
         
            -
                    ),
         
     | 
| 
       65 
     | 
    
         
            -
                )
         
     | 
| 
       66 
57 
     | 
    
         
             
                namespace = models.CharField(
         
     | 
| 
       67 
58 
     | 
    
         
             
                    max_length=1000,
         
     | 
| 
       68 
59 
     | 
    
         
             
                    default="",
         
     | 
| 
         @@ -72,6 +63,13 @@ class BackgroundTask(models.Model): 
     | 
|
| 
       72 
63 
     | 
    
         
             
                        "entire codebase, allowing them to be shorter and human readable"
         
     | 
| 
       73 
64 
     | 
    
         
             
                    ),
         
     | 
| 
       74 
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 
     | 
    
         
            +
                )
         
     | 
| 
       75 
73 
     | 
    
         | 
| 
       76 
74 
     | 
    
         
             
                STATES = Choices("not_started", "queued", "running", "success", "partial_success", "failed")
         
     | 
| 
       77 
75 
     | 
    
         
             
                state = models.CharField(max_length=16, default=STATES.not_started, choices=STATES)
         
     | 
| 
         @@ -100,16 +98,22 @@ class BackgroundTask(models.Model): 
     | 
|
| 
       100 
98 
     | 
    
         
             
                created = models.DateTimeField(auto_now_add=True)
         
     | 
| 
       101 
99 
     | 
    
         
             
                updated = models.DateTimeField(auto_now=True)
         
     | 
| 
       102 
100 
     | 
    
         | 
| 
      
 101 
     | 
    
         
            +
                objects = models.Manager.from_queryset(BackgroundTaskQuerySet)()
         
     | 
| 
      
 102 
     | 
    
         
            +
             
     | 
| 
       103 
103 
     | 
    
         
             
                class Meta:
         
     | 
| 
       104 
104 
     | 
    
         
             
                    ordering = ["created", "id"]
         
     | 
| 
       105 
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 
     | 
    
         
            +
             
     | 
| 
       106 
110 
     | 
    
         
             
                @property
         
     | 
| 
       107 
111 
     | 
    
         
             
                def task_dict(self):
         
     | 
| 
       108 
112 
     | 
    
         
             
                    task_dict = model_to_dict(self)
         
     | 
| 
       109 
113 
     | 
    
         
             
                    return {
         
     | 
| 
       110 
114 
     | 
    
         
             
                        "id": str(self.id),
         
     | 
| 
       111 
115 
     | 
    
         
             
                        "updated": self.updated.isoformat(),
         
     | 
| 
       112 
     | 
    
         
            -
                        "position_in_queue": self. 
     | 
| 
      
 116 
     | 
    
         
            +
                        "position_in_queue": self.position_in_queue,
         
     | 
| 
       113 
117 
     | 
    
         
             
                        **task_dict,
         
     | 
| 
       114 
118 
     | 
    
         
             
                    }
         
     | 
| 
       115 
119 
     | 
    
         | 
| 
         @@ -121,27 +125,32 @@ class BackgroundTask(models.Model): 
     | 
|
| 
       121 
125 
     | 
    
         
             
                def incomplete(self):
         
     | 
| 
       122 
126 
     | 
    
         
             
                    return self.state in [self.STATES.not_started, self.STATES.running]
         
     | 
| 
       123 
127 
     | 
    
         | 
| 
       124 
     | 
    
         
            -
                 
     | 
| 
       125 
     | 
    
         
            -
             
     | 
| 
       126 
     | 
    
         
            -
             
     | 
| 
      
 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 
     | 
    
         
            +
                    )
         
     | 
| 
       127 
137 
     | 
    
         | 
| 
       128 
     | 
    
         
            -
             
     | 
| 
       129 
     | 
    
         
            -
             
     | 
| 
       130 
     | 
    
         
            -
             
     | 
| 
       131 
     | 
    
         
            -
             
     | 
| 
       132 
     | 
    
         
            -
             
     | 
| 
       133 
     | 
    
         
            -
                        . 
     | 
| 
       134 
     | 
    
         
            -
                        . 
     | 
| 
      
 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")
         
     | 
| 
       135 
146 
     | 
    
         
             
                    ):
         
     | 
| 
       136 
     | 
    
         
            -
                         
     | 
| 
       137 
     | 
    
         
            -
             
     | 
| 
      
 147 
     | 
    
         
            +
                        queued_by_nsn[(task.namespace, task.name)].append(task)
         
     | 
| 
      
 148 
     | 
    
         
            +
                    return queued_by_nsn
         
     | 
| 
       138 
149 
     | 
    
         | 
| 
       139 
     | 
    
         
            -
             
     | 
| 
       140 
     | 
    
         
            -
                     
     | 
| 
       141 
     | 
    
         
            -
                     
     | 
| 
       142 
     | 
    
         
            -
                     
     | 
| 
       143 
     | 
    
         
            -
                        queued_at__lt=self.queued_at, state=self.STATES.queued
         
     | 
| 
       144 
     | 
    
         
            -
                    ).count()
         
     | 
| 
      
 150 
     | 
    
         
            +
                def set_steps_to_complete(self, steps_to_complete):
         
     | 
| 
      
 151 
     | 
    
         
            +
                    self.steps_to_complete = steps_to_complete
         
     | 
| 
      
 152 
     | 
    
         
            +
                    self.steps_completed = 0
         
     | 
| 
      
 153 
     | 
    
         
            +
                    self.save()
         
     | 
| 
       145 
154 
     | 
    
         | 
| 
       146 
155 
     | 
    
         
             
                @contextmanager
         
     | 
| 
       147 
156 
     | 
    
         
             
                def runs_single_step(self):
         
     | 
| 
         @@ -161,6 +170,14 @@ class BackgroundTask(models.Model): 
     | 
|
| 
       161 
170 
     | 
    
         
             
                    else:
         
     | 
| 
       162 
171 
     | 
    
         
             
                        self.succeed()
         
     | 
| 
       163 
172 
     | 
    
         | 
| 
      
 173 
     | 
    
         
            +
                @contextmanager
         
     | 
| 
      
 174 
     | 
    
         
            +
                def fails_if_exception(self):
         
     | 
| 
      
 175 
     | 
    
         
            +
                    try:
         
     | 
| 
      
 176 
     | 
    
         
            +
                        yield
         
     | 
| 
      
 177 
     | 
    
         
            +
                    except Exception as exc:
         
     | 
| 
      
 178 
     | 
    
         
            +
                        self.fail(exc)
         
     | 
| 
      
 179 
     | 
    
         
            +
                        raise
         
     | 
| 
      
 180 
     | 
    
         
            +
             
     | 
| 
       164 
181 
     | 
    
         
             
                @locked
         
     | 
| 
       165 
182 
     | 
    
         
             
                @only_if_state(STATES.not_started)
         
     | 
| 
       166 
183 
     | 
    
         
             
                def queue(self):
         
     | 
| 
         @@ -170,7 +187,13 @@ class BackgroundTask(models.Model): 
     | 
|
| 
       170 
187 
     | 
    
         
             
                    self.save()
         
     | 
| 
       171 
188 
     | 
    
         | 
| 
       172 
189 
     | 
    
         
             
                @locked
         
     | 
| 
       173 
     | 
    
         
            -
                @only_if_state( 
     | 
| 
      
 190 
     | 
    
         
            +
                @only_if_state(
         
     | 
| 
      
 191 
     | 
    
         
            +
                    (STATES.not_started, STATES.queued),
         
     | 
| 
      
 192 
     | 
    
         
            +
                    # Allow start() to be called while running so that if a task's subtasks are queued and
         
     | 
| 
      
 193 
     | 
    
         
            +
                    # asynchronous each can call .start() independently so the first one that is executed will
         
     | 
| 
      
 194 
     | 
    
         
            +
                    # start the task.
         
     | 
| 
      
 195 
     | 
    
         
            +
                    no_op_states=(STATES.running,),
         
     | 
| 
      
 196 
     | 
    
         
            +
                )
         
     | 
| 
       174 
197 
     | 
    
         
             
                def start(self):
         
     | 
| 
       175 
198 
     | 
    
         
             
                    log.info("Background Task starting: %s", self.id)
         
     | 
| 
       176 
199 
     | 
    
         
             
                    self.state = self.STATES.running
         
     | 
| 
         @@ -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>
         
     | 
| 
         @@ -1,7 +1,11 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            import pytest
         
     | 
| 
       2 
2 
     | 
    
         | 
| 
      
 3 
     | 
    
         
            +
            from django.utils import timezone
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
       3 
5 
     | 
    
         
             
            from bgtask.models import BackgroundTask
         
     | 
| 
       4 
6 
     | 
    
         | 
| 
      
 7 
     | 
    
         
            +
            pytestmark = pytest.mark.django_db
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
       5 
9 
     | 
    
         | 
| 
       6 
10 
     | 
    
         
             
            @pytest.fixture
         
     | 
| 
       7 
11 
     | 
    
         
             
            def a_task():
         
     | 
| 
         @@ -30,6 +34,37 @@ def test_bgtask_immediate_failure(a_task): 
     | 
|
| 
       30 
34 
     | 
    
         
             
                assert a_task.state == BackgroundTask.STATES.failed
         
     | 
| 
       31 
35 
     | 
    
         | 
| 
       32 
36 
     | 
    
         
             
                assert len(a_task.errors) == 1
         
     | 
| 
       33 
     | 
    
         
            -
                assert a_task.errors[0]["datetime"] ==  
     | 
| 
      
 37 
     | 
    
         
            +
                assert a_task.errors[0]["datetime"] == a_task.completed_at.isoformat()
         
     | 
| 
       34 
38 
     | 
    
         
             
                assert a_task.errors[0]["error_message"] == "Some global failure"
         
     | 
| 
       35 
     | 
    
         
            -
                assert "test_bgtask_immediate_failure" in a_task.errors[0][" 
     | 
| 
      
 39 
     | 
    
         
            +
                assert "test_bgtask_immediate_failure" in a_task.errors[0]["traceback"]
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
            def test_bgtask_start_again(a_task):
         
     | 
| 
      
 43 
     | 
    
         
            +
                a_task.start()
         
     | 
| 
      
 44 
     | 
    
         
            +
                import time
         
     | 
| 
      
 45 
     | 
    
         
            +
                time.sleep(0.001)
         
     | 
| 
      
 46 
     | 
    
         
            +
             
     | 
| 
      
 47 
     | 
    
         
            +
                curr_started_at = a_task.started_at
         
     | 
| 
      
 48 
     | 
    
         
            +
                assert timezone.now() > curr_started_at
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
                # Starting again is no-op
         
     | 
| 
      
 51 
     | 
    
         
            +
                a_task.start()
         
     | 
| 
      
 52 
     | 
    
         
            +
                assert a_task.started_at == curr_started_at
         
     | 
| 
      
 53 
     | 
    
         
            +
             
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
            def test_fails_if_exception(a_task):
         
     | 
| 
      
 56 
     | 
    
         
            +
                a_task.start()
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
                with pytest.raises(Exception):
         
     | 
| 
      
 59 
     | 
    
         
            +
                    with a_task.fails_if_exception():
         
     | 
| 
      
 60 
     | 
    
         
            +
                        raise Exception("fail!")
         
     | 
| 
      
 61 
     | 
    
         
            +
             
     | 
| 
      
 62 
     | 
    
         
            +
                assert a_task.state == BackgroundTask.STATES.failed
         
     | 
| 
      
 63 
     | 
    
         
            +
             
     | 
| 
      
 64 
     | 
    
         
            +
             
     | 
| 
      
 65 
     | 
    
         
            +
            def test_set_steps(a_task):
         
     | 
| 
      
 66 
     | 
    
         
            +
                a_task.steps_completed = 10
         
     | 
| 
      
 67 
     | 
    
         
            +
                a_task.set_steps_to_complete(20)
         
     | 
| 
      
 68 
     | 
    
         
            +
                a_task.refresh_from_db()
         
     | 
| 
      
 69 
     | 
    
         
            +
                assert a_task.steps_to_complete == 20
         
     | 
| 
      
 70 
     | 
    
         
            +
                assert a_task.steps_completed == 0
         
     | 
| 
         @@ -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
         
     | 
| 
         @@ -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 
     | 
    
         | 
| 
         @@ -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.4. 
     | 
| 
      
 7 
     | 
    
         
            +
            version = "0.4.3"
         
     | 
| 
       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>",
         
     | 
| 
         @@ -38,6 +38,7 @@ sphinx = ">=7.1.2" 
     | 
|
| 
       38 
38 
     | 
    
         
             
            sphinx-rtd-theme = ">=1.3.0"
         
     | 
| 
       39 
39 
     | 
    
         
             
            psycopg2 = ">=2.9.10"
         
     | 
| 
       40 
40 
     | 
    
         
             
            colorlog = ">=6.9.0"
         
     | 
| 
      
 41 
     | 
    
         
            +
            flake8 = ">=7.1.1"
         
     | 
| 
       41 
42 
     | 
    
         | 
| 
       42 
43 
     | 
    
         
             
            [tool.poetry.extras]
         
     | 
| 
       43 
44 
     | 
    
         
             
            # ℹ️ if you update any of these, be sure to update "all = [...]" as well, so that it's easy to get
         
     | 
| 
         
            File without changes
         
     | 
    
        {django_admin_background_task-0.4.0 → django_admin_background_task-0.4.3}/bgtask/__init__.py
    RENAMED
    
    | 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
    
        {django_admin_background_task-0.4.0 → django_admin_background_task-0.4.3}/bgtask/decorators.py
    RENAMED
    
    | 
         
            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
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     |