This commit is contained in:
Iliyan Angelov
2025-09-19 11:58:53 +03:00
parent 306b20e24a
commit 6b247e5b9f
11423 changed files with 1500615 additions and 778 deletions

View File

@@ -0,0 +1,36 @@
"""Celery result backends for Django."""
# :copyright: (c) 2016, Ask Solem.
# :copyright: (c) 2017-2033, Asif Saif Uddin.
# All rights reserved.
# :license: BSD (3 Clause), see LICENSE for more details.
import re
from collections import namedtuple
import django
__version__ = '2.6.0'
__author__ = 'Asif Saif Uddin'
__contact__ = 'auvipy@gmail.com'
__homepage__ = 'https://github.com/celery/django-celery-results'
__docformat__ = 'restructuredtext'
# -eof meta-
version_info_t = namedtuple('version_info_t', (
'major', 'minor', 'micro', 'releaselevel', 'serial',
))
# bumpversion can only search for {current_version}
# so we have to parse the version here.
_temp = re.match(
r'(\d+)\.(\d+).(\d+)(.+)?', __version__).groups()
VERSION = version_info = version_info_t(
int(_temp[0]), int(_temp[1]), int(_temp[2]), _temp[3] or '', '')
del _temp
del re
__all__ = []
if django.VERSION < (3, 2):
default_app_config = 'django_celery_results.apps.CeleryResultConfig'

View File

@@ -0,0 +1,85 @@
"""Result Task Admin interface."""
from django.conf import settings
from django.contrib import admin
from django.utils.translation import gettext_lazy as _
try:
ALLOW_EDITS = settings.DJANGO_CELERY_RESULTS['ALLOW_EDITS']
except (AttributeError, KeyError):
ALLOW_EDITS = False
pass
from .models import GroupResult, TaskResult
class TaskResultAdmin(admin.ModelAdmin):
"""Admin-interface for results of tasks."""
model = TaskResult
date_hierarchy = 'date_done'
list_display = ('task_id', 'periodic_task_name', 'task_name', 'date_done',
'status', 'worker')
list_filter = ('status', 'date_done', 'periodic_task_name', 'task_name',
'worker')
readonly_fields = ('date_created', 'date_started', 'date_done',
'result', 'meta')
search_fields = ('task_name', 'task_id', 'status', 'task_args',
'task_kwargs')
fieldsets = (
(None, {
'fields': (
'task_id',
'task_name',
'periodic_task_name',
'status',
'worker',
'content_type',
'content_encoding',
),
'classes': ('extrapretty', 'wide')
}),
(_('Parameters'), {
'fields': (
'task_args',
'task_kwargs',
),
'classes': ('extrapretty', 'wide')
}),
(_('Result'), {
'fields': (
'result',
'date_created',
'date_started',
'date_done',
'traceback',
'meta',
),
'classes': ('extrapretty', 'wide')
}),
)
def get_readonly_fields(self, request, obj=None):
if ALLOW_EDITS:
return self.readonly_fields
else:
return list({
field.name for field in self.opts.local_fields
})
admin.site.register(TaskResult, TaskResultAdmin)
class GroupResultAdmin(admin.ModelAdmin):
"""Admin-interface for results of grouped tasks."""
model = GroupResult
date_hierarchy = 'date_done'
list_display = ('group_id', 'date_done')
list_filter = ('date_done',)
readonly_fields = ('date_created', 'date_done', 'result')
search_fields = ('group_id',)
admin.site.register(GroupResult, GroupResultAdmin)

View File

@@ -0,0 +1,15 @@
"""Application configuration."""
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
__all__ = ['CeleryResultConfig']
class CeleryResultConfig(AppConfig):
"""Default configuration for the django_celery_results app."""
name = 'django_celery_results'
label = 'django_celery_results'
verbose_name = _('Celery Results')
default_auto_field = 'django.db.models.AutoField'

View File

@@ -0,0 +1,4 @@
from .cache import CacheBackend
from .database import DatabaseBackend
__all__ = ['CacheBackend', 'DatabaseBackend']

View File

