django-periodic-tasks 0.1.0a4__py3-none-any.whl → 0.1.0a5__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.
- django_periodic_tasks/admin.py +3 -5
- django_periodic_tasks/decorators.py +14 -6
- django_periodic_tasks/models.py +21 -8
- django_periodic_tasks/registry.py +48 -15
- django_periodic_tasks/scheduler.py +2 -3
- django_periodic_tasks/task_resolver.py +1 -2
- {django_periodic_tasks-0.1.0a4.dist-info → django_periodic_tasks-0.1.0a5.dist-info}/METADATA +1 -1
- {django_periodic_tasks-0.1.0a4.dist-info → django_periodic_tasks-0.1.0a5.dist-info}/RECORD +10 -10
- {django_periodic_tasks-0.1.0a4.dist-info → django_periodic_tasks-0.1.0a5.dist-info}/WHEEL +0 -0
- {django_periodic_tasks-0.1.0a4.dist-info → django_periodic_tasks-0.1.0a5.dist-info}/licenses/LICENSE +0 -0
django_periodic_tasks/admin.py
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
from typing import Any
|
|
2
|
-
|
|
3
1
|
from django.contrib import admin
|
|
4
2
|
from django.http import HttpRequest
|
|
5
3
|
|
|
@@ -62,7 +60,7 @@ class ScheduledTaskAdmin(admin.ModelAdmin[ScheduledTask]):
|
|
|
62
60
|
def has_delete_permission(
|
|
63
61
|
self,
|
|
64
62
|
request: HttpRequest,
|
|
65
|
-
obj:
|
|
63
|
+
obj: ScheduledTask | None = None,
|
|
66
64
|
) -> bool:
|
|
67
65
|
if obj is not None and obj.source == ScheduledTask.Source.CODE:
|
|
68
66
|
return False
|
|
@@ -79,8 +77,8 @@ class TaskExecutionAdmin(admin.ModelAdmin[TaskExecution]):
|
|
|
79
77
|
def has_add_permission(self, request: HttpRequest) -> bool:
|
|
80
78
|
return False
|
|
81
79
|
|
|
82
|
-
def has_change_permission(self, request: HttpRequest, obj:
|
|
80
|
+
def has_change_permission(self, request: HttpRequest, obj: TaskExecution | None = None) -> bool:
|
|
83
81
|
return False
|
|
84
82
|
|
|
85
|
-
def has_delete_permission(self, request: HttpRequest, obj:
|
|
83
|
+
def has_delete_permission(self, request: HttpRequest, obj: TaskExecution | None = None) -> bool:
|
|
86
84
|
return False
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from collections.abc import Callable
|
|
4
|
-
from typing import Any
|
|
5
4
|
import functools
|
|
6
5
|
import logging
|
|
7
6
|
|
|
@@ -10,8 +9,15 @@ from django.utils import timezone
|
|
|
10
9
|
|
|
11
10
|
logger = logging.getLogger(__name__)
|
|
12
11
|
|
|
12
|
+
_exactly_once_funcs: set[Callable[..., object]] = set()
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
|
|
15
|
+
def is_exactly_once(func: object) -> bool:
|
|
16
|
+
"""Check whether a function was decorated with ``@exactly_once``."""
|
|
17
|
+
return func in _exactly_once_funcs
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def exactly_once[R](func: Callable[..., R]) -> Callable[..., R | None]:
|
|
15
21
|
"""Decorator ensuring a scheduled task runs at most once per invocation.
|
|
16
22
|
|
|
17
23
|
When the scheduler creates a ``TaskExecution`` row and passes its ID via
|
|
@@ -27,12 +33,14 @@ def exactly_once(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
|
27
33
|
"""
|
|
28
34
|
|
|
29
35
|
@functools.wraps(func)
|
|
30
|
-
def wrapper(*args:
|
|
31
|
-
|
|
36
|
+
def wrapper(*args: object, **kwargs: object) -> R | None:
|
|
37
|
+
raw_execution_id = kwargs.pop("_periodic_tasks_execution_id", None)
|
|
32
38
|
|
|
33
|
-
if
|
|
39
|
+
if raw_execution_id is None:
|
|
34
40
|
return func(*args, **kwargs)
|
|
35
41
|
|
|
42
|
+
execution_id = str(raw_execution_id)
|
|
43
|
+
|
|
36
44
|
from django_periodic_tasks.models import (
|
|
37
45
|
TaskExecution, # Avoid AppRegistryNotReady
|
|
38
46
|
)
|
|
@@ -55,5 +63,5 @@ def exactly_once(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
|
55
63
|
|
|
56
64
|
return result
|
|
57
65
|
|
|
58
|
-
wrapper
|
|
66
|
+
_exactly_once_funcs.add(wrapper)
|
|
59
67
|
return wrapper
|
django_periodic_tasks/models.py
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
from
|
|
1
|
+
from collections.abc import Iterable
|
|
2
2
|
from zoneinfo import ZoneInfo
|
|
3
3
|
import uuid
|
|
4
4
|
|
|
5
5
|
from django.core.exceptions import ValidationError
|
|
6
6
|
from django.db import models
|
|
7
|
+
from django.db.models.base import ModelBase
|
|
7
8
|
|
|
8
9
|
from django_periodic_tasks.cron import compute_next_run_at, validate_cron_expression
|
|
9
10
|
|
|
@@ -87,7 +88,14 @@ class ScheduledTask(models.Model):
|
|
|
87
88
|
if errors:
|
|
88
89
|
raise ValidationError(errors)
|
|
89
90
|
|
|
90
|
-
def save(
|
|
91
|
+
def save(
|
|
92
|
+
self,
|
|
93
|
+
*,
|
|
94
|
+
force_insert: bool | tuple[ModelBase, ...] = False,
|
|
95
|
+
force_update: bool = False,
|
|
96
|
+
using: str | None = None,
|
|
97
|
+
update_fields: Iterable[str] | None = None,
|
|
98
|
+
) -> None:
|
|
91
99
|
original_next_run_at = self.next_run_at
|
|
92
100
|
|
|
93
101
|
if not self.enabled:
|
|
@@ -95,17 +103,22 @@ class ScheduledTask(models.Model):
|
|
|
95
103
|
elif self.next_run_at is None:
|
|
96
104
|
self.next_run_at = compute_next_run_at(self.cron_expression, self.timezone)
|
|
97
105
|
|
|
98
|
-
|
|
99
|
-
if
|
|
100
|
-
fields = set(
|
|
106
|
+
effective_update_fields: list[str] | None = None
|
|
107
|
+
if update_fields is not None:
|
|
108
|
+
fields = set(update_fields)
|
|
101
109
|
# Always include updated_at so auto_now fires
|
|
102
110
|
fields.add("updated_at")
|
|
103
111
|
# Include next_run_at if save() modified it
|
|
104
112
|
if self.next_run_at != original_next_run_at:
|
|
105
113
|
fields.add("next_run_at")
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
super().save(
|
|
114
|
+
effective_update_fields = list(fields)
|
|
115
|
+
|
|
116
|
+
super().save(
|
|
117
|
+
force_insert=force_insert,
|
|
118
|
+
force_update=force_update,
|
|
119
|
+
using=using,
|
|
120
|
+
update_fields=effective_update_fields,
|
|
121
|
+
)
|
|
109
122
|
|
|
110
123
|
|
|
111
124
|
class TaskExecution(models.Model):
|
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from collections.abc import Callable
|
|
3
4
|
from dataclasses import dataclass, field
|
|
4
|
-
from typing import
|
|
5
|
+
from typing import Protocol
|
|
5
6
|
|
|
6
7
|
from django_periodic_tasks.cron import validate_cron_expression
|
|
7
8
|
|
|
8
9
|
|
|
10
|
+
class TaskLike(Protocol):
|
|
11
|
+
"""Structural type describing the django-tasks ``Task`` interface.
|
|
12
|
+
|
|
13
|
+
Used instead of ``Task[..., object]`` because ``Task`` is invariant in its
|
|
14
|
+
type parameters — a concrete ``Task[[str], None]`` would not satisfy
|
|
15
|
+
``Task[..., object]``. This protocol captures just the attributes the
|
|
16
|
+
registry needs, so any ``Task[P, T]`` matches regardless of its concrete
|
|
17
|
+
parameter and return types.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def module_path(self) -> str: ...
|
|
22
|
+
|
|
23
|
+
|
|
9
24
|
@dataclass(frozen=True)
|
|
10
25
|
class ScheduleEntry:
|
|
11
26
|
"""An immutable record describing a single scheduled task.
|
|
@@ -25,12 +40,12 @@ class ScheduleEntry:
|
|
|
25
40
|
backend: Task backend name passed to ``task.using()``.
|
|
26
41
|
"""
|
|
27
42
|
|
|
28
|
-
task:
|
|
43
|
+
task: TaskLike
|
|
29
44
|
cron_expression: str
|
|
30
45
|
name: str
|
|
31
46
|
timezone: str = "UTC"
|
|
32
|
-
args: list[
|
|
33
|
-
kwargs: dict[str,
|
|
47
|
+
args: list[object] = field(default_factory=list)
|
|
48
|
+
kwargs: dict[str, object] = field(default_factory=dict)
|
|
34
49
|
queue_name: str = "default"
|
|
35
50
|
priority: int = 0
|
|
36
51
|
backend: str = "default"
|
|
@@ -44,13 +59,13 @@ class ScheduleRegistry:
|
|
|
44
59
|
|
|
45
60
|
def register(
|
|
46
61
|
self,
|
|
47
|
-
task:
|
|
62
|
+
task: TaskLike,
|
|
48
63
|
*,
|
|
49
64
|
cron: str,
|
|
50
65
|
name: str,
|
|
51
66
|
timezone: str = "UTC",
|
|
52
|
-
args: list[
|
|
53
|
-
kwargs: dict[str,
|
|
67
|
+
args: list[object] | None = None,
|
|
68
|
+
kwargs: dict[str, object] | None = None,
|
|
54
69
|
queue_name: str = "default",
|
|
55
70
|
priority: int = 0,
|
|
56
71
|
backend: str = "default",
|
|
@@ -95,13 +110,18 @@ class ScheduleRegistry:
|
|
|
95
110
|
schedule_registry = ScheduleRegistry()
|
|
96
111
|
|
|
97
112
|
|
|
98
|
-
def scheduled_task(
|
|
113
|
+
def scheduled_task[T: TaskLike](
|
|
99
114
|
*,
|
|
100
115
|
cron: str,
|
|
101
116
|
name: str | None = None,
|
|
102
117
|
registry: ScheduleRegistry | None = None,
|
|
103
|
-
|
|
104
|
-
|
|
118
|
+
timezone: str = "UTC",
|
|
119
|
+
args: list[object] | None = None,
|
|
120
|
+
kwargs: dict[str, object] | None = None,
|
|
121
|
+
queue_name: str = "default",
|
|
122
|
+
priority: int = 0,
|
|
123
|
+
backend: str = "default",
|
|
124
|
+
) -> Callable[[T], T]:
|
|
105
125
|
"""Decorator that registers a django-tasks ``Task`` with the schedule registry.
|
|
106
126
|
|
|
107
127
|
Apply this decorator **after** ``@task()`` to register the task for periodic
|
|
@@ -117,15 +137,28 @@ def scheduled_task(
|
|
|
117
137
|
name: Unique schedule name. Defaults to the task's ``module_path``.
|
|
118
138
|
registry: An alternate ``ScheduleRegistry`` instance (defaults to the
|
|
119
139
|
global ``schedule_registry``).
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
140
|
+
timezone: IANA timezone for cron matching (default ``"UTC"``).
|
|
141
|
+
args: Positional arguments for ``task.enqueue()``.
|
|
142
|
+
kwargs: Keyword arguments for ``task.enqueue()``.
|
|
143
|
+
queue_name: Queue name for ``task.using()``.
|
|
144
|
+
priority: Priority for ``task.using()``.
|
|
145
|
+
backend: Backend name for ``task.using()``.
|
|
123
146
|
"""
|
|
124
147
|
target_registry = registry or schedule_registry
|
|
125
148
|
|
|
126
|
-
def decorator(task_obj:
|
|
149
|
+
def decorator(task_obj: T) -> T:
|
|
127
150
|
actual_name = name or task_obj.module_path
|
|
128
|
-
target_registry.register(
|
|
151
|
+
target_registry.register(
|
|
152
|
+
task_obj,
|
|
153
|
+
cron=cron,
|
|
154
|
+
name=actual_name,
|
|
155
|
+
timezone=timezone,
|
|
156
|
+
args=args,
|
|
157
|
+
kwargs=kwargs,
|
|
158
|
+
queue_name=queue_name,
|
|
159
|
+
priority=priority,
|
|
160
|
+
backend=backend,
|
|
161
|
+
)
|
|
129
162
|
return task_obj
|
|
130
163
|
|
|
131
164
|
return decorator
|
|
@@ -7,6 +7,7 @@ from django.db.models import F
|
|
|
7
7
|
from django.utils import timezone
|
|
8
8
|
|
|
9
9
|
from django_periodic_tasks.cron import compute_next_run_at
|
|
10
|
+
from django_periodic_tasks.decorators import is_exactly_once
|
|
10
11
|
from django_periodic_tasks.models import ScheduledTask, TaskExecution
|
|
11
12
|
from django_periodic_tasks.sync import sync_code_schedules
|
|
12
13
|
from django_periodic_tasks.task_resolver import resolve_task
|
|
@@ -106,9 +107,7 @@ class PeriodicTaskScheduler(threading.Thread):
|
|
|
106
107
|
backend=st.backend,
|
|
107
108
|
)
|
|
108
109
|
|
|
109
|
-
is_exactly_once
|
|
110
|
-
|
|
111
|
-
if is_exactly_once:
|
|
110
|
+
if is_exactly_once(task_obj.func):
|
|
112
111
|
execution = TaskExecution.objects.create(scheduled_task=st)
|
|
113
112
|
enqueue_kwargs = {**st.kwargs, "_periodic_tasks_execution_id": str(execution.id)}
|
|
114
113
|
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
from importlib import import_module
|
|
2
|
-
from typing import Any
|
|
3
2
|
|
|
4
3
|
from django_tasks.base import Task
|
|
5
4
|
|
|
6
5
|
|
|
7
|
-
def resolve_task(task_path: str) -> Task[...,
|
|
6
|
+
def resolve_task(task_path: str) -> Task[..., object]:
|
|
8
7
|
"""Import and return a django-tasks Task object from its dotted module path.
|
|
9
8
|
|
|
10
9
|
The task_path should be a dotted path like "myapp.tasks.my_task".
|
{django_periodic_tasks-0.1.0a4.dist-info → django_periodic_tasks-0.1.0a5.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-periodic-tasks
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.0a5
|
|
4
4
|
Summary: Periodic/cron task scheduling for django-tasks. Backend-agnostic replacement for celery-beat.
|
|
5
5
|
Project-URL: Homepage, https://gitlab.com/thelabnyc/django-periodic-tasks
|
|
6
6
|
Project-URL: Repository, https://gitlab.com/thelabnyc/django-periodic-tasks
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
django_periodic_tasks/__init__.py,sha256=5cgybxXwf98hZnNAOX6DYHeeYuwzC9jgwJZU0XGUOeQ,316
|
|
2
|
-
django_periodic_tasks/admin.py,sha256
|
|
2
|
+
django_periodic_tasks/admin.py,sha256=-a7CcvAvh8W2SW7MwNqtlM_-qb9W0MCbLPqOOVMKnDE,2400
|
|
3
3
|
django_periodic_tasks/apps.py,sha256=KezRzFjYSleKustuoiG0KhFy5N2uUgmi8qrx5bqcN6k,1159
|
|
4
4
|
django_periodic_tasks/cron.py,sha256=PHVlX_YyzALgpYYBqWkKdvEm0uE0lnBTM5TUDcYLfS0,1332
|
|
5
|
-
django_periodic_tasks/decorators.py,sha256=
|
|
6
|
-
django_periodic_tasks/models.py,sha256=
|
|
5
|
+
django_periodic_tasks/decorators.py,sha256=oxabqjO7ZzvhOuUCzR4J9gIq2cu1gV-8R5f7tNfAiro,2248
|
|
6
|
+
django_periodic_tasks/models.py,sha256=EbTMPfvzy7i39mL69sTO8DnhCRtXVWRAn0jD0iPUJPg,5561
|
|
7
7
|
django_periodic_tasks/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
-
django_periodic_tasks/registry.py,sha256=
|
|
9
|
-
django_periodic_tasks/scheduler.py,sha256=
|
|
8
|
+
django_periodic_tasks/registry.py,sha256=itczcfG_PlKsUz4pA6vDwge2OL0jth7NAI0cm6IDGiY,5665
|
|
9
|
+
django_periodic_tasks/scheduler.py,sha256=BlsfdnF1qBULR5wk5WcODEQpZcXI5OHCBso1rsgYQcU,7602
|
|
10
10
|
django_periodic_tasks/sync.py,sha256=EfvcZlP5-199uxIMwJRdCnXzObQohHLYR4W819d5i5k,2118
|
|
11
|
-
django_periodic_tasks/task_resolver.py,sha256=
|
|
11
|
+
django_periodic_tasks/task_resolver.py,sha256=SFKRZe-bkKmcfS1r698ZbGb8VqECeTPUr8d7gH9B6PQ,664
|
|
12
12
|
django_periodic_tasks/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
13
|
django_periodic_tasks/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
14
|
django_periodic_tasks/management/commands/run_scheduler.py,sha256=kCVBi10KLHb_8iqizT3mlfW_cuQOLpsrF-VowdXkkAM,2194
|
|
@@ -16,7 +16,7 @@ django_periodic_tasks/migrations/0001_initial.py,sha256=h_AfdKIG9f-XmDZiqR_zzgtr
|
|
|
16
16
|
django_periodic_tasks/migrations/0002_remove_redundant_next_run_at_db_index.py,sha256=RPWdcpU7wkQd-47oLbr14q6JgfyaqfOkys2o8KmocoU,412
|
|
17
17
|
django_periodic_tasks/migrations/0003_add_task_execution.py,sha256=3cZrhQmBR2oIaIkTeMYGoztuPyO6rdh84wOlex5fUzs,1235
|
|
18
18
|
django_periodic_tasks/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
|
-
django_periodic_tasks-0.1.
|
|
20
|
-
django_periodic_tasks-0.1.
|
|
21
|
-
django_periodic_tasks-0.1.
|
|
22
|
-
django_periodic_tasks-0.1.
|
|
19
|
+
django_periodic_tasks-0.1.0a5.dist-info/METADATA,sha256=Jaa4P-GL8T9NwcwT6yNoBwQ_HtG_YuNH_H3DgxU0Rh4,1505
|
|
20
|
+
django_periodic_tasks-0.1.0a5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
21
|
+
django_periodic_tasks-0.1.0a5.dist-info/licenses/LICENSE,sha256=XdK5EYewCzTpCGUgHO6aUH6TlvzAdvQrtv1Xacd7OIQ,738
|
|
22
|
+
django_periodic_tasks-0.1.0a5.dist-info/RECORD,,
|
|
File without changes
|
{django_periodic_tasks-0.1.0a4.dist-info → django_periodic_tasks-0.1.0a5.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|