django-crontask 1.1.2__tar.gz → 1.2.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-crontask
3
- Version: 1.1.2
3
+ Version: 1.2.0
4
4
  Summary: Cron style scheduler for Django's task framework.
5
5
  Keywords: Django,cron,tasks,scheduler
6
6
  Author-email: Rust Saiargaliev <fly.amureki@gmail.com>, Johannes Maron <johannes@maron.family>, Mostafa Mohamed <mostafa.anm91@gmail.com>, Jacqueline Kraus <jacquelinekraus1992@gmail.com>
@@ -11,7 +11,9 @@ Classifier: Programming Language :: Python
11
11
  Classifier: Environment :: Web Environment
12
12
  Classifier: License :: OSI Approved :: BSD License
13
13
  Classifier: Intended Audience :: Developers
14
- Classifier: Operating System :: OS Independent
14
+ Classifier: Operating System :: Microsoft :: Windows
15
+ Classifier: Operating System :: MacOS :: MacOS X
16
+ Classifier: Operating System :: POSIX
15
17
  Classifier: Topic :: Communications :: Email
16
18
  Classifier: Topic :: Text Processing :: Markup :: Markdown
17
19
  Classifier: Topic :: Software Development
@@ -29,7 +31,12 @@ Requires-Dist: django>=6.0
29
31
  Requires-Dist: redis ; extra == "redis"
30
32
  Requires-Dist: sentry-sdk ; extra == "sentry"
31
33
  Project-URL: Changelog, https://github.com/codingjoe/django-crontask/releases
32
- Project-URL: Project-URL, https://github.com/codingjoe/django-crontask
34
+ Project-URL: Documentation, https://django-crontask.rtfd.io/
35
+ Project-URL: Funding, https://github.com/sponsors/codingjoe
36
+ Project-URL: Homepage, https://github.com/codingjoe/django-crontask
37
+ Project-URL: Issues, https://github.com/codingjoe/django-crontask/issues
38
+ Project-URL: Releasenotes, https://github.com/codingjoe/django-crontask/releases/latest
39
+ Project-URL: Source, https://github.com/codingjoe/django-crontask
33
40
  Provides-Extra: redis
34
41
  Provides-Extra: sentry
35
42
 
@@ -139,9 +146,12 @@ def my_interval_task():
139
146
 
140
147
  ### Sentry Cron Monitors
141
148
 
142
- If you use [Sentry] you can add cron monitors to your tasks.
149
+ If you use [Sentry] suitable tasks will be automatically monitored.
143
150
  The monitor's slug will be the actor's name. Like `my_task` in the example above.
144
151
 
152
+ Certain triggers are not supported by Sentry as well as sub-minute intervals.
153
+ In these cases, no monitor will be created and no telemetry will be sent.
154
+
145
155
  ### The crontask command
146
156
 
147
157
  ```ShellSession
@@ -104,9 +104,12 @@ def my_interval_task():
104
104
 
105
105
  ### Sentry Cron Monitors
106
106
 
107
- If you use [Sentry] you can add cron monitors to your tasks.
107
+ If you use [Sentry] suitable tasks will be automatically monitored.
108
108
  The monitor's slug will be the actor's name. Like `my_task` in the example above.
109
109
 
110
+ Certain triggers are not supported by Sentry as well as sub-minute intervals.
111
+ In these cases, no monitor will be created and no telemetry will be sent.
112
+
110
113
  ### The crontask command
111
114
 
112
115
  ```ShellSession
@@ -13,12 +13,13 @@ from django.tasks import Task
13
13
  from django.utils import timezone
14
14
 
15
15
  from . import _version
16
+ from .contrib import sentry
17
+
18
+ __all__ = ["cron", "interval", "scheduler", "VERSION", "__version__"]
16
19
 
17
20
  __version__ = _version.version
18
21
  VERSION = _version.version_tuple
19
22
 
20
- __all__ = ["cron", "interval", "scheduler"]
21
-
22
23
 
23
24
  class LazyBlockingScheduler(BlockingScheduler):
24
25
  """Avoid annoying info logs for pending jobs."""
@@ -45,21 +46,15 @@ def cron(schedule: str | BaseTrigger) -> typing.Callable[[Task], Task]:
45
46
  def cron_test():
46
47
  print("Cron test")
47
48
 
