dramatiq-crontab 1.0.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.
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2023, voiio GmbH
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,135 @@
1
+ Metadata-Version: 2.1
2
+ Name: dramatiq-crontab
3
+ Version: 1.0.0
4
+ Summary: Cron style scheduler for asynchronous Dramatiq tasks in Django.
5
+ Keywords: Django,Dramatiq,tasks,scheduler
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>
7
+ Requires-Python: >=3.8
8
+ Description-Content-Type: text/markdown
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Programming Language :: Python
11
+ Classifier: Environment :: Web Environment
12
+ Classifier: License :: OSI Approved :: BSD License
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Topic :: Communications :: Email
16
+ Classifier: Topic :: Text Processing :: Markup :: Markdown
17
+ Classifier: Topic :: Software Development
18
+ Classifier: Programming Language :: Python
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3 :: Only
21
+ Classifier: Programming Language :: Python :: 3.8
22
+ Classifier: Programming Language :: Python :: 3.9
23
+ Classifier: Programming Language :: Python :: 3.10
24
+ Classifier: Programming Language :: Python :: 3.11
25
+ Classifier: Framework :: Django
26
+ Classifier: Framework :: Django :: 3.2
27
+ Classifier: Framework :: Django :: 4.1
28
+ Classifier: Framework :: Django :: 4.2
29
+ Requires-Dist: dramatiq
30
+ Requires-Dist: apscheduler
31
+ Requires-Dist: django
32
+ Requires-Dist: redis ; extra == "redis"
33
+ Requires-Dist: sentry-sdk ; extra == "sentry"
34
+ Requires-Dist: pytest ; extra == "test"
35
+ Requires-Dist: pytest-cov ; extra == "test"
36
+ Requires-Dist: pytest-django ; extra == "test"
37
+ Requires-Dist: dramatiq ; extra == "test"
38
+ Requires-Dist: backports.zoneinfo ; extra == "test" and (python_version<'3.9')
39
+ Project-URL: Changelog, https://github.com/voiio/dramatiq-crontab/releases
40
+ Project-URL: Project-URL, https://github.com/voiio/dramatiq-crontab
41
+ Provides-Extra: redis
42
+ Provides-Extra: sentry
43
+ Provides-Extra: test
44
+
45
+ # Dramatiq Crontab
46
+
47
+ ![dramtiq-crontab logo: person in front of a schedule](https://raw.githubusercontent.com/voiio/dramatiq-crontab/main/dramatiq-crontab.png)
48
+
49
+ **Cron style scheduler for asynchronous Dramatiq tasks in Django.**
50
+
51
+ * setup recurring tasks via crontab syntax
52
+ * lightweight helpers build on robust tools like [Dramatiq][dramatiq] and [APScheduler][apscheduler]
53
+ * [Sentry][sentry] cron monitor support
54
+
55
+ [![PyPi Version](https://img.shields.io/pypi/v/dramatiq-crontab.svg)](https://pypi.python.org/pypi/dramatiq-crontab/)
56
+ [![Test Coverage](https://codecov.io/gh/voiio/dramatiq-crontab/branch/main/graph/badge.svg)](https://codecov.io/gh/voiio/dramatiq-crontab)
57
+ [![GitHub License](https://img.shields.io/github/license/voiio/dramatiq-crontab)](https://raw.githubusercontent.com/voiio/dramatiq-crontab/master/LICENSE)
58
+
59
+ ## Setup
60
+
61
+ You need to have [Dramatiq][dramatiq] installed and setup properly.
62
+
63
+ ```ShellSession
64
+ python3 -m pip install dramatiq-crontab
65
+ # or
66
+ python3 -m pip install dramatiq-crontab[sentry] # with sentry cron monitor support
67
+ ```
68
+
69
+ Add `dramatiq_crontab` to your `INSTALLED_APPS` in `settings.py`:
70
+
71
+ ```python
72
+ # settings.py
73
+ INSTALLED_APPS = [
74
+ 'dramatiq_crontab',
75
+ # ...
76
+ ]
77
+ ```
78
+
79
+ Finally, you lauch the scheduler in a separate process:
80
+
81
+ ```ShellSession
82
+ python3 manage.py crontab
83
+ ```
84
+
85
+ ### Setup Redis as a lock backend (optional)
86
+
87
+ If you use Redis as a broker, you can use Redis as a lock backend as well.
88
+ The lock backend is used to prevent multiple instances of the scheduler from running at the same time.
89
+
90
+ ```python
91
+ # settings.py
92
+ DRAMATIQ_CRONTAB = {
93
+ "REDIS_URL": "redis://localhost:6379/0",
94
+ }
95
+ ```
96
+
97
+ ## Usage
98
+
99
+ ```python
100
+ # tasks.py
101
+ import dramatiq
102
+ from dramatiq_crontab import cron
103
+
104
+
105
+ @cron("*/5 * * * *") # every 5 minutes
106
+ @dramatiq.actor
107
+ def my_task():
108
+ my_task.logger.info("Hello World")
109
+ ```
110
+
111
+ ### Sentry Cron Monitors
112
+
113
+ If you use [Sentry][sentry] you can add cron monitors to your tasks.
114
+ The monitor's slug will be the actor's name. Like `my_task` in the example above.
115
+
116
+
117
+ ### The crontab command
118
+
119
+ ```ShellSession
120
+ $ python3 manage.py crontab --help
121
+ usage: manage.py crontab [-h] [--no-task-loading] [--no-heartbeat] [--version] [-v {0,1,2,3}]
122
+ [--settings SETTINGS] [--pythonpath PYTHONPATH] [--traceback] [--no-color]
123
+ [--force-color] [--skip-checks]
124
+
125
+ Run dramatiq task scheduler for all tasks with the `cron` decorator.
126
+
127
+ options:
128
+ -h, --help show this help message and exit
129
+ --no-task-loading Don't load tasks from installed apps.
130
+ --no-heartbeat Don't start the heartbeat actor.
131
+ ```
132
+
133
+ [dramatiq]: https://dramatiq.io/
134
+ [apscheduler]: https://apscheduler.readthedocs.io/en/stable/
135
+ [sentry]: https://docs.sentry.io/product/crons/
@@ -0,0 +1,91 @@
1
+ # Dramatiq Crontab
2
+
3
+ ![dramtiq-crontab logo: person in front of a schedule](https://raw.githubusercontent.com/voiio/dramatiq-crontab/main/dramatiq-crontab.png)
4
+
5
+ **Cron style scheduler for asynchronous Dramatiq tasks in Django.**
6
+
7
+ * setup recurring tasks via crontab syntax
8
+ * lightweight helpers build on robust tools like [Dramatiq][dramatiq] and [APScheduler][apscheduler]
9
+ * [Sentry][sentry] cron monitor support
10
+
11
+ [![PyPi Version](https://img.shields.io/pypi/v/dramatiq-crontab.svg)](https://pypi.python.org/pypi/dramatiq-crontab/)
12
+ [![Test Coverage](https://codecov.io/gh/voiio/dramatiq-crontab/branch/main/graph/badge.svg)](https://codecov.io/gh/voiio/dramatiq-crontab)
13
+ [![GitHub License](https://img.shields.io/github/license/voiio/dramatiq-crontab)](https://raw.githubusercontent.com/voiio/dramatiq-crontab/master/LICENSE)
14
+
15
+ ## Setup
16
+
17
+ You need to have [Dramatiq][dramatiq] installed and setup properly.
18
+
19
+ ```ShellSession
20
+ python3 -m pip install dramatiq-crontab
21
+ # or
22
+ python3 -m pip install dramatiq-crontab[sentry] # with sentry cron monitor support
23
+ ```
24
+
25
+ Add `dramatiq_crontab` to your `INSTALLED_APPS` in `settings.py`:
26
+
27
+ ```python
28
+ # settings.py
29
+ INSTALLED_APPS = [
30
+ 'dramatiq_crontab',
31
+ # ...
32
+ ]
33
+ ```
34
+
35
+ Finally, you lauch the scheduler in a separate process:
36
+
37
+ ```ShellSession
38
+ python3 manage.py crontab
39
+ ```
40
+
41
+ ### Setup Redis as a lock backend (optional)
42
+
43
+ If you use Redis as a broker, you can use Redis as a lock backend as well.
44
+ The lock backend is used to prevent multiple instances of the scheduler from running at the same time.
45
+
46
+ ```python
47
+ # settings.py
48
+ DRAMATIQ_CRONTAB = {
49
+ "REDIS_URL": "redis://localhost:6379/0",
50
+ }
51
+ ```
52
+
53
+ ## Usage
54
+
55
+ ```python
56
+ # tasks.py
57
+ import dramatiq
58
+ from dramatiq_crontab import cron
59
+
60
+
61
+ @cron("*/5 * * * *") # every 5 minutes
62
+ @dramatiq.actor
63
+ def my_task():
64
+ my_task.logger.info("Hello World")
65
+ ```
66
+
67
+ ### Sentry Cron Monitors
68
+
69
+ If you use [Sentry][sentry] you can add cron monitors to your tasks.
70
+ The monitor's slug will be the actor's name. Like `my_task` in the example above.
71
+
72
+
73
+ ### The crontab command
74
+
75
+ ```ShellSession
76
+ $ python3 manage.py crontab --help
77
+ usage: manage.py crontab [-h] [--no-task-loading] [--no-heartbeat] [--version] [-v {0,1,2,3}]
78
+ [--settings SETTINGS] [--pythonpath PYTHONPATH] [--traceback] [--no-color]
79
+ [--force-color] [--skip-checks]
80
+
81
+ Run dramatiq task scheduler for all tasks with the `cron` decorator.
82
+
83
+ options:
84
+ -h, --help show this help message and exit
85
+ --no-task-loading Don't load tasks from installed apps.
86
+ --no-heartbeat Don't start the heartbeat actor.
87
+ ```
88
+
89
+ [dramatiq]: https://dramatiq.io/
90
+ [apscheduler]: https://apscheduler.readthedocs.io/en/stable/
91
+ [sentry]: https://docs.sentry.io/product/crons/
@@ -0,0 +1,85 @@
1
+ """Cron style scheduler for asynchronous Dramatiq tasks in Django."""
2
+
3
+ from apscheduler.schedulers.blocking import BlockingScheduler
4
+ from apscheduler.triggers.cron import CronTrigger
5
+
6
+ from . import _version
7
+
8
+ try:
9
+ from sentry_sdk.crons import monitor
10
+ except ImportError:
11
+ monitor = None
12
+
13
+ __version__ = _version.version
14
+ VERSION = _version.version_tuple
15
+
16
+
17
+ __all__ = ["cron", "scheduler"]
18
+
19
+ try:
20
+ from django.utils import timezone
21
+ except ImportError:
22
+ timezone = None
23
+
24
+
25
+ __all__ = ["cron"]
26
+
27
+ scheduler = BlockingScheduler()
28
+
29
+
30
+ def cron(schedule):
31
+ """
32
+ Run task on a scheduler with a cron schedule.
33
+
34
+ Usage:
35
+ @cron("0 0 * * *")
36
+ @dramatiq.actor
37
+ def cron_test():
38
+ print("Cron test")
39
+
40
+
41
+ Please don't forget to set up a sentry monitor for the actor, otherwise you won't
42
+ get any notifications if the cron job fails.
43
+
44
+ The monitor slug is your actor name, the schedule should be set to the same
45
+ cron schedule as the cron decorator. The schedule type should be set to cron.
46
+ The monitors timezone should be set to Europe/Berlin.
47
+ """
48
+
49
+ def decorator(func):
50
+ *_, day_of_week = schedule.split(" ")
51
+
52
+ if day_of_week.lower() not in (
53
+ "*",
54
+ "mon",
55
+ "tue",
56
+ "wed",
57
+ "thu",
58
+ "fri",
59
+ "sat",
60
+ "sun",
61
+ ):
62
+ # CronTrigger uses Python's timezone dependent first weekday,
63
+ # so in Berlin monday is 0 and sunday is 6. We use literals to avoid
64
+ # confusion. Literals are also more readable and crontab conform.
65
+ raise ValueError(
66
+ "Please use a literal day of week (Mon, Tue, Wed, Thu, Fri, Sat, Sun) or *"
67
+ )
68
+
69
+ fn = getattr(func, "send")
70
+ if monitor is not None:
71
+ fn = monitor(func.actor_name)(fn)
72
+
73
+ scheduler.add_job(
74
+ fn,
75
+ CronTrigger.from_crontab(
76
+ schedule,
77
+ timezone=timezone.get_default_timezone(),
78
+ ),
79
+ name=func.actor_name,
80
+ )
81
+ # We don't add the Sentry monitor on the actor itself, because we only want to
82
+ # monitor the cron job, not the actor itself, or it's direct invocations.
83
+ return func
84
+
85
+ return decorator
@@ -0,0 +1,4 @@
1
+ # file generated by setuptools_scm
2
+ # don't change, don't track in version control
3
+ __version__ = version = '1.0.0'
4
+ __version_tuple__ = version_tuple = (1, 0, 0)
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ from django.conf import settings
4
+
5
+
6
+ def get_settings():
7
+ return type(
8
+ "Settings",
9
+ (),
10
+ {
11
+ "REDIS_URL": None,
12
+ **getattr(settings, "DRAMATIQ_CRONTAB", {}),
13
+ },
14
+ )
@@ -0,0 +1,96 @@
1
+ import importlib
2
+ import signal
3
+
4
+ from django.apps import apps
5
+ from django.core.management import BaseCommand
6
+
7
+ try:
8
+ import redis
9
+ from redis.exceptions import LockError
10
+ except ImportError:
11
+ redis = None
12
+ LockError = Exception
13
+
14
+ try:
15
+ from sentry_sdk import capture_exception
16
+ except ImportError:
17
+ capture_exception = lambda e: None
18
+
19
+ from ... import scheduler
20
+ from ...conf import get_settings
21
+
22
+
23
+ def kill_softly(signum, frame):
24
+ """Raise a KeyboardInterrupt to stop the scheduler and release the lock."""
25
+ raise KeyboardInterrupt("Received SIGTERM!")
26
+
27
+
28
+ def get_redis_client():
29
+ redis_url = get_settings().REDIS_URL
30
+ if redis and redis_url:
31
+ return redis.Redis.from_url(redis_url)
32
+
33
+
34
+ class Command(BaseCommand):
35
+ """Run dramatiq task scheduler for all tasks with the `cron` decorator."""
36
+
37
+ help = __doc__
38
+
39
+ def add_arguments(self, parser):
40
+ parser.add_argument(
41
+ "--no-task-loading",
42
+ action="store_true",
43
+ help="Don't load tasks from installed apps.",
44
+ )
45
+ parser.add_argument(
46
+ "--no-heartbeat",
47
+ action="store_true",
48
+ help="Don't start the heartbeat actor.",
49
+ )
50
+
51
+ def handle(self, *args, **options):
52
+ if not options["no_task_loading"]:
53
+ self.load_tasks(options)
54
+ if not options["no_heartbeat"]:
55
+ importlib.import_module("dramatiq_crontab.tasks")
56
+ self.stdout.write("Scheduling heartbeat.")
57
+ redis = get_redis_client()
58
+ if redis:
59
+ try:
60
+ # Lock scheduler to prevent multiple instances from running.
61
+ with redis.lock("dramatiq-scheduler", blocking_timeout=0):
62
+ self.launch_scheduler()
63
+ except LockError as e:
64
+ capture_exception(e)
65
+ self.stderr.write("Another scheduler is already running.")
66
+ else:
67
+ self.launch_scheduler()
68
+
69
+ def launch_scheduler(self):
70
+ signal.signal(signal.SIGTERM, kill_softly)
71
+ self.stdout.write(self.style.SUCCESS("Starting scheduler…"))
72
+ try:
73
+ scheduler.start()
74
+ except KeyboardInterrupt as e:
75
+ self.stdout.write(self.style.WARNING(str(e)))
76
+ self.stdout.write(self.style.NOTICE("Shutting down scheduler…"))
77
+ scheduler.shutdown()
78
+
79
+ def load_tasks(self, options):
80
+ """
81
+ Load all tasks modules within installed apps.
82
+
83
+ If they are not imported, they will not have registered
84
+ their tasks with the scheduler.
85
+ """
86
+ for app in apps.get_app_configs():
87
+ if app.name == "dramatiq_crontab":
88
+ continue
89
+ if app.ready:
90
+ try:
91
+ importlib.import_module(f"{app.name}.tasks")
92
+ self.stdout.write(
93
+ f"Loaded tasks from {self.style.NOTICE(app.name)}."
94
+ )
95
+ except ImportError:
96
+ pass
@@ -0,0 +1,9 @@
1
+ import dramatiq
2
+
3
+ from . import cron
4
+
5
+
6
+ @cron("* * * * *")
7
+ @dramatiq.actor
8
+ def heartbeat():
9
+ heartbeat.logger.info("ﮩ٨ـﮩﮩ٨ـ♡ﮩ٨ـﮩﮩ٨ـ")
@@ -0,0 +1,91 @@
1
+ [build-system]
2
+ requires = ["flit_core>=3.2", "flit_scm", "wheel"]
3
+ build-backend = "flit_scm:buildapi"
4
+
5
+ [project]
6
+ name = "dramatiq-crontab"
7
+ authors = [
8
+ { name = "Rust Saiargaliev", email = "fly.amureki@gmail.com" },
9
+ { name = "Johannes Maron", email = "johannes@maron.family" },
10
+ { name = "Mostafa Mohamed", email = "mostafa.anm91@gmail.com" },
11
+ { name = "Jacqueline Kraus", email = "jacquelinekraus1992@gmail.com" },
12
+ ]
13
+ readme = "README.md"
14
+ license = { file = "LICENSE" }
15
+ keywords = ["Django", "Dramatiq", "tasks", "scheduler"]
16
+ dynamic = ["version", "description"]
17
+ classifiers = [
18
+ "Development Status :: 5 - Production/Stable",
19
+ "Programming Language :: Python",
20
+ "Environment :: Web Environment",
21
+ "License :: OSI Approved :: BSD License",
22
+ "Intended Audience :: Developers",
23
+ "Operating System :: OS Independent",
24
+ "Topic :: Communications :: Email",
25
+ "Topic :: Text Processing :: Markup :: Markdown",
26
+ "Topic :: Software Development",
27
+ "Programming Language :: Python",
28
+ "Programming Language :: Python :: 3",
29
+ "Programming Language :: Python :: 3 :: Only",
30
+ "Programming Language :: Python :: 3.8",
31
+ "Programming Language :: Python :: 3.9",
32
+ "Programming Language :: Python :: 3.10",
33
+ "Programming Language :: Python :: 3.11",
34
+ "Framework :: Django",
35
+ "Framework :: Django :: 3.2",
36
+ "Framework :: Django :: 4.1",
37
+ "Framework :: Django :: 4.2",
38
+ ]
39
+ requires-python = ">=3.8"
40
+ dependencies = ["dramatiq", "apscheduler", "django"]
41
+
42
+ [project.optional-dependencies]
43
+ test = [
44
+ "pytest",
45
+ "pytest-cov",
46
+ "pytest-django",
47
+ "dramatiq",
48
+ "backports.zoneinfo;python_version<'3.9'"
49
+ ]
50
+ sentry = ["sentry-sdk"]
51
+ redis = ["redis"]
52
+
53
+ [project.urls]
54
+ Project-URL = "https://github.com/voiio/dramatiq-crontab"
55
+ Changelog = "https://github.com/voiio/dramatiq-crontab/releases"
56
+
57
+ [tool.flit.module]
58
+ name = "dramatiq_crontab"
59
+
60
+ [tool.setuptools_scm]
61
+ write_to = "dramatiq_crontab/_version.py"
62
+
63
+ [tool.pytest.ini_options]
64
+ minversion = "6.0"
65
+ addopts = "--cov --tb=short -rxs"
66
+ testpaths = ["tests"]
67
+ DJANGO_SETTINGS_MODULE = "tests.testapp.settings"
68
+
69
+ [tool.coverage.run]
70
+ source = ["dramatiq_crontab"]
71
+
72
+ [tool.coverage.report]
73
+ show_missing = true
74
+
75
+ [tool.isort]
76
+ atomic = true
77
+ line_length = 88
78
+ known_first_party = "dramatiq_crontab, tests"
79
+ include_trailing_comma = true
80
+ default_section = "THIRDPARTY"
81
+ combine_as_imports = true
82
+ multi_line_output = 3
83
+
84
+ [tool.pydocstyle]
85
+ add_ignore = "D1"
86
+
87
+ [tool.djlint]
88
+ profile="django"
89
+ indent=2
90
+ max_line_length=120
91
+ exclude=".direnv,.venv,venv"