24 Commits

Author SHA1 Message Date
GDay
01cb652071 Release v1.4.11 2023-01-31 00:24:44 +01:00
Stan Triepels
b9bdd64568 Remove custom build (revert to auto create setup file) (#70) 2023-01-31 00:21:37 +01:00
Stan Triepels
b2bcffbcab Fix missing setup file for "No matching distribution" error (#69) 2023-01-31 00:09:33 +01:00
GDay
25cd180410 Release v1.4.10 2023-01-27 02:10:20 +01:00
Stan Triepels
f9e01699c5 Adding translation mo files automatically on build (#65) 2023-01-26 16:45:44 +01:00
Stan Triepels
2f3b0d0a31 Update all dependencies (#64) 2023-01-26 02:56:20 +01:00
Stan Triepels
264607f59c Bump translations to latest changes (#63) 2023-01-26 02:49:46 +01:00
msabatier
17c1609f10 Add meaningfull process titles with currently running task name (#57)
* Customize process names (in ps/top) and add task name in logs

* Increase severity of reincarnate log messages and add task name

* Document setproctitle optional dependency

Co-authored-by: Marc Sabatier <37879561+msabatier@users.noreply.github.com>
Co-authored-by: Stan Triepels <1939656+GDay@users.noreply.github.com>
2023-01-26 02:21:56 +01:00
msabatier
650c3b1524 Fix use of database router for write queries and remove Conf.HAS_REPLICA (#61)
Co-authored-by: Marc Sabatier
2023-01-18 02:12:13 +01:00
msabatier
a013591de5 Add intended_date_kwarg field to Schedule (#62)
Co-authored-by: Marc Sabatier <marc.sabatier@eco-adapt.com>
2023-01-17 22:52:11 +01:00
msabatier
865b1a5ba8 Change task timeout logic to have now() as execution time (#58) 2023-01-11 01:48:57 +01:00
msabatier
8cd1028391 More explicit log messages in exception handling (#59) 2023-01-11 00:36:35 +01:00
GDay
31e82ad028 Release v1.4.9 2022-12-22 03:06:01 +01:00
Stan Triepels
b66dfbd3af Fix DST timezone change (move from DST to normal jump) (#56) 2022-12-22 03:02:53 +01:00
GDay
08baaab125 Release v1.4.8 2022-12-21 03:46:01 +01:00
Stan Triepels
ce81059da8 Fix: allow both ZoneInfo and Pytz depending on django version (#55) 2022-12-21 03:42:38 +01:00
Stan Triepels
cf33275d92 Release v1.4.7 (#54) 2022-12-21 01:53:10 +01:00
Stan Triepels
e78e473be3 Fix: handling exceptions inside job function (#51) 2022-12-21 01:44:59 +01:00
Stan Triepels
4d454e4001 Fix: Daylight saving time issue with scheduler (#47) 2022-12-21 01:44:34 +01:00
Stan Triepels
cf33891291 Chore: Remove release drafter (#53) 2022-12-21 01:44:10 +01:00
Stan Triepels
134a54dbeb Chore: Fix badge and add download badge (#52) 2022-12-21 01:43:51 +01:00
Stan Triepels
c1d4858f62 Release 1.4.6 (#43) 2022-11-30 15:09:49 +01:00
Andreas Andersen
2566955782 Log exceptions with logger.exception (#42)
Co-authored-by: Andreas Bok Andersen <andreas.bokandersen@glooko.com>
2022-11-30 15:03:20 +01:00
Stan Triepels
3c0c32758d Chore: Flake8, isort and Black (#40) 2022-11-16 01:45:40 +01:00
53 changed files with 2342 additions and 1622 deletions

2
.git-blame-ignore-revs Normal file
View File

@@ -0,0 +1,2 @@
# flake8, black, isort
b1d000d007f3f77069719523268a0c6256dc0860

View File

@@ -1,37 +0,0 @@
categories:
-
label: breaking
title: Breaking
-
label: feature
title: New
-
label: bug
title: "Bug Fixes"
-
label: dependencies
title: "Dependency Updates"
-
label: security
title: Security
name-template: v$NEXT_PATCH_VERSION
tag-template: v$NEXT_PATCH_VERSION
template: |
# Changes
$CHANGES
version-resolver:
major:
labels:
- breaking
- major
minor:
labels:
- feature
- minor
patch:
labels:
- bug
- dependencies
- security
- patch
default: patch

View File

@@ -1,14 +0,0 @@
name: Update release draft
on:
push:
branches:
- master
jobs:
update_release_draft:
runs-on: ubuntu-latest
steps:
- uses: release-drafter/release-drafter@v5
with:
config-name: release-drafter.yml
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -73,7 +73,11 @@ jobs:
- name: Upload to coveralls
run: |
python -m pip install --upgrade pip
python -m pip install coveralls
python -m pip install coveralls flake8 black
coveralls --service=github --finish
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Check flake8/black
run: |
flake8 .
black --check .

View File

@@ -2,6 +2,54 @@
## [Unreleased](https://github.com/GDay/django-q2/tree/HEAD)
## [v1.4.11](https://github.com/GDay/django-q2/tree/v1.4.11) (2023-01-30)
**Merged pull requests:**
- Fix missing setup file for "No matching distribution" error https://github.com/GDay/django-q2/pull/69
- Remove custom build (revert to auto create setup file) https://github.com/GDay/django-q2/pull/70
## [v1.4.10](https://github.com/GDay/django-q2/tree/v1.4.10) (2023-01-26)
**Merged pull requests:**
- Adding translation mo files automatically on build https://github.com/GDay/django-q2/pull/65
- Update all dependencies https://github.com/GDay/django-q2/pull/64
- Bump translations to latest changes https://github.com/GDay/django-q2/pull/63
- Add meaningfull process titles with currently running task name https://github.com/GDay/django-q2/pull/57
- Fix use of database router for write queries and remove Conf.HAS_REPLICA https://github.com/GDay/django-q2/pull/61
- Add intended_date_kwarg field to Schedule https://github.com/GDay/django-q2/pull/62
- Change task timeout logic to have now() as execution time https://github.com/GDay/django-q2/pull/58
- More explicit log messages in exception handling https://github.com/GDay/django-q2/pull/59
## [v1.4.9](https://github.com/GDay/django-q2/tree/v1.4.9) (2022-12-22)
**Merged pull requests:**
- Fix DST timezone change (move from DST to normal jump) https://github.com/GDay/django-q2/pull/56
## [v1.4.8](https://github.com/GDay/django-q2/tree/v1.4.8) (2022-12-21)
**Merged pull requests:**
- Fix: allow both ZoneInfo and Pytz depending on django version https://github.com/GDay/django-q2/pull/55
## [v1.4.7](https://github.com/GDay/django-q2/tree/v1.4.7) (2022-12-21)
**Merged pull requests:**
- Fix: handling exceptions inside job function https://github.com/GDay/django-q2/pull/51
- Fix: Daylight saving time issue with scheduler https://github.com/GDay/django-q2/pull/47
- Chore: Fix badge and add download badge https://github.com/GDay/django-q2/pull/52
- Chore: Remove release drafter https://github.com/GDay/django-q2/pull/53
## [v1.4.6](https://github.com/GDay/django-q2/tree/v1.4.6) (2022-11-30)
**Merged pull requests:**
- Fix: Log exceptions with logger.exception https://github.com/GDay/django-q2/pull/42
- Chore: flake8, isort, black https://github.com/GDay/django-q2/pull/40
## [v1.4.5](https://github.com/GDay/django-q2/tree/v1.4.5) (2022-11-13)
- Fix release workflow

View File

@@ -1,7 +1,7 @@
A multiprocessing distributed task queue for Django
---------------------------------------------------
|image0| |image1| |docs|
|image0| |image1| |docs| |downloads|
::
@@ -245,4 +245,6 @@ Acknowledgements
.. |docs| image:: https://readthedocs.org/projects/docs/badge/?version=latest
:alt: Documentation Status
:scale: 100
:target: https://django-q.readthedocs.org/
:target: https://django-q2.readthedocs.org/
.. |downloads| image:: https://img.shields.io/pypi/dm/django-q2
:target: https://img.shields.io/pypi/dm/django-q2

View File

@@ -0,0 +1,9 @@
import subprocess
def generate_mo_files():
subprocess.run(["django-admin", "compilemessages"])
if __name__ == "__main__":
generate_mo_files()

View File

@@ -1,7 +1,7 @@
VERSION = (1, 3, 9)
import django
VERSION = (1, 4, 11)
if django.VERSION < (3, 2):
default_app_config = "django_q.apps.DjangoQConfig"

View File

@@ -1,9 +1,9 @@
"""Admin module for Django."""
from django.contrib import admin
from django.db.models.expressions import OuterRef, Subquery
from django.urls import reverse
from django.utils.html import format_html
from django.contrib import admin
from django.utils.translation import gettext_lazy as _
from django.db.models.expressions import OuterRef, Subquery
from django_q.conf import Conf, croniter
from django_q.models import Failure, OrmQ, Schedule, Success, Task
@@ -82,18 +82,27 @@ class ScheduleAdmin(admin.ModelAdmin):
readonly_fields = ("cron",)
list_filter = ("next_run", "schedule_type", "cluster")
search_fields = ("name", "func",)
search_fields = (
"name",
"func",
)
list_display_links = ("id", "name")
def get_queryset(self, request):
qs = super().get_queryset(request)
task_query = Task.objects.filter(id=OuterRef('task')).values('id', 'name', 'success')
qs = qs.annotate(task_id=Subquery(task_query.values('id')), task_name=Subquery(task_query.values('name')),
task_success=Subquery(task_query.values('success')))
task_query = Task.objects.filter(id=OuterRef("task")).values(
"id", "name", "success"
)
qs = qs.annotate(
task_id=Subquery(task_query.values("id")),
task_name=Subquery(task_query.values("name")),
task_success=Subquery(task_query.values("success")),
)
return qs
def get_success(self, obj):
return obj.task_success
get_success.boolean = True
get_success.short_description = _("success")
@@ -105,6 +114,7 @@ class ScheduleAdmin(admin.ModelAdmin):
url = reverse("admin:django_q_failure_change", args=(obj.task_id,))
return format_html(f'<a href="{url}">[{obj.task_name}]</a>')
return None
get_last_run.allow_tags = True
get_last_run.short_description = _("last_run")
@@ -112,7 +122,7 @@ class ScheduleAdmin(admin.ModelAdmin):
class QueueAdmin(admin.ModelAdmin):
"""queue admin for ORM broker"""
list_display = ("id", "key", "name", "group", "func", "lock", "task_id")
list_display = ("id", "key", "name", "group", "func", "lock", "task_id")
def save_model(self, request, obj, form, change):
obj.save(using=Conf.ORM)

View File

@@ -9,4 +9,4 @@ class DjangoQConfig(AppConfig):
default_auto_field = "django.db.models.AutoField"
def ready(self):
from django_q.signals import call_hook
from django_q.signals import call_hook # noqa: F401

View File

@@ -39,7 +39,8 @@ class Sqs(Broker):
raise ValueError("receive_message_wait_time_seconds should be int")
if wait_time_second > 20:
raise ValueError(
"receive_message_wait_time_seconds is invalid. Reason: Must be >= 0 and <= 20"
"receive_message_wait_time_seconds is invalid. Reason: Must be >= 0"
" and <= 20"
)
params.update({"WaitTimeSeconds": wait_time_second})

View File

@@ -11,7 +11,7 @@ from django_q.models import OrmQ
def _timeout():
return timezone.now() - timedelta(seconds=Conf.RETRY)
return timezone.now() + timedelta(seconds=Conf.RETRY)
class ORM(Broker):
@@ -31,13 +31,13 @@ class ORM(Broker):
def queue_size(self) -> int:
return (
self.get_connection()
.filter(key=self.list_key, lock__lte=_timeout())
.filter(key=self.list_key, lock__lte=timezone.now())
.count()
)
def lock_size(self) -> int:
return (
self.get_connection().filter(key=self.list_key, lock__gt=_timeout()).count()
self.get_connection().filter(key=self.list_key, lock__gt=timezone.now()).count()
)
def purge_queue(self):
@@ -56,13 +56,13 @@ class ORM(Broker):
def enqueue(self, task):
package = self.get_connection().create(
key=self.list_key, payload=task, lock=_timeout()
key=self.list_key, payload=task, lock=timezone.now()
)
return package.pk
def dequeue(self):
tasks = self.get_connection().filter(key=self.list_key, lock__lt=_timeout())[
0 : Conf.BULK
tasks = self.get_connection().filter(key=self.list_key, lock__lt=timezone.now())[
0 : Conf.BULK # noqa: E203
]
if tasks:
task_list = []
@@ -70,10 +70,11 @@ class ORM(Broker):
if (
self.get_connection()
.filter(id=task.id, lock=task.lock)
.update(lock=timezone.now())
.update(lock=_timeout())
):
task_list.append((task.pk, task.payload))
# else don't process, as another cluster has been faster than us on that task
# else don't process, as another cluster has been faster than us on
# that task
return task_list
# empty queue, spare the cpu
sleep(Conf.POLL)

View File

@@ -20,7 +20,6 @@ except core.exceptions.AppRegistryNotReady:
django.setup()
from django.conf import settings
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
@@ -34,6 +33,7 @@ from django_q.conf import (
get_ppid,
logger,
psutil,
setproctitle,
resource,
)
from django_q.humanhash import humanize
@@ -43,7 +43,7 @@ from django_q.signals import post_execute, pre_execute
from django_q.signing import BadSignature, SignedPackage
from django_q.status import Stat, Status
from .utils import add_months, add_years, get_func_repr
from .utils import get_func_repr, localtime
class Cluster:
@@ -60,6 +60,8 @@ class Cluster:
signal.signal(signal.SIGINT, self.sig_handler)
def start(self) -> int:
if setproctitle:
setproctitle.setproctitle(f"qcluster {current_process().name} {self.name}")
# Start Sentinel
self.stop_event = Event()
self.start_event = Event()
@@ -74,7 +76,7 @@ class Cluster:
),
)
self.sentinel.start()
logger.info(_("Q Cluster %(name)s starting.") % {'name': self.name})
logger.info(_("Q Cluster %(name)s starting.") % {"name": self.name})
while not self.start_event.is_set():
sleep(0.1)
return self.pid
@@ -82,19 +84,21 @@ class Cluster:
def stop(self) -> bool:
if not self.sentinel.is_alive():
return False
logger.info(_("Q Cluster %(name)s stopping.") % {'name': self.name})
logger.info(_("Q Cluster %(name)s stopping.") % {"name": self.name})
self.stop_event.set()
self.sentinel.join()
logger.info(_("Q Cluster %(name)s has stopped.") % {'name': self.name})
logger.info(_("Q Cluster %(name)s has stopped.") % {"name": self.name})
self.start_event = None
self.stop_event = None
return True
def sig_handler(self, signum, frame):
logger.debug(
_(
'%(name)s got signal %(signal)s'
) % {'name': current_process().name, 'signal': Conf.SIGNAL_NAMES.get(signum, "UNKNOWN")}
_("%(name)s got signal %(signal)s")
% {
"name": current_process().name,
"signal": Conf.SIGNAL_NAMES.get(signum, "UNKNOWN"),
}
)
self.stop()
@@ -216,21 +220,49 @@ class Sentinel:
db.connections.close_all()
if process == self.monitor:
self.monitor = self.spawn_monitor()
logger.error(_("reincarnated monitor %(name)s after sudden death") % {'name': process.name})
logger.critical(
_("reincarnated monitor %(name)s after sudden death")
% {"name": process.name}
)
elif process == self.pusher:
self.pusher = self.spawn_pusher()
logger.error(_("reincarnated pusher %(name)s after sudden death") % {'name': process.name})
logger.critical(
_("reincarnated pusher %(name)s after sudden death")
% {"name": process.name}
)
else:
self.pool.remove(process)
self.spawn_worker()
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
task_name = ""
if psutil:
try:
process_name = psutil.Process(process.pid).name()
name_splits = process_name.split(" ")
task_name = name_splits[3] if len(name_splits) >= 4 and name_splits[2] == "processing" else ""
except psutil.NoSuchProcess:
pass
process.terminate()
logger.warning(_("reincarnated worker %(name)s after timeout") % {'name': process.name})
if task_name:
msg = (
_("reincarnated worker %(name)s after timeout while processing task %(task_name)s")
% {"name": process.name, "task_name": task_name}
)
else:
msg = (
_("reincarnated worker %(name)s after timeout")
% {"name": process.name}
)
logger.critical(msg)
elif int(process.timer.value) == -2:
logger.info(_("recycled worker %(name)s") % {'name': process.name})
logger.info(_("recycled worker %(name)s") % {"name": process.name})
else:
logger.error(_("reincarnated worker %(name)s after death") % {'name': process.name})
logger.critical(
_("reincarnated worker %(name)s after death")
% {"name": process.name}
)
self.reincarnations += 1
@@ -252,13 +284,18 @@ class Sentinel:
def guard(self):
logger.info(
_(
"%(name)s guarding cluster %(cluster_name)s"
) % {'name': current_process().name, 'cluster_name': humanize(self.cluster_id.hex)}
_("%(name)s guarding cluster %(cluster_name)s")
% {
"name": current_process().name,
"cluster_name": humanize(self.cluster_id.hex),
}
)
self.start_event.set()
Stat(self).save()
logger.info(_("Q Cluster %(cluster_name)s running.") % {'cluster_name': humanize(self.cluster_id.hex)})
logger.info(
_("Q Cluster %(cluster_name)s running.")
% {"cluster_name": humanize(self.cluster_id.hex)}
)
counter = 0
cycle = Conf.GUARD_CYCLE # guard loop sleep in seconds
# Guard loop. Runs at least once
@@ -292,7 +329,7 @@ class Sentinel:
def stop(self):
Stat(self).save()
name = current_process().name
logger.info(_("%(name)s stopping cluster processes") % {'name': name})
logger.info(_("%(name)s stopping cluster processes") % {"name": name})
# Stopping pusher
self.event_out.set()
# Wait for it to stop
@@ -317,7 +354,7 @@ class Sentinel:
self.result_queue.close()
# Wait for the result queue to empty
self.result_queue.join_thread()
logger.info(_("%(name)s waiting for the monitor.") % {'name': name})
logger.info(_("%(name)s waiting for the monitor.") % {"name": name})
# Wait for everything to close or time out
count = 0
if not self.timeout:
@@ -339,12 +376,18 @@ def pusher(task_queue: Queue, event: Event, broker: Broker = None):
"""
if not broker:
broker = get_broker()
logger.info(_("%(process_name)s pushing tasks at %(id)s") % {'process_name': current_process().name, 'id': current_process().pid})
proc_name = current_process().name
if setproctitle:
setproctitle.setproctitle(f"qcluster {proc_name} pusher")
logger.info(
_("%(name)s pushing tasks at %(id)s")
% {"name": proc_name, "id": current_process().pid}
)
while True:
try:
task_set = broker.dequeue()
except Exception as e:
logger.error(e, traceback.format_exc())
except Exception:
logger.exception("Failed to pull task from broker")
# broker probably crashed. Let the sentinel handle it.
sleep(10)
break
@@ -354,16 +397,18 @@ def pusher(task_queue: Queue, event: Event, broker: Broker = None):
# unpack the task
try:
task = SignedPackage.loads(task[1])
except (TypeError, BadSignature) as e:
logger.error(e, traceback.format_exc())
except (TypeError, BadSignature):
logger.exception("Failed to push task to queue")
broker.fail(ack_id)
continue
task["ack_id"] = ack_id
task_queue.put(task)
logger.debug(_("queueing from %(list_key)s") % {'list_key': broker.list_key})
logger.debug(
_("queueing from %(list_key)s") % {"list_key": broker.list_key}
)
if event.is_set():
break
logger.info(_("%(name)s stopped pushing tasks") % {'name': current_process().name})
logger.info(_("%(name)s stopped pushing tasks") % {"name": current_process().name})
def monitor(result_queue: Queue, broker: Broker = None):
@@ -374,8 +419,12 @@ def monitor(result_queue: Queue, broker: Broker = None):
"""
if not broker:
broker = get_broker()
name = current_process().name
logger.info(_("%(name)s monitoring at %(id)s") % {'name': name, 'id': current_process().pid})
proc_name = current_process().name
if setproctitle:
setproctitle.setproctitle(f"qcluster {proc_name} monitor")
logger.info(
_("%(name)s monitoring at %(id)s") % {"name": proc_name, "id": current_process().pid}
)
for task in iter(result_queue.get, "STOP"):
# save the result
if task.get("cached", False):
@@ -389,28 +438,44 @@ def monitor(result_queue: Queue, broker: Broker = None):
# signal execution done
post_execute.send(sender="django_q", task=task)
# log the result
info_name = get_func_repr(task['func'])
info_name = get_func_repr(task["func"])
if task["success"]:
# log success
logger.info(_("Processed '%(info_name)s' (%(task_name)s)") % {'info_name': info_name, 'task_name': task['name']})
logger.info(
_("Processed '%(info_name)s' (%(task_name)s)")
% {"info_name": info_name, "task_name": task["name"]}
)
else:
# log failure
logger.error(_("Failed '%(info_name)s' (%(task_name)s) - %(task_result)s") % {'info_name': info_name, 'task_name': task['name'], 'task_result': task['result']})
logger.info(_("%(name)s stopped monitoring results") % {'name': name})
logger.error(
_("Failed '%(info_name)s' (%(task_name)s) - %(task_result)s")
% {
"info_name": info_name,
"task_name": task["name"],
"task_result": task["result"],
}
)
logger.info(_("%(name)s stopped monitoring results") % {"name": proc_name})
def worker(
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
:param timeout: number of seconds wait for a worker to finish.
:type task_queue: multiprocessing.Queue
:type result_queue: multiprocessing.Queue
:type timer: multiprocessing.Value
"""
proc_name = current_process().name
logger.info(_("%(proc_name)s ready for work at %(id)s") % {'proc_name': proc_name, 'id': current_process().pid})
logger.info(
_("%(proc_name)s ready for work at %(id)s")
% {"proc_name": proc_name, "id": current_process().pid}
)
if setproctitle:
setproctitle.setproctitle(f"qcluster {proc_name} idle")
task_count = 0
if timeout is None:
timeout = -1
@@ -419,13 +484,33 @@ def worker(
result = None
timer.value = -1 # Idle
task_count += 1
# Get the function from the task
func = task["func"]
func_name = get_func_repr(func)
logger.info(_("%(proc_name)s processing '%(func_name)s' (%(task_name)s)") % {'proc_name': proc_name, 'func_name': func_name, 'task_name': task['name']})
f = task["func"]
# Log task creation and set process name
# Get the function from the task
func_name = get_func_repr(f)
task_name = task["name"]
task_desc = (
_("%(proc_name)s processing %(task_name)s '%(func_name)s'")
% {
"proc_name": proc_name,
"func_name": func_name,
"task_name": task_name,
}
)
if "group" in task:
task_desc += f" [{task['group']}]"
logger.info(task_desc)
if setproctitle:
proc_title = f"qcluster {proc_name} processing {task_name} '{func_name}'"
if "group" in task:
proc_title += f" [{task['group']}]"
setproctitle.setproctitle(proc_title)
# if it's not an instance try to get it from the string
if not callable(task["func"]):
if not callable(f):
# locate() returns None if f cannot be loaded
f = pydoc.locate(f)
close_old_django_connections()
timer_value = task.pop("timeout", timeout)
@@ -434,14 +519,17 @@ def worker(
# execute the payload
timer.value = timer_value # Busy
try:
if f is None:
# raise a meaningfull error if task["func"] is not a valid function
raise ValueError(f"Function {task['func']} is not defined")
res = f(*task["args"], **task["kwargs"])
result = (res, True)
except Exception:
result = (_("Could not process '%(func_name)s'. Check the location of the function and the args/kwargs.") % {'func_name': func_name}, False)
except Exception as e:
result = (f"{e} : {traceback.format_exc()}", False)
if error_reporter:
error_reporter.report()
if task.get("sync", False):
raise Exception(result)
raise
with timer.get_lock():
# Process result
task["result"] = result[0]
@@ -449,11 +537,14 @@ def worker(
task["stopped"] = timezone.now()
result_queue.put(task)
timer.value = -1 # Idle
if setproctitle:
setproctitle.setproctitle(f"qcluster {proc_name} idle")
# Recycle
if task_count == Conf.RECYCLE or rss_check():
timer.value = -2 # Recycled
break
logger.info(_("%(proc_name)s stopped doing work") % {'proc_name': proc_name})
logger.info(_("%(proc_name)s stopped doing work") % {"proc_name": proc_name})
def save_task(task, broker: Broker):
"""
@@ -478,16 +569,22 @@ def save_task(task, broker: Broker):
try:
filters = {}
if Conf.SAVE_LIMIT_PER and Conf.SAVE_LIMIT_PER in {"group", "name", "func"} and Conf.SAVE_LIMIT_PER in task:
if (
Conf.SAVE_LIMIT_PER
and Conf.SAVE_LIMIT_PER in {"group", "name", "func"}
and Conf.SAVE_LIMIT_PER in task
):
value = task[Conf.SAVE_LIMIT_PER]
if Conf.SAVE_LIMIT_PER == "func":
value = get_func_repr(value)
filters[Conf.SAVE_LIMIT_PER] = value
database_to_use = {"using": Conf.ORM if Conf.ORM else Schedule.objects.db} if not Conf.HAS_REPLICA else {}
with db.transaction.atomic(**database_to_use):
with db.transaction.atomic(using=db.router.db_for_write(Success)):
last = Success.objects.filter(**filters).select_for_update().last()
if task["success"] and 0 < Conf.SAVE_LIMIT <= Success.objects.filter(**filters).count():
if (
task["success"]
and 0 < Conf.SAVE_LIMIT <= Success.objects.filter(**filters).count()
):
last.delete()
# check if this task has previous results
@@ -524,8 +621,8 @@ def save_task(task, broker: Broker):
success=task["success"],
attempt_count=1,
)
except Exception as e:
logger.error(e)
except Exception:
logger.exception("Could not save task result")
def save_cached(task, broker: Broker):
@@ -576,8 +673,8 @@ def save_cached(task, broker: Broker):
)
# save the task
broker.cache.set(task_key, SignedPackage.dumps(task), timeout)
except Exception as e:
logger.error(e)
except Exception:
logger.exception("Could not save task result")
def scheduler(broker: Broker = None):
@@ -588,8 +685,7 @@ def scheduler(broker: Broker = None):
broker = get_broker()
close_old_django_connections()
try:
database_to_use = {"using": Conf.ORM if Conf.ORM else Schedule.objects.db} if not Conf.HAS_REPLICA else {}
with db.transaction.atomic(**database_to_use):
with db.transaction.atomic(using=db.router.db_for_write(Schedule)):
for s in (
Schedule.objects.select_for_update()
.exclude(repeats=0)
@@ -608,8 +704,13 @@ def scheduler(broker: Broker = None):
except (SyntaxError, ValueError):
# else use the kwargs syntax
try:
parsed_kwargs = ast.parse(f"f({s.kwargs})").body[0].value.keywords
kwargs = {kwarg.arg: ast.literal_eval(kwarg.value) for kwarg in parsed_kwargs}
parsed_kwargs = (
ast.parse(f"f({s.kwargs})").body[0].value.keywords
)
kwargs = {
kwarg.arg: ast.literal_eval(kwarg.value)
for kwarg in parsed_kwargs
}
except (SyntaxError, ValueError):
kwargs = {}
if s.args:
@@ -618,38 +719,15 @@ def scheduler(broker: Broker = None):
if type(args) != tuple:
args = (args,)
q_options = kwargs.get("q_options", {})
if s.intended_date_kwarg:
kwargs[s.intended_date_kwarg] = s.next_run.isoformat()
if s.hook:
q_options["hook"] = s.hook
# set up the next run time
if s.schedule_type != s.ONCE:
next_run = s.next_run
while True:
if s.schedule_type == s.MINUTES:
next_run = next_run + timedelta(minutes=(s.minutes or 1))
elif s.schedule_type == s.HOURLY:
next_run = next_run + timedelta(hours=1)
elif s.schedule_type == s.DAILY:
next_run = next_run + timedelta(days=1)
elif s.schedule_type == s.WEEKLY:
next_run = next_run + timedelta(weeks=1)
elif s.schedule_type == s.BIWEEKLY:
next_run = next_run + timedelta(weeks=2)
elif s.schedule_type == s.MONTHLY:
next_run = add_months(next_run, 1)
elif s.schedule_type == s.BIMONTHLY:
next_run = add_months(next_run, 2)
elif s.schedule_type == s.QUARTERLY:
next_run = add_months(next_run, 3)
elif s.schedule_type == s.YEARLY:
next_run = add_years(next_run, 1)
elif s.schedule_type == s.CRON:
if not croniter:
raise ImportError(
_(
"Please install croniter to enable cron expressions"
)
)
next_run = croniter(s.cron, localtime()).get_next(datetime)
next_run = s.calculate_next_run(next_run)
if Conf.CATCH_UP or next_run > localtime():
break
@@ -659,7 +737,8 @@ def scheduler(broker: Broker = None):
scheduled_broker = broker
try:
scheduled_broker = get_broker(q_options["broker_name"])
except: # invalid broker_name or non existing broker with broker_name
except: # noqa: E722
# invalid broker_name or non existing broker with broker_name
pass
q_options["broker"] = scheduled_broker
q_options["group"] = q_options.get("group", s.name or s.id)
@@ -669,14 +748,25 @@ def scheduler(broker: Broker = None):
if not s.task:
logger.error(
_(
"%(process_name)s failed to create a task from schedule [%(schedule)s]"
) % {'process_name': current_process().name, 'schedule': s.name or s.id}
"%(process_name)s failed to create a task from schedule "
"[%(schedule)s]"
)
% {
"process_name": current_process().name,
"schedule": s.name or s.id,
}
)
else:
logger.info(
_(
"%(process_name)s created a task from schedule [%(schedule)s]"
) % {'process_name': current_process().name, 'schedule': s.name or s.id}
"%(process_name)s created task %(task_name)s from schedule "
"[%(schedule)s]"
)
% {
"process_name": current_process().name,
"task_name": humanize(s.task),
"schedule": s.name or s.id,
}
)
# default behavior is to delete a ONCE schedule
if s.schedule_type == s.ONCE:
@@ -687,8 +777,8 @@ def scheduler(broker: Broker = None):
s.repeats = 0
# save the schedule
s.save()
except Exception as e:
logger.error(e)
except Exception:
logger.exception("Could not create task from schedule")
def close_old_django_connections():
@@ -741,7 +831,10 @@ def set_cpu_affinity(n: int, process_ids: list, actual: bool = not Conf.TESTING)
p = psutil.Process(pid)
if actual:
p.cpu_affinity(affinity)
logger.info(_("%(pid)s will use cpu %(affinity)s") % {'pid': pid, 'affinity': affinity})
logger.info(
_("%(pid)s will use cpu %(affinity)s")
% {"pid": pid, "affinity": affinity}
)
def rss_check():
@@ -751,10 +844,3 @@ def rss_check():
elif psutil:
return psutil.Process().memory_info().rss >= Conf.MAX_RSS * 1024
return False
def localtime() -> datetime:
"""Override for timezone.localtime to deal with naive times and local times"""
if settings.USE_TZ:
return timezone.localtime()
return datetime.now()

View File

@@ -27,6 +27,11 @@ try:
except ModuleNotFoundError:
resource = None
try:
import setproctitle
except ModuleNotFoundError:
setproctitle = None
class Conf:
"""
@@ -54,9 +59,6 @@ class Conf:
# ORM broker
ORM = conf.get("orm", None)
# ORM support for read/write replicas
HAS_REPLICA = conf.get("has_replica", False)
# Custom broker class
BROKER_CLASS = conf.get("broker_class", None)
@@ -73,7 +75,8 @@ class Conf:
# Log output level
LOG_LEVEL = conf.get("log_level", "INFO")
# Maximum number of successful tasks kept in the database. 0 saves everything. -1 saves none
# Maximum number of successful tasks kept in the database. 0 saves everything.
# -1 saves none
# Failures are always saved
SAVE_LIMIT = conf.get("save_limit", 250)
@@ -82,7 +85,13 @@ class Conf:
# Verify SAVE_LIMIT_PER is valid
if SAVE_LIMIT_PER not in ["group", "name", "func", None]:
warn(_("SAVE_LIMIT_PER (%(option)s) is not a valid option. Options are: 'group', 'name', 'func' and None. Default is None.") % {'option': SAVE_LIMIT_PER})
warn(
_(
"SAVE_LIMIT_PER (%(option)s) is not a valid option. Options are: "
"'group', 'name', 'func' and None. Default is None."
)
% {"option": SAVE_LIMIT_PER}
)
# Guard loop sleep in seconds. Should be between 0 and 60 seconds.
GUARD_CYCLE = conf.get("guard_cycle", 0.5)
@@ -113,11 +122,12 @@ class Conf:
# Sets compression of redis packages
COMPRESSED = conf.get("compress", False)
# Number of tasks each worker can handle before it gets recycled. Useful for releasing memory
# Number of tasks each worker can handle before it gets recycled.
# Useful for releasing memory
RECYCLE = conf.get("recycle", 500)
# The maximum resident set size in kilobytes before a worker will recycle. Useful for limiting memory usage
# Not available on all platforms
# The maximum resident set size in kilobytes before a worker will recycle.
# Useful for limiting memory usage. Not available on all platforms
MAX_RSS = conf.get("max_rss", None)
# Number of seconds to wait for a worker to finish.
@@ -135,9 +145,10 @@ class Conf:
# Verify if retry and timeout settings are correct
if not TIMEOUT or (TIMEOUT > RETRY):
warn(
"""Retry and timeout are misconfigured. Set retry larger than timeout,
failure to do so will cause the tasks to be retriggered before completion.
See https://django-q2.readthedocs.io/en/master/configure.html#retry for details."""
"Retry and timeout are misconfigured. Set retry larger than timeout,"
"failure to do so will cause the tasks to be retriggered before completion."
"See https://django-q2.readthedocs.io/en/master/configure.html#retry "
"for details."
)
# Sets the amount of tasks the cluster will try to pop off the broker.
@@ -156,12 +167,14 @@ class Conf:
# The Django cache to use
CACHE = conf.get("cache", "default")
# Use the cache as result backend. Can be 'True' or an integer representing the global cache timeout.
# Use the cache as result backend. Can be 'True' or an integer representing the
# global cache timeout.
# i.e 'cached: 60' , will make all results go the cache and expire in 60 seconds.
CACHED = conf.get("cached", False)
# If set to False the scheduler won't execute tasks in the past.
# Instead it will run once and reschedule the next run in the future. Defaults to True.
# Instead it will run once and reschedule the next run in the future. Defaults to
# True.
CATCH_UP = conf.get("catch_up", True)
# Use the secret key for package signing
@@ -200,6 +213,11 @@ class Conf:
# to manage workarounds during testing
TESTING = conf.get("testing", False)
# Timezone for next_run, overrules Django timezone
TIME_ZONE = None
if settings.USE_TZ:
TIME_ZONE = conf.get("time_zone", settings.TIME_ZONE)
# logger
logger = logging.getLogger("django-q")
@@ -257,5 +275,6 @@ def get_ppid():
return psutil.Process(os.getpid()).ppid()
else:
raise OSError(
"Your OS does not support `os.getppid`. Please install `psutil` as an alternative provider."
"Your OS does not support `os.getppid`. Please install `psutil` as an "
"alternative provider."
)

View File

@@ -6,6 +6,7 @@ from django.core.signing import BadSignature, JSONSerializer, SignatureExpired
from django.core.signing import Signer as Sgnr
from django.core.signing import TimestampSigner as TsS
from django.core.signing import b64_decode, dumps
try:
from django.core.signing import base62
except ImportError:

View File

@@ -337,12 +337,18 @@ class HumanHasher:
# Split `bytes` into `target` segments.
seg_size = length // target
segments = [bytes[i * seg_size : (i + 1) * seg_size] for i in range(target)]
# fmt: off
segments = [
bytes[i * seg_size : (i + 1) * seg_size] for i in range(target) # noqa: E203 E501
]
# fmt: on
# Catch any left-over bytes in the last segment.
segments[-1].extend(bytes[target * seg_size :])
segments[-1].extend(bytes[target * seg_size :]) # noqa: E203 E501
# Use a simple XOR checksum-like function for compression.
checksum = lambda bytes: reduce(operator.xor, bytes, 0)
def checksum(bytes):
return reduce(operator.xor, bytes, 0)
checksums = list(map(checksum, segments))
return checksums

View File

@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-11-12 01:47+0000\n"
"POT-Creation-Date: 2023-01-26 01:38+0000\n"
"PO-Revision-Date: 2018-08-05 18:28+0200\n"
"Last-Translator: Jonas Winkler\n"
"Language-Team: \n"
@@ -17,177 +17,176 @@ msgstr ""
"X-Generator: Poedit 2.1.1\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: django_q/admin.py:43
#: admin.py:43
msgid "Resubmit selected tasks to queue"
msgstr "Ausgewählte Aufgaben erneut ausführen"
#: django_q/admin.py:98 django_q/models.py:228
#: admin.py:107 models.py:293
#, fuzzy
#| msgid "Success"
msgid "success"
msgstr "erfolg"
#: django_q/admin.py:109 django_q/models.py:230
#: admin.py:119 models.py:295
msgid "last_run"
msgstr ""
#: django_q/cluster.py:77
#: cluster.py:79
#, python-format
msgid "Q Cluster %(name)s starting."
msgstr "Q-Cluster %(name)s wird gestartet."
#: django_q/cluster.py:85
#: cluster.py:87
#, fuzzy, python-format
#| msgid "Q Cluster-{} stopping."
msgid "Q Cluster %(name)s stopping."
msgstr "Q-Cluster {name} wird gestoppt."
#: django_q/cluster.py:88
#: cluster.py:90
#, python-format
msgid "Q Cluster %(name)s has stopped."
msgstr "Q-Cluster %(name)s wurde gestoppt."
#: django_q/cluster.py:96
#: cluster.py:97
#, python-format
msgid "%(name)s got signal %(signal)s"
msgstr "%(name)s erhielt das Signal %(signal)s"
#: django_q/cluster.py:219
#: cluster.py:224
#, python-format
msgid "reincarnated monitor %(name)s after sudden death"
msgstr "Monitor %(name)s wurde nach unerwartetem Absturz neu gestartet"
#: django_q/cluster.py:222
#: cluster.py:230
#, python-format
msgid "reincarnated pusher %(name)s after sudden death"
msgstr "Pusher %(name)s wurde nach unerwartetem Absturz neu gestartet"
#: django_q/cluster.py:229
#: cluster.py:250
#, fuzzy, python-format
#| msgid "reincarnated worker %(name)s after timeout"
msgid ""
"reincarnated worker %(name)s after timeout while processing task "
"%(task_name)s"
msgstr "Worker %(name)s wurde nach Zeitüberschreitung neu gestartet"
#: cluster.py:255
#, python-format
msgid "reincarnated worker %(name)s after timeout"
msgstr "Worker %(name)s wurde nach Zeitüberschreitung neu gestartet"
#: django_q/cluster.py:231
#: cluster.py:260
#, python-format
msgid "recycled worker %(name)s"
msgstr "Worker %(name)s wurde wiederverwendet"
#: django_q/cluster.py:233
#: cluster.py:263
#, python-format
msgid "reincarnated worker %(name)s after death"
msgstr "Worker %(name)s wurde nach unerwartetem Absturz neu gestartet"
#: django_q/cluster.py:256
#: cluster.py:287
#, python-format
msgid "%(name)s guarding cluster %(cluster_name)s"
msgstr "%(name)s beschützt das Cluster %(cluster_name)s"
#: django_q/cluster.py:261
#: cluster.py:296
#, python-format
msgid "Q Cluster %(cluster_name)s running."
msgstr "Q-Cluster %(cluster_name)s läuft."
#: django_q/cluster.py:295
#: cluster.py:332
#, python-format
msgid "%(name)s stopping cluster processes"
msgstr "%(name)s hält Cluster-Prozesse an"
#: django_q/cluster.py:320
#: cluster.py:357
#, python-format
msgid "%(name)s waiting for the monitor."
msgstr "%(name)s wartet auf den Monitor."
#: django_q/cluster.py:342
#, python-format
msgid "%(process_name)s pushing tasks at %(id)s"
#: cluster.py:383
#, fuzzy, python-format
#| msgid "%(process_name)s pushing tasks at %(id)s"
msgid "%(name)s pushing tasks at %(id)s"
msgstr "%(process_name)s veröffentlicht Aufagaben auf %(id)s"
#: django_q/cluster.py:363
#: cluster.py:407
#, python-format
msgid "queueing from %(list_key)s"
msgstr "Einreihen von %(list_key)s"
#: django_q/cluster.py:366
#: cluster.py:411
#, python-format
msgid "%(name)s stopped pushing tasks"
msgstr "%(name)s veröffentlicht keine Aufgaben mehr"
#: django_q/cluster.py:378
#: cluster.py:426
#, python-format
msgid "%(name)s monitoring at %(id)s"
msgstr "%(name)s beobachtet auf %(id)s"
#: django_q/cluster.py:395
#: cluster.py:445
#, python-format
msgid "Processed '%(info_name)s' (%(task_name)s)"
msgstr "[%(task_name)s] - '%(info_name)s' wurde verarbeitet"
#: django_q/cluster.py:398
#: cluster.py:451
#, python-format
msgid "Failed '%(info_name)s' (%(task_name)s) - %(task_result)s"
msgstr "'%(info_name)s' (%(task_name)s) ist fehlgeschlagen - %(task_result)s"
#: django_q/cluster.py:399
#: cluster.py:458
#, python-format
msgid "%(name)s stopped monitoring results"
msgstr "%(name)s überwacht keine Ergebnisse mehr"
#: django_q/cluster.py:413
#: cluster.py:474
#, python-format
msgid "%(proc_name)s ready for work at %(id)s"
msgstr "%(proc_name)s ist bereit für Arbeit auf %(id)s"
#: django_q/cluster.py:425
#, python-format
msgid "%(proc_name)s processing '%(func_name)s' (%(task_name)s)"
#: cluster.py:494
#, fuzzy, python-format
#| msgid "%(proc_name)s processing '%(func_name)s' (%(task_name)s)"
msgid "%(proc_name)s processing %(task_name)s '%(func_name)s'"
msgstr "%(proc_name)s verarbeitet '%(func_name)s' (%(task_name)s)"
#: django_q/cluster.py:440
#, python-format
msgid ""
"Could not process '%(func_name)s'. Check the location of the function and "
"the args/kwargs."
msgstr ""
"Konnte '%(func_name)s' nicht verarbeiten. Überprüfen Sie den Ort der "
"Funktion und die args/kwargs."
#: django_q/cluster.py:456
#: cluster.py:546
#, python-format
msgid "%(proc_name)s stopped doing work"
msgstr "%(proc_name)s hat die Arbeit beendet"
#: django_q/cluster.py:649 django_q/models.py:144
msgid "Please install croniter to enable cron expressions"
msgstr "Bitte installieren Sie croniter, um Cron-Ausdrücke zu aktivieren"
#: django_q/cluster.py:672
#: cluster.py:751
#, python-format
msgid "%(process_name)s failed to create a task from schedule [%(schedule)s]"
msgstr ""
"%(process_name)s konnte keine Aufgabe von Zeitplan [%(schedule)s] erstellen"
#: django_q/cluster.py:678
#, python-format
msgid "%(process_name)s created a task from schedule [%(schedule)s]"
#: cluster.py:762
#, fuzzy, python-format
#| msgid "%(process_name)s created a task from schedule [%(schedule)s]"
msgid ""
"%(process_name)s created task %(task_name)s from schedule [%(schedule)s]"
msgstr ""
"%(process_name)s hat eine Aufgabe des Zeitplans [%(schedule)s] erstellt"
#: django_q/cluster.py:718
#: cluster.py:808
msgid "Skipping cpu affinity because psutil was not found."
msgstr "Cpu-Affinität wird übersprungen, da psutil nicht gefunden wurde."
#: django_q/cluster.py:723
#: cluster.py:813
msgid "Faking cpu affinity because it is not supported on this platform"
msgstr ""
"Vortäuschen von CPU-Affinität, da diese auf dieser Plattform nicht "
"unterstützt wird"
#: django_q/cluster.py:744
#: cluster.py:835
#, python-format
msgid "%(pid)s will use cpu %(affinity)s"
msgstr "%(pid)s wird CPU %(affinity)s benutzen"
#: django_q/conf.py:85
#: conf.py:90
#, python-format
msgid ""
"SAVE_LIMIT_PER (%(option)s) is not a valid option. Options are: 'group', "
@@ -197,291 +196,307 @@ msgstr ""
"'group', 'name', 'func' und None. Standard ist None."
#. Translators: Cluster status descriptions
#: django_q/conf.py:194
#: conf.py:207
msgid "Starting"
msgstr "Wird gestartet"
#: django_q/conf.py:195
#: conf.py:208
msgid "Working"
msgstr "Arbeitet"
#: django_q/conf.py:196
#: conf.py:209
msgid "Idle"
msgstr "Leerlauf"
#: django_q/conf.py:197
#: conf.py:210
msgid "Stopped"
msgstr "Gestoppt"
#: django_q/conf.py:198
#: conf.py:211
msgid "Stopping"
msgstr "Wird gestoppt"
#. Translators: help text for qcluster management command
#: django_q/management/commands/qcluster.py:9
#: management/commands/qcluster.py:9
msgid "Starts a Django Q Cluster."
msgstr "Startet ein Django-Q-Cluster."
#. Translators: help text for qinfo management command
#: django_q/management/commands/qinfo.py:11
#: management/commands/qinfo.py:11
msgid "General information over all clusters."
msgstr "Allgemeine Informationen über alle Cluster"
#. Translators: help text for qmemory management command
#: django_q/management/commands/qmemory.py:9
#: management/commands/qmemory.py:9
msgid "Monitors Q Cluster memory usage"
msgstr "Überwacht die Speichernutzung von Q Cluster"
#. Translators: help text for qmonitor management command
#: django_q/management/commands/qmonitor.py:9
#: management/commands/qmonitor.py:9
msgid "Monitors Q Cluster activity"
msgstr "Q-Cluster aktiv überwachen"
#: django_q/models.py:119
#: models.py:125
msgid "Successful task"
msgstr "Erfolgreiche Aufgabe"
#: django_q/models.py:120
#: models.py:126
msgid "Successful tasks"
msgstr "Erfolgreiche Aufgaben"
#: django_q/models.py:135
#: models.py:141
msgid "Failed task"
msgstr "Fehlgeschlagene Aufgabe"
#: django_q/models.py:136
#: models.py:142
msgid "Failed tasks"
msgstr "Fehlgeschlagene Aufgaben"
#: django_q/models.py:160
#: models.py:150 models.py:234
msgid "Please install croniter to enable cron expressions"
msgstr "Bitte installieren Sie croniter, um Cron-Ausdrücke zu aktivieren"
#: models.py:170
msgid "e.g. 1, 2, 'John'"
msgstr "zum Beispiel 1, 2, 'John'"
#: django_q/models.py:162
#: models.py:172
msgid "e.g. x=1, y=2, name='John'"
msgstr "zum Beispiel x=1, y=2, name='John'"
#: django_q/models.py:176
#: models.py:186
msgid "Once"
msgstr "Einmal"
#: django_q/models.py:177
#: models.py:187
msgid "Minutes"
msgstr "Minuten"
#: django_q/models.py:178
#: models.py:188
msgid "Hourly"
msgstr "Stündlich"
#: django_q/models.py:179
#: models.py:189
msgid "Daily"
msgstr "Täglich"
#: django_q/models.py:180
#: models.py:190
msgid "Weekly"
msgstr "Wöchentlich"
#: django_q/models.py:181
#: models.py:191
msgid "Biweekly"
msgstr "Zweiwöchentlich"
#: django_q/models.py:182
#: models.py:192
msgid "Monthly"
msgstr "Monatlich"
#: django_q/models.py:183
#: models.py:193
msgid "Bimonthly"
msgstr "Zweimonatlich"
#: django_q/models.py:184
#: models.py:194
msgid "Quarterly"
msgstr "Vierteljährlich"
#: django_q/models.py:185
#: models.py:195
msgid "Yearly"
msgstr "Jährlich"
#: django_q/models.py:186
#: models.py:196
msgid "Cron"
msgstr "Cron"
#: django_q/models.py:189
#: models.py:199
msgid "Schedule Type"
msgstr "Zeitplan-Typ"
#: django_q/models.py:192
#: models.py:202
msgid "Number of minutes for the Minutes type"
msgstr "Anzahl Minuten für den Typ 'Minuten'"
#: django_q/models.py:195
#: models.py:205
msgid "Repeats"
msgstr "Wiederhohlungen"
#: django_q/models.py:195
#: models.py:205
msgid "n = n times, -1 = forever"
msgstr "n = n mal, -1 = für immer"
#: django_q/models.py:198
#: models.py:208
msgid "Next Run"
msgstr "Nächste Ausführung"
#: django_q/models.py:205
#: models.py:215
msgid "Cron expression"
msgstr "Cron-Ausdruck"
#: django_q/models.py:235
#: models.py:224
msgid "Name of kwarg to pass intended schedule date"
msgstr "Name des zu passierenden Kwargs vorgesehenes Datum"
#: models.py:299
msgid "Scheduled task"
msgstr "Geplante Aufgabe"
#: django_q/models.py:236
#: models.py:300
msgid "Scheduled tasks"
msgstr "Geplante Aufgaben"
#: django_q/models.py:262
#: models.py:326
msgid "Queued task"
msgstr "Eingereihte Aufgabe"
#: django_q/models.py:263
#: models.py:327
msgid "Queued tasks"
msgstr "Eingereihte Aufgaben"
#: django_q/monitor.py:62 django_q/monitor.py:339
#: monitor.py:64 monitor.py:348
msgid "Host"
msgstr "Host"
#: django_q/monitor.py:66 django_q/monitor.py:343 django_q/monitor.py:450
#: monitor.py:68 monitor.py:352 monitor.py:459
msgid "Id"
msgstr "Id"
#: django_q/monitor.py:70
#: monitor.py:72
msgid "State"
msgstr "Status"
#: django_q/monitor.py:74
#: monitor.py:76
msgid "Pool"
msgstr "Pool"
#: django_q/monitor.py:78
#: monitor.py:80
msgid "TQ"
msgstr "TQ"
#: django_q/monitor.py:82
#: monitor.py:84
msgid "RQ"
msgstr "RQ"
#: django_q/monitor.py:86
#: monitor.py:88
msgid "RC"
msgstr "RC"
#: django_q/monitor.py:90
#: monitor.py:92
msgid "Up"
msgstr "Up"
#: django_q/monitor.py:170 django_q/monitor.py:279
#: monitor.py:172 monitor.py:286
msgid "Queued"
msgstr "Eingereiht"
#: django_q/monitor.py:178
#: monitor.py:180
msgid "Success"
msgstr "Erfolg"
#: django_q/monitor.py:188 django_q/monitor.py:287
#: monitor.py:190 monitor.py:294
msgid "Failures"
msgstr "Fehlschläge"
#: django_q/monitor.py:199 django_q/monitor.py:485
#: monitor.py:201 monitor.py:498
msgid "[Press q to quit]"
msgstr "[Drücken Sie q zum Beenden]"
#: django_q/monitor.py:223
#: monitor.py:227
msgid "day"
msgstr "Tag"
#: django_q/monitor.py:244
#: monitor.py:248
msgid "second"
msgstr "Sekunde"
#: django_q/monitor.py:247
#: monitor.py:251
msgid "minute"
msgstr "Minute"
#: django_q/monitor.py:250
#: monitor.py:254
msgid "hour"
msgstr "Stunde"
#: django_q/monitor.py:260
#: monitor.py:263
#, python-format
msgid "-- %(prefix)s %(version)s on %(info)s --"
msgstr "-- %(prefix)s %(version)s auf %(info)s --"
#: django_q/monitor.py:266
#: monitor.py:273
msgid "Clusters"
msgstr "Cluster"
#: django_q/monitor.py:270
#: monitor.py:277
msgid "Workers"
msgstr "Arbeiter"
#: django_q/monitor.py:274
#: monitor.py:281
msgid "Restarts"
msgstr "Neustarts"
#: django_q/monitor.py:283
#: monitor.py:290
msgid "Successes"
msgstr "Erfolge"
#: django_q/monitor.py:292
#: monitor.py:299
msgid "Schedules"
msgstr "Zeitpläne"
#: django_q/monitor.py:296
#: monitor.py:303
#, python-format
msgid "Tasks/%(per)s"
msgstr "Aufgaben/%(per)s"
#: django_q/monitor.py:300
#: monitor.py:307
msgid "Avg time"
msgstr "Durchschnittl. Zeit"
#: django_q/monitor.py:348
#: monitor.py:357
msgid "Available (%)"
msgstr "Verfügbar (%)"
#: django_q/monitor.py:354
#: monitor.py:363
msgid "Available (MB)"
msgstr "Verfügbar (MB)"
#: django_q/monitor.py:359
#: monitor.py:368
msgid "Total (MB)"
msgstr "Insgesamt (MB)"
#: django_q/monitor.py:364
#: monitor.py:373
msgid "Sentinel (MB)"
msgstr "Sentinel (MB)"
#: django_q/monitor.py:370
#: monitor.py:379
msgid "Monitor (MB)"
msgstr "Monitor (MB)"
#: django_q/monitor.py:376
#: monitor.py:385
msgid "Workers (MB)"
msgstr "Arbeiter (MB)"
#: django_q/monitor.py:478
#: monitor.py:487
#, python-format
msgid "Available lowest (): %(memory_percent)s ((at)s)"
msgstr "Niedrigste verfügbar (): %(memory_percent)s ((at)s)"
#: django_q/monitor.py:496
#: monitor.py:509
msgid "No clusters appear to be running."
msgstr "Es scheinen keine Cluster zu laufen."
#: django_q/signals.py:22
#: signals.py:22
#, python-format
msgid "malformed return hook '%(hook)s' for [%(name)s]"
msgstr "Ungültiger Return-Hook '%(hook)s' für [%(name)s]"
#: django_q/signals.py:30
#: signals.py:30
#, python-format
msgid "return hook %(hook)s failed on [%(name)s] because %(error)s"
msgstr "Return-Hook %(hook)s für [%(name)s] ist gescheitert: %(error)s"
#, python-format
#~ msgid ""
#~ "Could not process '%(func_name)s'. Check the location of the function and "
#~ "the args/kwargs."
#~ msgstr ""
#~ "Konnte '%(func_name)s' nicht verarbeiten. Überprüfen Sie den Ort der "
#~ "Funktion und die args/kwargs."

View File

@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-11-12 01:47+0000\n"
"POT-Creation-Date: 2023-01-26 01:38+0000\n"
"PO-Revision-Date: 2018-08-05 18:28+0200\n"
"Last-Translator: Thierry BOULOGNE <contact@tng-concepts.com>\n"
"Language-Team: \n"
@@ -17,176 +17,175 @@ msgstr ""
"X-Generator: Poedit 2.1.1\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: django_q/admin.py:43
#: admin.py:43
msgid "Resubmit selected tasks to queue"
msgstr "Resoumettre les tâches sélectionnées à la file d'attente"
#: django_q/admin.py:98 django_q/models.py:228
#: admin.py:107 models.py:293
#, fuzzy
#| msgid "Success"
msgid "success"
msgstr "succès"
#: django_q/admin.py:109 django_q/models.py:230
#: admin.py:119 models.py:295
msgid "last_run"
msgstr ""
#: django_q/cluster.py:77
#: cluster.py:79
#, python-format
msgid "Q Cluster %(name)s starting."
msgstr "Démarrage de Q Cluster-%(name)s."
#: django_q/cluster.py:85
#: cluster.py:87
#, python-format
msgid "Q Cluster %(name)s stopping."
msgstr "Arrêt de Q Cluster-%(name)s."
#: django_q/cluster.py:88
#: cluster.py:90
#, python-format
msgid "Q Cluster %(name)s has stopped."
msgstr "Q Cluster-%(name)s a été arrêté."
#: django_q/cluster.py:96
#: cluster.py:97
#, python-format
msgid "%(name)s got signal %(signal)s"
msgstr "%(name)s à reçu le signal %(signal)s"
#: django_q/cluster.py:219
#: cluster.py:224
#, python-format
msgid "reincarnated monitor %(name)s after sudden death"
msgstr "moniteur réintégré %(name)s après un arrêt intempestif"
msgstr "surveillant %(name)s réincarné après un arrêt intempestif"
#: django_q/cluster.py:222
#: cluster.py:230
#, python-format
msgid "reincarnated pusher %(name)s after sudden death"
msgstr "pousseur réintégré %(name)s après un arrêt intempestif"
msgstr "répartiteur %(name)s réincarné après un arrêt intempestif"
#: django_q/cluster.py:229
#: cluster.py:250
#, python-format
msgid ""
"reincarnated worker %(name)s after timeout while processing task "
"%(task_name)s"
msgstr ""
"processus %(name)s réincarné, délai de traitement dépassé pour la tâche "
"%(task_name)s"
#: cluster.py:255
#, python-format
msgid "reincarnated worker %(name)s after timeout"
msgstr "processus réintégré %(name)s après un arrêt une attente trop longue"
msgstr "processus %(name)s réincarné, délai de traitement dépassé"
#: django_q/cluster.py:231
#: cluster.py:260
#, python-format
msgid "recycled worker %(name)s"
msgstr "processus recyclé %(name)s"
#: django_q/cluster.py:233
#: cluster.py:263
#, python-format
msgid "reincarnated worker %(name)s after death"
msgstr "processus réintégré %(name)s après arrêt"
#: django_q/cluster.py:256
#: cluster.py:287
#, python-format
msgid "%(name)s guarding cluster %(cluster_name)s"
msgstr "%(name)s surveillance du cluster à %(cluster_name)s"
#: django_q/cluster.py:261
#: cluster.py:296
#, python-format
msgid "Q Cluster %(cluster_name)s running."
msgstr "Démarrage de Q Cluster-%(cluster_name)s."
#: django_q/cluster.py:295
#: cluster.py:332
#, python-format
msgid "%(name)s stopping cluster processes"
msgstr "%(name)s arrêt des processus de cluster"
msgstr "%(name)s arrêt des processus du cluster"
#: django_q/cluster.py:320
#: cluster.py:357
#, python-format
msgid "%(name)s waiting for the monitor."
msgstr "%(name)s en attente du moniteur."
msgstr "%(name)s en attente du surveillant."
#: django_q/cluster.py:342
#: cluster.py:383
#, python-format
msgid "%(process_name)s pushing tasks at %(id)s"
msgstr "%(process_name)s tâche envoyé à %(id)s"
msgid "%(name)s pushing tasks at %(id)s"
msgstr "%(name)s répartit les tâches %(id)s"
#: django_q/cluster.py:363
#: cluster.py:407
#, python-format
msgid "queueing from %(list_key)s"
msgstr "mise en file d'attente de %(list_key)s"
#: django_q/cluster.py:366
#: cluster.py:411
#, python-format
msgid "%(name)s stopped pushing tasks"
msgstr "%(name)s a cessé de pousser les tâches"
msgstr "%(name)s a cessé de répartir les tâches"
#: django_q/cluster.py:378
#: cluster.py:426
#, python-format
msgid "%(name)s monitoring at %(id)s"
msgstr "%(name)s Surveillance de %(id)s"
msgstr "%(name)s surveille les résultats %(id)s"
#: django_q/cluster.py:395
#: cluster.py:445
#, python-format
msgid "Processed '%(info_name)s' (%(task_name)s)"
msgstr "traité '%(info_name)s' (%(task_name)s)"
#: django_q/cluster.py:398
#: cluster.py:451
#, python-format
msgid "Failed '%(info_name)s' (%(task_name)s) - %(task_result)s"
msgstr "Manqué '%(info_name)s' (%(task_name)s) - %(task_result)s"
#: django_q/cluster.py:399
#: cluster.py:458
#, python-format
msgid "%(name)s stopped monitoring results"
msgstr "%(name)s arrêt des résultats de surveillance"
msgstr "%(name)s a cessé de de surveiller les résultats"
#: django_q/cluster.py:413
#: cluster.py:474
#, python-format
msgid "%(proc_name)s ready for work at %(id)s"
msgstr "%(proc_name)s prêt pour le travail à %(id)s"
#: django_q/cluster.py:425
#, python-format
msgid "%(proc_name)s processing '%(func_name)s' (%(task_name)s)"
msgstr "%(proc_name)s en traitement '%(func_name)s' (%(task_name)s)"
#: cluster.py:494
#, fuzzy, python-format
msgid "%(proc_name)s processing %(task_name)s '%(func_name)s'"
msgstr "%(proc_name)s exécute %(task_name)s '%(func_name)s'"
#: django_q/cluster.py:440
#, python-format
msgid ""
"Could not process '%(func_name)s'. Check the location of the function and "
"the args/kwargs."
msgstr ""
"Impossible de traiter '%(func_name)s'. Vérifiez l'emplacement de la fonction "
"et les args/kwargs."
#: django_q/cluster.py:456
#: cluster.py:546
#, python-format
msgid "%(proc_name)s stopped doing work"
msgstr "%(proc_name)s arrêté de travailler"
msgstr "%(proc_name)s a cessé de travailler"
#: django_q/cluster.py:649 django_q/models.py:144
msgid "Please install croniter to enable cron expressions"
msgstr "Veuillez installer croniter pour activer les expressions croniques."
#: django_q/cluster.py:672
#: cluster.py:751
#, python-format
msgid "%(process_name)s failed to create a task from schedule [%(schedule)s]"
msgstr ""
"%(process_name)s Echec de la création d'une tâche à partir de Schedule "
"[%(schedule)s]"
#: django_q/cluster.py:678
#: cluster.py:762
#, python-format
msgid "%(process_name)s created a task from schedule [%(schedule)s]"
msgstr "%(process_name)s a créé une tâche à partir de Schedule [%(schedule)s]"
msgid ""
"%(process_name)s created task %(task_name)s from schedule [%(schedule)s]"
msgstr ""
"%(process_name)s a créé la tâche %(task_name)s à partir de Schedule "
"[%(schedule)s]"
#: django_q/cluster.py:718
#: cluster.py:808
msgid "Skipping cpu affinity because psutil was not found."
msgstr "Sauter l'affinité du processeur parce que psutil n'a pas été trouvé."
msgstr "L'affinité cpu ne sera pas définie car psutil n'a pas été trouvé."
#: django_q/cluster.py:723
#: cluster.py:813
msgid "Faking cpu affinity because it is not supported on this platform"
msgstr ""
"Simulation de l'affinité du processeur parce qu'elle n'est pas supportée sur "
"cette plateforme."
"Simulation de l'affinité cpu parce qu'elle n'est pas supportée sur cette "
"plateforme."
#: django_q/cluster.py:744
#: cluster.py:835
#, python-format
msgid "%(pid)s will use cpu %(affinity)s"
msgstr "%(pid)s utilisera le CPU %(affinity)s"
#: django_q/conf.py:85
#: conf.py:90
#, python-format
msgid ""
"SAVE_LIMIT_PER (%(option)s) is not a valid option. Options are: 'group', "
@@ -196,299 +195,315 @@ msgstr ""
"'group', 'name', 'func' et None. La valeur par défaut est None."
#. Translators: Cluster status descriptions
#: django_q/conf.py:194
#: conf.py:207
msgid "Starting"
msgstr "Démarrage"
#: django_q/conf.py:195
#: conf.py:208
msgid "Working"
msgstr "Actif"
#: django_q/conf.py:196
#: conf.py:209
msgid "Idle"
msgstr "En attente"
#: django_q/conf.py:197
#: conf.py:210
msgid "Stopped"
msgstr "Arrêté"
#: django_q/conf.py:198
#: conf.py:211
msgid "Stopping"
msgstr "En cours darrêt"
#. Translators: help text for qcluster management command
#: django_q/management/commands/qcluster.py:9
#: management/commands/qcluster.py:9
msgid "Starts a Django Q Cluster."
msgstr "Démarre un cluster Django Q."
#. Translators: help text for qinfo management command
#: django_q/management/commands/qinfo.py:11
#: management/commands/qinfo.py:11
msgid "General information over all clusters."
msgstr "Informations générales sur tous les clusters."
#. Translators: help text for qmemory management command
#: django_q/management/commands/qmemory.py:9
#: management/commands/qmemory.py:9
#, fuzzy
#| msgid "Monitors Q Cluster activity"
msgid "Monitors Q Cluster memory usage"
msgstr "Surveille l'utilisation de la mémoire du cluster Q"
msgstr "Surveille l'utilisation mémoire du Q cluster"
#. Translators: help text for qmonitor management command
#: django_q/management/commands/qmonitor.py:9
#: management/commands/qmonitor.py:9
msgid "Monitors Q Cluster activity"
msgstr "Activité du cluster Moniteur Q"
msgstr "Surveille l'activité de Q cluster"
#: django_q/models.py:119
#: models.py:125
msgid "Successful task"
msgstr "Tâche réussie"
#: django_q/models.py:120
#: models.py:126
msgid "Successful tasks"
msgstr "Tâches réussies"
#: django_q/models.py:135
#: models.py:141
msgid "Failed task"
msgstr "Tâche échoué"
#: django_q/models.py:136
#: models.py:142
msgid "Failed tasks"
msgstr "Tâches échouées"
#: django_q/models.py:160
#: models.py:150 models.py:234
msgid "Please install croniter to enable cron expressions"
msgstr "Veuillez installer croniter pour activer les expressions cron."
#: models.py:170
msgid "e.g. 1, 2, 'John'"
msgstr "ex. 1, 2, Jean"
#: django_q/models.py:162
#: models.py:172
msgid "e.g. x=1, y=2, name='John'"
msgstr "p. ex. x = 1, y = 2, Nom = Jean"
#: django_q/models.py:176
#: models.py:186
msgid "Once"
msgstr "Une fois"
#: django_q/models.py:177
#: models.py:187
msgid "Minutes"
msgstr "Minutes"
#: django_q/models.py:178
#: models.py:188
msgid "Hourly"
msgstr "Toutes les heures"
#: django_q/models.py:179
#: models.py:189
msgid "Daily"
msgstr "Quotidien"
#: django_q/models.py:180
#: models.py:190
msgid "Weekly"
msgstr "Hebdomadaire"
#: django_q/models.py:181
#: models.py:191
#, fuzzy
#| msgid "Weekly"
msgid "Biweekly"
msgstr "Bihebdomadaire"
#: django_q/models.py:182
#: models.py:192
msgid "Monthly"
msgstr "Mensuel"
#: django_q/models.py:183
#: models.py:193
#, fuzzy
#| msgid "Monthly"
msgid "Bimonthly"
msgstr "Bimestriel"
#: django_q/models.py:184
#: models.py:194
msgid "Quarterly"
msgstr "Tous les quart-dheure"
#: django_q/models.py:185
#: models.py:195
msgid "Yearly"
msgstr "Annuel"
#: django_q/models.py:186
#: models.py:196
msgid "Cron"
msgstr "Cron"
#: django_q/models.py:189
#: models.py:199
msgid "Schedule Type"
msgstr "Type de plannification"
#: django_q/models.py:192
#: models.py:202
msgid "Number of minutes for the Minutes type"
msgstr "Nombre de minutes pour le type de minutes"
#: django_q/models.py:195
#: models.py:205
msgid "Repeats"
msgstr "Répéter"
#: django_q/models.py:195
#: models.py:205
msgid "n = n times, -1 = forever"
msgstr "n = n fois,-1 = Toujours"
#: django_q/models.py:198
#: models.py:208
msgid "Next Run"
msgstr "Prochaine exécution"
#: django_q/models.py:205
#: models.py:215
msgid "Cron expression"
msgstr "Expression du Cron"
msgstr "Expression Cron"
#: django_q/models.py:235
#: models.py:224
msgid "Name of kwarg to pass intended schedule date"
msgstr "Nom du kwarg à passer Date prévue de l'horaire"
#: models.py:299
msgid "Scheduled task"
msgstr "Tâche planifiée"
#: django_q/models.py:236
#: models.py:300
msgid "Scheduled tasks"
msgstr "Tâches planifiées"
#: django_q/models.py:262
#: models.py:326
msgid "Queued task"
msgstr "Tâche en file d'attente"
#: django_q/models.py:263
#: models.py:327
msgid "Queued tasks"
msgstr "Tâches en file d'attente"
#: django_q/monitor.py:62 django_q/monitor.py:339
#: monitor.py:64 monitor.py:348
msgid "Host"
msgstr "Hôte"
#: django_q/monitor.py:66 django_q/monitor.py:343 django_q/monitor.py:450
#: monitor.py:68 monitor.py:352 monitor.py:459
msgid "Id"
msgstr "Id"
#: django_q/monitor.py:70
#: monitor.py:72
msgid "State"
msgstr "Statut"
#: django_q/monitor.py:74
#: monitor.py:76
msgid "Pool"
msgstr "Piscine"
#: django_q/monitor.py:78
#: monitor.py:80
msgid "TQ"
msgstr "TQ"
#: django_q/monitor.py:82
#: monitor.py:84
msgid "RQ"
msgstr "RQ"
#: django_q/monitor.py:86
#: monitor.py:88
msgid "RC"
msgstr "RC"
#: django_q/monitor.py:90
#: monitor.py:92
msgid "Up"
msgstr "Haut"
#: django_q/monitor.py:170 django_q/monitor.py:279
#: monitor.py:172 monitor.py:286
msgid "Queued"
msgstr "En file d'attente"
#: django_q/monitor.py:178
#: monitor.py:180
msgid "Success"
msgstr "Succès"
#: django_q/monitor.py:188 django_q/monitor.py:287
#: monitor.py:190 monitor.py:294
msgid "Failures"
msgstr "Défaillances"
#: django_q/monitor.py:199 django_q/monitor.py:485
#: monitor.py:201 monitor.py:498
msgid "[Press q to quit]"
msgstr "[appuyez sur q pour quitter]"
#: django_q/monitor.py:223
#: monitor.py:227
msgid "day"
msgstr "jour"
#: django_q/monitor.py:244
#: monitor.py:248
msgid "second"
msgstr "seconde"
#: django_q/monitor.py:247
#: monitor.py:251
msgid "minute"
msgstr "minute"
#: django_q/monitor.py:250
#: monitor.py:254
msgid "hour"
msgstr "heure"
#: django_q/monitor.py:260
#: monitor.py:263
#, python-format
msgid "-- %(prefix)s %(version)s on %(info)s --"
msgstr "--%(prefix)s %(version)s sur %(info)s --"
#: django_q/monitor.py:266
#: monitor.py:273
msgid "Clusters"
msgstr "Grappes"
#: django_q/monitor.py:270
#: monitor.py:277
msgid "Workers"
msgstr "Processus"
#: django_q/monitor.py:274
#: monitor.py:281
msgid "Restarts"
msgstr "Redémarrages"
#: django_q/monitor.py:283
#: monitor.py:290
msgid "Successes"
msgstr "Succès"
#: django_q/monitor.py:292
#: monitor.py:299
msgid "Schedules"
msgstr "Planifications"
#: django_q/monitor.py:296
#: monitor.py:303
#, python-format
msgid "Tasks/%(per)s"
msgstr "Tâches/%(per)s"
#: django_q/monitor.py:300
#: monitor.py:307
msgid "Avg time"
msgstr "Temps Moyen"
#: django_q/monitor.py:348
#: monitor.py:357
msgid "Available (%)"
msgstr ""
#: django_q/monitor.py:354
#: monitor.py:363
msgid "Available (MB)"
msgstr "Disponible sur (MB)"
#: django_q/monitor.py:359
#: monitor.py:368
msgid "Total (MB)"
msgstr "Total (MB)"
#: django_q/monitor.py:364
#: monitor.py:373
msgid "Sentinel (MB)"
msgstr "Sentinel (MB)"
#: django_q/monitor.py:370
#: monitor.py:379
msgid "Monitor (MB)"
msgstr "Monitor (MB)"
#: django_q/monitor.py:376
#: monitor.py:385
#, fuzzy
#| msgid "Workers"
msgid "Workers (MB)"
msgstr "Processus (MB)"
#: django_q/monitor.py:478
#: monitor.py:487
#, python-format
msgid "Available lowest (): %(memory_percent)s ((at)s)"
msgstr "Disponible le plus bas () : %(memory_percent)s ((at)s)"
#: django_q/monitor.py:496
#: monitor.py:509
msgid "No clusters appear to be running."
msgstr "Aucun cluster ne semble être en cours d'exécution."
#: django_q/signals.py:22
#: signals.py:22
#, python-format
msgid "malformed return hook '%(hook)s' for [%(name)s]"
msgstr "hook de retour mal formé' %(hook)s 'pour [%(name)s]"
msgstr "hook de retour '%(hook)s' mal formé pour [%(name)s]"
#: django_q/signals.py:30
#: signals.py:30
#, python-format
msgid "return hook %(hook)s failed on [%(name)s] because %(error)s"
msgstr "le crochet de retour %(hook)s a échoué sur [%(name)s] parce que %(error)s"
msgstr "hook de retour %(hook)s a échoué sur [%(name)s] à cause de %(error)s"
#, python-format
#~ msgid ""
#~ "Could not process '%(func_name)s'. Check the location of the function and "
#~ "the args/kwargs."
#~ msgstr ""
#~ "Impossible de traiter '%(func_name)s'. Vérifiez l'emplacement de la "
#~ "fonction et les args/kwargs."

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-11-12 01:47+0000\n"
"POT-Creation-Date: 2023-01-26 01:38+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Ethem Güner <ethemguener@gmail.com>\n"
"Language-Team: \n"
@@ -18,172 +18,171 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: django_q/admin.py:43
#: admin.py:43
msgid "Resubmit selected tasks to queue"
msgstr "Seçili işleri kuyruğa tekrar gönder"
#: django_q/admin.py:98 django_q/models.py:228
#: admin.py:107 models.py:293
#, fuzzy
#| msgid "Success"
msgid "success"
msgstr "başarılı olanlar"
#: django_q/admin.py:109 django_q/models.py:230
#: admin.py:119 models.py:295
msgid "last_run"
msgstr ""
#: django_q/cluster.py:77
#: cluster.py:79
#, python-format
msgid "Q Cluster %(name)s starting."
msgstr "Q Cluster %(name)s başlatılıyor."
#: django_q/cluster.py:85
#: cluster.py:87
#, python-format
msgid "Q Cluster %(name)s stopping."
msgstr "Q Cluster %(name)s durduruluyor."
#: django_q/cluster.py:88
#: cluster.py:90
#, python-format
msgid "Q Cluster %(name)s has stopped."
msgstr "Q Cluster %(name)s durduruldu."
#: django_q/cluster.py:96
#: cluster.py:97
#, python-format
msgid "%(name)s got signal %(signal)s"
msgstr "%(name)s, %(signal)s pid'inde izleniyor/monitoring yapılıyor."
#: django_q/cluster.py:219
#: cluster.py:224
#, python-format
msgid "reincarnated monitor %(name)s after sudden death"
msgstr "Monitor %(name)s ani ölüm sonrası tekrar dirildi"
#: django_q/cluster.py:222
#: cluster.py:230
#, python-format
msgid "reincarnated pusher %(name)s after sudden death"
msgstr "Pusher %(name)s ani ölüm sonrası tekrar dirildi"
#: django_q/cluster.py:229
#: cluster.py:250
#, fuzzy, python-format
#| msgid "reincarnated worker %(name)s after timeout"
msgid ""
"reincarnated worker %(name)s after timeout while processing task "
"%(task_name)s"
msgstr "Worker %(name)s zaman aşımı sonrası tekrar dirildi"
#: cluster.py:255
#, python-format
msgid "reincarnated worker %(name)s after timeout"
msgstr "Worker %(name)s zaman aşımı sonrası tekrar dirildi"
#: django_q/cluster.py:231
#: cluster.py:260
#, python-format
msgid "recycled worker %(name)s"
msgstr "Worker %(name)s geri döndürüldü"
#: django_q/cluster.py:233
#: cluster.py:263
#, python-format
msgid "reincarnated worker %(name)s after death"
msgstr "Worker %(name)s ani ölüm sonrası tekrar dirildi"
#: django_q/cluster.py:256
#: cluster.py:287
#, python-format
msgid "%(name)s guarding cluster %(cluster_name)s"
msgstr "%(name)s, %(cluster_name)s cluster'ını koruyor"
#: django_q/cluster.py:261
#: cluster.py:296
#, python-format
msgid "Q Cluster %(cluster_name)s running."
msgstr "Q Cluster %(cluster_name)s başlatılıyor."
#: django_q/cluster.py:295
#: cluster.py:332
#, python-format
msgid "%(name)s stopping cluster processes"
msgstr "Cluster %(name)s işlemleri durduruluyor."
#: django_q/cluster.py:320
#: cluster.py:357
#, python-format
msgid "%(name)s waiting for the monitor."
msgstr "%(name)s monitor için bekliyor."
#: django_q/cluster.py:342
#, python-format
msgid "%(process_name)s pushing tasks at %(id)s"
#: cluster.py:383
#, fuzzy, python-format
#| msgid "%(process_name)s pushing tasks at %(id)s"
msgid "%(name)s pushing tasks at %(id)s"
msgstr "%(process_name)s, işleri %(id)s pid'ine gönderiyor."
#: django_q/cluster.py:363
#: cluster.py:407
#, python-format
msgid "queueing from %(list_key)s"
msgstr ""
#: django_q/cluster.py:366
#: cluster.py:411
#, python-format
msgid "%(name)s stopped pushing tasks"
msgstr "%(name)s işleri göndermeyi durdurdu"
#: django_q/cluster.py:378
#: cluster.py:426
#, python-format
msgid "%(name)s monitoring at %(id)s"
msgstr "%(name)s, %(id)s pid'inde izleniyor/monitoring yapılıyor."
#: django_q/cluster.py:395
#: cluster.py:445
#, python-format
msgid "Processed '%(info_name)s' (%(task_name)s)"
msgstr "[%(task_name)s] - '%(info_name)s işlendi."
#: django_q/cluster.py:398
#: cluster.py:451
#, python-format
msgid "Failed '%(info_name)s' (%(task_name)s) - %(task_result)s"
msgstr "[%(task_name)s] - '%(info_name)s' - %(task_result)s başarısız oldu"
#: django_q/cluster.py:399
#: cluster.py:458
#, python-format
msgid "%(name)s stopped monitoring results"
msgstr "%(name)s sonuçları göstermeyi bıraktı"
#: django_q/cluster.py:413
#: cluster.py:474
#, python-format
msgid "%(proc_name)s ready for work at %(id)s"
msgstr "%(proc_name)s, %(id)s pid'inde çalışmaya hazır"
#: django_q/cluster.py:425
#, python-format
msgid "%(proc_name)s processing '%(func_name)s' (%(task_name)s)"
#: cluster.py:494
#, fuzzy, python-format
#| msgid "%(proc_name)s processing '%(func_name)s' (%(task_name)s)"
msgid "%(proc_name)s processing %(task_name)s '%(func_name)s'"
msgstr "%(proc_name)s, '%(func_name)s' [%(task_name)s] işlerini işiyor"
#: django_q/cluster.py:440
#, python-format
msgid ""
"Could not process '%(func_name)s'. Check the location of the function and "
"the args/kwargs."
msgstr ""
"%(func_name)s' işlenemedi. İşlevin konumunu ve args/kwargs öğelerini kontrol "
"edin."
#: django_q/cluster.py:456
#: cluster.py:546
#, python-format
msgid "%(proc_name)s stopped doing work"
msgstr "%(proc_name)s çalışmayı bıraktı"
#: django_q/cluster.py:649 django_q/models.py:144
msgid "Please install croniter to enable cron expressions"
msgstr "Cron expressions'ları açmak için croniter yükleyin"
#: django_q/cluster.py:672
#: cluster.py:751
#, python-format
msgid "%(process_name)s failed to create a task from schedule [%(schedule)s]"
msgstr "%(process_name)s programdan bir görev oluşturamadı [%(schedule)s]"
#: django_q/cluster.py:678
#, python-format
msgid "%(process_name)s created a task from schedule [%(schedule)s]"
#: cluster.py:762
#, fuzzy, python-format
#| msgid "%(process_name)s created a task from schedule [%(schedule)s]"
msgid ""
"%(process_name)s created task %(task_name)s from schedule [%(schedule)s]"
msgstr "%(process_name)s programdan bir görev oluşturamadı [%(schedule)s]"
#: django_q/cluster.py:718
#: cluster.py:808
msgid "Skipping cpu affinity because psutil was not found."
msgstr "Psutil bulunamadığı için cpu benzeşimi atlanıyor."
#: django_q/cluster.py:723
#: cluster.py:813
msgid "Faking cpu affinity because it is not supported on this platform"
msgstr "Bu platformda desteklenmediği için sahte cpu benzeşimi"
#: django_q/cluster.py:744
#: cluster.py:835
#, python-format
msgid "%(pid)s will use cpu %(affinity)s"
msgstr "%(pid)s cpu %(affinity)s kullanacaktır"
#: django_q/conf.py:85
#: conf.py:90
#, python-format
msgid ""
"SAVE_LIMIT_PER (%(option)s) is not a valid option. Options are: 'group', "
@@ -193,295 +192,311 @@ msgstr ""
"'group', 'name', 'func' ve None. Varsayılan değer None'dır."
#. Translators: Cluster status descriptions
#: django_q/conf.py:194
#: conf.py:207
msgid "Starting"
msgstr "Başlıyor"
#: django_q/conf.py:195
#: conf.py:208
msgid "Working"
msgstr "Çalışıyor"
#: django_q/conf.py:196
#: conf.py:209
msgid "Idle"
msgstr "Boşta"
#: django_q/conf.py:197
#: conf.py:210
msgid "Stopped"
msgstr "Durdu"
#: django_q/conf.py:198
#: conf.py:211
msgid "Stopping"
msgstr "Durduruluyor"
#. Translators: help text for qcluster management command
#: django_q/management/commands/qcluster.py:9
#: management/commands/qcluster.py:9
msgid "Starts a Django Q Cluster."
msgstr "Bir Django Q Cluster çalıştırır."
#. Translators: help text for qinfo management command
#: django_q/management/commands/qinfo.py:11
#: management/commands/qinfo.py:11
msgid "General information over all clusters."
msgstr "Tüm cluster'lar için genel bilgiler."
#. Translators: help text for qmemory management command
#: django_q/management/commands/qmemory.py:9
#: management/commands/qmemory.py:9
msgid "Monitors Q Cluster memory usage"
msgstr "Q Cluster'ın bellek kullanımını izler"
#. Translators: help text for qmonitor management command
#: django_q/management/commands/qmonitor.py:9
#: management/commands/qmonitor.py:9
msgid "Monitors Q Cluster activity"
msgstr "Q Cluster'ın aktivitelerini izler"
#: django_q/models.py:119
#: models.py:125
msgid "Successful task"
msgstr "Başarılı iş"
#: django_q/models.py:120
#: models.py:126
msgid "Successful tasks"
msgstr "Başarılı işler"
#: django_q/models.py:135
#: models.py:141
msgid "Failed task"
msgstr "Başarısız iş"
#: django_q/models.py:136
#: models.py:142
msgid "Failed tasks"
msgstr "Başarısız işler"
#: django_q/models.py:160
#: models.py:150 models.py:234
msgid "Please install croniter to enable cron expressions"
msgstr "Cron expressions'ları açmak için croniter yükleyin"
#: models.py:170
msgid "e.g. 1, 2, 'John'"
msgstr "Örneğin: 1, 2, 'Melih'"
#: django_q/models.py:162
#: models.py:172
msgid "e.g. x=1, y=2, name='John'"
msgstr "Örneğin: x=1, y=2, name='Melih'"
#: django_q/models.py:176
#: models.py:186
msgid "Once"
msgstr "Bir kere"
#: django_q/models.py:177
#: models.py:187
msgid "Minutes"
msgstr "Dakika"
#: django_q/models.py:178
#: models.py:188
msgid "Hourly"
msgstr "Saatlik"
#: django_q/models.py:179
#: models.py:189
msgid "Daily"
msgstr "Günlük"
#: django_q/models.py:180
#: models.py:190
msgid "Weekly"
msgstr "Haftalık"
#: django_q/models.py:181
#: models.py:191
#, fuzzy
#| msgid "Weekly"
msgid "Biweekly"
msgstr "İki haftada bir"
#: django_q/models.py:182
#: models.py:192
msgid "Monthly"
msgstr "Aylık"
#: django_q/models.py:183
#: models.py:193
#, fuzzy
#| msgid "Monthly"
msgid "Bimonthly"
msgstr "İki ayda bir"
#: django_q/models.py:184
#: models.py:194
msgid "Quarterly"
msgstr "Bir Çeyrek (3 Ay)"
#: django_q/models.py:185
#: models.py:195
msgid "Yearly"
msgstr "Yıllık"
#: django_q/models.py:186
#: models.py:196
msgid "Cron"
msgstr ""
#: django_q/models.py:189
#: models.py:199
msgid "Schedule Type"
msgstr "Zamanlama Tipi"
#: django_q/models.py:192
#: models.py:202
msgid "Number of minutes for the Minutes type"
msgstr "Dakika tipine göre dakika sayısı"
#: django_q/models.py:195
#: models.py:205
msgid "Repeats"
msgstr "Tekrar eder"
#: django_q/models.py:195
#: models.py:205
msgid "n = n times, -1 = forever"
msgstr "n = n kere, -1 = sonsuza kadar"
#: django_q/models.py:198
#: models.py:208
msgid "Next Run"
msgstr "Bir dahaki çalışma tarihi"
#: django_q/models.py:205
#: models.py:215
msgid "Cron expression"
msgstr ""
#: django_q/models.py:235
#: models.py:224
msgid "Name of kwarg to pass intended schedule date"
msgstr "Geçilecek kwarg'ın adı öngörülen program tarihi"
#: models.py:299
msgid "Scheduled task"
msgstr "Zamanlanmış iş"
#: django_q/models.py:236
#: models.py:300
msgid "Scheduled tasks"
msgstr "Zamanlanmış işler"
#: django_q/models.py:262
#: models.py:326
msgid "Queued task"
msgstr "Sıraya alınmış iş"
#: django_q/models.py:263
#: models.py:327
msgid "Queued tasks"
msgstr "Sıraya alınmış işler"
#: django_q/monitor.py:62 django_q/monitor.py:339
#: monitor.py:64 monitor.py:348
msgid "Host"
msgstr ""
#: django_q/monitor.py:66 django_q/monitor.py:343 django_q/monitor.py:450
#: monitor.py:68 monitor.py:352 monitor.py:459
msgid "Id"
msgstr ""
#: django_q/monitor.py:70
#: monitor.py:72
msgid "State"
msgstr "Durum"
#: django_q/monitor.py:74
#: monitor.py:76
msgid "Pool"
msgstr "Havuz"
#: django_q/monitor.py:78
#: monitor.py:80
msgid "TQ"
msgstr ""
#: django_q/monitor.py:82
#: monitor.py:84
msgid "RQ"
msgstr ""
#: django_q/monitor.py:86
#: monitor.py:88
msgid "RC"
msgstr ""
#: django_q/monitor.py:90
#: monitor.py:92
msgid "Up"
msgstr ""
#: django_q/monitor.py:170 django_q/monitor.py:279
#: monitor.py:172 monitor.py:286
msgid "Queued"
msgstr "Sıraya alınmış"
#: django_q/monitor.py:178
#: monitor.py:180
msgid "Success"
msgstr "Başarılı olanlar"
#: django_q/monitor.py:188 django_q/monitor.py:287
#: monitor.py:190 monitor.py:294
msgid "Failures"
msgstr "Başarısız olanlar"
#: django_q/monitor.py:199 django_q/monitor.py:485
#: monitor.py:201 monitor.py:498
msgid "[Press q to quit]"
msgstr "[Çıkmak için q'ya basın]"
#: django_q/monitor.py:223
#: monitor.py:227
msgid "day"
msgstr "gün"
#: django_q/monitor.py:244
#: monitor.py:248
msgid "second"
msgstr "saniye"
#: django_q/monitor.py:247
#: monitor.py:251
msgid "minute"
msgstr "dakika"
#: django_q/monitor.py:250
#: monitor.py:254
msgid "hour"
msgstr "saat"
#: django_q/monitor.py:260
#: monitor.py:263
#, python-format
msgid "-- %(prefix)s %(version)s on %(info)s --"
msgstr "-- %(prefix)s %(version)s üzerinde %(info)s --"
#: django_q/monitor.py:266
#: monitor.py:273
msgid "Clusters"
msgstr ""
#: django_q/monitor.py:270
#: monitor.py:277
msgid "Workers"
msgstr ""
#: django_q/monitor.py:274
#: monitor.py:281
msgid "Restarts"
msgstr "Yeniden çalıştırmalar"
#: django_q/monitor.py:283
#: monitor.py:290
msgid "Successes"
msgstr "Başarılı olanlar"
#: django_q/monitor.py:292
#: monitor.py:299
msgid "Schedules"
msgstr "Zamanlanmışlar"
#: django_q/monitor.py:296
#: monitor.py:303
#, python-format
msgid "Tasks/%(per)s"
msgstr "İş/%(per)s"
#: django_q/monitor.py:300
#: monitor.py:307
msgid "Avg time"
msgstr "Ortalama süre"
#: django_q/monitor.py:348
#: monitor.py:357
msgid "Available (%)"
msgstr "Müsait (%) "
#: django_q/monitor.py:354
#: monitor.py:363
msgid "Available (MB)"
msgstr "Müsait (MB)"
#: django_q/monitor.py:359
#: monitor.py:368
msgid "Total (MB)"
msgstr "Toplam (MB)"
#: django_q/monitor.py:364
#: monitor.py:373
msgid "Sentinel (MB)"
msgstr ""
#: django_q/monitor.py:370
#: monitor.py:379
msgid "Monitor (MB)"
msgstr "İzleme (MB)"
#: django_q/monitor.py:376
#: monitor.py:385
msgid "Workers (MB)"
msgstr ""
#: django_q/monitor.py:478
#: monitor.py:487
#, python-format
msgid "Available lowest (): %(memory_percent)s ((at)s)"
msgstr "Mevcut en düşük (): %(memory_percent)s ((at)s)"
#: django_q/monitor.py:496
#: monitor.py:509
msgid "No clusters appear to be running."
msgstr "Hiçbir küme çalışıyor görünmüyor."
#: django_q/signals.py:22
#: signals.py:22
#, python-format
msgid "malformed return hook '%(hook)s' for [%(name)s]"
msgstr ""
#: django_q/signals.py:30
#: signals.py:30
#, python-format
msgid "return hook %(hook)s failed on [%(name)s] because %(error)s"
msgstr ""
#, python-format
#~ msgid ""
#~ "Could not process '%(func_name)s'. Check the location of the function and "
#~ "the args/kwargs."
#~ msgstr ""
#~ "%(func_name)s' işlenemedi. İşlevin konumunu ve args/kwargs öğelerini "
#~ "kontrol edin."

View File

@@ -5,61 +5,142 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
]
dependencies = []
operations = [
migrations.CreateModel(
name='Schedule',
name="Schedule",
fields=[
('id', models.AutoField(verbose_name='ID', auto_created=True, serialize=False, primary_key=True)),
('func', models.CharField(max_length=256, help_text='e.g. module.tasks.function')),
('hook', models.CharField(null=True, blank=True, max_length=256, help_text='e.g. module.tasks.result_function')),
('args', models.CharField(null=True, blank=True, max_length=256, help_text="e.g. 1, 2, 'John'")),
('kwargs', models.CharField(null=True, blank=True, max_length=256, help_text="e.g. x=1, y=2, name='John'")),
('schedule_type', models.CharField(verbose_name='Schedule Type', choices=[('O', 'Once'), ('H', 'Hourly'), ('D', 'Daily'), ('W', 'Weekly'), ('M', 'Monthly'), ('Q', 'Quarterly'), ('Y', 'Yearly')], default='O', max_length=1)),
('repeats', models.SmallIntegerField(verbose_name='Repeats', default=-1, help_text='n = n times, -1 = forever')),
('next_run', models.DateTimeField(verbose_name='Next Run', default=django.utils.timezone.now, null=True)),
('task', models.CharField(editable=False, null=True, max_length=100)),
(
"id",
models.AutoField(
verbose_name="ID",
auto_created=True,
serialize=False,
primary_key=True,
),
),
(
"func",
models.CharField(
max_length=256, help_text="e.g. module.tasks.function"
),
),
(
"hook",
models.CharField(
null=True,
blank=True,
max_length=256,
help_text="e.g. module.tasks.result_function",
),
),
(
"args",
models.CharField(
null=True,
blank=True,
max_length=256,
help_text="e.g. 1, 2, 'John'",
),
),
(
"kwargs",
models.CharField(
null=True,
blank=True,
max_length=256,
help_text="e.g. x=1, y=2, name='John'",
),
),
(
"schedule_type",
models.CharField(
verbose_name="Schedule Type",
choices=[
("O", "Once"),
("H", "Hourly"),
("D", "Daily"),
("W", "Weekly"),
("M", "Monthly"),
("Q", "Quarterly"),
("Y", "Yearly"),
],
default="O",
max_length=1,
),
),
(
"repeats",
models.SmallIntegerField(
verbose_name="Repeats",
default=-1,
help_text="n = n times, -1 = forever",
),
),
(
"next_run",
models.DateTimeField(
verbose_name="Next Run",
default=django.utils.timezone.now,
null=True,
),
),
("task", models.CharField(editable=False, null=True, max_length=100)),
],
options={
'verbose_name': 'Scheduled task',
'ordering': ['next_run'],
"verbose_name": "Scheduled task",
"ordering": ["next_run"],
},
),
migrations.CreateModel(
name='Task',
name="Task",
fields=[
('id', models.AutoField(verbose_name='ID', auto_created=True, serialize=False, primary_key=True)),
('name', models.CharField(editable=False, max_length=100)),
('func', models.CharField(max_length=256)),
('hook', models.CharField(null=True, max_length=256)),
('args', picklefield.fields.PickledObjectField(editable=False, null=True)),
('kwargs', picklefield.fields.PickledObjectField(editable=False, null=True)),
('result', picklefield.fields.PickledObjectField(editable=False, null=True)),
('started', models.DateTimeField(editable=False)),
('stopped', models.DateTimeField(editable=False)),
('success', models.BooleanField(editable=False, default=True)),
(
"id",
models.AutoField(
verbose_name="ID",
auto_created=True,
serialize=False,
primary_key=True,
),
),
("name", models.CharField(editable=False, max_length=100)),
("func", models.CharField(max_length=256)),
("hook", models.CharField(null=True, max_length=256)),
(
"args",
picklefield.fields.PickledObjectField(editable=False, null=True),
),
(
"kwargs",
picklefield.fields.PickledObjectField(editable=False, null=True),
),
(
"result",
picklefield.fields.PickledObjectField(editable=False, null=True),
),
("started", models.DateTimeField(editable=False)),
("stopped", models.DateTimeField(editable=False)),
("success", models.BooleanField(editable=False, default=True)),
],
),
migrations.CreateModel(
name='Failure',
fields=[
],
name="Failure",
fields=[],
options={
'verbose_name': 'Failed task',
'proxy': True,
"verbose_name": "Failed task",
"proxy": True,
},
bases=('django_q.task',),
bases=("django_q.task",),
),
migrations.CreateModel(
name='Success',
fields=[
],
name="Success",
fields=[],
options={
'verbose_name': 'Successful task',
'proxy': True,
"verbose_name": "Successful task",
"proxy": True,
},
bases=('django_q.task',),
bases=("django_q.task",),
),
]

View File

@@ -4,18 +4,22 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('django_q', '0001_initial'),
("django_q", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name='schedule',
name='args',
field=models.TextField(help_text="e.g. 1, 2, 'John'", blank=True, null=True),
model_name="schedule",
name="args",
field=models.TextField(
help_text="e.g. 1, 2, 'John'", blank=True, null=True
),
),
migrations.AlterField(
model_name='schedule',
name='kwargs',
field=models.TextField(help_text="e.g. x=1, y=2, name='John'", blank=True, null=True),
model_name="schedule",
name="kwargs",
field=models.TextField(
help_text="e.g. x=1, y=2, name='John'", blank=True, null=True
),
),
]

View File

@@ -4,29 +4,41 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('django_q', '0002_auto_20150630_1624'),
("django_q", "0002_auto_20150630_1624"),
]
operations = [
migrations.AlterModelOptions(
name='failure',
options={'verbose_name_plural': 'Failed tasks', 'verbose_name': 'Failed task'},
name="failure",
options={
"verbose_name_plural": "Failed tasks",
"verbose_name": "Failed task",
},
),
migrations.AlterModelOptions(
name='schedule',
options={'verbose_name_plural': 'Scheduled tasks', 'ordering': ['next_run'], 'verbose_name': 'Scheduled task'},
name="schedule",
options={
"verbose_name_plural": "Scheduled tasks",
"ordering": ["next_run"],
"verbose_name": "Scheduled task",
},
),
migrations.AlterModelOptions(
name='success',
options={'verbose_name_plural': 'Successful tasks', 'verbose_name': 'Successful task'},
name="success",
options={
"verbose_name_plural": "Successful tasks",
"verbose_name": "Successful task",
},
),
migrations.RemoveField(
model_name='task',
name='id',
model_name="task",
name="id",
),
migrations.AddField(
model_name='task',
name='id',
field=models.CharField(max_length=32, primary_key=True, editable=False, serialize=False),
model_name="task",
name="id",
field=models.CharField(
max_length=32, primary_key=True, editable=False, serialize=False
),
),
]

View File

@@ -1,23 +1,31 @@
from django.db import migrations, models
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('django_q', '0003_auto_20150708_1326'),
("django_q", "0003_auto_20150708_1326"),
]
operations = [
migrations.AlterModelOptions(
name='failure',
options={'verbose_name_plural': 'Failed tasks', 'verbose_name': 'Failed task', 'ordering': ['-stopped']},
name="failure",
options={
"verbose_name_plural": "Failed tasks",
"verbose_name": "Failed task",
"ordering": ["-stopped"],
},
),
migrations.AlterModelOptions(
name='success',
options={'verbose_name_plural': 'Successful tasks', 'verbose_name': 'Successful task', 'ordering': ['-stopped']},
name="success",
options={
"verbose_name_plural": "Successful tasks",
"verbose_name": "Successful task",
"ordering": ["-stopped"],
},
),
migrations.AlterModelOptions(
name='task',
options={'ordering': ['-stopped']},
name="task",
options={"ordering": ["-stopped"]},
),
]

View File

@@ -4,18 +4,18 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('django_q', '0004_auto_20150710_1043'),
("django_q", "0004_auto_20150710_1043"),
]
operations = [
migrations.AddField(
model_name='schedule',
name='name',
model_name="schedule",
name="name",
field=models.CharField(max_length=100, null=True),
),
migrations.AddField(
model_name='task',
name='group',
model_name="task",
name="group",
field=models.CharField(max_length=100, null=True, editable=False),
),
]

View File

@@ -4,18 +4,36 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('django_q', '0005_auto_20150718_1506'),
("django_q", "0005_auto_20150718_1506"),
]
operations = [
migrations.AddField(
model_name='schedule',
name='minutes',
field=models.PositiveSmallIntegerField(help_text='Number of minutes for the Minutes type', blank=True, null=True),
model_name="schedule",
name="minutes",
field=models.PositiveSmallIntegerField(
help_text="Number of minutes for the Minutes type",
blank=True,
null=True,
),
),
migrations.AlterField(
model_name='schedule',
name='schedule_type',
field=models.CharField(max_length=1, choices=[('O', 'Once'), ('I', 'Minutes'), ('H', 'Hourly'), ('D', 'Daily'), ('W', 'Weekly'), ('M', 'Monthly'), ('Q', 'Quarterly'), ('Y', 'Yearly')], default='O', verbose_name='Schedule Type'),
model_name="schedule",
name="schedule_type",
field=models.CharField(
max_length=1,
choices=[
("O", "Once"),
("I", "Minutes"),
("H", "Hourly"),
("D", "Daily"),
("W", "Weekly"),
("M", "Monthly"),
("Q", "Quarterly"),
("Y", "Yearly"),
],
default="O",
verbose_name="Schedule Type",
),
),
]

View File

@@ -4,21 +4,29 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('django_q', '0006_auto_20150805_1817'),
("django_q", "0006_auto_20150805_1817"),
]
operations = [
migrations.CreateModel(
name='OrmQ',
name="OrmQ",
fields=[
('id', models.AutoField(primary_key=True, auto_created=True, verbose_name='ID', serialize=False)),
('key', models.CharField(max_length=100)),
('payload', models.TextField()),
('lock', models.DateTimeField(null=True)),
(
"id",
models.AutoField(
primary_key=True,
auto_created=True,
verbose_name="ID",
serialize=False,
),
),
("key", models.CharField(max_length=100)),
("payload", models.TextField()),
("lock", models.DateTimeField(null=True)),
],
options={
'verbose_name_plural': 'Queued tasks',
'verbose_name': 'Queued task',
"verbose_name_plural": "Queued tasks",
"verbose_name": "Queued task",
},
),
]

View File

@@ -4,13 +4,13 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('django_q', '0007_ormq'),
("django_q", "0007_ormq"),
]
operations = [
migrations.AlterField(
model_name='schedule',
name='name',
model_name="schedule",
name="name",
field=models.CharField(blank=True, max_length=100, null=True),
),
]

View File

@@ -4,13 +4,17 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('django_q', '0008_auto_20160224_1026'),
("django_q", "0008_auto_20160224_1026"),
]
operations = [
migrations.AlterField(
model_name='schedule',
name='repeats',
field=models.IntegerField(default=-1, help_text='n = n times, -1 = forever', verbose_name='Repeats'),
model_name="schedule",
name="repeats",
field=models.IntegerField(
default=-1,
help_text="n = n times, -1 = forever",
verbose_name="Repeats",
),
),
]

View File

@@ -5,23 +5,29 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('django_q', '0009_auto_20171009_0915'),
("django_q", "0009_auto_20171009_0915"),
]
operations = [
migrations.AlterField(
model_name='task',
name='args',
field=picklefield.fields.PickledObjectField(editable=False, null=True, protocol=-1),
model_name="task",
name="args",
field=picklefield.fields.PickledObjectField(
editable=False, null=True, protocol=-1
),
),
migrations.AlterField(
model_name='task',
name='kwargs',
field=picklefield.fields.PickledObjectField(editable=False, null=True, protocol=-1),
model_name="task",
name="kwargs",
field=picklefield.fields.PickledObjectField(
editable=False, null=True, protocol=-1
),
),
migrations.AlterField(
model_name='task',
name='result',
field=picklefield.fields.PickledObjectField(editable=False, null=True, protocol=-1),
model_name="task",
name="result",
field=picklefield.fields.PickledObjectField(
editable=False, null=True, protocol=-1
),
),
]

View File

@@ -6,18 +6,35 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('django_q', '0010_auto_20200610_0856'),
("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),
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'),
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",
),
),
]

View File

@@ -8,13 +8,19 @@ import django_q.models
class Migration(migrations.Migration):
dependencies = [
('django_q', '0011_auto_20200628_1055'),
("django_q", "0011_auto_20200628_1055"),
]
operations = [
migrations.AlterField(
model_name='schedule',
name='cron',
field=models.CharField(blank=True, help_text='Cron expression', max_length=100, null=True, validators=[django_q.models.validate_cron]),
model_name="schedule",
name="cron",
field=models.CharField(
blank=True,
help_text="Cron expression",
max_length=100,
null=True,
validators=[django_q.models.validate_cron],
),
),
]

View File

@@ -6,13 +6,13 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('django_q', '0012_auto_20200702_1608'),
("django_q", "0012_auto_20200702_1608"),
]
operations = [
migrations.AddField(
model_name='task',
name='attempt_count',
model_name="task",
name="attempt_count",
field=models.IntegerField(default=0),
),
]

View File

@@ -6,13 +6,13 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('django_q', '0013_task_attempt_count'),
("django_q", "0013_task_attempt_count"),
]
operations = [
migrations.AddField(
model_name='schedule',
name='cluster',
model_name="schedule",
name="cluster",
field=models.CharField(blank=True, default=None, max_length=100, null=True),
),
]

View File

@@ -6,13 +6,30 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('django_q', '0014_schedule_cluster'),
("django_q", "0014_schedule_cluster"),
]
operations = [
migrations.AlterField(
model_name='schedule',
name='schedule_type',
field=models.CharField(choices=[('O', 'Once'), ('I', 'Minutes'), ('H', 'Hourly'), ('D', 'Daily'), ('W', 'Weekly'), ('BW', 'Biweekly'), ('M', 'Monthly'), ('BM', 'Bimonthly'), ('Q', 'Quarterly'), ('Y', 'Yearly'), ('C', 'Cron')], default='O', max_length=2, verbose_name='Schedule Type'),
model_name="schedule",
name="schedule_type",
field=models.CharField(
choices=[
("O", "Once"),
("I", "Minutes"),
("H", "Hourly"),
("D", "Daily"),
("W", "Weekly"),
("BW", "Biweekly"),
("M", "Monthly"),
("BM", "Bimonthly"),
("Q", "Quarterly"),
("Y", "Yearly"),
("C", "Cron"),
],
default="O",
max_length=2,
verbose_name="Schedule Type",
),
),
]

View File

@@ -0,0 +1,25 @@
# Generated by Django 4.1.2 on 2023-01-15 22:34
from django.db import migrations, models
import django_q.models
class Migration(migrations.Migration):
dependencies = [
("django_q", "0015_alter_schedule_schedule_type"),
]
operations = [
migrations.AddField(
model_name="schedule",
name="intended_date_kwarg",
field=models.CharField(
blank=True,
help_text="Name of kwarg to pass intended schedule date",
max_length=100,
null=True,
validators=[django_q.models.validate_kwarg],
),
),
]

View File

@@ -1,3 +1,6 @@
from datetime import datetime, timedelta
from keyword import iskeyword
# Django
from django import get_version
from django.core.exceptions import ValidationError
@@ -5,6 +8,7 @@ from django.db import models
from django.template.defaultfilters import truncatechars
from django.urls import reverse
from django.utils import timezone
from django.utils.timezone import is_aware
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
@@ -13,8 +17,10 @@ from picklefield import PickledObjectField
from picklefield.fields import dbsafe_decode
# Local
from django_q.conf import croniter
from django_q.conf import croniter, Conf
from django_q.signing import SignedPackage
from django_q.utils import localtime, add_months, add_years
from .utils import get_func_repr
@@ -148,6 +154,10 @@ def validate_cron(value):
raise ValidationError(e)
def validate_kwarg(value):
return value.isidentifier() and not iskeyword(value)
class Schedule(models.Model):
name = models.CharField(max_length=100, null=True, blank=True)
func = models.CharField(max_length=256, help_text="e.g. module.tasks.function")
@@ -206,6 +216,61 @@ class Schedule(models.Model):
)
task = models.CharField(max_length=100, null=True, editable=False)
cluster = models.CharField(max_length=100, default=None, null=True, blank=True)
intended_date_kwarg = models.CharField(
max_length=100,
null=True,
blank=True,
validators=[validate_kwarg],
help_text=_("Name of kwarg to pass intended schedule date"),
)
def calculate_next_run(self, next_run=None):
# next run is always in UTC
next_run = next_run or self.next_run
if self.schedule_type == self.CRON:
if not croniter:
raise ImportError(
_("Please install croniter to enable cron expressions")
)
return croniter(self.cron, localtime()).get_next(datetime)
if self.schedule_type == self.MINUTES:
add = timedelta(minutes=(self.minutes or 1))
elif self.schedule_type == self.HOURLY:
add = timedelta(hours=1)
elif self.schedule_type == self.DAILY:
add = timedelta(days=1)
elif self.schedule_type == self.WEEKLY:
add = timedelta(weeks=1)
elif self.schedule_type == self.BIWEEKLY:
add = timedelta(weeks=2)
elif self.schedule_type == self.MONTHLY:
add = timedelta(days=(add_months(next_run, 1) - next_run).days)
elif self.schedule_type == self.BIMONTHLY:
add = timedelta(days=(add_months(next_run, 2) - next_run).days)
elif self.schedule_type == self.QUARTERLY:
add = timedelta(days=(add_months(next_run, 3) - next_run).days)
elif self.schedule_type == self.YEARLY:
add = timedelta(days=(add_years(next_run, 1) - next_run).days)
# add normal timedelta, we will correct this later based on timezone
next_run += add
# DST differencers don't matter with minutes, hourly or yearly, so skip those
if self.schedule_type not in [self.MINUTES, self.HOURLY, self.YEARLY]:
# Get localtimes and then remove the tzinfo, so we can get the actual difference
current_next_run = localtime(next_run - add).replace(tzinfo=None)
new_next_run = localtime(next_run).replace(tzinfo=None)
# get the difference between them, this should be (-)1 or (-)0.5 hour
# based on DST active or not
extra_diff = (new_next_run - current_next_run) - add
# subtract difference
next_run -= extra_diff
return next_run
def success(self):
if self.task and Task.objects.filter(id=self.task):
@@ -229,7 +294,6 @@ class Schedule(models.Model):
last_run.allow_tags = True
last_run.short_description = _("last_run")
class Meta:
app_label = "django_q"
verbose_name = _("Scheduled task")

