Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
tejasjadhav committed Mar 26, 2017
0 parents commit a17a6c2
Show file tree
Hide file tree
Showing 12 changed files with 549 additions and 0 deletions.
107 changes: 107 additions & 0 deletions .gitignore
@@ -0,0 +1,107 @@

# Created by https://www.gitignore.io/api/python,django

### Django ###
*.log
*.pot
*.pyc
__pycache__/
local_settings.py
db.sqlite3
media

### Python ###
# Byte-compiled / optimized / DLL files
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/

# Translations
*.mo

# Django stuff:

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# pyenv
.python-version

# celery beat schedule file
celerybeat-schedule

# SageMath parsed files
*.sage.py

# dotenv
.env

# virtualenv
.venv
venv/
ENV/

# Spyder project settings
.spyderproject

# Rope project settings
.ropeproject

# End of https://www.gitignore.io/api/python,django

.vscode/
78 changes: 78 additions & 0 deletions README.md
@@ -0,0 +1,78 @@
Scheduler
=========
Task Scheduler for Django
-------------------------

Scheduler is a simple Django app for scheduling tasks at a specific time or repeating in a pattern based on the rules defined in **Recurrence Rule _(RRULE)_** syntax as per the [RFC2445](https://tools.ietf.org/html/rfc2445) standard.

# Special Thanks
* [Django](https://github.com/django/django/) - ORM and app system.
* [Celery](https://github.com/celery/celery/) - Task management.
* [Dateutil](https://github.com/dateutil/dateutil/) - Interpreting _RRULE_ syntax.

# Requirements
* [Django](https://github.com/django/django/) 1.8+
* [Celery](https://github.com/celery/celery/) 4.0+

# Usage
* Add the `celery.task` or `shared_task` decorator and specify `scheduler.tasks.RepeatTask` as the base.
```python
from celery import shared_task
from scheduler.tasks import RepeatTask

@shared_task(base=RepeatTask)
def say_hello(fruit):
print(f'Hello, I ate {fruit} today.')
```
* Make sure atleast one Celery worker is running. Restart your workers so that the changes are reflected in the workers.
* Use `TaskScheduler` to schedule the task. You can specify the starting time, the ending time and the recurrence rule for the task. Arguments and keyword arguments should be specified separately. Task description is mandatory. The task scheduler returns a `task_id` which can be used to cancel the task.
```python
from datetime import timedelta

from django.utils import timezone
from scheduler.scheduler import TaskScheduler

now = timezone.now() + timedelta(hours=3)
tomorrow = now + timedelta(hours=7)

task_id = TaskScheduler.schedule(say_hello,
description='Speaks out which fruit you ate today',
args=('Apple', )
kwargs=None,
rrule_string='RRULE:FREQ=HOURLY;INTERVAL=2', # Run every 2 hours...
trigger_at=now, # ...starting 3 hours from now...
until=tomorrow # ...till 10 hours from now..
)
print(f'Task ID: {task_id}') # Task ID: UUID('53f81020-1b77-45cd-8f0d-2c5a8acee6a8')
```
* Suppose, you run the above snippet at `2017-03-25 01:30:00`, the task will execute at the following times,
```
[2017-03-25 04:30:00] Hello, I ate Apple today.
[2017-03-25 06:30:00] Hello, I ate Apple today.
[2017-03-25 08:30:00] Hello, I ate Apple today.
[2017-03-25 10:30:00] Hello, I ate Apple today.
```
* To run this task immediately, set `trigger_at` as `None`.
* To run this task infinitely, set `until` as `None`.
* To cancel a schedule, use `TaskScheduler.cancel` method and pass the `task_id` that was generated by the `TaskScheduler`.
```python
TaskScheduler.cancel(task_id)
```

# API
## `TaskScheduler.schedule`
**Signature:** `TaskScheduler.schedule(func, description, args=None, kwargs=None, rrule_string=None, trigger_at=None, until=None)`
Parameter | Type | Description
--- | --- | --
`func` | `function` | **Required.** Task function.
`description` | `str` | **Required.** Description of the task. **This may get deprecated in future.**
`args` | `list` or `tuple` | **Optional.** **Default:** `None`. Arguments to be passed to `func`. **All values should be JSON serializable.**
`kwargs` | `dict` | **Optional.** **Default:** `None`. Keyword arguments to be passed to `func`. **All values should be JSON serializable.**
`rrule_string` | `str` | **Optional.** **Default:** `None`. Recurrence rule. `func` will be executed as per the pattern defined by this rule. If not specified, `func` will be executed only once.
`trigger_at` | `datetime` with `tzinfo` | **Optional.** **Default:** `None`. Sets the datetime for the first run of the task. If not specified, it defaults to `datetime.now()`.
`until` | `datetime` with `tzinfo` | **Optional.** **Default:** `None`. Sets the end time for the task. The task will never execute after this datetime. If not specified, `task` will run as long as it is defined in the recurrence rule.

> **NOTE:** It is important that all the `datetime` instances passed should have timezone information, i. e., they should be [timezone aware](https://docs.djangoproject.com/en/1.10/topics/i18n/timezones/#naive-and-aware-datetime-objects). Use Django's [`timezone.now()`](https://docs.djangoproject.com/en/1.10/ref/utils/#django.utils.timezone.now) utility to get a timezone aware instance with `USE_TZ=True` in settings.
# Limitations
* Currently, there is no support for `DTSTART` and `UNTIL` rules. Use `trigger_at` and `until` parameters. Even if you specify `DTSTART` and `UNTIL` rules, they will get overidden.
Empty file added __init__.py
Empty file.
22 changes: 22 additions & 0 deletions admin.py
@@ -0,0 +1,22 @@
from django.contrib import admin

from .models import (
ScheduledTask,
ScheduledTaskRunLog
)


class ScheduledTaskRunLogTabularInline(admin.TabularInline):
readonly_fields = [field.name
for field in ScheduledTaskRunLog._meta.fields
if field.name != 'id']
ordering = ('created_at', )
model = ScheduledTaskRunLog


@admin.register(ScheduledTask)
class ScheduledTaskModelAdmin(admin.ModelAdmin):
list_display = [field.name for field in ScheduledTask._meta.fields]
inlines = (ScheduledTaskRunLogTabularInline, )
ordering = ('-created_at', )
model = ScheduledTask
5 changes: 5 additions & 0 deletions apps.py
@@ -0,0 +1,5 @@
from django.apps import AppConfig


class SchedulerConfig(AppConfig):
name = 'scheduler'
42 changes: 42 additions & 0 deletions migrations/0001_initial.py
@@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.6 on 2017-03-26 10:02
from __future__ import unicode_literals

import django.contrib.postgres.fields.jsonb
from django.db import migrations, models
import django.db.models.deletion
import uuid


class Migration(migrations.Migration):

initial = True

dependencies = [
]

operations = [
migrations.CreateModel(
name='ScheduledTask',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('function_name', models.CharField(max_length=1000)),
('description', models.TextField(blank=True, null=True)),
('args', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict, null=True)),
('kwargs', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict, null=True)),
('rrule', models.TextField(blank=True, null=True)),
('status', models.CharField(choices=[('created', 'Created'), ('running', 'Running'), ('success', 'Success'), ('failure', 'Failure'), ('cancelled', 'Cancelled')], default='created', max_length=255)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
),
migrations.CreateModel(
name='ScheduledTaskRunLog',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('task_id', models.UUIDField()),
('created_at', models.DateTimeField(auto_now_add=True)),
('scheduled_task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tasker.ScheduledTask')),
],
),
]
Empty file added migrations/__init__.py
Empty file.
67 changes: 67 additions & 0 deletions models.py
@@ -0,0 +1,67 @@
from __future__ import unicode_literals

from uuid import uuid4

from django.contrib.postgres.fields import JSONField
from django.db import models


class ScheduledTask(models.Model):
id = models.UUIDField(primary_key=True, editable=False, default=uuid4)
function_name = models.CharField(max_length=1000)
description = models.TextField(blank=True, null=True)

args = JSONField(blank=True, null=True, default=dict)
kwargs = JSONField(blank=True, null=True, default=dict)
rrule = models.TextField(blank=True, null=True)

STATUS_CREATED = 'created'
STATUS_RUNNING = 'running'
STATUS_SUCCESS = 'success'
STATUS_FAILURE = 'failure'
STATUS_CANCELLED = 'cancelled'
STATUS_CHOICES = (
(STATUS_CREATED, 'Created'),
(STATUS_RUNNING, 'Running'),
(STATUS_SUCCESS, 'Success'),
(STATUS_FAILURE, 'Failure'),
(STATUS_CANCELLED, 'Cancelled'),
)

status = models.CharField(max_length=255, choices=STATUS_CHOICES,
default=STATUS_CREATED)

created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

def __unicode__(self):
return self.function_name

def __str__(self):
return self.function_name

def save_status(self, status):
"""Sets and saves the status to the scheduled task.
Arguments:
status (str): Status.
Returns:
ScheduledTask: Current scheduled task instance.
"""
self.status = status
self.save(update_fields=('status', ))
return self


class ScheduledTaskRunLog(models.Model):
task_id = models.UUIDField()
scheduled_task = models.ForeignKey(ScheduledTask)

created_at = models.DateTimeField(auto_now_add=True)

def __unicode__(self):
return str(self.task_id)

def __str__(self):
return str(self.task_id)
3 changes: 3 additions & 0 deletions requirements.txt
@@ -0,0 +1,3 @@
celery==4.0.2
Django==1.10.6
python-dateutil==2.6.0

0 comments on commit a17a6c2

Please sign in to comment.