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.
@@ -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: Any = None,
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: Any = None) -> bool:
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: Any = None) -> bool:
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
- def exactly_once(func: Callable[..., Any]) -> Callable[..., Any]:
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: Any, **kwargs: Any) -> Any:
31
- execution_id: str | None = kwargs.pop("_periodic_tasks_execution_id", None)
36
+ def wrapper(*args: object, **kwargs: object) -> R | None:
37
+ raw_execution_id = kwargs.pop("_periodic_tasks_execution_id", None)
32
38
 
33
- if execution_id is None:
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._exactly_once = True # type: ignore[attr-defined]
66
+ _exactly_once_funcs.add(wrapper)
59
67
  return wrapper
@@ -1,9 +1,10 @@
1
- from typing import Any
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(self, *args: Any, **kwargs: Any) -> None:
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
- raw_update_fields: list[str] | None = kwargs.get("update_fields")
99
- if raw_update_fields is not None:
100
- fields = set(raw_update_fields)
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
- kwargs["update_fields"] = list(fields)
107
-
108
- super().save(*args, **kwargs)
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 Any
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: Any
43
+ task: TaskLike
29
44
  cron_expression: str
30
45
  name: str
31
46
  timezone: str = "UTC"
32
- args: list[Any] = field(default_factory=list)
33
- kwargs: dict[str, Any] = field(default_factory=dict)
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: Any,
62
+ task: TaskLike,
48
63
  *,
49
64
  cron: str,
50
65
  name: str,
51
66
  timezone: str = "UTC",
52
- args: list[Any] | None = None,
53
- kwargs: dict[str, Any] | None = None,
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
- **kwargs: Any,
104
- ) -> Any:
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
- **kwargs: Extra options forwarded to
121
- :meth:`ScheduleRegistry.register` (``timezone``, ``args``,
122
- ``kwargs``, ``queue_name``, ``priority``, ``backend``).
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: Any) -> Any:
149
+ def decorator(task_obj: T) -> T:
127
150
  actual_name = name or task_obj.module_path
128
- target_registry.register(task_obj, cron=cron, name=actual_name, **kwargs)
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 = getattr(task_obj.func, "_exactly_once", False)
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[..., Any]:
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".
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-periodic-tasks
3
- Version: 0.1.0a4
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=87o6_Z-f79ZhdAoTpFWdVhV9rZQQb-k17FGR_phHPuw,2373
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=GhkNUqlPubUj6ZhS4Nf1FsTUMZaihNyD8wrTFOzbiKU,2024
6
- django_periodic_tasks/models.py,sha256=qJEwSnBJ-L54qr0Dl9gvHWZ0h6k2H6x5nMS7f9O2L3k,5199
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=sS897SBu1RP7JVQDNkMZQfahhCU_9ZpV6qHmdwvKiik,4532
9
- django_periodic_tasks/scheduler.py,sha256=zl-LsAmO9Io2rZG1P4WrG-is3yGxAX0430Dmd0zVEBs,7600
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=OMlH3s9b4rerLOItm3KdazWLZPiSTbjHTZlWpd9qt_8,684
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.0a4.dist-info/METADATA,sha256=0aD4pQZ-mAo_GND1sIcVqZAo04zUUNWO1xujJMA61tU,1505
20
- django_periodic_tasks-0.1.0a4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
21
- django_periodic_tasks-0.1.0a4.dist-info/licenses/LICENSE,sha256=XdK5EYewCzTpCGUgHO6aUH6TlvzAdvQrtv1Xacd7OIQ,738
22
- django_periodic_tasks-0.1.0a4.dist-info/RECORD,,
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,,