@@ -0,0 +1,39 @@
"""Celery cache backend using the Django Cache Framework."""
from celery.backends.base import KeyValueStoreBackend
from django.core.cache import cache as default_cache
from django.core.cache import caches
from kombu.utils.encoding import bytes_to_str
class CacheBackend(KeyValueStoreBackend):
"""Backend using the Django cache framework to store task metadata."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Must make sure backend doesn't convert exceptions to dict.
self.serializer = 'pickle'
def get(self, key):
key = bytes_to_str(key)
return self.cache_backend.get(key)
def set(self, key, value):
key = bytes_to_str(key)
self.cache_backend.set(key, value, self.expires)
def delete(self, key):
key = bytes_to_str(key)
self.cache_backend.delete(key)
def encode(self, data):
return data
def decode(self, data):
return data
@property
def cache_backend(self):
backend = self.app.conf.cache_backend
return caches[backend] if backend else default_cache

View File

@@ -0,0 +1,314 @@
import binascii
import json
from celery import maybe_signature, states
from celery.backends.base import BaseDictBackend, get_current_task
from celery.exceptions import ChordError
from celery.result import GroupResult, allow_join_result, result_from_tuple
from celery.utils.log import get_logger
from celery.utils.serialization import b64decode, b64encode
from django.db import connection, router, transaction
from django.db.models.functions import Now
from django.db.utils import InterfaceError
from kombu.exceptions import DecodeError
from ..models import ChordCounter
from ..models import GroupResult as GroupResultModel
from ..models import TaskResult
EXCEPTIONS_TO_CATCH = (InterfaceError,)
try:
from psycopg2 import InterfaceError as Psycopg2InterfaceError
EXCEPTIONS_TO_CATCH += (Psycopg2InterfaceError,)
except ImportError:
pass
logger = get_logger(__name__)
class DatabaseBackend(BaseDictBackend):
"""The Django database backend, using models to store task state."""
TaskModel = TaskResult
GroupModel = GroupResultModel
subpolling_interval = 0.5
def exception_safe_to_retry(self, exc):
"""Check if an exception is safe to retry.
Backends have to overload this method with correct predicates
dealing with their exceptions.
By default no exception is safe to retry, it's up to
backend implementation to define which exceptions are safe.
For Celery / django-celery-results, retry Django / Psycopg2
InterfaceErrors, like "Connection already closed", with new connection.
Set result_backend_always_retry to True in order to enable retries.
"""
for exc_type in EXCEPTIONS_TO_CATCH:
if isinstance(exc, exc_type):
# Only called if InterfaceError occurs and always_retry is True
connection.close()
return True
return False
def _get_extended_properties(self, request, traceback):
extended_props = {
'periodic_task_name': None,
'task_args': None,
'task_kwargs': None,
'task_name': None,
'traceback': None,
'worker': None,
}
if request and self.app.conf.find_value_for_key('extended', 'result'):
if getattr(request, 'argsrepr', None) is not None:
# task protocol 2
task_args = request.argsrepr
else:
# task protocol 1
task_args = getattr(request, 'args', None)
if getattr(request, 'kwargsrepr', None) is not None:
# task protocol 2
task_kwargs = request.kwargsrepr
else:
# task protocol 1
task_kwargs = getattr(request, 'kwargs', None)
# Encode input arguments
if task_args is not None:
_, _, task_args = self.encode_content(task_args)
if task_kwargs is not None:
_, _, task_kwargs = self.encode_content(task_kwargs)
periodic_task_name = getattr(request, 'periodic_task_name', None)
extended_props.update({
'periodic_task_name': periodic_task_name,
'task_args': task_args,
'task_kwargs': task_kwargs,
'task_name': getattr(request, 'task', None),
'traceback': traceback,
'worker': getattr(request, 'hostname', None),
})
return extended_props
def _get_meta_from_request(self, request=None):
"""
Use the request or get_current_task to evaluate the `meta` attribute.
With this, is possible to assign arbitrary data in request.meta to be
retrieve and stored on the TaskResult.
"""
request = request or getattr(get_current_task(), "request", None)
return getattr(request, "meta", {})
def _store_result(
self,
task_id,
result,
status,
traceback=None,
request=None,
using=None
):
"""Store return value and status of an executed task."""
content_type, content_encoding, result = self.encode_content(result)
meta = {
**self._get_meta_from_request(request),
"children": self.current_task_children(request),
}
_, _, encoded_meta = self.encode_content(
meta,
)
task_props = {
'content_encoding': content_encoding,
'content_type': content_type,
'meta': encoded_meta,
'result': result,
'status': status,
'task_id': task_id,
'traceback': traceback,
'using': using,
}
task_props.update(
self._get_extended_properties(request, traceback)
)
if status == states.STARTED:
task_props['date_started'] = Now()
self.TaskModel._default_manager.store_result(**task_props)
return result
def _get_task_meta_for(self, task_id):
"""Get task metadata for a task by id."""
obj = self.TaskModel._default_manager.get_task(task_id)
res = obj.as_dict()
meta = self.decode_content(obj, res.pop('meta', None)) or {}
result = self.decode_content(obj, res.get('result'))
task_args = res.get('task_args')
task_kwargs = res.get('task_kwargs')
try:
task_args = self.decode_content(obj, task_args)
task_kwargs = self.decode_content(obj, task_kwargs)
except (DecodeError, binascii.Error):
pass
# the right names are args/kwargs, not task_args/task_kwargs,
# keep both for backward compatibility
res.update(
meta,
result=result,
task_args=task_args,
task_kwargs=task_kwargs,
args=task_args,
kwargs=task_kwargs,
)
return self.meta_from_decoded(res)
def encode_content(self, data):
content_type, content_encoding, content = self._encode(data)
if content_encoding == 'binary':
content = b64encode(content)
return content_type, content_encoding, content
def decode_content(self, obj, content):
if content:
if obj.content_encoding == 'binary':
content = b64decode(content)
return self.decode(content)
def _forget(self, task_id):
try:
self.TaskModel._default_manager.get(task_id=task_id).delete()
except self.TaskModel.DoesNotExist:
pass
def cleanup(self):
"""Delete expired metadata."""
if not self.expires:
return
self.TaskModel._default_manager.delete_expired(self.expires)
self.GroupModel._default_manager.delete_expired(self.expires)
def _restore_group(self, group_id):
"""return result value for a group by id."""
group_result = self.GroupModel._default_manager.get_group(group_id)
if group_result:
res = group_result.as_dict()
decoded_result = self.decode_content(group_result, res["result"])
res["result"] = None
if decoded_result:
res["result"] = result_from_tuple(decoded_result, app=self.app)
return res
def _save_group(self, group_id, group_result):
"""Store return value of group"""
content_type, content_encoding, result = self.encode_content(
group_result.as_tuple()
)
self.GroupModel._default_manager.store_group_result(
content_type, content_encoding, group_id, result
)
return group_result
def _delete_group(self, group_id):
try:
self.GroupModel._default_manager.get_group(group_id).delete()
except self.TaskModel.DoesNotExist:
pass
def apply_chord(self, header_result_args, body, **kwargs):
"""Add a ChordCounter with the expected number of results"""
if not isinstance(header_result_args, GroupResult):
# Celery 5.1 provides the GroupResult args
header_result = self.app.GroupResult(*header_result_args)
else:
# celery <5.1 will pass a GroupResult object
header_result = header_result_args
results = [r.as_tuple() for r in header_result]
chord_size = body.get("chord_size", None) or len(results)
data = json.dumps(results)
ChordCounter.objects.create(
group_id=header_result.id, sub_tasks=data, count=chord_size
)
def on_chord_part_return(self, request, state, result, **kwargs):
"""Called on finishing each part of a Chord header"""
tid, gid = request.id, request.group
if not gid or not tid:
return
call_callback = False
with transaction.atomic(using=router.db_for_write(ChordCounter)):
# We need to know if `count` hits 0.
# wrap the update in a transaction
# with a `select_for_update` lock to prevent race conditions.
# SELECT FOR UPDATE is not supported on all databases
try:
chord_counter = (
ChordCounter.objects.select_for_update()
.get(group_id=gid)
)
except ChordCounter.DoesNotExist:
logger.warning("Can't find ChordCounter for Group %s", gid)
return
chord_counter.count -= 1
if chord_counter.count != 0:
chord_counter.save(update_fields=["count"])
else:
# Last task in the chord header has finished
call_callback = True
chord_counter.delete()
if call_callback:
deps = chord_counter.group_result(app=self.app)
if deps.ready():
callback = maybe_signature(request.chord, app=self.app)
trigger_callback(
app=self.app,
callback=callback,
group_result=deps
)
def trigger_callback(app, callback, group_result):
"""Add the callback to the queue or mark the callback as failed
Implementation borrowed from `celery.app.builtins.unlock_chord`
"""
if group_result.supports_native_join:
j = group_result.join_native
else:
j = group_result.join
try:
with allow_join_result():
ret = j(timeout=app.conf.result_chord_join_timeout, propagate=True)
except Exception as exc: # pylint: disable=broad-except
try:
culprit = next(group_result._failed_join_report())
reason = f"Dependency {culprit.id} raised {exc!r}"
except StopIteration:
reason = repr(exc)
logger.exception("Chord %r raised: %r", group_result.id, exc)
app.backend.chord_error_from_stack(callback, ChordError(reason))
else:
try:
callback.delay(ret)
except Exception as exc: # pylint: disable=broad-except
logger.exception("Chord %r raised: %r", group_result.id, exc)
app.backend.chord_error_from_stack(
callback, exc=ChordError(f"Callback error: {exc!r}")
)

View File

@@ -0,0 +1,150 @@
# Spanish translation strings for django-celery-results.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as django-celery-results.
# <mondejar1994@gmail.com>, 2020.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version:\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-02-26 18:34+0100\n"
"PO-Revision-Date: 2020-02-26 20:25-0015\n"
"Last-Translator: <mondejar1994@gmail.com>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: django_celery_results/admin.py:39
msgid "Parameters"
msgstr "Parámetros"
#: django_celery_results/admin.py:46
msgid "Result"
msgstr "Resultado"
#: django_celery_results/apps.py:15
msgid "Celery Results"
msgstr "Resultados Celery"
#: django_celery_results/models.py:28
msgid "Task ID"
msgstr "ID de Tarea"
#: django_celery_results/models.py:29
msgid "Celery ID for the Task that was run"
msgstr "ID de Celery para la tarea que fue ejecutada"
#: django_celery_results/models.py:32
msgid "Task Name"
msgstr "Nombre de Tarea"
#: django_celery_results/models.py:33
msgid "Name of the Task which was run"
msgstr "Nombre de la Tarea que fue ejecutada"
#: django_celery_results/models.py:36
msgid "Task Positional Arguments"
msgstr "Argumentos posicionales de la Tarea"
#: django_celery_results/models.py:37
msgid "JSON representation of the positional arguments used with the task"
msgstr "Representación JSON de los argumentos posicionales usados en la tarea"
#: django_celery_results/models.py:41
msgid "Task Named Arguments"
msgstr "Argumentos opcionales de la tarea"
#: django_celery_results/models.py:42
msgid "JSON representation of the named arguments used with the task"
msgstr "Representación JSON de los argumentos opcionales usados en la tarea"
#: django_celery_results/models.py:47
msgid "Task State"
msgstr "Estado de la Tarea"
#: django_celery_results/models.py:48
msgid "Current state of the task being run"
msgstr "Estado actual en el que se encuentra la tarea en ejecución"
#: django_celery_results/models.py:51
msgid "Worker"
msgstr "Worker"
#: django_celery_results/models.py:51
msgid "Worker that executes the task"
msgstr "Worker que ejecuta la tarea"
#: django_celery_results/models.py:55
msgid "Result Content Type"
msgstr "Content Type del resultado"
#: django_celery_results/models.py:56
msgid "Content type of the result data"
msgstr "Atributo Content type de los datos del resultado"
#: django_celery_results/models.py:59
msgid "Result Encoding"
msgstr "Codificación del resultado"
#: django_celery_results/models.py:60
msgid "The encoding used to save the task result data"
msgstr "La codificación usada para guardar los datos del resultado"
#: django_celery_results/models.py:63
msgid "Result Data"
msgstr "Datos del resultado"
#: django_celery_results/models.py:64
msgid ""
"The data returned by the task. Use content_encoding and content_type fields"
" to read."
msgstr ""
"Datos devueltos por la tarea. Usa los campos content_encoding y content_type"
" para leerlos."
#: django_celery_results/models.py:68
msgid "Created DateTime"
msgstr "Fecha de creación"
#: django_celery_results/models.py:69
msgid "Datetime field when the task result was created in UTC"
msgstr "Fecha de creación de la tarea en UTC"
#: django_celery_results/models.py:72
msgid "Completed DateTime"
msgstr "Fecha de terminación"
#: django_celery_results/models.py:73
msgid "Datetime field when the task was completed in UTC"
msgstr "Fecha de completitud de la tarea en UTC"
#: django_celery_results/models.py:76
msgid "Traceback"
msgstr "Traceback"
#: django_celery_results/models.py:77
msgid "Text of the traceback if the task generated one"
msgstr "Texto del traceback si la tarea generó uno"
#: django_celery_results/models.py:80
msgid "Task Meta Information"
msgstr "Metadatos de la tarea"
#: django_celery_results/models.py:81
msgid ""
"JSON meta information about the task, such as information on child tasks"
msgstr ""
"Metainformación sobre la tarea en formato JSON, como la información de las "
"tareas hijas"
#: django_celery_results/models.py:91
msgid "task result"
msgstr "resultado de la tarea"
#: django_celery_results/models.py:92
msgid "task results"
msgstr "resultados de tareas"

View File

@@ -0,0 +1,191 @@
# Brazilian portuguese translation strings for django-celery-results.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as django-celery-results.
# Eduardo Oliveira <eduardo_y05@outlook.com>, 2022.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-01-04 19:52-0300\n"
"PO-Revision-Date: 2022-01-04 19:52-0300\n"
"Last-Translator: Eduardo Oliveira <eduardo_y05@outlook.com>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: pt_BR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: admin.py:40
msgid "Parameters"
msgstr "Parâmetros"
#: admin.py:47
msgid "Result"
msgstr "Resultado"
#: apps.py:14
msgid "Celery Results"
msgstr "Resultados do celery"
#: models.py:28
msgid "Task ID"
msgstr "Id da tarefa"
#: models.py:29
msgid "Celery ID for the Task that was run"
msgstr "Id do celery em que a tarefa foi executada"
#: models.py:32
msgid "Periodic Task Name"
msgstr "Nome da tarefa periódica"
#: models.py:33
msgid "Name of the Periodic Task which was run"
msgstr "Nome da tarefa periódica que foi executada"
#: models.py:36
msgid "Task Name"
msgstr "Nome da tarefa"
#: models.py:37
msgid "Name of the Task which was run"
msgstr "Nome da tarefa que foi executada"
#: models.py:40
msgid "Task Positional Arguments"
msgstr "Argumentos posicionais da tarefa"
#: models.py:41
msgid "JSON representation of the positional arguments used with the task"
msgstr "Representação JSON dos argumentos posicionais usados pela tarefa"
#: models.py:45
msgid "Task Named Arguments"
msgstr "Argumentos nomeados da tarefa"
#: models.py:46
msgid "JSON representation of the named arguments used with the task"
msgstr "Representação JSON dos argumentos nomeados usados pela tarefa"
#: models.py:51
msgid "Task State"
msgstr "Status da tarefa"
#: models.py:52
msgid "Current state of the task being run"
msgstr "Status atual da tarefa em execução"
#: models.py:55
msgid "Worker"
msgstr "Worker"
#: models.py:55
msgid "Worker that executes the task"
msgstr "Worker que executa a tarefa"
#: models.py:59 models.py:190
msgid "Result Content Type"
msgstr "Tipo de conteúdo do resultado"
#: models.py:60 models.py:191
msgid "Content type of the result data"
msgstr "Tipo de conteúdo dos dados do resultado"
#: models.py:63 models.py:195
msgid "Result Encoding"
msgstr "Codificação do resultado"
#: models.py:64 models.py:196
msgid "The encoding used to save the task result data"
msgstr "A codificação usada para salvar os dados de resultado da tarefa"
#: models.py:67 models.py:200
msgid "Result Data"
msgstr "Dados do resultado"
#: models.py:68 models.py:201
msgid ""
"The data returned by the task. Use content_encoding and content_type fields "
"to read."
msgstr "Os dados retornados pela tarefa. Use os campos content_encoding e content_type para ler."
#: models.py:72 models.py:180
msgid "Created DateTime"
msgstr "Data/Horário de criação"
#: models.py:73
msgid "Datetime field when the task result was created in UTC"
msgstr "Data/Horário em que o resultado da tarefa foi criado (em UTC)"
#: models.py:76 models.py:185
msgid "Completed DateTime"
msgstr "Data/Horário em que foi concluída"
#: models.py:77
msgid "Datetime field when the task was completed in UTC"
msgstr "Data/Horário em que a tarefa foi concluída (em UTC)"
#: models.py:80
msgid "Traceback"
msgstr "Traceback"
#: models.py:81
msgid "Text of the traceback if the task generated one"
msgstr "Texto de traceback se a tarefa gerou um"
#: models.py:84
msgid "Task Meta Information"
msgstr "Meta informação da tarefa"
#: models.py:85
msgid ""
"JSON meta information about the task, such as information on child tasks"
msgstr "Meta informação JSON sobre a tarefa, como informações sobre as subtarefas"
#: models.py:95
msgid "task result"
msgstr "resultado da tarefa"
#: models.py:96
msgid "task results"
msgstr "resultados das tarefas"
#: models.py:133 models.py:175
msgid "Group ID"
msgstr "Id do grupo"
#: models.py:134
msgid "Celery ID for the Chord header group"
msgstr "Id do celery para o grupo de cabeçalho Chord"
#: models.py:138
msgid ""
"JSON serialized list of task result tuples. use .group_result() to decode"
msgstr "lista de tuplas de resultados de tarefas serializadas como JSON. Use .group_result() para decodificar"
#: models.py:144
msgid "Starts at len(chord header) and decrements after each task is finished"
msgstr "Começa em len(chord header) e decaí após o término de cada tarefa"
#: models.py:176
msgid "Celery ID for the Group that was run"
msgstr "Id do celery para o grupo que foi executado"
#: models.py:181
msgid "Datetime field when the group result was created in UTC"
msgstr "Data/Horário em que o resultado do grupo foi criado (em UTC)"
#: models.py:186
msgid "Datetime field when the group was completed in UTC"
msgstr "Data/Horário em que o grupo foi concluída (em UTC)"
#: models.py:221
msgid "group result"
msgstr "resultado do grupo"
#: models.py:222
msgid "group results"
msgstr "resultados dos grupos"

View File

@@ -0,0 +1,185 @@
# Russian translation strings for django-celery-results.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# ILDAR MINNAKHMETOV <ildarworld@gmail.com>, 2021.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-11-09 19:16+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: ILDAR MINNAKHMETOV <ildarworld@gmail.com>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: ru\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: django_celery_results/admin.py:38
msgid "Parameters"
msgstr "Параметры"
#: django_celery_results/admin.py:45
msgid "Result"
msgstr "Результаты"
#: django_celery_results/apps.py:14
msgid "Celery Results"
msgstr "Результаты Celery"
#: django_celery_results/models.py:28
msgid "Task ID"
msgstr "ID Задачи"
#: django_celery_results/models.py:29
msgid "Celery ID for the Task that was run"
msgstr "Celery ID задачи"
#: django_celery_results/models.py:32
msgid "Task Name"
msgstr "Название задачи"
#: django_celery_results/models.py:33
msgid "Name of the Task which was run"
msgstr "Название задачи которая была запущена"
#: django_celery_results/models.py:36
msgid "Task Positional Arguments"
msgstr "Аргументы задачи"
#: django_celery_results/models.py:37
msgid "JSON representation of the positional arguments used with the task"
msgstr "JSON с позиционными аргументами задачи (*args)"
#: django_celery_results/models.py:41
msgid "Task Named Arguments"
msgstr "Именованные аргументы задачи"
#: django_celery_results/models.py:42
msgid "JSON representation of the named arguments used with the task"
msgstr "JSON с именованными аргументами задачи (**kwargs)"
#: django_celery_results/models.py:47
msgid "Task State"
msgstr "Статус задачи"
#: django_celery_results/models.py:48
msgid "Current state of the task being run"
msgstr "Текущий статус запущенной задачи"
#: django_celery_results/models.py:51
msgid "Worker"
msgstr "Воркер"
#: django_celery_results/models.py:51
msgid "Worker that executes the task"
msgstr "Воркер который выполняет задачу"
#: django_celery_results/models.py:55 django_celery_results/models.py:186
msgid "Result Content Type"
msgstr "Тип контента результата"
#: django_celery_results/models.py:56 django_celery_results/models.py:187
msgid "Content type of the result data"
msgstr "Тип контента данных результата"
#: django_celery_results/models.py:59 django_celery_results/models.py:191
msgid "Result Encoding"
msgstr "Кодировка результата"
#: django_celery_results/models.py:60 django_celery_results/models.py:192
msgid "The encoding used to save the task result data"
msgstr "Кодировка использованная для сохранения данных результата"
#: django_celery_results/models.py:63 django_celery_results/models.py:196
msgid "Result Data"
msgstr "Данные результата"
#: django_celery_results/models.py:64 django_celery_results/models.py:197
msgid ""
"The data returned by the task. Use content_encoding and content_type fields "
"to read."
msgstr "Данные, которые вернула задача. Используйте content_encoding и content_type для чтения."
#: django_celery_results/models.py:68 django_celery_results/models.py:176
msgid "Created DateTime"
msgstr "Дата и время создания"
#: django_celery_results/models.py:69
msgid "Datetime field when the task result was created in UTC"
msgstr "Дата и время когда результат был создан (UTC)"
#: django_celery_results/models.py:72 django_celery_results/models.py:181
msgid "Completed DateTime"
msgstr "Дата и время завершения"
#: django_celery_results/models.py:73
msgid "Datetime field when the task was completed in UTC"
msgstr "Дата и время когда задача была завершена (UTC)"
#: django_celery_results/models.py:76
msgid "Traceback"
msgstr "Traceback"
#: django_celery_results/models.py:77
msgid "Text of the traceback if the task generated one"
msgstr "Текст traceback, если есть"
#: django_celery_results/models.py:80
msgid "Task Meta Information"
msgstr "Метаинформация задачи"
#: django_celery_results/models.py:81
msgid ""
"JSON meta information about the task, such as information on child tasks"
msgstr ""
"JSON мета-информация о задаче, к примеру о дочерних задачах"
#: django_celery_results/models.py:91
msgid "task result"
msgstr "результат задачи"
#: django_celery_results/models.py:92
msgid "task results"
msgstr "результаты задач"
#: django_celery_results/models.py:129 django_celery_results/models.py:171
msgid "Group ID"
msgstr "ID группы"
#: django_celery_results/models.py:130
msgid "Celery ID for the Chord header group"
msgstr "Celery ID для заголовка группы"
#: django_celery_results/models.py:134
msgid ""
"JSON serialized list of task result tuples. use .group_result() to decode"
msgstr ""
"JSON-список кортежей результата. Используйте .group_result() для декодирования"
#: django_celery_results/models.py:140
msgid "Starts at len(chord header) and decrements after each task is finished"
msgstr "Начинается в len(chord header) и уменьшается после каждого завершенного задания"
#: django_celery_results/models.py:172
msgid "Celery ID for the Group that was run"
msgstr "Celery ID для группы которая была запущена"
#: django_celery_results/models.py:177
msgid "Datetime field when the group result was created in UTC"
msgstr "Дата и время если результат группы был создан (UTC)"
#: django_celery_results/models.py:182
msgid "Datetime field when the group was completed in UTC"
msgstr "Дата и время, когда группа была завершена (UTC)"
#: django_celery_results/models.py:217
msgid "group result"
msgstr "результат группы"
#: django_celery_results/models.py:218
msgid "group results"
msgstr "результаты групп"

View File

@@ -0,0 +1,191 @@
# Simplified Chinese translation strings for django-celery-results.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as django-celery-results.
# <acwzy@live.com>, 2021.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version:\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-03-19 22:23+0800\n"
"PO-Revision-Date: 2021-11-20 23:00+0800\n"
"Last-Translator: ifmos <acwzy@live.com>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: zh-hans\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: django_celery_results/admin.py:41
msgid "Parameters"
msgstr "参数"
#: django_celery_results/admin.py:48
msgid "Result"
msgstr "结果"
#: django_celery_results/apps.py:14
msgid "Celery Results"
msgstr "Celery 结果"
#: django_celery_results/models.py:28
msgid "Task ID"
msgstr "任务 ID"
#: django_celery_results/models.py:29
msgid "Celery ID for the Task that was run"
msgstr "已运行任务 Celery ID"
#: django_celery_results/models.py:32
msgid "Periodic Task Name"
msgstr "周期任务名称"
#: django_celery_results/models.py:33
msgid "Name of the Periodic Task which was run"
msgstr "已运行周期任务名称"
#: django_celery_results/models.py:40
msgid "Task Name"
msgstr "任务名称"
#: django_celery_results/models.py:41
msgid "Name of the Task which was run"
msgstr "已运行任务名称"
#: django_celery_results/models.py:44
msgid "Task Positional Arguments"
msgstr "任务位置参数"
#: django_celery_results/models.py:45
msgid "JSON representation of the positional arguments used with the task"
msgstr "该任务位置参数的 JSON 字符串"
#: django_celery_results/models.py:49
msgid "Task Named Arguments"
msgstr "任务具名参数"
#: django_celery_results/models.py:50
msgid "JSON representation of the named arguments used with the task"
msgstr "该任务具名参数的 JSON 字符串"
#: django_celery_results/models.py:55
msgid "Task State"
msgstr "任务状态"
#: django_celery_results/models.py:56
msgid "Current state of the task being run"
msgstr "运行中任务的当前状态"
#: django_celery_results/models.py:59
msgid "Worker"
msgstr "Worker"
#: django_celery_results/models.py:59
msgid "Worker that executes the task"
msgstr "执行该任务的 Worker"
#: django_celery_results/models.py:63 django_celery_results/models.py:200
msgid "Result Content Type"
msgstr "结果内容类型"
#: django_celery_results/models.py:64 django_celery_results/models.py:201
msgid "Content type of the result data"
msgstr "结果数据的内容类型"
#: django_celery_results/models.py:67 django_celery_results/models.py:205
msgid "Result Encoding"
msgstr "结果编码格式"
#: django_celery_results/models.py:68 django_celery_results/models.py:206
msgid "The encoding used to save the task result data"
msgstr "保存结果数据的编码格式"
#: django_celery_results/models.py:71 django_celery_results/models.py:210
msgid "Result Data"
msgstr "结果数据"
#: django_celery_results/models.py:72 django_celery_results/models.py:211
msgid ""
"The data returned by the task. Use content_encoding and content_type fields "
"to read."
msgstr "该任务返回数据,根据 content_encoding 和 content_type 字段读取。"
#: django_celery_results/models.py:76 django_celery_results/models.py:190
msgid "Created DateTime"
msgstr "创建时间"
#: django_celery_results/models.py:77
msgid "Datetime field when the task result was created in UTC"
msgstr "UTC格式的任务创建时间字段"
#: django_celery_results/models.py:80 django_celery_results/models.py:195
msgid "Completed DateTime"
msgstr "完成时间"
#: django_celery_results/models.py:81
msgid "Datetime field when the task was completed in UTC"
msgstr "UTC格式的任务完成时间字段"
#: django_celery_results/models.py:84
msgid "Traceback"
msgstr "Traceback"
#: django_celery_results/models.py:85
msgid "Text of the traceback if the task generated one"
msgstr "任务生成报错时的 traceback 文本"
#: django_celery_results/models.py:88
msgid "Task Meta Information"
msgstr "任务元信息"
#: django_celery_results/models.py:89
msgid ""
"JSON meta information about the task, such as information on child tasks"
msgstr "关于该任务的 JSON 元信息,如子任务的信息"
#: django_celery_results/models.py:99
msgid "task result"
msgstr "任务结果"
#: django_celery_results/models.py:100
msgid "task results"
msgstr "任务结果"
#: django_celery_results/models.py:143 django_celery_results/models.py:185
msgid "Group ID"
msgstr "分组 ID"
#: django_celery_results/models.py:144
msgid "Celery ID for the Chord header group"
msgstr "Chord header 分组的 Celery ID"
#: django_celery_results/models.py:148
msgid ""
"JSON serialized list of task result tuples. use .group_result() to decode"
msgstr "任务结果元组的 JSON 序列化列表。使用 .group_result() 进行解码"
#: django_celery_results/models.py:154
msgid "Starts at len(chord header) and decrements after each task is finished"
msgstr "在 len(chord header) 处开始并且会在每个任务结束后递减"
#: django_celery_results/models.py:186
msgid "Celery ID for the Group that was run"
msgstr "已运行分组的 Celery ID"
#: django_celery_results/models.py:191
msgid "Datetime field when the group result was created in UTC"
msgstr "分组结果创建时的 UTC 格式 datetime 字段"
#: django_celery_results/models.py:196
msgid "Datetime field when the group was completed in UTC"
msgstr "分组结果完成时的 UTC 格式 datetime 字段"
#: django_celery_results/models.py:231
msgid "group result"
msgstr "分组结果"
#: django_celery_results/models.py:232
msgid "group results"
msgstr "分组结果"

View File

@@ -0,0 +1,217 @@
"""Model managers."""
import warnings
from functools import wraps
from itertools import count
from celery.utils.time import maybe_timedelta
from django.conf import settings
from django.db import connections, models, router, transaction
from .utils import now
W_ISOLATION_REP = """
Polling results with transaction isolation level 'repeatable-read'
within the same transaction may give outdated results.
Be sure to commit the transaction for each poll iteration.
"""
class TxIsolationWarning(UserWarning):
"""Warning emitted if the transaction isolation level is suboptimal."""
def transaction_retry(max_retries=1):
"""Decorate a function to retry database operations.
For functions doing database operations, adding
retrying if the operation fails.
Keyword Arguments:
max_retries (int): Maximum number of retries. Default one retry.
"""
def _outer(fun):
@wraps(fun)
def _inner(*args, **kwargs):
_max_retries = kwargs.pop('exception_retry_count', max_retries)
for retries in count(0):
try:
return fun(*args, **kwargs)
except Exception: # pragma: no cover
# Depending on the database backend used we can experience
# various exceptions. E.g. psycopg2 raises an exception
# if some operation breaks the transaction, so saving
# the task result won't be possible until we rollback
# the transaction.
if retries >= _max_retries:
raise
return _inner
return _outer
class ResultManager(models.Manager):
"""Generic manager for celery results."""
def warn_if_repeatable_read(self):
if 'mysql' in self.current_engine().lower():
cursor = self.connection_for_read().cursor()
# MariaDB and MySQL since 8.0 have different transaction isolation
# variables: the former has tx_isolation, while the latter has
# transaction_isolation
if cursor.execute("SHOW VARIABLES WHERE variable_name IN "
"('tx_isolation', 'transaction_isolation');"):
isolation = cursor.fetchone()[1]
if isolation == 'REPEATABLE-READ':
warnings.warn(TxIsolationWarning(W_ISOLATION_REP.strip()))
def connection_for_write(self):
return connections[router.db_for_write(self.model)]
def connection_for_read(self):
return connections[self.db]
def current_engine(self):
try:
return settings.DATABASES[self.db]['ENGINE']
except AttributeError:
return settings.DATABASE_ENGINE
def get_all_expired(self, expires):
"""Get all expired results."""
return self.filter(date_done__lt=now() - maybe_timedelta(expires))
def delete_expired(self, expires):
"""Delete all expired results."""
with transaction.atomic(using=self.db):
self.get_all_expired(expires).delete()
class TaskResultManager(ResultManager):
"""Manager for :class:`~.models.TaskResult` models."""
_last_id = None
def get_task(self, task_id):
"""Get result for task by ``task_id``.
Keyword Arguments:
exception_retry_count (int): How many times to retry by
transaction rollback on exception. This could
happen in a race condition if another worker is trying to
create the same task. The default is to retry once.
"""
try:
return self.get(task_id=task_id)
except self.model.DoesNotExist:
if self._last_id == task_id:
self.warn_if_repeatable_read()
self._last_id = task_id
return self.model(task_id=task_id)
@transaction_retry(max_retries=2)
def store_result(self, content_type, content_encoding,
task_id, result, status,
traceback=None, meta=None,
periodic_task_name=None,
task_name=None, task_args=None, task_kwargs=None,
worker=None, using=None, **kwargs):
"""Store the result and status of a task.
Arguments:
content_type (str): Mime-type of result and meta content.
content_encoding (str): Type of encoding (e.g. binary/utf-8).
task_id (str): Id of task.
periodic_task_name (str): Celery Periodic task name.
task_name (str): Celery task name.
task_args (str): Task arguments.
task_kwargs (str): Task kwargs.
result (str): The serialized return value of the task,
or an exception instance raised by the task.
status (str): Task status. See :mod:`celery.states` for a list of
possible status values.
worker (str): Worker that executes the task.
using (str): Django database connection to use.
traceback (str): The traceback string taken at the point of
exception (only passed if the task failed).
meta (str): Serialized result meta data (this contains e.g.
children).
Keyword Arguments:
exception_retry_count (int): How many times to retry by
transaction rollback on exception. This could
happen in a race condition if another worker is trying to
create the same task. The default is to retry twice.
"""
fields = {
'status': status,
'result': result,
'traceback': traceback,
'meta': meta,
'content_encoding': content_encoding,
'content_type': content_type,
'periodic_task_name': periodic_task_name,
'task_name': task_name,
'task_args': task_args,
'task_kwargs': task_kwargs,
'worker': worker
}
if 'date_started' in kwargs:
fields['date_started'] = kwargs['date_started']
obj, created = self.using(using).get_or_create(task_id=task_id,
defaults=fields)
if not created:
for k, v in fields.items():
setattr(obj, k, v)
obj.save(using=using)
return obj
class GroupResultManager(ResultManager):
"""Manager for :class:`~.models.GroupResult` models."""
_last_id = None
def get_group(self, group_id):
"""Get result for group by ``group_id``.
Keyword Arguments:
exception_retry_count (int): How many times to retry by
transaction rollback on exception. This could
happen in a race condition if another worker is trying to
create the same task. The default is to retry once.
"""
try:
return self.get(group_id=group_id)
except self.model.DoesNotExist:
if self._last_id == group_id:
self.warn_if_repeatable_read()
self._last_id = group_id
return self.model(group_id=group_id)
@transaction_retry(max_retries=2)
def store_group_result(self, content_type, content_encoding,
group_id, result, using=None):
fields = {
'result': result,
'content_encoding': content_encoding,
'content_type': content_type,
}
if not using:
using = self.db
obj, created = self.using(using).get_or_create(group_id=group_id,
defaults=fields)
if not created:
for k, v in fields.items():
setattr(obj, k, v)
obj.save(using=self.db)
return obj

View File

@@ -0,0 +1,59 @@
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='TaskResult',
fields=[
('id', models.AutoField(auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID')),
('task_id', models.CharField(
max_length=getattr(
settings,
'DJANGO_CELERY_RESULTS_TASK_ID_MAX_LENGTH',
255
),
unique=True,
verbose_name='task id'
)),
('status', models.CharField(choices=[('FAILURE', 'FAILURE'),
('PENDING', 'PENDING'),
('RECEIVED', 'RECEIVED'),
('RETRY', 'RETRY'),
('REVOKED', 'REVOKED'),
('STARTED', 'STARTED'),
('SUCCESS', 'SUCCESS')],
default='PENDING',
max_length=50,
verbose_name='state')),
('content_type', models.CharField(
max_length=128, verbose_name='content type')),
('content_encoding', models.CharField(
max_length=64, verbose_name='content encoding')),
('result', models.TextField(default=None, editable=False,
null=True)),
('date_done', models.DateTimeField(
auto_now=True, verbose_name='done at')),
('traceback', models.TextField(
blank=True, null=True, verbose_name='traceback')),
('hidden', models.BooleanField(
db_index=True, default=False, editable=False)),
('meta', models.TextField(default=None, editable=False,
null=True)),
],
options={
'verbose_name': 'task result',
'verbose_name_plural': 'task results',
},
),
]

View File

@@ -0,0 +1,30 @@
# Generated by Django 1.9.1 on 2017-10-26 16:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('django_celery_results', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='taskresult',
name='task_args',
field=models.TextField(null=True, verbose_name='task arguments'),
),
migrations.AddField(
model_name='taskresult',
name='task_kwargs',
field=models.TextField(null=True, verbose_name='task kwargs'),
),
migrations.AddField(
model_name='taskresult',
name='task_name',
field=models.CharField(max_length=255, null=True,
verbose_name='task name'
),
),
]

View File

@@ -0,0 +1,21 @@
# Generated by Django 2.1 on 2018-11-06 11:01
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('django_celery_results', '0002_add_task_name_args_kwargs'),
]
operations = [
migrations.AlterModelOptions(
name='taskresult',
options={
'ordering': ['-date_done'],
'verbose_name': 'task result',
'verbose_name_plural': 'task results'
},
),
]

View File

@@ -0,0 +1,97 @@
# Generated by Django 1.11.20 on 2019-05-16 04:12
# this file is auto-generated so don't do flake8 on it
# flake8: noqa
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('django_celery_results', '0003_auto_20181106_1101'),
]
operations = [
migrations.AlterField(
model_name='taskresult',
name='content_encoding',
field=models.CharField(help_text='The encoding used to save the task result data', max_length=64, verbose_name='Result Encoding'),
),
migrations.AlterField(
model_name='taskresult',
name='content_type',
field=models.CharField(help_text='Content type of the result data', max_length=128, verbose_name='Result Content Type'),
),
migrations.AlterField(
model_name='taskresult',
name='date_done',
field=models.DateTimeField(auto_now=True, db_index=True, help_text='Datetime field when the task was completed in UTC', verbose_name='Completed DateTime'),
),
migrations.AlterField(
model_name='taskresult',
name='hidden',
field=models.BooleanField(db_index=True, default=False, editable=False, help_text='Soft Delete flag that can be used instead of full delete', verbose_name='Hidden'),
),
migrations.AlterField(
model_name='taskresult',
name='meta',
field=models.TextField(default=None, editable=False, help_text='JSON meta information about the task, such as information on child tasks', null=True, verbose_name='Task Meta Information'),
),
migrations.AlterField(
model_name='taskresult',
name='result',
field=models.TextField(default=None, editable=False, help_text='The data returned by the task. Use content_encoding and content_type fields to read.', null=True, verbose_name='Result Data'),
),
migrations.AlterField(
model_name='taskresult',
name='status',
field=models.CharField(choices=[('FAILURE', 'FAILURE'), ('PENDING', 'PENDING'), ('RECEIVED', 'RECEIVED'), ('RETRY', 'RETRY'), ('REVOKED', 'REVOKED'), ('STARTED', 'STARTED'), ('SUCCESS', 'SUCCESS')], db_index=True, default='PENDING', help_text='Current state of the task being run', max_length=50, verbose_name='Task State'),
),
migrations.AlterField(
model_name='taskresult',
name='task_args',
field=models.TextField(help_text='JSON representation of the positional arguments used with the task', null=True, verbose_name='Task Positional Arguments'),
),
migrations.AlterField(
model_name='taskresult',
name='task_id',
field=models.CharField(
db_index=True,
help_text='Celery ID for the Task that was run',
max_length=getattr(
settings,
'DJANGO_CELERY_RESULTS_TASK_ID_MAX_LENGTH',
255
),
unique=True,
verbose_name='Task ID'
),
),
migrations.AlterField(
model_name='taskresult',
name='task_kwargs',
field=models.TextField(help_text='JSON representation of the named arguments used with the task', null=True, verbose_name='Task Named Arguments'),
),
migrations.AlterField(
model_name='taskresult',
name='task_name',
field=models.CharField(
db_index=True,
help_text='Name of the Task which was run',
max_length=getattr(
settings,
'DJANGO_CELERY_RESULTS_TASK_ID_MAX_LENGTH',
255
),
null=True,
verbose_name='Task Name'),
),
migrations.AlterField(
model_name='taskresult',
name='traceback',
field=models.TextField(blank=True, help_text='Text of the traceback if the task generated one', null=True, verbose_name='Traceback'),
),
]

View File

@@ -0,0 +1,25 @@
# Generated by Django 1.11.22 on 2019-07-24 15:38
# this file is auto-generated so don't do flake8 on it
# flake8: noqa
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('django_celery_results', '0004_auto_20190516_0412'),
]
operations = [
migrations.AddField(
model_name='taskresult',
name='worker',
field=models.CharField(db_index=True, default=None,
help_text='Worker that executes the task',
max_length=100, null=True,
verbose_name='Worker'),
),
]

View File

@@ -0,0 +1,46 @@
# Generated by Django 2.2.4 on 2019-08-21 19:53
# this file is auto-generated so don't do flake8 on it
# flake8: noqa
import django.utils.timezone
from django.db import migrations, models
def copy_date_done_to_date_created(apps, schema_editor):
TaskResult = apps.get_model('django_celery_results', 'taskresult')
db_alias = schema_editor.connection.alias
TaskResult.objects.using(db_alias).all().update(
date_created=models.F('date_done')
)
def reverse_copy_date_done_to_date_created(app, schema_editor):
# the reverse of 'copy_date_done_to_date_created' is do nothing
# because the 'date_created' will be removed.
pass
class Migration(migrations.Migration):
dependencies = [
('django_celery_results', '0005_taskresult_worker'),
]
operations = [
migrations.AddField(
model_name='taskresult',
name='date_created',
field=models.DateTimeField(
auto_now_add=True,
db_index=True,
default=django.utils.timezone.now,
help_text='Datetime field when the task result was created in UTC',
verbose_name='Created DateTime'
),
preserve_default=False,
),
migrations.RunPython(copy_date_done_to_date_created,
reverse_copy_date_done_to_date_created),
]

View File

@@ -0,0 +1,21 @@
# Generated by Django 2.2.6 on 2019-10-27 11:29
# this file is auto-generated so don't do flake8 on it
# flake8: noqa
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('django_celery_results', '0006_taskresult_date_created'),
]
operations = [
migrations.RemoveField(
model_name='taskresult',
name='hidden',
),
]

View File

@@ -0,0 +1,40 @@
# Generated by Django 3.0.6 on 2020-05-12 12:05
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('django_celery_results', '0007_remove_taskresult_hidden'),
]
operations = [
migrations.CreateModel(
name='ChordCounter',
fields=[
('id', models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID')),
('group_id', models.CharField(
db_index=True,
help_text='Celery ID for the Chord header group',
max_length=getattr(
settings,
'DJANGO_CELERY_RESULTS_TASK_ID_MAX_LENGTH',
255
),
unique=True,
verbose_name='Group ID')),
('sub_tasks', models.TextField(
help_text='JSON serialized list of task result tuples. '
'use .group_result() to decode')),
('count', models.PositiveIntegerField(
help_text='Starts at len(chord header) '
'and decrements after each task is finished')),
],
),
]

View File

@@ -0,0 +1,222 @@
# Generated by Django 3.2 on 2021-04-19 14:55
from django.conf import settings
from django.db import migrations, models
class FakeAddIndex(migrations.AddIndex):
"""Fake AddIndex to correct for duplicate index
added in the original 0009 migration
"""
def database_forwards(self, *args, **kwargs):
"""Don't do anything"""
def database_backwards(self, *args, **kwargs):
"""Also don't do anything on reverting this migration
The duplicate index will be cleaned up when migrating from the
original 0009 to the cleanup 0010
"""
class Migration(migrations.Migration):
dependencies = [
('django_celery_results', '0008_chordcounter'),
]
operations = [
migrations.CreateModel(
name='GroupResult',
fields=[
('id', models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID')),
('group_id', models.CharField(
help_text='Celery ID for the Group that was run',
max_length=getattr(
settings,
'DJANGO_CELERY_RESULTS_TASK_ID_MAX_LENGTH',
255
),
unique=True,
verbose_name='Group ID')),
('date_created', models.DateTimeField(
auto_now_add=True,
help_text='Datetime field when the group result '
'was created in UTC',
verbose_name='Created DateTime')),
('date_done', models.DateTimeField(
auto_now=True,
help_text='Datetime field when the group was '
'completed in UTC',
verbose_name='Completed DateTime')),
('content_type', models.CharField(
help_text='Content type of the result data',
max_length=128,
verbose_name='Result Content Type')),
('content_encoding', models.CharField(
help_text='The encoding used to save the task '
'result data',
max_length=64,
verbose_name='Result Encoding')),
('result', models.TextField(
default=None,
editable=False,
help_text='The data returned by the task. Use '
'content_encoding and content_type '
'fields to read.',
null=True,
verbose_name='Result Data')),
],
options={
'verbose_name': 'group result',
'verbose_name_plural': 'group results',
'ordering': ['-date_done'],
},
),
migrations.AlterField(
model_name='chordcounter',
name='group_id',
field=models.CharField(
help_text='Celery ID for the Chord header group',
max_length=getattr(
settings,
'DJANGO_CELERY_RESULTS_TASK_ID_MAX_LENGTH',
255
),
unique=True,
verbose_name='Group ID'),
),
migrations.AlterField(
model_name='taskresult',
name='date_created',
field=models.DateTimeField(
auto_now_add=True,
help_text='Datetime field when the task result '
'was created in UTC',
verbose_name='Created DateTime'),
),
migrations.AlterField(
model_name='taskresult',
name='date_done',
field=models.DateTimeField(
auto_now=True,
help_text='Datetime field when the task was completed in UTC',
verbose_name='Completed DateTime'),
),
migrations.AlterField(
model_name='taskresult',
name='status',
field=models.CharField(
choices=[
('FAILURE', 'FAILURE'),
('PENDING', 'PENDING'),
('RECEIVED', 'RECEIVED'),
('RETRY', 'RETRY'),
('REVOKED', 'REVOKED'),
('STARTED', 'STARTED'),
('SUCCESS', 'SUCCESS')],
default='PENDING',
help_text='Current state of the task being run',
max_length=50,
verbose_name='Task State'),
),
migrations.AlterField(
model_name='taskresult',
name='task_id',
field=models.CharField(
help_text='Celery ID for the Task that was run',
max_length=getattr(
settings,
'DJANGO_CELERY_RESULTS_TASK_ID_MAX_LENGTH',
255
),
unique=True,
verbose_name='Task ID'),
),
migrations.AlterField(
model_name='taskresult',
name='task_name',
field=models.CharField(
help_text='Name of the Task which was run',
max_length=getattr(
settings,
'DJANGO_CELERY_RESULTS_TASK_ID_MAX_LENGTH',
255
),
null=True,
verbose_name='Task Name'),
),
migrations.AlterField(
model_name='taskresult',
name='worker',
field=models.CharField(
default=None,
help_text='Worker that executes the task',
max_length=100,
null=True,
verbose_name='Worker'),
),
FakeAddIndex(
model_name='chordcounter',
index=models.Index(
fields=['group_id'],
name='django_cele_group_i_299b0d_idx'),
),
FakeAddIndex(
model_name='taskresult',
index=models.Index(
fields=['task_id'],
name='django_cele_task_id_7f8fca_idx'),
),
migrations.AddIndex(
model_name='taskresult',
index=models.Index(
fields=['task_name'],
name='django_cele_task_na_08aec9_idx'),
),
migrations.AddIndex(
model_name='taskresult',
index=models.Index(
fields=['status'],
name='django_cele_status_9b6201_idx'),
),
migrations.AddIndex(
model_name='taskresult',
index=models.Index(
fields=['worker'],
name='django_cele_worker_d54dd8_idx'),
),
migrations.AddIndex(
model_name='taskresult',
index=models.Index(
fields=['date_created'],
name='django_cele_date_cr_f04a50_idx'),
),
migrations.AddIndex(
model_name='taskresult',
index=models.Index(
fields=['date_done'],
name='django_cele_date_do_f59aad_idx'),
),
FakeAddIndex(
model_name='groupresult',
index=models.Index(
fields=['group_id'],
name='django_cele_group_i_3cddec_idx'),
),
migrations.AddIndex(
model_name='groupresult',
index=models.Index(
fields=['date_created'],
name='django_cele_date_cr_bd6c1d_idx'),
),
migrations.AddIndex(
model_name='groupresult',
index=models.Index(
fields=['date_done'],
name='django_cele_date_do_caae0e_idx'),
),
]

View File

@@ -0,0 +1,53 @@
"""
Migration to amend the 0009 migration released on django_celery_results 2.1.0
That migration introduced duplicate indexes breaking Oracle support.
This migration will remove those indexes (on non-Oracle db's)
while in-place changing migration 0009
to not add the duplicates for new installs
"""
from django.db import DatabaseError, migrations
class TryRemoveIndex(migrations.RemoveIndex):
"""Operation to remove the Index
without reintroducing it on reverting the migration
"""
def database_forwards(self, *args, **kwargs):
"""Remove the index on the database if it exists"""
try:
super().database_forwards(*args, **kwargs)
except DatabaseError:
pass
except Exception:
# Not all DB engines throw DatabaseError when the
# index does not exist.
pass
def database_backwards(self, *args, **kwargs):
"""Don't re-add the index when reverting this migration"""
pass
class Migration(migrations.Migration):
dependencies = [
('django_celery_results', '0009_groupresult'),
]
operations = [
TryRemoveIndex(
model_name='chordcounter',
name='django_cele_group_i_299b0d_idx',
),
TryRemoveIndex(
model_name='groupresult',
name='django_cele_group_i_3cddec_idx',
),
TryRemoveIndex(
model_name='taskresult',
name='django_cele_task_id_7f8fca_idx',
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 3.2.8 on 2021-11-10 08:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('django_celery_results', '0010_remove_duplicate_indices'),
]
operations = [
migrations.AddField(
model_name='taskresult',
name='periodic_task_name',
field=models.CharField(
help_text='Name of the Periodic Task which was run',
max_length=255,
null=True,
verbose_name='Periodic Task Name'),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 4.2.13 on 2024-06-02 07:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('django_celery_results', '0011_taskresult_periodic_task_name'),
]
operations = [
migrations.AddField(
model_name='taskresult',
name='date_started',
field=models.DateTimeField(
default=None,
help_text='Datetime field when the task was started in UTC',
null=True,
verbose_name='Started DateTime',
),
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 5.1.3 on 2024-11-05 13:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('django_celery_results', '0012_taskresult_date_started'),
]
operations = [
migrations.AddIndex(
model_name='taskresult',
index=models.Index(
fields=['periodic_task_name'],
name='django_cele_periodi_1993cf_idx'
),
),
]

View File

@@ -0,0 +1,26 @@
# Generated by Django 5.1.7 on 2025-03-08 06:18
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
(
"django_celery_results",
"0013_taskresult_django_cele_periodi_1993cf_idx"
),
]
operations = [
migrations.AlterField(
model_name="taskresult",
name="status",
field=models.CharField(
default="PENDING",
help_text="Current state of the task being run",
max_length=50,
verbose_name="Task State",
),
),
]

View File

@@ -0,0 +1,245 @@
"""Database models."""
import json
from celery import states
from celery.result import GroupResult as CeleryGroupResult
from celery.result import result_from_tuple
from django.conf import settings
from django.db import models
from django.utils.translation import gettext_lazy as _
from . import managers
ALL_STATES = sorted(states.ALL_STATES)
TASK_STATE_CHOICES = sorted(zip(ALL_STATES, ALL_STATES))
class TaskResult(models.Model):
"""Task result/status."""
task_id = models.CharField(
max_length=getattr(
settings,
'DJANGO_CELERY_RESULTS_TASK_ID_MAX_LENGTH',
255
),
unique=True,
verbose_name=_('Task ID'),
help_text=_('Celery ID for the Task that was run'))
periodic_task_name = models.CharField(
null=True, max_length=255,
verbose_name=_('Periodic Task Name'),
help_text=_('Name of the Periodic Task which was run'))
task_name = models.CharField(
null=True, max_length=getattr(
settings,
'DJANGO_CELERY_RESULTS_TASK_ID_MAX_LENGTH',
255
),
verbose_name=_('Task Name'),
help_text=_('Name of the Task which was run'))
task_args = models.TextField(
null=True,
verbose_name=_('Task Positional Arguments'),
help_text=_('JSON representation of the positional arguments '
'used with the task'))
task_kwargs = models.TextField(
null=True,
verbose_name=_('Task Named Arguments'),
help_text=_('JSON representation of the named arguments '
'used with the task'))
status = models.CharField(
max_length=50, default=states.PENDING,
verbose_name=_('Task State'),
help_text=_('Current state of the task being run'))
worker = models.CharField(
max_length=100, default=None, null=True,
verbose_name=_('Worker'), help_text=_('Worker that executes the task')
)
content_type = models.CharField(
max_length=128,
verbose_name=_('Result Content Type'),
help_text=_('Content type of the result data'))
content_encoding = models.CharField(
max_length=64,
verbose_name=_('Result Encoding'),
help_text=_('The encoding used to save the task result data'))
result = models.TextField(
null=True, default=None, editable=False,
verbose_name=_('Result Data'),
help_text=_('The data returned by the task. '
'Use content_encoding and content_type fields to read.'))
date_created = models.DateTimeField(
auto_now_add=True,
verbose_name=_('Created DateTime'),
help_text=_('Datetime field when the task result was created in UTC'))
date_started = models.DateTimeField(
null=True, default=None,
verbose_name=_('Started DateTime'),
help_text=_('Datetime field when the task was started in UTC'))
date_done = models.DateTimeField(
auto_now=True,
verbose_name=_('Completed DateTime'),
help_text=_('Datetime field when the task was completed in UTC'))
traceback = models.TextField(
blank=True, null=True,
verbose_name=_('Traceback'),
help_text=_('Text of the traceback if the task generated one'))
meta = models.TextField(
null=True, default=None, editable=False,
verbose_name=_('Task Meta Information'),
help_text=_('JSON meta information about the task, '
'such as information on child tasks'))
objects = managers.TaskResultManager()
class Meta:
"""Table information."""
ordering = ['-date_done']
verbose_name = _('task result')
verbose_name_plural = _('task results')
# Explicit names to solve https://code.djangoproject.com/ticket/33483
indexes = [
models.Index(fields=['task_name'],
name='django_cele_task_na_08aec9_idx'),
models.Index(fields=['status'],
name='django_cele_status_9b6201_idx'),
models.Index(fields=['worker'],
name='django_cele_worker_d54dd8_idx'),
models.Index(fields=['date_created'],
name='django_cele_date_cr_f04a50_idx'),
models.Index(fields=['date_done'],
name='django_cele_date_do_f59aad_idx'),
models.Index(fields=['periodic_task_name'],
name='django_cele_periodi_1993cf_idx'),
]
def as_dict(self):
return {
'task_id': self.task_id,
'task_name': self.task_name,
'task_args': self.task_args,
'task_kwargs': self.task_kwargs,
'status': self.status,
'result': self.result,
'date_done': self.date_done,
'traceback': self.traceback,
'meta': self.meta,
'worker': self.worker
}
def __str__(self):
return '<Task: {0.task_id} ({0.status})>'.format(self)
class ChordCounter(models.Model):
"""Chord synchronisation."""
group_id = models.CharField(
max_length=getattr(
settings,
"DJANGO_CELERY_RESULTS_TASK_ID_MAX_LENGTH",
255),
unique=True,
verbose_name=_("Group ID"),
help_text=_("Celery ID for the Chord header group"),
)
sub_tasks = models.TextField(
help_text=_(
"JSON serialized list of task result tuples. "
"use .group_result() to decode"
)
)
count = models.PositiveIntegerField(
help_text=_(
"Starts at len(chord header) and decrements after each task is "
"finished"
)
)
def group_result(self, app=None):
"""Return the :class:`celery.result.GroupResult` of self.
Arguments:
app (celery.app.base.Celery): app instance to create the
:class:`celery.result.GroupResult` with.
"""
return CeleryGroupResult(
self.group_id,
[result_from_tuple(r, app=app)
for r in json.loads(self.sub_tasks)],
app=app
)
class GroupResult(models.Model):
"""Task Group result/status."""
group_id = models.CharField(
max_length=getattr(
settings,
"DJANGO_CELERY_RESULTS_TASK_ID_MAX_LENGTH",
255
),
unique=True,
verbose_name=_("Group ID"),
help_text=_("Celery ID for the Group that was run"),
)
date_created = models.DateTimeField(
auto_now_add=True,
verbose_name=_("Created DateTime"),
help_text=_("Datetime field when the group result was created in UTC"),
)
date_done = models.DateTimeField(
auto_now=True,
verbose_name=_("Completed DateTime"),
help_text=_("Datetime field when the group was completed in UTC"),
)
content_type = models.CharField(
max_length=128,
verbose_name=_("Result Content Type"),
help_text=_("Content type of the result data"),
)
content_encoding = models.CharField(
max_length=64,
verbose_name=_("Result Encoding"),
help_text=_("The encoding used to save the task result data"),
)
result = models.TextField(
null=True, default=None, editable=False,
verbose_name=_('Result Data'),
help_text=_('The data returned by the task. '
'Use content_encoding and content_type fields to read.'))
def as_dict(self):
return {
'group_id': self.group_id,
'result': self.result,
'date_done': self.date_done,
}
def __str__(self):
return f'<Group: {self.group_id}>'
objects = managers.GroupResultManager()
class Meta:
"""Table information."""
ordering = ['-date_done']
verbose_name = _('group result')
verbose_name_plural = _('group results')
# Explicit names to solve https://code.djangoproject.com/ticket/33483
indexes = [
models.Index(fields=['date_created'],
name='django_cele_date_cr_bd6c1d_idx'),
models.Index(fields=['date_done'],
name='django_cele_date_do_caae0e_idx'),
]

View File

@@ -0,0 +1,86 @@
"""URLs defined for celery.
* ``/$task_id/done/``
URL to :func:`~celery.views.is_successful`.
* ``/$task_id/status/``
URL to :func:`~celery.views.task_status`.
"""
import warnings
from django.conf import settings
from django.urls import path, register_converter
from . import views
class TaskPatternConverter:
"""Custom path converter for task & group id's.
They are slightly different from the built `uuid`
"""
regex = r'[\w\d\-\.]+'
def to_python(self, value):
"""Convert url to python value."""
return str(value)
def to_url(self, value):
"""Convert python value into url, just a string."""
return value
register_converter(TaskPatternConverter, 'task_pattern')
urlpatterns = [
path(
'task/done/<task_pattern:task_id>/',
views.is_task_successful,
name='celery-is_task_successful'
),
path(
'task/status/<task_pattern:task_id>/',
views.task_status,
name='celery-task_status'
),
path(
'group/done/<task_pattern:group_id>/',
views.is_group_successful,
name='celery-is_group_successful'
),
path(
'group/status/<task_pattern:group_id>/',
views.group_status,
name='celery-group_status'
),
]
if getattr(settings, 'DJANGO_CELERY_RESULTS_ID_FIRST_URLS', True):
warnings.warn(
"ID first urls depricated, use noun first urls instead."
"Will be removed in 2022.",
DeprecationWarning
)
urlpatterns += [
path(
'<task_pattern:task_id>/done/',
views.is_task_successful,
name='celery-is_task_successful'
),
path(
'<task_pattern:task_id>/status/',
views.task_status,
name='celery-task_status'
),
path(
'<task_pattern:group_id>/group/done/',
views.is_group_successful,
name='celery-is_group_successful'
),
path(
'<task_pattern:group_id>/group/status/',
views.group_status,
name='celery-group_status'
),
]

View File

@@ -0,0 +1,17 @@
"""Utilities."""
# -- XXX This module must not use translation as that causes
# -- a recursive loader import!
from django.conf import settings
from django.utils import timezone
# see Issue celery/django-celery#222
now_localtime = getattr(timezone, 'template_localtime', timezone.localtime)
def now():
"""Return the current date and time."""
if getattr(settings, 'USE_TZ', False):
return now_localtime(timezone.now())
else:
return timezone.now()

View File

@@ -0,0 +1,53 @@
"""Views."""
from celery import states
from celery.result import AsyncResult, GroupResult
from celery.utils import get_full_cls_name
from django.http import JsonResponse
from kombu.utils.encoding import safe_repr
def is_task_successful(request, task_id):
"""Return task execution status in JSON format."""
return JsonResponse({'task': {
'id': task_id,
'executed': AsyncResult(task_id).successful(),
}})
def task_status(request, task_id):
"""Return task status and result in JSON format."""
result = AsyncResult(task_id)
state, retval = result.state, result.result
response_data = {'id': task_id, 'status': state, 'result': retval}
if state in states.EXCEPTION_STATES:
traceback = result.traceback
response_data.update({'result': safe_repr(retval),
'exc': get_full_cls_name(retval.__class__),
'traceback': traceback})
return JsonResponse({'task': response_data})
def is_group_successful(request, group_id):
"""Return if group was successfull as boolean."""
results = GroupResult.restore(group_id)
return JsonResponse({
'group': {
'id': group_id,
'results': [
{'id': task.id, 'executed': task.successful()}
for task in results
] if results else []
}
})
def group_status(request, group_id):
"""Return group id and its async results status & result in JSON format."""
result = GroupResult.restore(group_id)
retval = [
{"result": async_result.result, "status": async_result.status}
for async_result in result.results
]
response_data = {'id': group_id, 'results': retval}
return JsonResponse({'group': response_data})