View File

@@ -19,28 +19,30 @@ try:
except ImportError:
psutil = None
# optional
try:
from blessed import Terminal
except ImportError:
pass
def get_process_mb(pid):
try:
process = psutil.Process(pid)
mb_used = round(process.memory_info().rss / 1024 ** 2, 2)
mb_used = round(process.memory_info().rss / 1024**2, 2)
except psutil.NoSuchProcess:
mb_used = "NO_PROCESS_FOUND"
return mb_used
BLESSED_INSTALL_MESSAGE = "Blessed is not installed. Please install blessed to use this: https://pypi.org/project/blessed/"
BLESSED_INSTALL_MESSAGE = (
"Blessed is not installed. Please install blessed to use this: "
"https://pypi.org/project/blessed/"
)
def monitor(run_once=False, broker=None):
if not broker:
broker = get_broker()
try:
from blessed import Terminal
term = Terminal()
except:
except ImportError:
print(BLESSED_INSTALL_MESSAGE)
return
@@ -204,8 +206,10 @@ def info(broker=None):
if not broker:
broker = get_broker()
try:
from blessed import Terminal
term = Terminal()
except:
except ImportError:
print(BLESSED_INSTALL_MESSAGE)
return
@@ -256,9 +260,12 @@ def info(broker=None):
print(
term.black_on_green(
term.center(
_(
'-- %(prefix)s %(version)s on %(info)s --'
) % {'prefix': Conf.PREFIX.capitalize(), 'version': ".".join(str(v) for v in VERSION), 'info': broker.info()}
_("-- %(prefix)s %(version)s on %(info)s --")
% {
"prefix": Conf.PREFIX.capitalize(),
"version": ".".join(str(v) for v in VERSION),
"info": broker.info(),
}
)
)
)
@@ -293,7 +300,7 @@ def info(broker=None):
+ term.move_x(1 * col_width)
+ term.white(str(models.Schedule.objects.count()))
+ term.move_x(2 * col_width)
+ term.cyan(_("Tasks/%(per)s") % {'per': per})
+ term.cyan(_("Tasks/%(per)s") % {"per": per})
+ term.move_x(3 * col_width)
+ term.white(f"{tasks_per:.2f}")
+ term.move_x(4 * col_width)
@@ -308,8 +315,10 @@ def memory(run_once=False, workers=False, broker=None):
if not broker:
broker = get_broker()
try:
from blessed import Terminal
term = Terminal()
except:
except ImportError:
print(BLESSED_INSTALL_MESSAGE)
return
broker.ping()
@@ -389,7 +398,7 @@ def memory(run_once=False, workers=False, broker=None):
)
# memory available (MB)
memory_available = round(
psutil.virtual_memory().available / 1024 ** 2, 2
psutil.virtual_memory().available / 1024**2, 2
)
if memory_available_percentage < MEMORY_AVAILABLE_LOWEST_PERCENTAGE:
MEMORY_AVAILABLE_LOWEST_PERCENTAGE = memory_available_percentage
@@ -413,7 +422,7 @@ def memory(run_once=False, workers=False, broker=None):
print(
term.move(row, 4 * col_width)
+ term.center(
round(psutil.virtual_memory().total / 1024 ** 2, 2),
round(psutil.virtual_memory().total / 1024**2, 2),
width=col_width - 1,
)
)
@@ -475,9 +484,13 @@ def memory(run_once=False, workers=False, broker=None):
row += 1
print(
term.move(row, 0)
+ _("Available lowest (): %(memory_percent)s ((at)s)") % { 'memory_percent': str(MEMORY_AVAILABLE_LOWEST_PERCENTAGE), 'at': MEMORY_AVAILABLE_LOWEST_PERCENTAGE_AT.strftime(
"%Y-%m-%d %H:%M:%S+00:00"
)}
+ _("Available lowest (): %(memory_percent)s ((at)s)")
% {
"memory_percent": str(MEMORY_AVAILABLE_LOWEST_PERCENTAGE),
"at": MEMORY_AVAILABLE_LOWEST_PERCENTAGE_AT.strftime(
"%Y-%m-%d %H:%M:%S+00:00"
),
}
)
# for testing
if run_once:

