mirror of
https://github.com/django-q2/django-q2.git
synced 2026-10-02 23:58:11 +08:00
+2
-1
@@ -66,4 +66,5 @@ db.sqlite3
|
|||||||
.venv
|
.venv
|
||||||
.idea
|
.idea
|
||||||
djq
|
djq
|
||||||
node_modules
|
node_modules
|
||||||
|
/c.cache/
|
||||||
|
|||||||
+3
-3
@@ -1,7 +1,7 @@
|
|||||||
language: python
|
language: python
|
||||||
|
|
||||||
services:
|
services:
|
||||||
- redis-server
|
- redis
|
||||||
- mongodb
|
- mongodb
|
||||||
|
|
||||||
python:
|
python:
|
||||||
@@ -9,8 +9,8 @@ python:
|
|||||||
- "3.8"
|
- "3.8"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
- DJANGO=3.0.7
|
- DJANGO=3.0.8
|
||||||
- DJANGO=2.2.13
|
- DJANGO=2.2.14
|
||||||
|
|
||||||
sudo: true
|
sudo: true
|
||||||
dist: xenial
|
dist: xenial
|
||||||
|
|||||||
+7
-1
@@ -13,7 +13,7 @@ Features
|
|||||||
|
|
||||||
- Multiprocessing worker pool
|
- Multiprocessing worker pool
|
||||||
- Asynchronous tasks
|
- Asynchronous tasks
|
||||||
- Scheduled and repeated tasks
|
- Scheduled, cron and repeated tasks
|
||||||
- Signed and compressed packages
|
- Signed and compressed packages
|
||||||
- Failure and success database or cache
|
- Failure and success database or cache
|
||||||
- Result hooks, groups and chains
|
- Result hooks, groups and chains
|
||||||
@@ -180,6 +180,12 @@ Admin page or directly from your code:
|
|||||||
repeats=24,
|
repeats=24,
|
||||||
next_run=arrow.utcnow().replace(hour=18, minute=0))
|
next_run=arrow.utcnow().replace(hour=18, minute=0))
|
||||||
|
|
||||||
|
# Use a cron expression
|
||||||
|
schedule('math.hypot',
|
||||||
|
3, 4,
|
||||||
|
schedule_type=Schedule.CRON,
|
||||||
|
cron = '0 22 * * 1-5')
|
||||||
|
|
||||||
For more info check the `Schedules <https://django-q.readthedocs.org/en/latest/schedules.html>`__ documentation.
|
For more info check the `Schedules <https://django-q.readthedocs.org/en/latest/schedules.html>`__ documentation.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
VERSION = (1, 2, 4)
|
VERSION = (1, 2, 4)
|
||||||
|
|
||||||
default_app_config = 'django_q.apps.DjangoQConfig'
|
default_app_config = "django_q.apps.DjangoQConfig"
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['conf', 'cluster', 'models', 'tasks']
|
__all__ = ["conf", "cluster", "models", "tasks", "croniter"]
|
||||||
|
|
||||||
|
# Optional Imports
|
||||||
|
try:
|
||||||
|
from croniter import croniter
|
||||||
|
except ImportError:
|
||||||
|
croniter = None
|
||||||
|
|||||||
+24
-38
@@ -5,19 +5,13 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from django_q.conf import Conf
|
from django_q.conf import Conf
|
||||||
from django_q.models import Success, Failure, Schedule, OrmQ
|
from django_q.models import Success, Failure, Schedule, OrmQ
|
||||||
from django_q.tasks import async_task
|
from django_q.tasks import async_task
|
||||||
|
from django_q import croniter
|
||||||
|
|
||||||
|
|
||||||
class TaskAdmin(admin.ModelAdmin):
|
class TaskAdmin(admin.ModelAdmin):
|
||||||
"""model admin for success tasks."""
|
"""model admin for success tasks."""
|
||||||
|
|
||||||
list_display = (
|
list_display = ("name", "func", "started", "stopped", "time_taken", "group")
|
||||||
'name',
|
|
||||||
'func',
|
|
||||||
'started',
|
|
||||||
'stopped',
|
|
||||||
'time_taken',
|
|
||||||
'group'
|
|
||||||
)
|
|
||||||
|
|
||||||
def has_add_permission(self, request):
|
def has_add_permission(self, request):
|
||||||
"""Don't allow adds."""
|
"""Don't allow adds."""
|
||||||
@@ -28,9 +22,9 @@ class TaskAdmin(admin.ModelAdmin):
|
|||||||
qs = super(TaskAdmin, self).get_queryset(request)
|
qs = super(TaskAdmin, self).get_queryset(request)
|
||||||
return qs.filter(success=True)
|
return qs.filter(success=True)
|
||||||
|
|
||||||
search_fields = ('name', 'func', 'group')
|
search_fields = ("name", "func", "group")
|
||||||
readonly_fields = []
|
readonly_fields = []
|
||||||
list_filter = ('group',)
|
list_filter = ("group",)
|
||||||
|
|
||||||
def get_readonly_fields(self, request, obj=None):
|
def get_readonly_fields(self, request, obj=None):
|
||||||
"""Set all fields readonly."""
|
"""Set all fields readonly."""
|
||||||
@@ -50,21 +44,15 @@ retry_failed.short_description = _("Resubmit selected tasks to queue")
|
|||||||
class FailAdmin(admin.ModelAdmin):
|
class FailAdmin(admin.ModelAdmin):
|
||||||
"""model admin for failed tasks."""
|
"""model admin for failed tasks."""
|
||||||
|
|
||||||
list_display = (
|
list_display = ("name", "func", "started", "stopped", "short_result")
|
||||||
'name',
|
|
||||||
'func',
|
|
||||||
'started',
|
|
||||||
'stopped',
|
|
||||||
'short_result'
|
|
||||||
)
|
|
||||||
|
|
||||||
def has_add_permission(self, request):
|
def has_add_permission(self, request):
|
||||||
"""Don't allow adds."""
|
"""Don't allow adds."""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
actions = [retry_failed]
|
actions = [retry_failed]
|
||||||
search_fields = ('name', 'func')
|
search_fields = ("name", "func")
|
||||||
list_filter = ('group',)
|
list_filter = ("group",)
|
||||||
readonly_fields = []
|
readonly_fields = []
|
||||||
|
|
||||||
def get_readonly_fields(self, request, obj=None):
|
def get_readonly_fields(self, request, obj=None):
|
||||||
@@ -76,31 +64,29 @@ class ScheduleAdmin(admin.ModelAdmin):
|
|||||||
""" model admin for schedules """
|
""" model admin for schedules """
|
||||||
|
|
||||||
list_display = (
|
list_display = (
|
||||||
'id',
|
"id",
|
||||||
'name',
|
"name",
|
||||||
'func',
|
"func",
|
||||||
'schedule_type',
|
"schedule_type",
|
||||||
'repeats',
|
"repeats",
|
||||||
'next_run',
|
"next_run",
|
||||||
'last_run',
|
"last_run",
|
||||||
'success'
|
"success",
|
||||||
)
|
)
|
||||||
|
|
||||||
list_filter = ('next_run', 'schedule_type')
|
# optional cron strings
|
||||||
search_fields = ('func',)
|
if not croniter:
|
||||||
list_display_links = ('id', 'name')
|
readonly_fields = ("cron",)
|
||||||
|
|
||||||
|
list_filter = ("next_run", "schedule_type")
|
||||||
|
search_fields = ("func",)
|
||||||
|
list_display_links = ("id", "name")
|
||||||
|
|
||||||
|
|
||||||
class QueueAdmin(admin.ModelAdmin):
|
class QueueAdmin(admin.ModelAdmin):
|
||||||
""" queue admin for ORM broker """
|
""" queue admin for ORM broker """
|
||||||
list_display = (
|
|
||||||
'id',
|
list_display = ("id", "key", "task_id", "name", "func", "lock")
|
||||||
'key',
|
|
||||||
'task_id',
|
|
||||||
'name',
|
|
||||||
'func',
|
|
||||||
'lock'
|
|
||||||
)
|
|
||||||
|
|
||||||
def save_model(self, request, obj, form, change):
|
def save_model(self, request, obj, form, change):
|
||||||
obj.save(using=Conf.ORM)
|
obj.save(using=Conf.ORM)
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ from django_q.conf import Conf
|
|||||||
|
|
||||||
|
|
||||||
class DjangoQConfig(AppConfig):
|
class DjangoQConfig(AppConfig):
|
||||||
name = 'django_q'
|
name = "django_q"
|
||||||
verbose_name = Conf.LABEL
|
verbose_name = Conf.LABEL
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import random
|
import random
|
||||||
|
|
||||||
|
# External
|
||||||
import redis
|
import redis
|
||||||
from redis import Redis
|
from redis import Redis
|
||||||
|
|
||||||
|
# Django
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django_q.brokers import Broker
|
from django_q.brokers import Broker
|
||||||
from django_q.conf import Conf
|
from django_q.conf import Conf
|
||||||
|
|
||||||
@@ -52,6 +55,8 @@ class Disque(Broker):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_connection(list_key: str = Conf.PREFIX) -> Redis:
|
def get_connection(list_key: str = Conf.PREFIX) -> Redis:
|
||||||
|
if not Conf.DISQUE_NODES:
|
||||||
|
raise redis.exceptions.ConnectionError(_("No Disque nodes configured"))
|
||||||
# randomize nodes
|
# randomize nodes
|
||||||
random.shuffle(Conf.DISQUE_NODES)
|
random.shuffle(Conf.DISQUE_NODES)
|
||||||
# find one that works
|
# find one that works
|
||||||
@@ -67,4 +72,6 @@ class Disque(Broker):
|
|||||||
return redis_client
|
return redis_client
|
||||||
except redis.exceptions.ConnectionError:
|
except redis.exceptions.ConnectionError:
|
||||||
continue
|
continue
|
||||||
raise redis.exceptions.ConnectionError("Could not connect to any Disque nodes")
|
raise redis.exceptions.ConnectionError(
|
||||||
|
_("Could not connect to any Disque nodes")
|
||||||
|
)
|
||||||
|
|||||||
+29
-20
@@ -1,6 +1,5 @@
|
|||||||
import ast
|
|
||||||
|
|
||||||
# Standard
|
# Standard
|
||||||
|
import ast
|
||||||
import importlib
|
import importlib
|
||||||
import signal
|
import signal
|
||||||
import socket
|
import socket
|
||||||
@@ -9,9 +8,8 @@ import uuid
|
|||||||
from multiprocessing import Event, Process, Value, current_process
|
from multiprocessing import Event, Process, Value, current_process
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
# external
|
# External
|
||||||
import arrow
|
import arrow
|
||||||
|
|
||||||
# Django
|
# Django
|
||||||
from django import db
|
from django import db
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -28,6 +26,7 @@ from django_q.queues import Queue
|
|||||||
from django_q.signals import pre_execute
|
from django_q.signals import pre_execute
|
||||||
from django_q.signing import SignedPackage, BadSignature
|
from django_q.signing import SignedPackage, BadSignature
|
||||||
from django_q.status import Stat, Status
|
from django_q.status import Stat, Status
|
||||||
|
from django_q import croniter
|
||||||
|
|
||||||
|
|
||||||
class Cluster:
|
class Cluster:
|
||||||
@@ -103,10 +102,10 @@ class Cluster:
|
|||||||
@property
|
@property
|
||||||
def is_stopping(self) -> bool:
|
def is_stopping(self) -> bool:
|
||||||
return (
|
return (
|
||||||
self.stop_event
|
self.stop_event
|
||||||
and self.start_event
|
and self.start_event
|
||||||
and self.start_event.is_set()
|
and self.start_event.is_set()
|
||||||
and self.stop_event.is_set()
|
and self.stop_event.is_set()
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -116,13 +115,13 @@ class Cluster:
|
|||||||
|
|
||||||
class Sentinel:
|
class Sentinel:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
stop_event,
|
stop_event,
|
||||||
start_event,
|
start_event,
|
||||||
cluster_id,
|
cluster_id,
|
||||||
broker=None,
|
broker=None,
|
||||||
timeout=Conf.TIMEOUT,
|
timeout=Conf.TIMEOUT,
|
||||||
start=True,
|
start=True,
|
||||||
):
|
):
|
||||||
# Make sure we catch signals for the pool
|
# Make sure we catch signals for the pool
|
||||||
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
||||||
@@ -208,7 +207,7 @@ class Sentinel:
|
|||||||
if process.timer.value == 0:
|
if process.timer.value == 0:
|
||||||
# only need to terminate on timeout, otherwise we risk destabilizing the queues
|
# only need to terminate on timeout, otherwise we risk destabilizing the queues
|
||||||
process.terminate()
|
process.terminate()
|
||||||
logger.warn(_(f"reincarnated worker {process.name} after timeout"))
|
logger.warning(_(f"reincarnated worker {process.name} after timeout"))
|
||||||
elif int(process.timer.value) == -2:
|
elif int(process.timer.value) == -2:
|
||||||
logger.info(_(f"recycled worker {process.name}"))
|
logger.info(_(f"recycled worker {process.name}"))
|
||||||
else:
|
else:
|
||||||
@@ -377,7 +376,7 @@ def monitor(result_queue: Queue, broker: Broker = None):
|
|||||||
|
|
||||||
|
|
||||||
def worker(
|
def worker(
|
||||||
task_queue: Queue, result_queue: Queue, timer: Value, timeout: int = Conf.TIMEOUT
|
task_queue: Queue, result_queue: Queue, timer: Value, timeout: int = Conf.TIMEOUT
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Takes a task from the task queue, tries to execute it and puts the result back in the result queue
|
Takes a task from the task queue, tries to execute it and puts the result back in the result queue
|
||||||
@@ -552,9 +551,9 @@ def scheduler(broker: Broker = None):
|
|||||||
try:
|
try:
|
||||||
with db.transaction.atomic(using=Schedule.objects.db):
|
with db.transaction.atomic(using=Schedule.objects.db):
|
||||||
for s in (
|
for s in (
|
||||||
Schedule.objects.select_for_update()
|
Schedule.objects.select_for_update()
|
||||||
.exclude(repeats=0)
|
.exclude(repeats=0)
|
||||||
.filter(next_run__lt=timezone.now())
|
.filter(next_run__lt=timezone.now())
|
||||||
):
|
):
|
||||||
args = ()
|
args = ()
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
@@ -591,6 +590,16 @@ def scheduler(broker: Broker = None):
|
|||||||
next_run = next_run.shift(months=+3)
|
next_run = next_run.shift(months=+3)
|
||||||
elif s.schedule_type == s.YEARLY:
|
elif s.schedule_type == s.YEARLY:
|
||||||
next_run = next_run.shift(years=+1)
|
next_run = next_run.shift(years=+1)
|
||||||
|
elif s.schedule_type == s.CRON:
|
||||||
|
if not croniter:
|
||||||
|
raise ImportError(
|
||||||
|
_(
|
||||||
|
"Please install croniter to enable cron expressions"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
next_run = arrow.get(
|
||||||
|
croniter(s.cron, timezone.now()).get_next()
|
||||||
|
)
|
||||||
if Conf.CATCH_UP or next_run > arrow.utcnow():
|
if Conf.CATCH_UP or next_run > arrow.utcnow():
|
||||||
break
|
break
|
||||||
# arrow always returns a tz aware datetime, and we don't want
|
# arrow always returns a tz aware datetime, and we don't want
|
||||||
|
|||||||
@@ -24,11 +24,11 @@ The difference is that `this` loads function calls `TimestampSigner` and `Signer
|
|||||||
|
|
||||||
|
|
||||||
def loads(
|
def loads(
|
||||||
s,
|
s,
|
||||||
key=None,
|
key=None,
|
||||||
salt: str = "django.core.signing",
|
salt: str = "django.core.signing",
|
||||||
serializer=JSONSerializer,
|
serializer=JSONSerializer,
|
||||||
max_age=None,
|
max_age=None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Reverse of dumps(), raise BadSignature if signature fails.
|
Reverse of dumps(), raise BadSignature if signature fails.
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--run-once',
|
"--run-once",
|
||||||
action='store_true',
|
action="store_true",
|
||||||
dest='run_once',
|
dest="run_once",
|
||||||
default=False,
|
default=False,
|
||||||
help='Run once and then stop.',
|
help="Run once and then stop.",
|
||||||
)
|
)
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
monitor(run_once=options.get('run_once', False))
|
monitor(run_once=options.get("run_once", False))
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 3.0.7 on 2020-06-28 10:55
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('django_q', '0010_auto_20200610_0856'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='schedule',
|
||||||
|
name='cron',
|
||||||
|
field=models.CharField(blank=True, help_text='Cron expression', max_length=100, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='schedule',
|
||||||
|
name='schedule_type',
|
||||||
|
field=models.CharField(choices=[('O', 'Once'), ('I', 'Minutes'), ('H', 'Hourly'), ('D', 'Daily'), ('W', 'Weekly'), ('M', 'Monthly'), ('Q', 'Quarterly'), ('Y', 'Yearly'), ('C', 'Cron')], default='O', max_length=1, verbose_name='Schedule Type'),
|
||||||
|
),
|
||||||
|
]
|
||||||
+27
-3
@@ -1,15 +1,20 @@
|
|||||||
|
# Django
|
||||||
from django import get_version
|
from django import get_version
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db import models
|
||||||
from django.template.defaultfilters import truncatechars
|
from django.template.defaultfilters import truncatechars
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.db import models
|
|
||||||
from django.utils import timezone
|
# External
|
||||||
from picklefield import PickledObjectField
|
from picklefield import PickledObjectField
|
||||||
from picklefield.fields import dbsafe_decode
|
from picklefield.fields import dbsafe_decode
|
||||||
|
|
||||||
|
# Local
|
||||||
from django_q.signing import SignedPackage
|
from django_q.signing import SignedPackage
|
||||||
|
from django_q import croniter
|
||||||
|
|
||||||
|
|
||||||
class Task(models.Model):
|
class Task(models.Model):
|
||||||
@@ -131,6 +136,16 @@ class Failure(Task):
|
|||||||
proxy = True
|
proxy = True
|
||||||
|
|
||||||
|
|
||||||
|
# Optional Cron validator
|
||||||
|
def validate_cron(value):
|
||||||
|
if not croniter:
|
||||||
|
raise ImportError(_("Please install croniter to enable cron expressions"))
|
||||||
|
try:
|
||||||
|
croniter.expand(value)
|
||||||
|
except ValueError as e:
|
||||||
|
raise ValidationError(e)
|
||||||
|
|
||||||
|
|
||||||
class Schedule(models.Model):
|
class Schedule(models.Model):
|
||||||
name = models.CharField(max_length=100, null=True, blank=True)
|
name = models.CharField(max_length=100, null=True, blank=True)
|
||||||
func = models.CharField(max_length=256, help_text="e.g. module.tasks.function")
|
func = models.CharField(max_length=256, help_text="e.g. module.tasks.function")
|
||||||
@@ -152,6 +167,7 @@ class Schedule(models.Model):
|
|||||||
MONTHLY = "M"
|
MONTHLY = "M"
|
||||||
QUARTERLY = "Q"
|
QUARTERLY = "Q"
|
||||||
YEARLY = "Y"
|
YEARLY = "Y"
|
||||||
|
CRON = "C"
|
||||||
TYPE = (
|
TYPE = (
|
||||||
(ONCE, _("Once")),
|
(ONCE, _("Once")),
|
||||||
(MINUTES, _("Minutes")),
|
(MINUTES, _("Minutes")),
|
||||||
@@ -161,6 +177,7 @@ class Schedule(models.Model):
|
|||||||
(MONTHLY, _("Monthly")),
|
(MONTHLY, _("Monthly")),
|
||||||
(QUARTERLY, _("Quarterly")),
|
(QUARTERLY, _("Quarterly")),
|
||||||
(YEARLY, _("Yearly")),
|
(YEARLY, _("Yearly")),
|
||||||
|
(CRON, _("Cron")),
|
||||||
)
|
)
|
||||||
schedule_type = models.CharField(
|
schedule_type = models.CharField(
|
||||||
max_length=1, choices=TYPE, default=TYPE[0][0], verbose_name=_("Schedule Type")
|
max_length=1, choices=TYPE, default=TYPE[0][0], verbose_name=_("Schedule Type")
|
||||||
@@ -174,6 +191,13 @@ class Schedule(models.Model):
|
|||||||
next_run = models.DateTimeField(
|
next_run = models.DateTimeField(
|
||||||
verbose_name=_("Next Run"), default=timezone.now, null=True
|
verbose_name=_("Next Run"), default=timezone.now, null=True
|
||||||
)
|
)
|
||||||
|
cron = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
validators=[validate_cron],
|
||||||
|
help_text=_("Cron expression"),
|
||||||
|
)
|
||||||
task = models.CharField(max_length=100, null=True, editable=False)
|
task = models.CharField(max_length=100, null=True, editable=False)
|
||||||
|
|
||||||
def success(self):
|
def success(self):
|
||||||
|
|||||||
+8
-1
@@ -88,6 +88,7 @@ def schedule(func, *args, **kwargs):
|
|||||||
:param repeats: how many times to repeat. 0=never, -1=always.
|
:param repeats: how many times to repeat. 0=never, -1=always.
|
||||||
:param next_run: Next scheduled run.
|
:param next_run: Next scheduled run.
|
||||||
:type next_run: datetime.datetime
|
:type next_run: datetime.datetime
|
||||||
|
:param cron: optional cron expression
|
||||||
:param kwargs: function keyword arguments.
|
:param kwargs: function keyword arguments.
|
||||||
:return: the schedule object.
|
:return: the schedule object.
|
||||||
:rtype: Schedule
|
:rtype: Schedule
|
||||||
@@ -98,13 +99,14 @@ def schedule(func, *args, **kwargs):
|
|||||||
minutes = kwargs.pop("minutes", None)
|
minutes = kwargs.pop("minutes", None)
|
||||||
repeats = kwargs.pop("repeats", -1)
|
repeats = kwargs.pop("repeats", -1)
|
||||||
next_run = kwargs.pop("next_run", timezone.now())
|
next_run = kwargs.pop("next_run", timezone.now())
|
||||||
|
cron = kwargs.pop("cron", None)
|
||||||
|
|
||||||
# check for name duplicates instead of am unique constraint
|
# check for name duplicates instead of am unique constraint
|
||||||
if name and Schedule.objects.filter(name=name).exists():
|
if name and Schedule.objects.filter(name=name).exists():
|
||||||
raise IntegrityError("A schedule with the same name already exists.")
|
raise IntegrityError("A schedule with the same name already exists.")
|
||||||
|
|
||||||
# create and return the schedule
|
# create and return the schedule
|
||||||
return Schedule.objects.create(
|
s = Schedule(
|
||||||
name=name,
|
name=name,
|
||||||
func=func,
|
func=func,
|
||||||
hook=hook,
|
hook=hook,
|
||||||
@@ -114,7 +116,12 @@ def schedule(func, *args, **kwargs):
|
|||||||
minutes=minutes,
|
minutes=minutes,
|
||||||
repeats=repeats,
|
repeats=repeats,
|
||||||
next_run=next_run,
|
next_run=next_run,
|
||||||
|
cron=cron,
|
||||||
)
|
)
|
||||||
|
# make sure we trigger validation
|
||||||
|
s.full_clean()
|
||||||
|
s.save()
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
def result(task_id, wait=0, cached=Conf.CACHED):
|
def result(task_id, wait=0, cached=Conf.CACHED):
|
||||||
|
|||||||
+102
-84
@@ -11,72 +11,72 @@ from django_q.humanhash import uuid
|
|||||||
|
|
||||||
def test_broker(monkeypatch):
|
def test_broker(monkeypatch):
|
||||||
broker = Broker()
|
broker = Broker()
|
||||||
broker.enqueue('test')
|
broker.enqueue("test")
|
||||||
broker.dequeue()
|
broker.dequeue()
|
||||||
broker.queue_size()
|
broker.queue_size()
|
||||||
broker.lock_size()
|
broker.lock_size()
|
||||||
broker.purge_queue()
|
broker.purge_queue()
|
||||||
broker.delete('id')
|
broker.delete("id")
|
||||||
broker.delete_queue()
|
broker.delete_queue()
|
||||||
broker.acknowledge('test')
|
broker.acknowledge("test")
|
||||||
broker.ping()
|
broker.ping()
|
||||||
broker.info()
|
broker.info()
|
||||||
# stats
|
# stats
|
||||||
assert broker.get_stat('test_1') is None
|
assert broker.get_stat("test_1") is None
|
||||||
broker.set_stat('test_1', 'test', 3)
|
broker.set_stat("test_1", "test", 3)
|
||||||
assert broker.get_stat('test_1') == 'test'
|
assert broker.get_stat("test_1") == "test"
|
||||||
assert broker.get_stats('test:*')[0] == 'test'
|
assert broker.get_stats("test:*")[0] == "test"
|
||||||
# stats with no cache
|
# stats with no cache
|
||||||
monkeypatch.setattr(Conf, 'CACHE', 'not_configured')
|
monkeypatch.setattr(Conf, "CACHE", "not_configured")
|
||||||
broker.cache = broker.get_cache()
|
broker.cache = broker.get_cache()
|
||||||
assert broker.get_stat('test_1') is None
|
assert broker.get_stat("test_1") is None
|
||||||
broker.set_stat('test_1', 'test', 3)
|
broker.set_stat("test_1", "test", 3)
|
||||||
assert broker.get_stat('test_1') is None
|
assert broker.get_stat("test_1") is None
|
||||||
assert broker.get_stats('test:*') is None
|
assert broker.get_stats("test:*") is None
|
||||||
|
|
||||||
|
|
||||||
def test_redis(monkeypatch):
|
def test_redis(monkeypatch):
|
||||||
monkeypatch.setattr(Conf, 'DJANGO_REDIS', None)
|
monkeypatch.setattr(Conf, "DJANGO_REDIS", None)
|
||||||
broker = get_broker()
|
broker = get_broker()
|
||||||
assert broker.ping() is True
|
assert broker.ping() is True
|
||||||
assert broker.info() is not None
|
assert broker.info() is not None
|
||||||
monkeypatch.setattr(Conf, 'REDIS', {'host': '127.0.0.1', 'port': 7799})
|
monkeypatch.setattr(Conf, "REDIS", {"host": "127.0.0.1", "port": 7799})
|
||||||
broker = get_broker()
|
broker = get_broker()
|
||||||
with pytest.raises(Exception):
|
with pytest.raises(Exception):
|
||||||
broker.ping()
|
broker.ping()
|
||||||
monkeypatch.setattr(Conf, 'REDIS', 'redis://127.0.0.1:7799')
|
monkeypatch.setattr(Conf, "REDIS", "redis://127.0.0.1:7799")
|
||||||
broker = get_broker()
|
broker = get_broker()
|
||||||
with pytest.raises(Exception):
|
with pytest.raises(Exception):
|
||||||
broker.ping()
|
broker.ping()
|
||||||
|
|
||||||
|
|
||||||
def test_custom(monkeypatch):
|
def test_custom(monkeypatch):
|
||||||
monkeypatch.setattr(Conf, 'BROKER_CLASS', 'brokers.redis_broker.Redis')
|
monkeypatch.setattr(Conf, "BROKER_CLASS", "brokers.redis_broker.Redis")
|
||||||
broker = get_broker()
|
broker = get_broker()
|
||||||
assert broker.ping() is True
|
assert broker.ping() is True
|
||||||
assert broker.info() is not None
|
assert broker.info() is not None
|
||||||
assert broker.__class__.__name__ == 'Redis'
|
assert broker.__class__.__name__ == "Redis"
|
||||||
|
|
||||||
|
|
||||||
def test_disque(monkeypatch):
|
def test_disque(monkeypatch):
|
||||||
monkeypatch.setattr(Conf, 'DISQUE_NODES', ['127.0.0.1:7711'])
|
monkeypatch.setattr(Conf, "DISQUE_NODES", ["127.0.0.1:7711"])
|
||||||
# check broker
|
# check broker
|
||||||
broker = get_broker(list_key='disque_test')
|
broker = get_broker(list_key="disque_test")
|
||||||
assert broker.ping() is True
|
assert broker.ping() is True
|
||||||
assert broker.info() is not None
|
assert broker.info() is not None
|
||||||
# clear before we start
|
# clear before we start
|
||||||
broker.delete_queue()
|
broker.delete_queue()
|
||||||
# async_task
|
# async_task
|
||||||
broker.enqueue('test')
|
broker.enqueue("test")
|
||||||
assert broker.queue_size() == 1
|
assert broker.queue_size() == 1
|
||||||
# dequeue
|
# dequeue
|
||||||
task = broker.dequeue()[0]
|
task = broker.dequeue()[0]
|
||||||
assert task[1] == 'test'
|
assert task[1] == "test"
|
||||||
broker.acknowledge(task[0])
|
broker.acknowledge(task[0])
|
||||||
assert broker.queue_size() == 0
|
assert broker.queue_size() == 0
|
||||||
# Retry test
|
# Retry test
|
||||||
monkeypatch.setattr(Conf, 'RETRY', 1)
|
monkeypatch.setattr(Conf, "RETRY", 1)
|
||||||
broker.enqueue('test')
|
broker.enqueue("test")
|
||||||
assert broker.queue_size() == 1
|
assert broker.queue_size() == 1
|
||||||
broker.dequeue()
|
broker.dequeue()
|
||||||
assert broker.queue_size() == 0
|
assert broker.queue_size() == 0
|
||||||
@@ -88,17 +88,17 @@ def test_disque(monkeypatch):
|
|||||||
sleep(1.5)
|
sleep(1.5)
|
||||||
assert broker.queue_size() == 0
|
assert broker.queue_size() == 0
|
||||||
# delete job
|
# delete job
|
||||||
task_id = broker.enqueue('test')
|
task_id = broker.enqueue("test")
|
||||||
broker.delete(task_id)
|
broker.delete(task_id)
|
||||||
assert broker.dequeue() is None
|
assert broker.dequeue() is None
|
||||||
# fail
|
# fail
|
||||||
task_id = broker.enqueue('test')
|
task_id = broker.enqueue("test")
|
||||||
broker.fail(task_id)
|
broker.fail(task_id)
|
||||||
# bulk test
|
# bulk test
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
broker.enqueue('test')
|
broker.enqueue("test")
|
||||||
monkeypatch.setattr(Conf, 'BULK', 5)
|
monkeypatch.setattr(Conf, "BULK", 5)
|
||||||
monkeypatch.setattr(Conf, 'DISQUE_FASTACK', True)
|
monkeypatch.setattr(Conf, "DISQUE_FASTACK", True)
|
||||||
tasks = broker.dequeue()
|
tasks = broker.dequeue()
|
||||||
for task in tasks:
|
for task in tasks:
|
||||||
assert task is not None
|
assert task is not None
|
||||||
@@ -106,35 +106,46 @@ def test_disque(monkeypatch):
|
|||||||
# test duplicate acknowledge
|
# test duplicate acknowledge
|
||||||
broker.acknowledge(task[0])
|
broker.acknowledge(task[0])
|
||||||
# delete queue
|
# delete queue
|
||||||
broker.enqueue('test')
|
broker.enqueue("test")
|
||||||
broker.enqueue('test')
|
broker.enqueue("test")
|
||||||
broker.delete_queue()
|
broker.delete_queue()
|
||||||
assert broker.queue_size() == 0
|
assert broker.queue_size() == 0
|
||||||
# connection test
|
# connection test
|
||||||
monkeypatch.setattr(Conf, 'DISQUE_NODES', ['127.0.0.1:7798', '127.0.0.1:7799'])
|
monkeypatch.setattr(Conf, "DISQUE_NODES", ["127.0.0.1:7798", "127.0.0.1:7799"])
|
||||||
|
with pytest.raises(redis.exceptions.ConnectionError):
|
||||||
|
broker.get_connection()
|
||||||
|
# connection test with no nodes
|
||||||
|
monkeypatch.setattr(Conf, "DISQUE_NODES", None)
|
||||||
with pytest.raises(redis.exceptions.ConnectionError):
|
with pytest.raises(redis.exceptions.ConnectionError):
|
||||||
broker.get_connection()
|
broker.get_connection()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(not os.getenv('IRON_MQ_TOKEN'),
|
@pytest.mark.skipif(
|
||||||
reason="requires IronMQ credentials")
|
not os.getenv("IRON_MQ_TOKEN"), reason="requires IronMQ credentials"
|
||||||
|
)
|
||||||
def test_ironmq(monkeypatch):
|
def test_ironmq(monkeypatch):
|
||||||
monkeypatch.setattr(Conf, 'IRON_MQ', {'token': os.getenv('IRON_MQ_TOKEN'),
|
monkeypatch.setattr(
|
||||||
'project_id': os.getenv('IRON_MQ_PROJECT_ID')})
|
Conf,
|
||||||
|
"IRON_MQ",
|
||||||
|
{
|
||||||
|
"token": os.getenv("IRON_MQ_TOKEN"),
|
||||||
|
"project_id": os.getenv("IRON_MQ_PROJECT_ID"),
|
||||||
|
},
|
||||||
|
)
|
||||||
# check broker
|
# check broker
|
||||||
broker = get_broker(list_key=uuid()[0])
|
broker = get_broker(list_key=uuid()[0])
|
||||||
assert broker.ping() is True
|
assert broker.ping() is True
|
||||||
assert broker.info() is not None
|
assert broker.info() is not None
|
||||||
# initialize the queue
|
# initialize the queue
|
||||||
broker.enqueue('test')
|
broker.enqueue("test")
|
||||||
# clear before we start
|
# clear before we start
|
||||||
broker.purge_queue()
|
broker.purge_queue()
|
||||||
assert broker.queue_size() == 0
|
assert broker.queue_size() == 0
|
||||||
# async_task
|
# async_task
|
||||||
broker.enqueue('test')
|
broker.enqueue("test")
|
||||||
# dequeue
|
# dequeue
|
||||||
task = broker.dequeue()[0]
|
task = broker.dequeue()[0]
|
||||||
assert task[1] == 'test'
|
assert task[1] == "test"
|
||||||
broker.acknowledge(task[0])
|
broker.acknowledge(task[0])
|
||||||
assert broker.dequeue() is None
|
assert broker.dequeue() is None
|
||||||
# Retry test
|
# Retry test
|
||||||
@@ -148,16 +159,16 @@ def test_ironmq(monkeypatch):
|
|||||||
# broker.acknowledge(task[0])
|
# broker.acknowledge(task[0])
|
||||||
# sleep(3)
|
# sleep(3)
|
||||||
# delete job
|
# delete job
|
||||||
task_id = broker.enqueue('test')
|
task_id = broker.enqueue("test")
|
||||||
broker.delete(task_id)
|
broker.delete(task_id)
|
||||||
assert broker.dequeue() is None
|
assert broker.dequeue() is None
|
||||||
# fail
|
# fail
|
||||||
task_id = broker.enqueue('test')
|
task_id = broker.enqueue("test")
|
||||||
broker.fail(task_id)
|
broker.fail(task_id)
|
||||||
# bulk test
|
# bulk test
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
broker.enqueue('test')
|
broker.enqueue("test")
|
||||||
monkeypatch.setattr(Conf, 'BULK', 5)
|
monkeypatch.setattr(Conf, "BULK", 5)
|
||||||
tasks = broker.dequeue()
|
tasks = broker.dequeue()
|
||||||
for task in tasks:
|
for task in tasks:
|
||||||
assert task is not None
|
assert task is not None
|
||||||
@@ -165,62 +176,69 @@ def test_ironmq(monkeypatch):
|
|||||||
# duplicate acknowledge
|
# duplicate acknowledge
|
||||||
broker.acknowledge(task[0])
|
broker.acknowledge(task[0])
|
||||||
# delete queue
|
# delete queue
|
||||||
broker.enqueue('test')
|
broker.enqueue("test")
|
||||||
broker.enqueue('test')
|
broker.enqueue("test")
|
||||||
broker.purge_queue()
|
broker.purge_queue()
|
||||||
assert broker.dequeue() is None
|
assert broker.dequeue() is None
|
||||||
broker.delete_queue()
|
broker.delete_queue()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(not os.getenv('AWS_ACCESS_KEY_ID'),
|
@pytest.mark.skipif(
|
||||||
reason="requires AWS credentials")
|
not os.getenv("AWS_ACCESS_KEY_ID"), reason="requires AWS credentials"
|
||||||
|
)
|
||||||
def canceled_sqs(monkeypatch):
|
def canceled_sqs(monkeypatch):
|
||||||
monkeypatch.setattr(Conf, 'SQS', {'aws_region': os.getenv('AWS_REGION'),
|
monkeypatch.setattr(
|
||||||
'aws_access_key_id': os.getenv('AWS_ACCESS_KEY_ID'),
|
Conf,
|
||||||
'aws_secret_access_key': os.getenv('AWS_SECRET_ACCESS_KEY')})
|
"SQS",
|
||||||
|
{
|
||||||
|
"aws_region": os.getenv("AWS_REGION"),
|
||||||
|
"aws_access_key_id": os.getenv("AWS_ACCESS_KEY_ID"),
|
||||||
|
"aws_secret_access_key": os.getenv("AWS_SECRET_ACCESS_KEY"),
|
||||||
|
},
|
||||||
|
)
|
||||||
# check broker
|
# check broker
|
||||||
broker = get_broker(list_key=uuid()[0])
|
broker = get_broker(list_key=uuid()[0])
|
||||||
assert broker.ping() is True
|
assert broker.ping() is True
|
||||||
assert broker.info() is not None
|
assert broker.info() is not None
|
||||||
assert broker.queue_size() == 0
|
assert broker.queue_size() == 0
|
||||||
# async_task
|
# async_task
|
||||||
broker.enqueue('test')
|
broker.enqueue("test")
|
||||||
# dequeue
|
# dequeue
|
||||||
task = broker.dequeue()[0]
|
task = broker.dequeue()[0]
|
||||||
assert task[1] == 'test'
|
assert task[1] == "test"
|
||||||
broker.acknowledge(task[0])
|
broker.acknowledge(task[0])
|
||||||
assert broker.dequeue() is None
|
assert broker.dequeue() is None
|
||||||
# Retry test
|
# Retry test
|
||||||
monkeypatch.setattr(Conf, 'RETRY', 1)
|
monkeypatch.setattr(Conf, "RETRY", 1)
|
||||||
broker.enqueue('test')
|
broker.enqueue("test")
|
||||||
sleep(2)
|
sleep(2)
|
||||||
# Sometimes SQS is not linear
|
# Sometimes SQS is not linear
|
||||||
task = broker.dequeue()
|
task = broker.dequeue()
|
||||||
if not task:
|
if not task:
|
||||||
pytest.skip('SQS being weird')
|
pytest.skip("SQS being weird")
|
||||||
task = task[0]
|
task = task[0]
|
||||||
assert len(task) > 0
|
assert len(task) > 0
|
||||||
broker.acknowledge(task[0])
|
broker.acknowledge(task[0])
|
||||||
sleep(2)
|
sleep(2)
|
||||||
# delete job
|
# delete job
|
||||||
monkeypatch.setattr(Conf, 'RETRY', 60)
|
monkeypatch.setattr(Conf, "RETRY", 60)
|
||||||
broker.enqueue('test')
|
broker.enqueue("test")
|
||||||
sleep(1)
|
sleep(1)
|
||||||
task = broker.dequeue()
|
task = broker.dequeue()
|
||||||
if not task:
|
if not task:
|
||||||
pytest.skip('SQS being weird')
|
pytest.skip("SQS being weird")
|
||||||
task_id = task[0][0]
|
task_id = task[0][0]
|
||||||
broker.delete(task_id)
|
broker.delete(task_id)
|
||||||
assert broker.dequeue() is None
|
assert broker.dequeue() is None
|
||||||
# fail
|
# fail
|
||||||
broker.enqueue('test')
|
broker.enqueue("test")
|
||||||
while task is None:
|
while task is None:
|
||||||
task = broker.dequeue()[0]
|
task = broker.dequeue()[0]
|
||||||
broker.fail(task[0])
|
broker.fail(task[0])
|
||||||
# bulk test
|
# bulk test
|
||||||
for i in range(10):
|
for i in range(10):
|
||||||
broker.enqueue('test')
|
broker.enqueue("test")
|
||||||
monkeypatch.setattr(Conf, 'BULK', 12)
|
monkeypatch.setattr(Conf, "BULK", 12)
|
||||||
tasks = broker.dequeue()
|
tasks = broker.dequeue()
|
||||||
for task in tasks:
|
for task in tasks:
|
||||||
assert task is not None
|
assert task is not None
|
||||||
@@ -229,31 +247,31 @@ def canceled_sqs(monkeypatch):
|
|||||||
broker.acknowledge(task[0])
|
broker.acknowledge(task[0])
|
||||||
assert broker.lock_size() == 0
|
assert broker.lock_size() == 0
|
||||||
# delete queue
|
# delete queue
|
||||||
broker.enqueue('test')
|
broker.enqueue("test")
|
||||||
broker.purge_queue()
|
broker.purge_queue()
|
||||||
broker.delete_queue()
|
broker.delete_queue()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_orm(monkeypatch):
|
def test_orm(monkeypatch):
|
||||||
monkeypatch.setattr(Conf, 'ORM', 'default')
|
monkeypatch.setattr(Conf, "ORM", "default")
|
||||||
# check broker
|
# check broker
|
||||||
broker = get_broker(list_key='orm_test')
|
broker = get_broker(list_key="orm_test")
|
||||||
assert broker.ping() is True
|
assert broker.ping() is True
|
||||||
assert broker.info() is not None
|
assert broker.info() is not None
|
||||||
# clear before we start
|
# clear before we start
|
||||||
broker.delete_queue()
|
broker.delete_queue()
|
||||||
# async_task
|
# async_task
|
||||||
broker.enqueue('test')
|
broker.enqueue("test")
|
||||||
assert broker.queue_size() == 1
|
assert broker.queue_size() == 1
|
||||||
# dequeue
|
# dequeue
|
||||||
task = broker.dequeue()[0]
|
task = broker.dequeue()[0]
|
||||||
assert task[1] == 'test'
|
assert task[1] == "test"
|
||||||
broker.acknowledge(task[0])
|
broker.acknowledge(task[0])
|
||||||
assert broker.queue_size() == 0
|
assert broker.queue_size() == 0
|
||||||
# Retry test
|
# Retry test
|
||||||
monkeypatch.setattr(Conf, 'RETRY', 1)
|
monkeypatch.setattr(Conf, "RETRY", 1)
|
||||||
broker.enqueue('test')
|
broker.enqueue("test")
|
||||||
assert broker.queue_size() == 1
|
assert broker.queue_size() == 1
|
||||||
broker.dequeue()
|
broker.dequeue()
|
||||||
assert broker.queue_size() == 0
|
assert broker.queue_size() == 0
|
||||||
@@ -265,16 +283,16 @@ def test_orm(monkeypatch):
|
|||||||
sleep(1.5)
|
sleep(1.5)
|
||||||
assert broker.queue_size() == 0
|
assert broker.queue_size() == 0
|
||||||
# delete job
|
# delete job
|
||||||
task_id = broker.enqueue('test')
|
task_id = broker.enqueue("test")
|
||||||
broker.delete(task_id)
|
broker.delete(task_id)
|
||||||
assert broker.dequeue() is None
|
assert broker.dequeue() is None
|
||||||
# fail
|
# fail
|
||||||
task_id = broker.enqueue('test')
|
task_id = broker.enqueue("test")
|
||||||
broker.fail(task_id)
|
broker.fail(task_id)
|
||||||
# bulk test
|
# bulk test
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
broker.enqueue('test')
|
broker.enqueue("test")
|
||||||
monkeypatch.setattr(Conf, 'BULK', 5)
|
monkeypatch.setattr(Conf, "BULK", 5)
|
||||||
tasks = broker.dequeue()
|
tasks = broker.dequeue()
|
||||||
assert broker.lock_size() == Conf.BULK
|
assert broker.lock_size() == Conf.BULK
|
||||||
for task in tasks:
|
for task in tasks:
|
||||||
@@ -285,32 +303,32 @@ def test_orm(monkeypatch):
|
|||||||
# test duplicate acknowledge
|
# test duplicate acknowledge
|
||||||
broker.acknowledge(task[0])
|
broker.acknowledge(task[0])
|
||||||
# delete queue
|
# delete queue
|
||||||
broker.enqueue('test')
|
broker.enqueue("test")
|
||||||
broker.enqueue('test')
|
broker.enqueue("test")
|
||||||
broker.delete_queue()
|
broker.delete_queue()
|
||||||
assert broker.queue_size() == 0
|
assert broker.queue_size() == 0
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_mongo(monkeypatch):
|
def test_mongo(monkeypatch):
|
||||||
monkeypatch.setattr(Conf, 'MONGO', {'host': '127.0.0.1', 'port': 27017})
|
monkeypatch.setattr(Conf, "MONGO", {"host": "127.0.0.1", "port": 27017})
|
||||||
# check broker
|
# check broker
|
||||||
broker = get_broker(list_key='mongo_test')
|
broker = get_broker(list_key="mongo_test")
|
||||||
assert broker.ping() is True
|
assert broker.ping() is True
|
||||||
assert broker.info() is not None
|
assert broker.info() is not None
|
||||||
# clear before we start
|
# clear before we start
|
||||||
broker.delete_queue()
|
broker.delete_queue()
|
||||||
# async_task
|
# async_task
|
||||||
broker.enqueue('test')
|
broker.enqueue("test")
|
||||||
assert broker.queue_size() == 1
|
assert broker.queue_size() == 1
|
||||||
# dequeue
|
# dequeue
|
||||||
task = broker.dequeue()[0]
|
task = broker.dequeue()[0]
|
||||||
assert task[1] == 'test'
|
assert task[1] == "test"
|
||||||
broker.acknowledge(task[0])
|
broker.acknowledge(task[0])
|
||||||
assert broker.queue_size() == 0
|
assert broker.queue_size() == 0
|
||||||
# Retry test
|
# Retry test
|
||||||
monkeypatch.setattr(Conf, 'RETRY', 1)
|
monkeypatch.setattr(Conf, "RETRY", 1)
|
||||||
broker.enqueue('test')
|
broker.enqueue("test")
|
||||||
assert broker.queue_size() == 1
|
assert broker.queue_size() == 1
|
||||||
broker.dequeue()
|
broker.dequeue()
|
||||||
assert broker.queue_size() == 0
|
assert broker.queue_size() == 0
|
||||||
@@ -322,15 +340,15 @@ def test_mongo(monkeypatch):
|
|||||||
sleep(1.5)
|
sleep(1.5)
|
||||||
assert broker.queue_size() == 0
|
assert broker.queue_size() == 0
|
||||||
# delete job
|
# delete job
|
||||||
task_id = broker.enqueue('test')
|
task_id = broker.enqueue("test")
|
||||||
broker.delete(task_id)
|
broker.delete(task_id)
|
||||||
assert broker.dequeue() is None
|
assert broker.dequeue() is None
|
||||||
# fail
|
# fail
|
||||||
task_id = broker.enqueue('test')
|
task_id = broker.enqueue("test")
|
||||||
broker.fail(task_id)
|
broker.fail(task_id)
|
||||||
# bulk test
|
# bulk test
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
broker.enqueue('test')
|
broker.enqueue("test")
|
||||||
tasks = []
|
tasks = []
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
tasks.append(broker.dequeue()[0])
|
tasks.append(broker.dequeue()[0])
|
||||||
@@ -343,8 +361,8 @@ def test_mongo(monkeypatch):
|
|||||||
# test duplicate acknowledge
|
# test duplicate acknowledge
|
||||||
broker.acknowledge(task[0])
|
broker.acknowledge(task[0])
|
||||||
# delete queue
|
# delete queue
|
||||||
broker.enqueue('test')
|
broker.enqueue("test")
|
||||||
broker.enqueue('test')
|
broker.enqueue("test")
|
||||||
broker.purge_queue()
|
broker.purge_queue()
|
||||||
broker.delete_queue()
|
broker.delete_queue()
|
||||||
assert broker.queue_size() == 0
|
assert broker.queue_size() == 0
|
||||||
|
|||||||
@@ -142,6 +142,8 @@ def test_enqueue(broker, admin_user):
|
|||||||
# q_options and save opt_out test
|
# q_options and save opt_out test
|
||||||
k = async_task('django_q.tests.tasks.get_user_id', admin_user,
|
k = async_task('django_q.tests.tasks.get_user_id', admin_user,
|
||||||
q_options={'broker': broker, 'group': 'test_k', 'save': False, 'timeout': 90})
|
q_options={'broker': broker, 'group': 'test_k', 'save': False, 'timeout': 90})
|
||||||
|
# test unicode
|
||||||
|
assert Task(name='Amalia').__unicode__()=='Amalia'
|
||||||
# check if everything has a task id
|
# check if everything has a task id
|
||||||
assert isinstance(a, str)
|
assert isinstance(a, str)
|
||||||
assert isinstance(b, str)
|
assert isinstance(b, str)
|
||||||
|
|||||||
@@ -16,3 +16,4 @@ def test_qmonitor():
|
|||||||
def test_qinfo():
|
def test_qinfo():
|
||||||
call_command('qinfo')
|
call_command('qinfo')
|
||||||
call_command('qinfo', config=True)
|
call_command('qinfo', config=True)
|
||||||
|
call_command('qinfo', ids=True)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import uuid
|
|||||||
from django_q.tasks import async_task
|
from django_q.tasks import async_task
|
||||||
from django_q.brokers import get_broker
|
from django_q.brokers import get_broker
|
||||||
from django_q.cluster import Cluster
|
from django_q.cluster import Cluster
|
||||||
from django_q.monitor import monitor, info
|
from django_q.monitor import monitor, info, get_ids
|
||||||
from django_q.status import Stat
|
from django_q.status import Stat
|
||||||
from django_q.conf import Conf
|
from django_q.conf import Conf
|
||||||
|
|
||||||
@@ -16,6 +16,7 @@ def test_monitor(monkeypatch):
|
|||||||
c = Cluster()
|
c = Cluster()
|
||||||
c.start()
|
c.start()
|
||||||
stats = monitor(run_once=True)
|
stats = monitor(run_once=True)
|
||||||
|
assert get_ids() is True
|
||||||
c.stop()
|
c.stop()
|
||||||
assert len(stats) > 0
|
assert len(stats) > 0
|
||||||
found_c = False
|
found_c = False
|
||||||
|
|||||||
@@ -3,14 +3,15 @@ from multiprocessing import Event, Value
|
|||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import pytest
|
import pytest
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from django_q.brokers import get_broker
|
from django_q.brokers import get_broker
|
||||||
from django_q.cluster import pusher, worker, monitor, scheduler
|
from django_q.cluster import pusher, worker, monitor, scheduler
|
||||||
from django_q.conf import Conf
|
from django_q.conf import Conf
|
||||||
from django_q.tasks import Schedule, fetch, schedule as create_schedule
|
|
||||||
from django_q.queues import Queue
|
from django_q.queues import Queue
|
||||||
|
from django_q.tasks import Schedule, fetch, schedule as create_schedule
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -91,8 +92,25 @@ def test_scheduler(broker, monkeypatch):
|
|||||||
schedule_type=Schedule.MINUTES,
|
schedule_type=Schedule.MINUTES,
|
||||||
minutes=10)
|
minutes=10)
|
||||||
assert hasattr(minute_schedule, 'pk') is True
|
assert hasattr(minute_schedule, 'pk') is True
|
||||||
|
# Cron schedule
|
||||||
|
cron_schedule = create_schedule('django_q.tests.tasks.word_multiply',
|
||||||
|
2,
|
||||||
|
word='django',
|
||||||
|
schedule_type=Schedule.CRON,
|
||||||
|
cron="0 22 * * 1-5")
|
||||||
|
assert hasattr(cron_schedule, 'pk') is True
|
||||||
|
assert cron_schedule.full_clean() is None
|
||||||
|
assert cron_schedule.__unicode__() == 'django_q.tests.tasks.word_multiply'
|
||||||
|
with pytest.raises(ValidationError):
|
||||||
|
create_schedule('django_q.tests.tasks.word_multiply',
|
||||||
|
2,
|
||||||
|
word='django',
|
||||||
|
schedule_type=Schedule.CRON,
|
||||||
|
cron="0 22 * * 1-12")
|
||||||
# All other types
|
# All other types
|
||||||
for t in Schedule.TYPE:
|
for t in Schedule.TYPE:
|
||||||
|
if t[0] == Schedule.CRON:
|
||||||
|
continue
|
||||||
schedule = create_schedule('django_q.tests.tasks.word_multiply',
|
schedule = create_schedule('django_q.tests.tasks.word_multiply',
|
||||||
2,
|
2,
|
||||||
word='django',
|
word='django',
|
||||||
|
|||||||
@@ -88,6 +88,13 @@ Optional
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- `Croniter <https://github.com/kiorky/croniter>`__ is an optional package that is used to parse cron expressions for the scheduler::
|
||||||
|
|
||||||
|
$ pip install croniter
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Add-ons
|
Add-ons
|
||||||
-------
|
-------
|
||||||
- `django-q-rollbar <https://github.com/danielwelch/django-q-rollbar>`__ is a Rollbar error reporter::
|
- `django-q-rollbar <https://github.com/danielwelch/django-q-rollbar>`__ is a Rollbar error reporter::
|
||||||
|
|||||||
+20
-2
@@ -45,6 +45,13 @@ You can manage them through the :ref:`admin_page` or directly from your code wit
|
|||||||
repeats=24,
|
repeats=24,
|
||||||
next_run=arrow.utcnow().replace(hour=18, minute=0))
|
next_run=arrow.utcnow().replace(hour=18, minute=0))
|
||||||
|
|
||||||
|
# Use a cron expression
|
||||||
|
schedule('math.hypot',
|
||||||
|
3, 4,
|
||||||
|
schedule_type=Schedule.CRON,
|
||||||
|
cron = '0 22 * * 1-5')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Missed schedules
|
Missed schedules
|
||||||
----------------
|
----------------
|
||||||
@@ -99,8 +106,9 @@ Reference
|
|||||||
:param args: arguments for the scheduled function.
|
:param args: arguments for the scheduled function.
|
||||||
:param str name: An optional name for your schedule.
|
:param str name: An optional name for your schedule.
|
||||||
:param str hook: optional result hook function. Dotted strings only.
|
:param str hook: optional result hook function. Dotted strings only.
|
||||||
:param str schedule_type: (O)nce, M(I)nutes, (H)ourly, (D)aily, (W)eekly, (M)onthly, (Q)uarterly, (Y)early or :attr:`Schedule.TYPE`
|
:param str schedule_type: (O)nce, M(I)nutes, (H)ourly, (D)aily, (W)eekly, (M)onthly, (Q)uarterly, (Y)early or (C)ron :attr:`Schedule.TYPE`
|
||||||
:param int minutes: Number of minutes for the Minutes type.
|
:param int minutes: Number of minutes for the Minutes type.
|
||||||
|
:param str cron: Cron expression for the Cron type.
|
||||||
:param int repeats: Number of times to repeat schedule. -1=Always, 0=Never, n =n.
|
:param int repeats: Number of times to repeat schedule. -1=Always, 0=Never, n =n.
|
||||||
:param datetime next_run: Next or first scheduled execution datetime.
|
:param datetime next_run: Next or first scheduled execution datetime.
|
||||||
:param dict q_options: options passed to async_task for this schedule
|
:param dict q_options: options passed to async_task for this schedule
|
||||||
@@ -140,7 +148,7 @@ Reference
|
|||||||
|
|
||||||
.. py:attribute:: TYPE
|
.. py:attribute:: TYPE
|
||||||
|
|
||||||
:attr:`ONCE`, :attr:`MINUTES`, :attr:`HOURLY`, :attr:`DAILY`, :attr:`WEEKLY`, :attr:`MONTHLY`, :attr:`QUARTERLY`, :attr:`YEARLY`
|
:attr:`ONCE`, :attr:`MINUTES`, :attr:`HOURLY`, :attr:`DAILY`, :attr:`WEEKLY`, :attr:`MONTHLY`, :attr:`QUARTERLY`, :attr:`YEARLY`, :attr:`CRON`
|
||||||
|
|
||||||
|
|
||||||
.. py:attribute:: minutes
|
.. py:attribute:: minutes
|
||||||
@@ -148,6 +156,10 @@ Reference
|
|||||||
The number of minutes the :attr:`MINUTES` schedule should use.
|
The number of minutes the :attr:`MINUTES` schedule should use.
|
||||||
Is ignored for other schedule types.
|
Is ignored for other schedule types.
|
||||||
|
|
||||||
|
.. py:attribute:: cron
|
||||||
|
|
||||||
|
A cron string describing the schedule. You need the optional `croniter` package installed for this.
|
||||||
|
|
||||||
.. py:attribute:: repeats
|
.. py:attribute:: repeats
|
||||||
|
|
||||||
Number of times to repeat the schedule. -1=Always, 0=Never, n =n.
|
Number of times to repeat the schedule. -1=Always, 0=Never, n =n.
|
||||||
@@ -208,3 +220,9 @@ Reference
|
|||||||
|
|
||||||
`'Y'` only runs once a year. The same caution as with months apply;
|
`'Y'` only runs once a year. The same caution as with months apply;
|
||||||
If you set this to february 29th, it will run on february 28th in the following years.
|
If you set this to february 29th, it will run on february 28th in the following years.
|
||||||
|
|
||||||
|
.. py:attribute:: CRON
|
||||||
|
|
||||||
|
`'C'` uses the optional `croniter` package to determine a schedule based a cron expression.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Generated
+43
-11
@@ -71,10 +71,10 @@ description = "The AWS SDK for Python"
|
|||||||
name = "boto3"
|
name = "boto3"
|
||||||
optional = true
|
optional = true
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
version = "1.14.12"
|
version = "1.14.14"
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
botocore = ">=1.17.12,<1.18.0"
|
botocore = ">=1.17.14,<1.18.0"
|
||||||
jmespath = ">=0.7.1,<1.0.0"
|
jmespath = ">=0.7.1,<1.0.0"
|
||||||
s3transfer = ">=0.3.0,<0.4.0"
|
s3transfer = ">=0.3.0,<0.4.0"
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ description = "Low-level, data-driven core of boto 3."
|
|||||||
name = "botocore"
|
name = "botocore"
|
||||||
optional = true
|
optional = true
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
version = "1.17.12"
|
version = "1.17.14"
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
docutils = ">=0.10,<0.16"
|
docutils = ">=0.10,<0.16"
|
||||||
@@ -120,13 +120,25 @@ optional = false
|
|||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
category = "main"
|
||||||
|
description = "croniter provides iteration for datetime object with cron like format"
|
||||||
|
name = "croniter"
|
||||||
|
optional = true
|
||||||
|
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||||
|
version = "0.3.34"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
natsort = "*"
|
||||||
|
python-dateutil = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "main"
|
category = "main"
|
||||||
description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design."
|
description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design."
|
||||||
name = "django"
|
name = "django"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6"
|
python-versions = ">=3.6"
|
||||||
version = "3.0.7"
|
version = "3.0.8"
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
asgiref = ">=3.2,<4.0"
|
asgiref = ">=3.2,<4.0"
|
||||||
@@ -270,6 +282,18 @@ optional = false
|
|||||||
python-versions = ">=3.5"
|
python-versions = ">=3.5"
|
||||||
version = "8.4.0"
|
version = "8.4.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
category = "main"
|
||||||
|
description = "Simple yet flexible natural sorting in Python."
|
||||||
|
name = "natsort"
|
||||||
|
optional = true
|
||||||
|
python-versions = ">=3.4"
|
||||||
|
version = "7.0.1"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
fast = ["fastnumbers (>=2.0.0)"]
|
||||||
|
icu = ["PyICU (>=1.0.0)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "dev"
|
category = "dev"
|
||||||
description = "Core utilities for Python packages"
|
description = "Core utilities for Python packages"
|
||||||
@@ -497,7 +521,7 @@ rollbar = ["django-q-rollbar"]
|
|||||||
sentry = []
|
sentry = []
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
content-hash = "d89c929e8b6951968228b1aab1aa5c0a7c014acceb3b986bbaa3e41df57d8b39"
|
content-hash = "6d89bff8bd465aa4a5facd812172a970bf5edebdd8099141054f72aa63260c2a"
|
||||||
python-versions = ">=3.6"
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
@@ -526,12 +550,12 @@ blessed = [
|
|||||||
{file = "blessed-1.17.8.tar.gz", hash = "sha256:7671d057b2df6ddbefd809009fb08feb2f8d2d163d240b5e765088a90519b2f1"},
|
{file = "blessed-1.17.8.tar.gz", hash = "sha256:7671d057b2df6ddbefd809009fb08feb2f8d2d163d240b5e765088a90519b2f1"},
|
||||||
]
|
]
|
||||||
boto3 = [
|
boto3 = [
|
||||||
{file = "boto3-1.14.12-py2.py3-none-any.whl", hash = "sha256:7daad26a008c91dd7b82fde17d246d1fe6e4b3813426689ef8bac9017a277cfb"},
|
{file = "boto3-1.14.14-py2.py3-none-any.whl", hash = "sha256:4c2f5f9f28930e236845e2cddbe01cb093ca96dc1f5c6e2b2b254722018a2268"},
|
||||||
{file = "boto3-1.14.12.tar.gz", hash = "sha256:2616351c98eec18d20a1d64b33355c86cd855ac96219d1b8428c9bfc590bde53"},
|
{file = "boto3-1.14.14.tar.gz", hash = "sha256:87beffba2360b8077413f2d473cb828d0a5bda513bd1d6fb7b137c57b686aeb6"},
|
||||||
]
|
]
|
||||||
botocore = [
|
botocore = [
|
||||||
{file = "botocore-1.17.12-py2.py3-none-any.whl", hash = "sha256:45934d880378777cefeca727f369d1f5aebf6b254e9be58e7c77dd0b059338bb"},
|
{file = "botocore-1.17.14-py2.py3-none-any.whl", hash = "sha256:6a2e9768dad8ae9771302d5922b977dca6bb9693f9b6a5f6ed0e7ac375e2ca40"},
|
||||||
{file = "botocore-1.17.12.tar.gz", hash = "sha256:a94e0e2307f1b9fe3a84660842909cd2680b57a9fc9fb0c3a03b0afb2eadbe21"},
|
{file = "botocore-1.17.14.tar.gz", hash = "sha256:96d668ae5246d236ea83e4586349552d6584e8b1551ae2fccc0bd4ed528a746f"},
|
||||||
]
|
]
|
||||||
certifi = [
|
certifi = [
|
||||||
{file = "certifi-2020.6.20-py2.py3-none-any.whl", hash = "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"},
|
{file = "certifi-2020.6.20-py2.py3-none-any.whl", hash = "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"},
|
||||||
@@ -545,9 +569,13 @@ colorama = [
|
|||||||
{file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"},
|
{file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"},
|
||||||
{file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"},
|
{file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"},
|
||||||
]
|
]
|
||||||
|
croniter = [
|
||||||
|
{file = "croniter-0.3.34-py2.py3-none-any.whl", hash = "sha256:15597ef0639f8fbab09cbf8c277fa8c65c8b9dbe818c4b2212f95dbc09c6f287"},
|
||||||
|
{file = "croniter-0.3.34.tar.gz", hash = "sha256:7186b9b464f45cf3d3c83a18bc2344cc101d7b9fd35a05f2878437b14967e964"},
|
||||||
|
]
|
||||||
django = [
|
django = [
|
||||||
{file = "Django-3.0.7-py3-none-any.whl", hash = "sha256:e1630333248c9b3d4e38f02093a26f1e07b271ca896d73097457996e0fae12e8"},
|
{file = "Django-3.0.8-py3-none-any.whl", hash = "sha256:5457fc953ec560c5521b41fad9e6734a4668b7ba205832191bbdff40ec61073c"},
|
||||||
{file = "Django-3.0.7.tar.gz", hash = "sha256:5052b34b34b3425233c682e0e11d658fd6efd587d11335a0203d827224ada8f2"},
|
{file = "Django-3.0.8.tar.gz", hash = "sha256:31a5fbbea5fc71c99e288ec0b2f00302a0a92c44b13ede80b73a6a4d6d205582"},
|
||||||
]
|
]
|
||||||
django-picklefield = [
|
django-picklefield = [
|
||||||
{file = "django-picklefield-3.0.1.tar.gz", hash = "sha256:15ccba592ca953b9edf9532e64640329cd47b136b7f8f10f2939caa5f9ce4287"},
|
{file = "django-picklefield-3.0.1.tar.gz", hash = "sha256:15ccba592ca953b9edf9532e64640329cd47b136b7f8f10f2939caa5f9ce4287"},
|
||||||
@@ -638,6 +666,10 @@ more-itertools = [
|
|||||||
{file = "more-itertools-8.4.0.tar.gz", hash = "sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5"},
|
{file = "more-itertools-8.4.0.tar.gz", hash = "sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5"},
|
||||||
{file = "more_itertools-8.4.0-py3-none-any.whl", hash = "sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2"},
|
{file = "more_itertools-8.4.0-py3-none-any.whl", hash = "sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2"},
|
||||||
]
|
]
|
||||||
|
natsort = [
|
||||||
|
{file = "natsort-7.0.1-py3-none-any.whl", hash = "sha256:d3fd728a3ceb7c78a59aa8539692a75e37cbfd9b261d4d702e8016639820f90a"},
|
||||||
|
{file = "natsort-7.0.1.tar.gz", hash = "sha256:a633464dc3a22b305df0f27abcb3e83515898aa1fd0ed2f9726c3571a27258cf"},
|
||||||
|
]
|
||||||
packaging = [
|
packaging = [
|
||||||
{file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"},
|
{file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"},
|
||||||
{file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"},
|
{file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"},
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "django-q"
|
name = "django-q"
|
||||||
version = "1.2.4"
|
version = "1.3.0"
|
||||||
description = "A multiprocessing distributed task queue for Django"
|
description = "A multiprocessing distributed task queue for Django"
|
||||||
authors = ["Ilan Steemers <koed00@gmail.com>"]
|
authors = ["Ilan Steemers <koed00@gmail.com>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
@@ -52,6 +52,7 @@ django-redis = {version = "^4.12.1", optional = true}
|
|||||||
iron-mq = {version = "^0.9", optional = true}
|
iron-mq = {version = "^0.9", optional = true}
|
||||||
boto3 = {version = "^1.14.12", optional = true}
|
boto3 = {version = "^1.14.12", optional = true}
|
||||||
pymongo = {version = "^3.10.1", optional = true}
|
pymongo = {version = "^3.10.1", optional = true}
|
||||||
|
croniter = {version = "^0.3.34", optional = true}
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
pytest = "^5.4.2"
|
pytest = "^5.4.2"
|
||||||
|
|||||||
@@ -8,3 +8,4 @@ django-redis
|
|||||||
iron-mq
|
iron-mq
|
||||||
boto3
|
boto3
|
||||||
pymongo
|
pymongo
|
||||||
|
croniter
|
||||||
|
|||||||
+6
-4
@@ -7,22 +7,24 @@
|
|||||||
arrow==0.15.7 # via -r requirements.in
|
arrow==0.15.7 # via -r requirements.in
|
||||||
asgiref==3.2.10 # via django
|
asgiref==3.2.10 # via django
|
||||||
blessed==1.17.8 # via -r requirements.in
|
blessed==1.17.8 # via -r requirements.in
|
||||||
boto3==1.14.12 # via -r requirements.in
|
boto3==1.14.14 # via -r requirements.in
|
||||||
botocore==1.17.12 # via boto3, s3transfer
|
botocore==1.17.14 # via boto3, s3transfer
|
||||||
certifi==2020.6.20 # via requests
|
certifi==2020.6.20 # via requests
|
||||||
chardet==3.0.4 # via requests
|
chardet==3.0.4 # via requests
|
||||||
|
croniter==0.3.34 # via -r requirements.in
|
||||||
django-picklefield==3.0.1 # via -r requirements.in
|
django-picklefield==3.0.1 # via -r requirements.in
|
||||||
django-redis==4.12.1 # via -r requirements.in
|
django-redis==4.12.1 # via -r requirements.in
|
||||||
django==3.0.7 # via django-picklefield, django-redis
|
django==3.0.8 # via django-picklefield, django-redis
|
||||||
docutils==0.15.2 # via botocore
|
docutils==0.15.2 # via botocore
|
||||||
hiredis==1.0.1 # via -r requirements.in
|
hiredis==1.0.1 # via -r requirements.in
|
||||||
idna==2.10 # via requests
|
idna==2.10 # via requests
|
||||||
iron-core==1.2.0 # via iron-mq
|
iron-core==1.2.0 # via iron-mq
|
||||||
iron-mq==0.9 # via -r requirements.in
|
iron-mq==0.9 # via -r requirements.in
|
||||||
jmespath==0.10.0 # via boto3, botocore
|
jmespath==0.10.0 # via boto3, botocore
|
||||||
|
natsort==7.0.1 # via croniter
|
||||||
psutil==5.7.0 # via -r requirements.in
|
psutil==5.7.0 # via -r requirements.in
|
||||||
pymongo==3.10.1 # via -r requirements.in
|
pymongo==3.10.1 # via -r requirements.in
|
||||||
python-dateutil==2.8.1 # via arrow, botocore, iron-core
|
python-dateutil==2.8.1 # via arrow, botocore, croniter, iron-core
|
||||||
pytz==2020.1 # via django
|
pytz==2020.1 # via django
|
||||||
redis==3.5.3 # via -r requirements.in, django-redis
|
redis==3.5.3 # via -r requirements.in, django-redis
|
||||||
requests==2.24.0 # via iron-core
|
requests==2.24.0 # via iron-core
|
||||||
|
|||||||
Reference in New Issue
Block a user