Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Dependencies
## Python
pyproject.toml @herrbenesch @amureki
## GitHub Actions
.github/workflows/ci.yml @herrbenesch @amureki
.github/workflows/release.yml @herrbenesch @amureki
3 changes: 3 additions & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
github:
- amureki
- codingjoe
4 changes: 2 additions & 2 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ updates:
- package-ecosystem: pip
directory: "/"
schedule:
interval: weekly
interval: daily
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weekly
interval: daily
13 changes: 11 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,24 @@ jobs:
os:
- "ubuntu-latest"
python-version:
- "3.10"
- "3.11"
- "3.12"
- "3.13"
- "3.14"
django-version:
- "6.0"
- "4.2" # LTS
- "5.1"
- "5.2"
extras:
- "test"
- "test,sentry"
- "test,redis"
include:
# 4.2 is the last version to support Python 3.9
- os: "ubuntu-latest"
python-version: "3.9"
django-version: "4.2"
extras: "test"
services:
redis:
image: redis
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -131,4 +131,4 @@ dmypy.json

# Packaging

crontask/_version.py
dramatiq_crontab/_version.py
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
BSD 3-Clause License

Copyright (c) 2025, Johannes Maron, voiio GmbH & contributors
Copyright (c) 2023, voiio GmbH

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
Expand Down
50 changes: 22 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,47 +1,41 @@
# Django CronTask
# Dramatiq Crontab