View File

@@ -1,5 +1,6 @@
"""
The code is derived from https://github.com/althonos/pronto/commit/3384010dfb4fc7c66a219f59276adef3288a886b
The code is derived from
https://github.com/althonos/pronto/commit/3384010dfb4fc7c66a219f59276adef3288a886b
"""
import multiprocessing
import multiprocessing.queues

View File

@@ -19,16 +19,16 @@ def call_hook(sender, instance, **kwargs):
f = getattr(m, func)
except (ValueError, ImportError, AttributeError):
logger.error(
_("malformed return hook '%(hook)s' for [%(name)s]") % {'hook': instance.hook, 'name': instance.name}
_("malformed return hook '%(hook)s' for [%(name)s]")
% {"hook": instance.hook, "name": instance.name}
)
return
try:
f(instance)
except Exception as e:
logger.error(
_(
"return hook %(hook)s failed on [%(name)s] because %(error)s"
) % {'hook': instance.hook, 'name': instance.name, 'error': str(e)}
_("return hook %(hook)s failed on [%(name)s] because %(error)s")
% {"hook": instance.hook, "name": instance.name, "error": str(e)}
)

View File

@@ -90,6 +90,7 @@ def schedule(func, *args, **kwargs):
:type next_run: datetime.datetime
:param cluster: optional cluster name.
:param cron: optional cron expression
:param intended_date_kwarg: optional identifier to pass intended schedule date.
:param kwargs: function keyword arguments.
:return: the schedule object.
:rtype: Schedule
@@ -102,6 +103,7 @@ def schedule(func, *args, **kwargs):
next_run = kwargs.pop("next_run", timezone.now())
cron = kwargs.pop("cron", None)
cluster = kwargs.pop("cluster", None)
intended_date_kwarg = kwargs.pop("intended_date_kwarg", None)
# check for name duplicates instead of am unique constraint
if name and Schedule.objects.filter(name=name).exists():
@@ -120,6 +122,7 @@ def schedule(func, *args, **kwargs):
next_run=next_run,
cron=cron,
cluster=cluster,
intended_date_kwarg=intended_date_kwarg,
)
# make sure we trigger validation
s.full_clean()
@@ -600,7 +603,8 @@ class Chain:
def result(self, wait=0):
"""
return the full list of results from the chain when it finishes. blocks until timeout.
return the full list of results from the chain when it finishes. blocks until
timeout.
:param int wait: how many milliseconds to wait for a result
:return: an unsorted list of results
"""
@@ -611,7 +615,8 @@ class Chain:
def fetch(self, failures=True, wait=0):
"""
get the task result objects from the chain when it finishes. blocks until timeout.
get the task result objects from the chain when it finishes. blocks until
timeout.
:param failures: include failed tasks
:param int wait: how many milliseconds to wait for a result
:return: an unsorted list of task objects

View File

@@ -1,7 +1,5 @@
import os
import django
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -77,7 +75,7 @@ DATABASES = {
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
TIME_ZONE = "Europe/Amsterdam"
USE_I18N = True
@@ -130,5 +128,5 @@ Q_CLUSTER = {
"testing": True,
"log_level": "DEBUG",
"django_redis": "default",
"redis": f"redis://{REDIS_HOST}:6379/0"
"redis": f"redis://{REDIS_HOST}:6379/0",
}

View File

@@ -2,12 +2,11 @@ import os
from time import sleep
import pytest
import redis
from django_q.brokers import Broker, get_broker
from django_q.conf import Conf
from django_q.humanhash import uuid
from django_q.tests.settings import REDIS_HOST, MONGO_HOST
from django_q.tests.settings import MONGO_HOST, REDIS_HOST
def test_broker(monkeypatch):

View File

@@ -1,8 +1,8 @@
from datetime import datetime
import os
import sys
import threading
import uuid as uuidlib
from datetime import datetime
from math import copysign
from multiprocessing import Event, Value
from time import sleep
@@ -11,9 +11,6 @@ from typing import Optional
import pytest
from django.utils import timezone
myPath = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, myPath + "/../")
from django_q.brokers import Broker, get_broker
from django_q.cluster import Cluster, Sentinel, monitor, pusher, save_task, worker
from django_q.conf import Conf
@@ -32,9 +29,12 @@ from django_q.tasks import (
result,
result_group,
)
from django_q.tests.tasks import TaskError, multiply
from django_q.tests.tasks import multiply, TaskError
from django_q.utils import add_months, add_years
myPath = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, myPath + "/../")
class WordClass:
def __init__(self):
@@ -64,7 +64,7 @@ def test_sync(broker):
@pytest.mark.django_db
def test_sync_raise_exception(broker):
with pytest.raises(Exception):
with pytest.raises(TaskError):
async_task("django_q.tests.tasks.raise_exception", broker=broker, sync=True)
@@ -409,6 +409,7 @@ def test_recycle(broker, monkeypatch):
assert Success.objects.count() == Conf.SAVE_LIMIT
broker.delete_queue()
@pytest.mark.django_db
def test_save_limit_per_func(broker, monkeypatch):
# set up the Sentinel
@@ -442,15 +443,14 @@ def test_save_limit_per_func(broker, monkeypatch):
# run monitor
monitor(result_queue)
assert Success.objects.count() == 3
assert set(Success.objects.filter().values_list('func', flat=True)) == {
'django_q.tests.tasks.countdown',
'django_q.tests.tasks.hello',
'django_q.tests.tasks.multiply',
assert set(Success.objects.filter().values_list("func", flat=True)) == {
"django_q.tests.tasks.countdown",
"django_q.tests.tasks.hello",
"django_q.tests.tasks.multiply",
}
broker.delete_queue()
@pytest.mark.django_db
def test_max_rss(broker, monkeypatch):
# set up the Sentinel
@@ -538,7 +538,6 @@ def test_attempt_count(broker, monkeypatch):
assert saved_task.attempt_count == 1
sleep(0.5)
# second save
old_stopped = task["stopped"]
task["stopped"] = timezone.now()
save_task(task, broker)
saved_task = Task.objects.get(id=task["id"])
@@ -770,6 +769,7 @@ def test_add_months():
assert new_date.month == 2
assert new_date.day == 29
@pytest.mark.django_db
def test_add_years():
# add some months

View File

@@ -1,9 +1,10 @@
import os
from datetime import timedelta
from datetime import datetime, timedelta
from multiprocessing import Event, Value
from unittest import mock
import pytest
import django
from django.core.exceptions import ValidationError
from django.db import IntegrityError
from django.test import override_settings
@@ -11,7 +12,7 @@ from django.utils import timezone
from django.utils.timezone import is_naive
from django_q.brokers import Broker, get_broker
from django_q.cluster import monitor, pusher, scheduler, worker, localtime
from django_q.cluster import localtime, monitor, pusher, scheduler, worker
from django_q.conf import Conf
from django_q.queues import Queue
from django_q.tasks import Schedule, fetch
@@ -21,12 +22,24 @@ from django_q.tests.testing_utilities.multiple_database_routers import (
TestingMultipleAppsDatabaseRouter,
TestingReplicaDatabaseRouter,
)
from django_q.utils import add_months, add_years
from django_q.utils import add_months
if django.VERSION < (4, 0):
# pytz is the default in django 3.2. Remove when no support for 3.2
from pytz import timezone as ZoneInfo
else:
try:
from zoneinfo import ZoneInfo
except ImportError:
from backports.zoneinfo import ZoneInfo
@pytest.fixture
def broker(monkeypatch) -> Broker:
"""Patches the Conf object setting the DJANGO_REDIS attribute allowing a default redis configuration."""
"""
Patches the Conf object setting the DJANGO_REDIS attribute allowing a default
redis configuration.
"""
monkeypatch.setattr(Conf, "DJANGO_REDIS", "default")
return get_broker()
@@ -37,25 +50,11 @@ def orm_broker(monkeypatch) -> None:
monkeypatch.setattr(Conf, "ORM", "default")
@pytest.fixture
def orm_no_replica_broker(orm_broker, monkeypatch) -> Broker:
"""Generates a Broker with a disabled read replica database configuration."""
monkeypatch.setattr(Conf, "HAS_REPLICA", False)
return get_broker(list_key="scheduler_test:q")
@pytest.fixture
def orm_replica_broker(orm_broker, monkeypatch) -> Broker:
"""Generates a Broker with read replica database configuration."""
monkeypatch.setattr(Conf, "HAS_REPLICA", True)
return get_broker(list_key="scheduler_test:q")
REPLICA_DATABASE_ROUTERS = [
f"{TestingReplicaDatabaseRouter.__module__}.{TestingReplicaDatabaseRouter.__name__}"
]
REPLICA_DATABASES = {
"default": {
"writable": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
},
@@ -66,7 +65,7 @@ REPLICA_DATABASES = {
}
MULTIPLE_APPS_DATABASE_ROUTERS = [
f"{TestingMultipleAppsDatabaseRouter.__module__}.{TestingMultipleAppsDatabaseRouter.__name__}"
f"{TestingMultipleAppsDatabaseRouter.__module__}.{TestingMultipleAppsDatabaseRouter.__name__}" # noqa: E501
]
MULTIPLE_APPS_DATABASES = {
"default": {
@@ -80,6 +79,109 @@ MULTIPLE_APPS_DATABASES = {
}
@pytest.mark.django_db
def test_scheduler_daylight_saving_time_daily(broker, monkeypatch):
# Set up a startdate in the Amsterdam timezone (without dst 1 hour ahead). The
# 28th of March 2021 is the day when sunlight saving starts (at 2 am)
monkeypatch.setattr(Conf, "TIME_ZONE", "Europe/Amsterdam")
tz = ZoneInfo('Europe/Amsterdam')
broker.list_key = "scheduler_test:q"
# Let's start a schedule at 1 am on the 27th of March. This is in AMS timezone.
# So, 2021-03-27 00:00:00 when saved (due to TZ being Amsterdam and saved in UTC)
start_date = datetime(2021, 3, 27, 1, 0, 0)
# Create schedule with the next run date on the start date. It will move one day
# forward when we run the scheduler
schedule = create_schedule(
"math.copysign",
1,
-1,
name="test math",
schedule_type=Schedule.DAILY,
next_run=start_date,
)
# Run scheduler so we get the next run date
scheduler(broker=broker)
schedule.refresh_from_db()
# It's now the day after exactly at midnight UTC
next_run = schedule.next_run
assert str(next_run) == "2021-03-28 00:00:00+00:00"
# In the Amsterdam timezone, it's 1 hour over midnight (+01)
next_run = next_run.astimezone(tz)
assert str(next_run) == "2021-03-28 01:00:00+01:00"
# Run scheduler so we get the next run date
scheduler(broker=broker)
schedule.refresh_from_db()
next_run = schedule.next_run
assert str(next_run) == "2021-03-28 23:00:00+00:00"
next_run = next_run.astimezone(tz)
# In the Amsterdam timezone, it's 1 hour over midnight (+02)
assert str(next_run) == "2021-03-29 01:00:00+02:00"
# Run scheduler so we get the next run date
scheduler(broker=broker)
schedule.refresh_from_db()
next_run = schedule.next_run
assert str(next_run) == "2021-03-29 23:00:00+00:00"
next_run = next_run.astimezone(tz)
assert str(next_run) == "2021-03-30 01:00:00+02:00"
# Create second schedule with the next run date on the start date. It will move
# one day forward when we run the scheduler
start_date = datetime(2021, 10, 29, 1, 0, 0)
schedule = create_schedule(
"django_q.tests.tasks.word_multiply",
2,
name="multiply",
schedule_type=Schedule.DAILY,
next_run=start_date,
)
# Run scheduler so we get the next run date
scheduler(broker=broker)
schedule.refresh_from_db()
next_run = schedule.next_run
assert str(next_run) == "2021-10-29 23:00:00+00:00"
# In the Amsterdam timezone, it's 1 hour over midnight (+02)
next_run = next_run.astimezone(tz)
assert str(next_run) == "2021-10-30 01:00:00+02:00"
# Run scheduler so we get the next run date
scheduler(broker=broker)
schedule.refresh_from_db()
next_run = schedule.next_run
assert str(next_run) == "2021-10-30 23:00:00+00:00"
# In the Amsterdam timezone, it's 1 hour over midnight (+02)
next_run = next_run.astimezone(tz)
assert str(next_run) == "2021-10-31 01:00:00+02:00"
# Run scheduler so we get the next run date
scheduler(broker=broker)
schedule.refresh_from_db()
next_run = schedule.next_run
assert str(next_run) == "2021-11-01 00:00:00+00:00"
# In the Amsterdam timezone, it's 1 hour over midnight (+01)
# Switch of DST
next_run = next_run.astimezone(tz)
assert str(next_run) == "2021-11-01 01:00:00+01:00"
@pytest.mark.django_db
def test_scheduler(broker, monkeypatch):
broker.list_key = "scheduler_test:q"
@@ -234,7 +336,7 @@ def test_scheduler(broker, monkeypatch):
"django_q.tests.tasks.word_multiply",
2,
word="catch_up",
schedule_type=Schedule.BIMONTHLY
schedule_type=Schedule.BIMONTHLY,
)
scheduler(broker=broker)
schedule = Schedule.objects.get(pk=schedule.pk)
@@ -245,7 +347,7 @@ def test_scheduler(broker, monkeypatch):
"django_q.tests.tasks.word_multiply",
2,
word="catch_up",
schedule_type=Schedule.BIWEEKLY
schedule_type=Schedule.BIWEEKLY,
)
scheduler(broker=broker)
schedule = Schedule.objects.get(pk=schedule.pk)
@@ -301,61 +403,72 @@ def test_scheduler(broker, monkeypatch):
assert task_queue.qsize() == 1
@override_settings(
DATABASE_ROUTERS=REPLICA_DATABASE_ROUTERS, DATABASES=REPLICA_DATABASES
)
@pytest.mark.django_db
def test_scheduler_atomic_transaction_must_specify_a_database_when_no_replicas_are_used(
orm_no_replica_broker: Broker,
):
"""
GIVEN a environment without a read replica database
WHEN the scheduler is called
THEN the transaction atomic must be called using the configured database in the Conf.ORM settings.
"""
broker = orm_no_replica_broker
with mock.patch("django_q.cluster.db") as mocked_db:
scheduler(broker=broker)
# The router should correctly set the database to use!
mocked_db.transaction.atomic.assert_called_with(using=broker.connection.db)
def test_intended_schedule_kwarg(broker, monkeypatch):
broker.list_key = "scheduler_test:q"
broker.delete_queue()
run_date = timezone.now()-timedelta(hours=1)
schedule = create_schedule(
"math.copysign",
1,
-1,
name="test math",
hook="django_q.tests.tasks.result",
schedule_type=Schedule.HOURLY,
repeats=1,
next_run=run_date,
intended_date_kwarg='intended_date',
)
assert schedule.last_run() is None
assert schedule.intended_date_kwarg == 'intended_date'
# run scheduler
scheduler(broker=broker)
# set up the workflow
task_queue = Queue()
stop_event = Event()
stop_event.set()
# push it
pusher(task_queue, stop_event, broker=broker)
assert task_queue.qsize() == 1
task = task_queue.get()
assert 'intended_date' in task['kwargs']
assert task['kwargs']['intended_date'] == run_date.isoformat()
@override_settings(
DATABASE_ROUTERS=REPLICA_DATABASE_ROUTERS, DATABASES=REPLICA_DATABASES
)
@pytest.mark.django_db
def test_scheduler_atomic_transaction_must_specify_no_database_when_read_write_replicas_are_used(
orm_replica_broker: Broker,
def test_scheduler_atomic_must_specify_the_write_db(
orm_broker: Broker,
):
"""
GIVEN a environment with a read/write configured replica database
WHEN the scheduler is called
THEN the transaction must be called without a specific database, thus letting the database router pick.
THEN the transaction must be called with the write database.
"""
with mock.patch("django_q.cluster.db") as mocked_db:
scheduler(broker=orm_replica_broker)
# No specific databases should be set here, this is the job of the router!
mocked_db.transaction.atomic.assert_called_with()
broker = get_broker(list_key="scheduler_test:q")
with mock.patch("django_q.cluster.db.transaction") as mocked_db:
scheduler(broker=broker)
mocked_db.atomic.assert_called_with(using="writable")
@override_settings(
DATABASE_ROUTERS=MULTIPLE_APPS_DATABASE_ROUTERS, DATABASES=MULTIPLE_APPS_DATABASES
)
@pytest.mark.django_db
def test_scheduler_atomic_transaction_must_specify_the_database_based_on_router_redirection(
orm_no_replica_broker: Broker,
def test_scheduler_atomic_must_specify_the_database_based_on_router_redirection(
orm_broker: Broker,
):
"""
GIVEN a environment without a read replica database
WHEN the scheduler is called
THEN the transaction atomic must be called using the configured database in the Conf.ORM settings.
THEN the transaction atomic must be called using the default connection.
"""
broker = orm_no_replica_broker
with mock.patch("django_q.cluster.db") as mocked_db:
broker = get_broker(list_key="scheduler_test:q")
with mock.patch("django_q.cluster.db.transaction") as mocked_db:
scheduler(broker=broker)
# The router should correctly set the database to use!
assert broker.connection.db == "default"
mocked_db.transaction.atomic.assert_called_with(using=broker.connection.db)
mocked_db.atomic.assert_called_with(using="default")
def test_localtime():

View File

@@ -12,9 +12,9 @@ class TestingReplicaDatabaseRouter:
def db_for_write(self, model, **hints):
"""
Always write to DEFAULT database
Always write to WRITABLE database
"""
return "default"
return "writable"
class TestingMultipleAppsDatabaseRouter:

View File

@@ -1,6 +1,23 @@
from datetime import datetime
import calendar
import inspect
from datetime import date
import calendar
import django
from django.utils import timezone
from django.conf import settings
from django_q.conf import Conf
if django.VERSION < (4, 0):
# pytz is the default in django 3.2. Remove when no support for 3.2
from pytz import timezone as ZoneInfo
else:
try:
from zoneinfo import ZoneInfo
except ImportError:
from backports.zoneinfo import ZoneInfo
# credits: https://stackoverflow.com/a/4131114
# Made them aware of timezone
@@ -8,21 +25,21 @@ def add_months(d, months):
month = d.month - 1 + months
year = d.year + month // 12
month = month % 12 + 1
day = min(d.day, calendar.monthrange(year,month)[1])
day = min(d.day, calendar.monthrange(year, month)[1])
return d.replace(year=year, month=month, day=day)
# credits: https://stackoverflow.com/a/15743908
# Changed the last line to make it a little easier to read and changed it to move February 29 to 28 next year
# Also made them aware of timezone
# Changed the last line to make it a little easier to read and changed it to move
# February 29 to 28 next year.
def add_years(d, years):
"""Return a date that's `years` years after the date (or datetime)
object `d`. Return the same calendar date (month and day) in the
destination year, if it exists, otherwise use the previous day
(thus changing February 29 to February 28).
"""
try:
return d.replace(year = d.year + years)
return d.replace(year=d.year + years)
except ValueError:
new_date = d + (date(d.year + years, 3, 1) - date(d.year, 3, 1))
return d.replace(year=new_date.year, month=new_date.month, day=new_date.day)
@@ -32,11 +49,26 @@ def get_func_repr(func):
# convert func to string
if inspect.isfunction(func):
return f"{func.__module__}.{func.__name__}"
elif inspect.ismethod(func) and hasattr(func.__self__, '__name__'):
elif inspect.ismethod(func) and hasattr(func.__self__, "__name__"):
return (
f"{func.__self__.__module__}."
f"{func.__self__.__name__}.{func.__name__}"
f"{func.__self__.__module__}." f"{func.__self__.__name__}.{func.__name__}"
)
else:
return str(func)
def localtime(value=None) -> datetime:
"""Override for timezone.localtime to deal with naive times and local times"""
if settings.USE_TZ:
if django.VERSION >= (4, 0) and settings.USE_DEPRECATED_PYTZ:
import pytz
convert_to_tz = pytz.timezone(Conf.TIME_ZONE)
else:
convert_to_tz = ZoneInfo(Conf.TIME_ZONE)
return timezone.localtime(value=value, timezone=convert_to_tz)
if value is None:
return datetime.now()
else:
return value

View File

@@ -16,12 +16,10 @@
import os
import sys
import sphinx_rtd_theme
myPath = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, myPath + '/../')
os.environ['DJANGO_SETTINGS_MODULE'] = 'django_q.tests.settings'
nitpick_ignore = [('py:class', 'datetime')]
sys.path.insert(0, myPath + "/../")
os.environ["DJANGO_SETTINGS_MODULE"] = "django_q.tests.settings"
nitpick_ignore = [("py:class", "datetime")]
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
@@ -37,50 +35,54 @@ nitpick_ignore = [('py:class', 'datetime')]
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx_rtd_theme',
'sphinx.ext.todo',
'sphinx.ext.intersphinx',
"sphinx_rtd_theme",
"sphinx.ext.todo",
"sphinx.ext.intersphinx",
# 'sphinx.ext.autodoc'
]
intersphinx_mapping = {'python': ('https://docs.python.org/3.8', None),
'django': ('https://docs.djangoproject.com/en/2.2/',
'https://docs.djangoproject.com/en/2.2/_objects/')}
intersphinx_mapping = {
"python": ("https://docs.python.org/3.8", None),
"django": (
"https://docs.djangoproject.com/en/2.2/",
"https://docs.djangoproject.com/en/2.2/_objects/",
),
}
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
templates_path = ["_templates"]
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
source_suffix = ".rst"
# The encoding of source files.
# source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
master_doc = "index"
# General information about the project.
project = 'Django Q2'
copyright = '2015-2021, Ilan Steemers - 2022, Stan Triepels'
author = 'Ilan Steemers, Stan Triepels'
project = "Django Q2"
copyright = "2015-2021, Ilan Steemers - 2022, Stan Triepels"
author = "Ilan Steemers, Stan Triepels"
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '1.4'
version = "1.4"
# The full version, including alpha/beta/rc tags.
release = '1.4.5'
release = "1.4.11"
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = 'en'
language = "en"
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
@@ -90,7 +92,7 @@ language = 'en'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
exclude_patterns = ["_build"]
# The reST default role (used for this markup: `text`) to use for all
# documents.
@@ -108,7 +110,7 @@ add_module_names = False
# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
pygments_style = "sphinx"
# A list of ignored prefixes for module index sorting.
# modindex_common_prefix = []
@@ -124,7 +126,7 @@ todo_include_todos = True
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'sphinx_rtd_theme'
html_theme = "sphinx_rtd_theme"
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
@@ -136,11 +138,11 @@ html_theme_options = {
# 'github_banner': True,
}
html_sidebars = {
'**': [
'about.html',
'navigation.html',
'relations.html',
'searchbox.html',
"**": [
"about.html",
"navigation.html",
"relations.html",
"searchbox.html",
]
}
# Add any paths that contain custom themes here, relative to this directory.
@@ -161,12 +163,12 @@ html_sidebars = {
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
html_favicon = '_static/favicon.ico'
html_favicon = "_static/favicon.ico"
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
html_static_path = ["_static"]
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
@@ -229,20 +231,17 @@ html_static_path = ['_static']
# html_search_scorer = 'scorer.js'
# Output file base name for HTML help builder.
htmlhelp_basename = 'DjangoQ2doc'
htmlhelp_basename = "DjangoQ2doc"
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
# 'preamble': '',
# Latex figure (float) alignment
# 'figure_align': 'htbp',
}
@@ -251,8 +250,7 @@ latex_elements = {
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'DjangoQ2.tex', 'Django Q2 Documentation',
'Ilan Steemers', 'manual'),
(master_doc, "DjangoQ2.tex", "Django Q2 Documentation", "Ilan Steemers", "manual"),
]
# The name of an image file (relative to this directory) to place at the top of
@@ -280,10 +278,7 @@ latex_documents = [
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'djangoq2', 'Django Q2 Documentation',
[author], 1)
]
man_pages = [(master_doc, "djangoq2", "Django Q2 Documentation", [author], 1)]
# If true, show URL addresses after external links.
@@ -296,9 +291,15 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'DjangoQ2', 'Django Q2 Documentation',
author, 'DjangoQ2', 'A multiprocessing distributed task queue for Django.',
'Miscellaneous'),
(
master_doc,
"DjangoQ2",
"Django Q2 Documentation",
author,
"DjangoQ2",
"A multiprocessing distributed task queue for Django.",
"Miscellaneous",
),
]
# Documents to append as an appendix to all manuals.

View File

@@ -70,6 +70,13 @@ Set this to something that makes sense for your project. Can be overridden for i
See :ref:`retry` for details how to set values for timeout and retry.
.. _time_zone:
time_zone
~~~~~~~
The timezone that is used for task scheduling. Use this if you are having issue with DST. The scheduler uses UTC to calculate the next date and will therefore ignore any DST changes. This will cause 1 hour or 0.5 hour changes in the schedule when time is moved one hour ahead or back. Defaults to `settings.TIME_ZONE` if `USE_TZ` is enabled.
.. _ack_failures:
ack_failures
@@ -310,16 +317,6 @@ Using the Django ORM backend will also enable the Queued Tasks table in the Admi
If you need better performance , you should consider using a different database backend than the main project.
Set ``orm`` to the name of that database connection and make sure you run migrations on it using the ``--database`` option.
When using the Django database as a message broker, you can set the ``has_replica`` boolean keyword to ensure Django-Q2 works properly letting a `Database Router <https://docs.djangoproject.com/en/3.2/topics/db/multi-db/>`__. ::
# example ORM broker connection with replica database
Q_CLUSTER = {
...
'orm': 'default',
'has_replica': True
}
.. _mongo_configuration:
mongo

View File

@@ -55,6 +55,10 @@ Optional
$ pip install psutil
- `setproctitle <https://github.com/dvarrazzo/py-setproctitle>`__ python module to customize the process title by Daniele Varrazzo', is an optional requirement used to set informative process titles::
$ pip install setproctitle
- `Hiredis <https://github.com/redis/hiredis>`__ parser. This C library maintained by the core Redis team is faster than the standard PythonParser during high loads::
$ pip install hiredis

View File

@@ -69,6 +69,10 @@ You can change this by setting the :ref:`catch_up` configuration setting to ``Fa
The scheduler will then skip execution of scheduled events in the past.
Instead those tasks will run once when the cluster starts again and the scheduler will find the next available slot in the future according to original schedule parameters.
When :ref:`catch_up` is to ``True`` it may be useful for the task to know what was the date and time it was originally intended to run at.
To achieve this, pass an identifier name to parameter `intended_date_kwarg` when creating the schedule. The intended datetime will then be passed - in isoformat string - as
a kwarg with that identifier name to the task that has been created.
Management Commands
-------------------
@@ -123,9 +127,10 @@ Reference
: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 str cluster: optional cluster name. Task will be executed only on a cluster with a matching :ref:`name`.
:param str intended_date_kwarg: optional identifier to pass intended schedule date.
:param dict q_options: options passed to async_task for this schedule
:param kwargs: optional keyword arguments for the scheduled function.
.. note::
q_options does not accept the 'broker' key with a broker instance but accepts a 'broker_name' key instead. This can be used to specify the broker connection name to assign the task. If a broker with the specified name does not exist or is not running at the moment of placing the task in queue it fallbacks to the random broker/queue that handled the schedule.
@@ -183,9 +188,13 @@ Reference
When set to -1, this will keep counting down.
.. py:attribute:: cluster
Task will be executed only on a cluster with a matching :ref:`name`.
.. py:attribute:: intended_date_kwarg
Name of kwarg to pass intended schedule date.
.. py:attribute:: next_run
Datetime of the next scheduled execution.

1616
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "django-q2"
version = "1.4.5"
version = "1.4.11"
packages = [
{ include = "django_q" },
]
@@ -59,6 +59,7 @@ croniter = { version = "^1.3.7", optional = true }
django-q-rollbar = {version = ">=0.1", optional = true}
django-q-sentry = {version = ">=0.1", optional = true}
redis = {version = "^4.3.4", optional = true}
setproctitle = {version = "^1.3.2", optional = true}
[tool.poetry.dev-dependencies]
@@ -72,7 +73,7 @@ isort = {extras = ["requirements_deprecated_finder"], version = "^5.10.1"}
[tool.poetry.extras]
requires = ["poetry_core>=1.0.0"]
build-backend = ["poetry.core.masonry.api"]
testing = ["django-redis", "croniter", "hiredis", "psutil", "iron-mq", "boto3", "pymongo", "blessed", "redis"]
testing = ["django-redis", "croniter", "hiredis", "psutil", "iron-mq", "boto3", "pymongo", "blessed", "redis", "setproctitle"]
rollbar = ["django-q-rollbar"]
sentry = ["django-q-sentry"]

2
tox.ini Normal file
View File

@@ -0,0 +1,2 @@
[flake8]
max-line-length=88