48
-
49
- Please don't forget to set up a sentry monitor for the actor, otherwise you won't
50
- get any notifications if the cron job fails.
51
-
52
- The monitor slug is your actor name, the schedule should be set to the same
53
- cron schedule as the cron decorator. The schedule type should be set to cron.
54
- The monitors timezone should be set to Europe/Berlin.
49
+ Sentry cron monitors are automatically upserted on every check-in
50
+ using the task name as the monitor slug.
55
51
  """
56
52
 
57
53
  def decorator(task: Task) -> Task:
58
- trigger = schedule
59
54
  if isinstance(schedule, str):
60
55
  *_, day_schedule = schedule.split(" ")
61
- # CronTrigger uses Python's timezone dependent first weekday,
62
- # so in Berlin monday is 0 and sunday is 6. We use literals to avoid
56
+ # CronTrigger uses Python's timezone-dependent first weekday,
57
+ # so in Berlin Monday is 0, and Sunday is 6. We use literals to avoid
63
58
  # confusion. Literals are also more readable and crontab conform.
64
59
  if any(i.isdigit() for i in day_schedule):
65
60
  raise ValueError(
@@ -69,22 +64,10 @@ def cron(schedule: str | BaseTrigger) -> typing.Callable[[Task], Task]:
69
64
  schedule,
70
65
  timezone=timezone.get_default_timezone(),
71
66
  )
72
-
73
- try:
74
- from sentry_sdk.crons import monitor
75
- except ImportError:
76
- fn = task.func
77
67
  else:
78
- fn = monitor(task.name)(task.func)
79
-
80
- task = type(task)(
81
- priority=task.priority,
82
- func=fn,
83
- queue_name=task.queue_name,
84
- backend=task.backend,
85
- takes_context=task.takes_context,
86
- run_after=task.run_after,
87
- )
68
+ trigger = schedule
69
+
70
+ task = sentry.monitor_cron_task(task, trigger)
88
71
 
89
72
  scheduler.add_job(
90
73
  func=task.enqueue,
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '1.2.0'
22
+ __version_tuple__ = version_tuple = (1, 2, 0)
23
+
24
+ __commit_id__ = commit_id = 'gae12a07e2'
@@ -0,0 +1,88 @@
1
+ import typing
2
+
3
+ from apscheduler.triggers.base import BaseTrigger
4
+ from apscheduler.triggers.calendarinterval import CalendarIntervalTrigger
5
+ from apscheduler.triggers.cron import CronTrigger
6
+ from apscheduler.triggers.interval import IntervalTrigger
7
+ from django.tasks import Task
8
+ from django.utils import timezone
9
+
10
+
11
+ def trigger_to_monitor_config(trigger: BaseTrigger) -> dict[str, typing.Any] | None:
12
+ """Convert an APScheduler trigger to a Sentry monitor configuration if possible."""
13
+ tz = str(timezone.get_default_timezone())
14
+ match trigger:
15
+ case CronTrigger():
16
+ fields = {f.name: str(f) for f in trigger.fields}
17
+ return {
18
+ "schedule": {
19
+ "type": "crontab",
20
+ "value": "{minute} {hour} {day} {month} {day_of_week}".format(
21
+ **fields
22
+ ),
23
+ },
24
+ "timezone": tz,
25
+ }
26
+ case IntervalTrigger():
27
+ seconds = trigger.interval.total_seconds()
28
+ for unit, size in (
29
+ ("year", 31536000),
30
+ ("month", 2592000),
31
+ ("week", 604800),
32
+ ("day", 86400),
33
+ ("hour", 3600),
34
+ ("minute", 60),
35
+ ):
36
+ if seconds % size == 0:
37
+ return {
38
+ "schedule": {
39
+ "type": "interval",
40
+ "value": seconds // size,
41
+ "unit": unit,
42
+ },
43
+ "timezone": tz,
44
+ }
45
+ return None # Less than a minute
46
+ case CalendarIntervalTrigger():
47
+ fields = {
48
+ "year": trigger.years,
49
+ "month": trigger.months,
50
+ "week": trigger.weeks,
51
+ "day": trigger.days,
52
+ }
53
+ set_fields = {k: v for k, v in fields.items() if v}
54
+ if len(set_fields) == 1:
55
+ ((unit, value),) = set_fields.items()
56
+ return {
57
+ "schedule": {"type": "interval", "value": value, "unit": unit},
58
+ "timezone": tz,
59
+ }
60
+ return None
61
+ case _:
62
+ return None
63
+
64
+
65
+ def monitor_cron_task(task: Task, trigger: BaseTrigger) -> Task:
66
+ """
67
+ Wrap the task function in a Sentry monitor for a suitable trigger.
68
+
69
+ You don't need to create the monitor in advance since
70
+ the Sentry monitor configuration is upserted on each task execution.
71
+ If the trigger is not supported, the task is returned unchanged.
72
+ """
73
+ try:
74
+ from sentry_sdk import monitor
75
+ except ImportError:
76
+ return task
77
+ else:
78
+ if monitor_config := trigger_to_monitor_config(trigger):
79
+ return type(task)(
80
+ priority=task.priority,
81
+ func=monitor(task.name, monitor_config=monitor_config)(task.func),
82
+ queue_name=task.queue_name,
83
+ backend=task.backend,
84
+ takes_context=task.takes_context,
85
+ run_after=task.run_after,
86
+ )
87
+
88
+ return task
@@ -1,5 +1,6 @@
1
1
  import importlib
2
2
  import signal
3
+ import sys
3
4
 
4
5
  from apscheduler.triggers.interval import IntervalTrigger
5
6
  from django.apps import apps
@@ -60,7 +61,11 @@ class Command(BaseCommand):
60
61
  self.stderr.write("Another scheduler is already running.")
61
62
 
62
63
  def launch_scheduler(self, lock, scheduler):
63
- signal.signal(signal.SIGHUP, kill_softly)
64
+ match sys.platform:
65
+ case "win32":
66
+ signal.signal(signal.SIGBREAK, kill_softly)
67
+ case _:
68
+ signal.signal(signal.SIGHUP, kill_softly)
64
69
  signal.signal(signal.SIGTERM, kill_softly)
65
70
  signal.signal(signal.SIGINT, kill_softly)
66
71
  self.stdout.write(self.style.SUCCESS("Starting scheduler…"))
@@ -20,7 +20,9 @@ classifiers = [
20
20
  "Environment :: Web Environment",
21
21
  "License :: OSI Approved :: BSD License",
22
22
  "Intended Audience :: Developers",
23
- "Operating System :: OS Independent",
23
+ "Operating System :: Microsoft :: Windows",
24
+ "Operating System :: MacOS :: MacOS X",
25
+ "Operating System :: POSIX",
24
26
  "Topic :: Communications :: Email",
25
27
  "Topic :: Text Processing :: Markup :: Markdown",
26
28
  "Topic :: Software Development",
@@ -41,8 +43,14 @@ sentry = ["sentry-sdk"]
41
43
  redis = ["redis"]
42
44
 
43
45
  [project.urls]
44
- Project-URL = "https://github.com/codingjoe/django-crontask"
46
+ # https://packaging.python.org/en/latest/specifications/well-known-project-urls/#well-known-labels
47
+ Homepage = "https://github.com/codingjoe/django-crontask"
45
48
  Changelog = "https://github.com/codingjoe/django-crontask/releases"
49
+ Source = "https://github.com/codingjoe/django-crontask"
50
+ Releasenotes = "https://github.com/codingjoe/django-crontask/releases/latest"
51
+ Documentation = "https://django-crontask.rtfd.io/"
52
+ Issues = "https://github.com/codingjoe/django-crontask/issues"
53
+ Funding = "https://github.com/sponsors/codingjoe"
46
54
 
47
55
  [tool.flit.module]
48
56
  name = "crontask"
@@ -1,34 +0,0 @@
1
- # file generated by setuptools-scm
2
- # don't change, don't track in version control
3
-
4
- __all__ = [
5
- "__version__",
6
- "__version_tuple__",
7
- "version",
8
- "version_tuple",
9
- "__commit_id__",
10
- "commit_id",
11
- ]
12
-
13
- TYPE_CHECKING = False
14
- if TYPE_CHECKING:
15
- from typing import Tuple
16
- from typing import Union
17
-
18
- VERSION_TUPLE = Tuple[Union[int, str], ...]
19
- COMMIT_ID = Union[str, None]
20
- else:
21
- VERSION_TUPLE = object
22
- COMMIT_ID = object
23
-
24
- version: str
25
- __version__: str
26
- __version_tuple__: VERSION_TUPLE
27
- version_tuple: VERSION_TUPLE
28
- commit_id: COMMIT_ID
29
- __commit_id__: COMMIT_ID
30
-
31
- __version__ = version = '1.1.2'
32
- __version_tuple__ = version_tuple = (1, 1, 2)
33
-
34
- __commit_id__ = commit_id = 'g08c90d2cc'
File without changes