<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./images/logo-dark.svg">
<source media="(prefers-color-scheme: light)" srcset="./images/logo-light.svg">
<img alt="Django crontask: Cron style scheduler for Django's task framework" src="./images/logo-light.svg">
</picture>
</p>
![dramtiq-crontab logo: person in front of a schedule](https://raw.githubusercontent.com/voiio/dramatiq-crontab/main/dramatiq-crontab.png)
Comment thread
codingjoe marked this conversation as resolved.

**Cron style scheduler for asynchronous tasks in Django.**
**Cron style scheduler for asynchronous Dramatiq tasks in Django.**

- setup recurring tasks via crontab syntax
- lightweight helpers build [APScheduler]
- lightweight helpers build on robust tools like [Dramatiq] and [APScheduler]
Comment thread
codingjoe marked this conversation as resolved.
- [Sentry] cron monitor support

[![PyPi Version](https://img.shields.io/pypi/v/django-crontask.svg)](https://pypi.python.org/pypi/django-crontask/)
[![Test Coverage](https://codecov.io/gh/codingjoe/django-crontask/branch/main/graph/badge.svg)](https://codecov.io/gh/codingjoe/django-crontask)
[![GitHub License](https://img.shields.io/github/license/codingjoe/django-crontask)](https://raw.githubusercontent.com/codingjoe/django-crontask/master/LICENSE)
[![PyPi Version](https://img.shields.io/pypi/v/dramatiq-crontab.svg)](https://pypi.python.org/pypi/dramatiq-crontab/)
[![Test Coverage](https://codecov.io/gh/voiio/dramatiq-crontab/branch/main/graph/badge.svg)](https://codecov.io/gh/voiio/dramatiq-crontab)
[![GitHub License](https://img.shields.io/github/license/voiio/dramatiq-crontab)](https://raw.githubusercontent.com/voiio/dramatiq-crontab/master/LICENSE)

## Setup

You need to have [Django's Task framework][django-tasks] setup properly.
You need to have [Dramatiq] installed and setup properly.

```ShellSession
python3 -m pip install django-crontask
python3 -m pip install dramatiq-crontab
# or
python3 -m pip install django-crontask[sentry] # with sentry cron monitor support
python3 -m pip install dramatiq-crontab[sentry] # with sentry cron monitor support
```

Add `crontask` to your `INSTALLED_APPS` in `settings.py`:
Add `dramatiq_crontab` to your `INSTALLED_APPS` in `settings.py`:

```python
# settings.py
INSTALLED_APPS = [
"crontask",
"dramatiq_crontab",
# ...
]
```

Finally, you lauch the scheduler in a separate process:

```ShellSession
python3 manage.py crontask
python3 manage.py crontab
```

### Setup Redis as a lock backend (optional)
Expand All @@ -53,7 +47,7 @@ instances of your application running.

```python
# settings.py
CRONTASK = {
DRAMATIQ_CRONTAB = {
"REDIS_URL": "redis://localhost:6379/0",
}
```
Expand All @@ -62,12 +56,12 @@ CRONTASK = {

```python
# tasks.py
from django.tasks import task
from crontask import cron
import dramatiq
from dramatiq_crontab import cron


@cron("*/5 * * * *") # every 5 minutes
@task
@dramatiq.actor
def my_task():
my_task.logger.info("Hello World")
```
Expand All @@ -79,12 +73,12 @@ If you want to run a task more frequently than once a minute, you can use the

```python
# tasks.py
from django.tasks import task
from crontask import interval
import dramatiq
from dramatiq_crontab import interval


@interval(seconds=30)
@task
@dramatiq.actor
def my_task():
my_task.logger.info("Hello World")
```
Expand All @@ -107,7 +101,7 @@ usage: manage.py crontab [-h] [--no-task-loading] [--no-heartbeat] [--version] [
[--settings SETTINGS] [--pythonpath PYTHONPATH] [--traceback] [--no-color]
[--force-color] [--skip-checks]

Run task scheduler for all tasks with the `cron` decorator.
Run dramatiq task scheduler for all tasks with the `cron` decorator.

options:
-h, --help show this help message and exit
Expand All @@ -116,5 +110,5 @@ options:
```

[apscheduler]: https://apscheduler.readthedocs.io/en/stable/
[django-tasks]: https://docs.djangoproject.com/en/6.0/topics/tasks/
[dramatiq]: https://dramatiq.io/
[sentry]: https://docs.sentry.io/product/crons/
13 changes: 0 additions & 13 deletions crontask/tasks.py

This file was deleted.

Binary file added dramatiq-crontab.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
40 changes: 13 additions & 27 deletions crontask/__init__.py → dramatiq_crontab/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Cron style scheduler for Django's task framework."""
"""Cron style scheduler for asynchronous Dramatiq tasks in Django."""

from unittest.mock import Mock

Expand Down Expand Up @@ -42,7 +42,7 @@ def cron(schedule):

Usage:
@cron("0 0 * * *")
@task
@dramatiq.actor
def cron_test():
print("Cron test")

Expand All @@ -55,7 +55,7 @@ def cron_test():
The monitors timezone should be set to Europe/Berlin.
"""

def decorator(task):
def decorator(actor):
*_, day_schedule = schedule.split(" ")

# CronTrigger uses Python's timezone dependent first weekday,
Expand All @@ -67,26 +67,19 @@ def decorator(task):
)

if monitor is not None:
task = type(task)(
priority=task.priority,
func=monitor(task.name)(task.func),
queue_name=task.queue_name,
backend=task.backend,
takes_context=task.takes_context,
run_after=task.run_after,
)
actor.fn = monitor(actor.actor_name)(actor.fn)

scheduler.add_job(
task.enqueue,
actor.send,
CronTrigger.from_crontab(
schedule,
timezone=timezone.get_default_timezone(),
),
name=task.name,
name=actor.actor_name,
)
# We don't add the Sentry monitor on the actor itself, because we only want to
# monitor the cron job, not the actor itself, or it's direct invocations.
return task
return actor

return decorator

Expand All @@ -97,7 +90,7 @@ def interval(*, seconds):

Usage:
@interval(seconds=30)
@task
@dramatiq.actor
def interval_test():
print("Interval test")

Expand All @@ -109,25 +102,18 @@ def interval_test():
For an interval that is consistent with the clock, use the `cron` decorator instead.
"""

def decorator(task):
def decorator(actor):
if monitor is not None:
task = type(task)(
priority=task.priority,
func=monitor(task.name)(task.func),
queue_name=task.queue_name,
backend=task.backend,
takes_context=task.takes_context,
run_after=task.run_after,
)
actor.fn = monitor(actor.actor_name)(actor.fn)

scheduler.add_job(
task.enqueue,
actor.send,
IntervalTrigger(
seconds=seconds,
timezone=timezone.get_default_timezone(),
),
name=task.name,
name=actor.actor_name,
)
return task
return actor

return decorator
2 changes: 1 addition & 1 deletion crontask/conf.py → dramatiq_crontab/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ def get_settings():
"LOCK_REFRESH_INTERVAL": 5,
"LOCK_TIMEOUT": 10,
"LOCK_BLOCKING_TIMEOUT": 15,
**getattr(settings, "CRONTASK", {}),
**getattr(settings, "DRAMATIQ_CRONTAB", {}),
},
)
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def kill_softly(signum, frame):


class Command(BaseCommand):
"""Run task scheduler for all tasks with the `cron` decorator."""
"""Run dramatiq task scheduler for all tasks with the `cron` decorator."""

help = __doc__

Expand All @@ -42,7 +42,7 @@ def handle(self, *args, **options):
if not options["no_task_loading"]:
self.load_tasks(options)
if not options["no_heartbeat"]:
importlib.import_module("crontask.tasks")
importlib.import_module("dramatiq_crontab.tasks")
self.stdout.write("Scheduling heartbeat.")
try:
if not isinstance(utils.lock, utils.FakeLock):
Expand Down Expand Up @@ -70,7 +70,7 @@ def launch_scheduler(self, lock, scheduler):
utils.extend_lock,
IntervalTrigger(seconds=conf.get_settings().LOCK_REFRESH_INTERVAL),
args=(lock, scheduler),
name="contask.utils.lock.extend",
name="dramatiq_crontab.utils.lock.extend",
)
try:
scheduler.start()
Expand All @@ -87,7 +87,7 @@ def load_tasks(self, options):
their tasks with the scheduler.
"""
for app in apps.get_app_configs():
if app.name == "contask":
if app.name == "dramatiq_crontab":
continue
if app.ready:
try:
Expand Down
9 changes: 9 additions & 0 deletions dramatiq_crontab/tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import dramatiq

from . import cron


@cron("* * * * *")
@dramatiq.actor
def heartbeat():
heartbeat.logger.info("ﮩ٨ـﮩﮩ٨ـ♡ﮩ٨ـﮩﮩ٨ـ")
4 changes: 2 additions & 2 deletions crontask/utils.py → dramatiq_crontab/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from crontask.conf import get_settings
from dramatiq_crontab.conf import get_settings

__all__ = ["LockError", "lock"]

Expand All @@ -20,7 +20,7 @@ def extend(self, additional_time=None, replace_ttl=False):

redis_client = redis.Redis.from_url(redis_url)
lock = redis_client.lock(
"crontask-lock",
"dramatiq-scheduler",
blocking_timeout=get_settings().LOCK_BLOCKING_TIMEOUT,
timeout=get_settings().LOCK_TIMEOUT,
thread_local=False,
Expand Down
13 changes: 0 additions & 13 deletions images/logo-dark.svg

This file was deleted.

13 changes: 0 additions & 13 deletions images/logo-light.svg

This file was deleted.

Loading
Loading