diff -Nru python-django-celery-results-1.0.4/.bumpversion.cfg python-django-celery-results-2.0.0/.bumpversion.cfg --- python-django-celery-results-1.0.4/.bumpversion.cfg 2018-11-13 16:28:15.000000000 +0000 +++ python-django-celery-results-2.0.0/.bumpversion.cfg 2020-11-19 11:02:44.000000000 +0000 @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.0.4 +current_version = 2.0.0 commit = True tag = True parse = (?P\d+)\.(?P\d+)\.(?P\d+)(?P[a-z]+)? diff -Nru python-django-celery-results-1.0.4/Changelog python-django-celery-results-2.0.0/Changelog --- python-django-celery-results-1.0.4/Changelog 2018-11-13 16:28:15.000000000 +0000 +++ python-django-celery-results-2.0.0/Changelog 2020-11-19 11:02:44.000000000 +0000 @@ -4,6 +4,45 @@ Change history ================ +.. _version-2.0.0: + +2.0.0 +===== +:release-date: +:release-by: + +- Add Spanish translations (#134) +- Add support for Django 3.0 and 3.1 (#145, #163) +- Add support for Celery 5 (#163) +- Drop support for Django < 2.2 (#147, #152) +- Drop support for Python < 3.6 (#146, #147, #152) +- Add Chord syncronisation from the database (#144) +- Encode `task_args` and `task_kwargs` of `TaskResult` using `json.dumps` instead of using `str` (#78) + +.. _version-1.1.2: + +1.1.2 +===== +:release-date: 2019-06-06 00:00 a.m. UTC+6:00 +:release-by: Asif Saif Uddin + + +- Fixed few regressions + +.. _version-1.1.0: + +1.1.0 +===== +:release-date: 2019-05-21 17:00 p.m. UTC+6:00 +:release-by: Asif Saif Uddin + + +- Django 2.2+. +- Drop python 3.4 and django 2.0 +- Support specifying the database to use for the store_result method (#63) +- Fix MySQL8 system variable tx_isolation issue (#84) + + .. _version-1.0.4: 1.0.4 diff -Nru python-django-celery-results-1.0.4/conftest.py python-django-celery-results-2.0.0/conftest.py --- python-django-celery-results-1.0.4/conftest.py 1970-01-01 00:00:00.000000000 +0000 +++ python-django-celery-results-2.0.0/conftest.py 2020-11-19 11:02:44.000000000 +0000 @@ -0,0 +1,38 @@ +import pytest + + +def pytest_addoption(parser): + parser.addoption( + '-B', + '--run-benchmarks', + action='store_true', + default=False, + help='run benchmarks', + ) + + +def pytest_runtest_setup(item): + """ + Skip tests marked benchmark unless --run-benchmark is given to pytest + """ + run_benchmarks = item.config.getoption('--run-benchmarks') + + is_benchmark = any(item.iter_markers(name="benchmark")) + + if is_benchmark: + if run_benchmarks: + return + + pytest.skip( + 'need --run-benchmarks to run benchmarks' + ) + + +def pytest_collection_modifyitems(items): + """ + Add the "benchmark" mark to tests that start with "benchmark_". + """ + for item in items: + test_class_name = item.cls.__name__ + if test_class_name.startswith("benchmark_"): + item.add_marker(pytest.mark.benchmark) diff -Nru python-django-celery-results-1.0.4/debian/changelog python-django-celery-results-2.0.0/debian/changelog --- python-django-celery-results-1.0.4/debian/changelog 2018-11-18 16:48:58.000000000 +0000 +++ python-django-celery-results-2.0.0/debian/changelog 2020-11-30 20:04:31.000000000 +0000 @@ -1,3 +1,24 @@ +python-django-celery-results (2.0.0-1) unstable; urgency=low + + [ Ondřej Nový ] + * Use debhelper-compat instead of debian/compat. + * d/control: Update Maintainer field with new Debian Python Team + contact address. + * d/control: Update Vcs-* fields with new Debian Python Team Salsa + layout. + + [ Michael Fladischer ] + * New upstream release (Closes: #973210). + * Bump debhelper version to 13. + * Bump Standards-Version to 4.5.1. + * Use uscan version 4. + * Set Rules-Requires-Root: no. + * Add python3-mock to Build-Depends, required by tests. + * Add d/upstream/metadata. + * Add patch to skip concurrent transaction test if SQLite is used. + + -- Michael Fladischer Mon, 30 Nov 2020 21:04:31 +0100 + python-django-celery-results (1.0.4-1) unstable; urgency=low [ Ondřej Nový ] diff -Nru python-django-celery-results-1.0.4/debian/compat python-django-celery-results-2.0.0/debian/compat --- python-django-celery-results-1.0.4/debian/compat 2018-11-18 16:48:58.000000000 +0000 +++ python-django-celery-results-2.0.0/debian/compat 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -11 diff -Nru python-django-celery-results-1.0.4/debian/control python-django-celery-results-2.0.0/debian/control --- python-django-celery-results-1.0.4/debian/control 2018-11-18 16:48:58.000000000 +0000 +++ python-django-celery-results-2.0.0/debian/control 2020-11-30 20:04:31.000000000 +0000 @@ -1,26 +1,28 @@ Source: python-django-celery-results Section: python Priority: optional -Maintainer: Debian Python Modules Team +Maintainer: Debian Python Team Uploaders: Michael Fladischer , Build-Depends: - debhelper (>= 11), + debhelper-compat (= 13), dh-python, python3-all, python3-case, python3-celery (>= 4.1.0), python3-django, + python3-mock, python3-pytest, python3-pytest-django, python3-setuptools, python3-sphinx, python3-sphinx-celery, -Standards-Version: 4.2.1 +Standards-Version: 4.5.1 Homepage: https://github.com/celery/django-celery-results/ -Vcs-Browser: https://salsa.debian.org/python-team/modules/python-django-celery-results -Vcs-Git: https://salsa.debian.org/python-team/modules/python-django-celery-results.git +Vcs-Browser: https://salsa.debian.org/python-team/packages/python-django-celery-results +Vcs-Git: https://salsa.debian.org/python-team/packages/python-django-celery-results.git Testsuite: autopkgtest-pkg-python +Rules-Requires-Root: no Package: python-django-celery-results-doc Section: doc diff -Nru python-django-celery-results-1.0.4/debian/patches/0002-Skip-concurrent-transaction-test-if-SQLite-is-used.patch python-django-celery-results-2.0.0/debian/patches/0002-Skip-concurrent-transaction-test-if-SQLite-is-used.patch --- python-django-celery-results-1.0.4/debian/patches/0002-Skip-concurrent-transaction-test-if-SQLite-is-used.patch 1970-01-01 00:00:00.000000000 +0000 +++ python-django-celery-results-2.0.0/debian/patches/0002-Skip-concurrent-transaction-test-if-SQLite-is-used.patch 2020-11-30 20:04:31.000000000 +0000 @@ -0,0 +1,29 @@ +From: Michael Fladischer +Date: Mon, 30 Nov 2020 20:50:21 +0100 +Subject: Skip concurrent transaction test if SQLite is used. + +--- + t/unit/test_models.py | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +diff --git a/t/unit/test_models.py b/t/unit/test_models.py +index ae04c04..4a4dfa1 100644 +--- a/t/unit/test_models.py ++++ b/t/unit/test_models.py +@@ -4,7 +4,7 @@ import pytest + + from datetime import datetime, timedelta + +-from django.db import transaction ++from django.db import transaction, connection + from django.test import TransactionTestCase + + from celery import states, uuid +@@ -65,6 +65,7 @@ class test_Models(TransactionTestCase): + ) + assert m1 not in TaskResult.objects.all() + ++ @pytest.mark.skipif(connection.vendor == "sqlite", reason="Concurrent transactions not supported by SQLite") + def test_store_result(self, ctype='application/json', cenc='utf-8'): + """ + Test the `using` argument. diff -Nru python-django-celery-results-1.0.4/debian/patches/series python-django-celery-results-2.0.0/debian/patches/series --- python-django-celery-results-1.0.4/debian/patches/series 2018-11-18 16:48:58.000000000 +0000 +++ python-django-celery-results-2.0.0/debian/patches/series 2020-11-30 20:04:31.000000000 +0000 @@ -1 +1,2 @@ 0001-Disable-intersphinx-mapping-for-now.patch +0002-Skip-concurrent-transaction-test-if-SQLite-is-used.patch diff -Nru python-django-celery-results-1.0.4/debian/upstream/metadata python-django-celery-results-2.0.0/debian/upstream/metadata --- python-django-celery-results-1.0.4/debian/upstream/metadata 1970-01-01 00:00:00.000000000 +0000 +++ python-django-celery-results-2.0.0/debian/upstream/metadata 2020-11-30 20:04:31.000000000 +0000 @@ -0,0 +1,4 @@ +Bug-Database: https://github.com/celery/django-celery-results/issues +Bug-Submit: https://github.com/celery/django-celery-results/issues/new +Repository: https://github.com/celery/django-celery-results.git +Repository-Browse: https://github.com/celery/django-celery-results diff -Nru python-django-celery-results-1.0.4/debian/watch python-django-celery-results-2.0.0/debian/watch --- python-django-celery-results-1.0.4/debian/watch 2018-11-18 16:48:58.000000000 +0000 +++ python-django-celery-results-2.0.0/debian/watch 2020-11-30 20:04:31.000000000 +0000 @@ -1,4 +1,4 @@ -version=3 +version=4 opts=filenamemangle=s/.*\/v([\d\.]+.*)$/python-django-celery-results-$1/ \ https://github.com/celery/django-celery-results/releases \ /celery/django-celery-results/archive/v([\d\.]+)\.tar\.gz diff -Nru python-django-celery-results-1.0.4/django_celery_results/admin.py python-django-celery-results-2.0.0/django_celery_results/admin.py --- python-django-celery-results-1.0.4/django_celery_results/admin.py 2018-11-13 16:28:15.000000000 +0000 +++ python-django-celery-results-2.0.0/django_celery_results/admin.py 2020-11-19 11:02:44.000000000 +0000 @@ -4,10 +4,12 @@ from django.contrib import admin from django.conf import settings +from django.utils.translation import gettext_lazy as _ + try: ALLOW_EDITS = settings.DJANGO_CELERY_RESULTS['ALLOW_EDITS'] -except (AttributeError, KeyError) as e: - ALLOW_EDITS = True +except (AttributeError, KeyError): + ALLOW_EDITS = False pass from .models import TaskResult @@ -18,9 +20,9 @@ model = TaskResult date_hierarchy = 'date_done' - list_display = ('task_id', 'task_name', 'date_done', 'status') - list_filter = ('status', 'date_done', 'task_name',) - readonly_fields = ('date_done', 'result', 'hidden', 'meta') + list_display = ('task_id', 'task_name', 'date_done', 'status', 'worker') + list_filter = ('status', 'date_done', 'task_name', 'worker') + readonly_fields = ('date_created', 'date_done', 'result', 'meta') search_fields = ('task_name', 'task_id', 'status') fieldsets = ( (None, { @@ -28,24 +30,25 @@ 'task_id', 'task_name', 'status', + 'worker', 'content_type', 'content_encoding', ), 'classes': ('extrapretty', 'wide') }), - ('Parameters', { + (_('Parameters'), { 'fields': ( 'task_args', 'task_kwargs', ), 'classes': ('extrapretty', 'wide') }), - ('Result', { + (_('Result'), { 'fields': ( 'result', + 'date_created', 'date_done', 'traceback', - 'hidden', 'meta', ), 'classes': ('extrapretty', 'wide') diff -Nru python-django-celery-results-1.0.4/django_celery_results/apps.py python-django-celery-results-2.0.0/django_celery_results/apps.py --- python-django-celery-results-1.0.4/django_celery_results/apps.py 2018-11-13 16:28:15.000000000 +0000 +++ python-django-celery-results-2.0.0/django_celery_results/apps.py 2020-11-19 11:02:44.000000000 +0000 @@ -2,7 +2,7 @@ from __future__ import absolute_import, unicode_literals from django.apps import AppConfig -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ __all__ = ['CeleryResultConfig'] diff -Nru python-django-celery-results-1.0.4/django_celery_results/backends/database.py python-django-celery-results-2.0.0/django_celery_results/backends/database.py --- python-django-celery-results-1.0.4/django_celery_results/backends/database.py 2018-11-13 16:28:15.000000000 +0000 +++ python-django-celery-results-2.0.0/django_celery_results/backends/database.py 2020-11-19 11:02:44.000000000 +0000 @@ -1,9 +1,19 @@ from __future__ import absolute_import, unicode_literals +import json + +from celery import maybe_signature from celery.backends.base import BaseDictBackend +from celery.exceptions import ChordError +from celery.result import allow_join_result from celery.utils.serialization import b64encode, b64decode +from celery.utils.log import get_logger +from django.db import transaction + +from ..models import TaskResult, ChordCounter + -from ..models import TaskResult +logger = get_logger(__name__) class DatabaseBackend(BaseDictBackend): @@ -14,16 +24,21 @@ subpolling_interval = 0.5 def _store_result(self, task_id, result, status, - traceback=None, request=None): + 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.encode_content({ 'children': self.current_task_children(request), }) - task_name = getattr(request, 'task', None) if request else None - task_args = getattr(request, 'args', None) if request else None - task_kwargs = getattr(request, 'kwargs', None) if request else None + task_name = getattr(request, 'task', None) + _, _, task_args = self.encode_content( + getattr(request, 'argsrepr', getattr(request, 'args', None)) + ) + _, _, task_kwargs = self.encode_content( + getattr(request, 'kwargsrepr', getattr(request, 'kwargs', None)) + ) + worker = getattr(request, 'hostname', None) self.TaskModel._default_manager.store_result( content_type, content_encoding, @@ -33,6 +48,8 @@ task_name=task_name, task_args=task_args, task_kwargs=task_kwargs, + worker=worker, + using=using, ) return result @@ -41,8 +58,13 @@ obj = self.TaskModel._default_manager.get_task(task_id) res = obj.as_dict() meta = self.decode_content(obj, res.pop('meta', None)) or {} - res.update(meta, - result=self.decode_content(obj, res.get('result'))) + result = self.decode_content(obj, res.get('result')) + task_args = self.decode_content(obj, res.get('task_args')) + task_kwargs = self.decode_content(obj, res.get('task_kwargs')) + res.update( + meta, result=result, task_args=task_args, + task_kwargs=task_kwargs, + ) return self.meta_from_decoded(res) def encode_content(self, data): @@ -66,3 +88,76 @@ def cleanup(self): """Delete expired metadata.""" self.TaskModel._default_manager.delete_expired(self.expires) + + def apply_chord(self, header_result, body, **kwargs): + """Add a ChordCounter with the expected number of results""" + results = [r.as_tuple() for r in header_result] + data = json.dumps(results) + ChordCounter.objects.create( + group_id=header_result.id, sub_tasks=data, count=len(results) + ) + + 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(): + # 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 + chord_counter = ( + ChordCounter.objects.select_for_update() + .get(group_id=gid) + ) + chord_counter.count -= 1 + if chord_counter.count != 0: + chord_counter.save() + 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` + """ + j = ( + group_result.join_native + if group_result.supports_native_join + else 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 = "Dependency {0.id} raised {1!r}".format(culprit, exc) + 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("Callback error: {0!r}".format(exc)) + ) diff -Nru python-django-celery-results-1.0.4/django_celery_results/__init__.py python-django-celery-results-2.0.0/django_celery_results/__init__.py --- python-django-celery-results-1.0.4/django_celery_results/__init__.py 2018-11-13 16:28:15.000000000 +0000 +++ python-django-celery-results-2.0.0/django_celery_results/__init__.py 2020-11-19 11:02:44.000000000 +0000 @@ -10,9 +10,9 @@ from collections import namedtuple -__version__ = '1.0.4' -__author__ = 'Ask Solem' -__contact__ = 'ask@celeryproject.org' +__version__ = '2.0.0' +__author__ = 'Asif Saif Uddin, Ask Solem' +__contact__ = 'auvipy@gmai.com, ask@celeryproject.org' __homepage__ = 'https://github.com/celery/django-celery-results' __docformat__ = 'restructuredtext' diff -Nru python-django-celery-results-1.0.4/django_celery_results/managers.py python-django-celery-results-2.0.0/django_celery_results/managers.py --- python-django-celery-results-1.0.4/django_celery_results/managers.py 2018-11-13 16:28:15.000000000 +0000 +++ python-django-celery-results-2.0.0/django_celery_results/managers.py 2020-11-19 11:02:44.000000000 +0000 @@ -6,19 +6,14 @@ from functools import wraps from itertools import count +from celery.utils.time import maybe_timedelta + from django.db import connections, router, transaction from django.db import models from django.conf import settings -from celery.five import items - from .utils import now -try: - from celery.utils.time import maybe_timedelta -except ImportError: # pragma: no cover - from celery.utils.timeutils import maybe_timedelta # noqa - W_ISOLATION_REP = """ Polling results with transaction isolation level 'repeatable-read' within the same transaction may give outdated results. @@ -38,7 +33,9 @@ retrying if the operation fails. Keyword Arguments: + ----------------- max_retries (int): Maximum number of retries. Default one retry. + """ def _outer(fun): @@ -70,10 +67,12 @@ """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) @@ -87,10 +86,12 @@ def store_result(self, content_type, content_encoding, task_id, result, status, traceback=None, meta=None, - task_name=None, task_args=None, task_kwargs=None): + task_name=None, task_args=None, task_kwargs=None, + worker=None, using=None): """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. @@ -101,16 +102,20 @@ or an exception instance raised by the task. status (str): Task status. See :mod:`celery.states` for a list of possible status values. - - Keyword Arguments: + 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, @@ -122,19 +127,25 @@ 'task_name': task_name, 'task_args': task_args, 'task_kwargs': task_kwargs, + 'worker': worker } - obj, created = self.get_or_create(task_id=task_id, defaults=fields) + obj, created = self.using(using).get_or_create(task_id=task_id, + defaults=fields) if not created: - for k, v in items(fields): + for k, v in fields.items(): setattr(obj, k, v) - obj.save() + obj.save(using=using) return obj def warn_if_repeatable_read(self): if 'mysql' in self.current_engine().lower(): cursor = self.connection_for_read().cursor() - if cursor.execute('SELECT @@tx_isolation'): - isolation = cursor.fetchone()[0] + # 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())) @@ -156,11 +167,5 @@ def delete_expired(self, expires): """Delete all expired results.""" - meta = self.model._meta with transaction.atomic(): - self.get_all_expired(expires).update(hidden=True) - cursor = self.connection_for_write().cursor() - cursor.execute( - 'DELETE FROM {0.db_table} WHERE hidden=%s'.format(meta), - (True, ), - ) + self.get_all_expired(expires).delete() diff -Nru python-django-celery-results-1.0.4/django_celery_results/migrations/0004_auto_20190516_0412.py python-django-celery-results-2.0.0/django_celery_results/migrations/0004_auto_20190516_0412.py --- python-django-celery-results-1.0.4/django_celery_results/migrations/0004_auto_20190516_0412.py 1970-01-01 00:00:00.000000000 +0000 +++ python-django-celery-results-2.0.0/django_celery_results/migrations/0004_auto_20190516_0412.py 2020-11-19 11:02:44.000000000 +0000 @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +# 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 __future__ import absolute_import, unicode_literals + +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=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'), + ), + ] diff -Nru python-django-celery-results-1.0.4/django_celery_results/migrations/0005_taskresult_worker.py python-django-celery-results-2.0.0/django_celery_results/migrations/0005_taskresult_worker.py --- python-django-celery-results-1.0.4/django_celery_results/migrations/0005_taskresult_worker.py 1970-01-01 00:00:00.000000000 +0000 +++ python-django-celery-results-2.0.0/django_celery_results/migrations/0005_taskresult_worker.py 2020-11-19 11:02:44.000000000 +0000 @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# 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 __future__ import absolute_import, unicode_literals + +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'), + ), + ] diff -Nru python-django-celery-results-1.0.4/django_celery_results/migrations/0006_taskresult_date_created.py python-django-celery-results-2.0.0/django_celery_results/migrations/0006_taskresult_date_created.py --- python-django-celery-results-1.0.4/django_celery_results/migrations/0006_taskresult_date_created.py 1970-01-01 00:00:00.000000000 +0000 +++ python-django-celery-results-2.0.0/django_celery_results/migrations/0006_taskresult_date_created.py 2020-11-19 11:02:44.000000000 +0000 @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# 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 + +from __future__ import absolute_import, unicode_literals + +from django.db import migrations, models +import django.utils.timezone + + +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), + ] diff -Nru python-django-celery-results-1.0.4/django_celery_results/migrations/0007_remove_taskresult_hidden.py python-django-celery-results-2.0.0/django_celery_results/migrations/0007_remove_taskresult_hidden.py --- python-django-celery-results-1.0.4/django_celery_results/migrations/0007_remove_taskresult_hidden.py 1970-01-01 00:00:00.000000000 +0000 +++ python-django-celery-results-2.0.0/django_celery_results/migrations/0007_remove_taskresult_hidden.py 2020-11-19 11:02:44.000000000 +0000 @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# 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 __future__ import absolute_import, unicode_literals + +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', + ), + ] diff -Nru python-django-celery-results-1.0.4/django_celery_results/migrations/0008_chordcounter.py python-django-celery-results-2.0.0/django_celery_results/migrations/0008_chordcounter.py --- python-django-celery-results-1.0.4/django_celery_results/migrations/0008_chordcounter.py 1970-01-01 00:00:00.000000000 +0000 +++ python-django-celery-results-2.0.0/django_celery_results/migrations/0008_chordcounter.py 2020-11-19 11:02:44.000000000 +0000 @@ -0,0 +1,41 @@ +# Generated by Django 3.0.6 on 2020-05-12 12:05 +from __future__ import unicode_literals, absolute_import + +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')), + ], + ), + ] diff -Nru python-django-celery-results-1.0.4/django_celery_results/models.py python-django-celery-results-2.0.0/django_celery_results/models.py --- python-django-celery-results-1.0.4/django_celery_results/models.py 2018-11-13 16:28:15.000000000 +0000 +++ python-django-celery-results-2.0.0/django_celery_results/models.py 2020-11-19 11:02:44.000000000 +0000 @@ -1,12 +1,14 @@ """Database models.""" from __future__ import absolute_import, unicode_literals +import json + from django.conf import settings from django.db import models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from celery import states -from celery.five import python_2_unicode_compatible +from celery.result import GroupResult, result_from_tuple from . import managers @@ -14,33 +16,71 @@ TASK_STATE_CHOICES = sorted(zip(ALL_STATES, ALL_STATES)) -@python_2_unicode_compatible class TaskResult(models.Model): """Task result/status.""" task_id = models.CharField( - _('task id'), max_length=getattr( settings, 'DJANGO_CELERY_RESULTS_TASK_ID_MAX_LENGTH', 255 ), - unique=True + unique=True, db_index=True, + verbose_name=_('Task ID'), + help_text=_('Celery ID for the Task that was run')) + task_name = models.CharField( + null=True, max_length=255, db_index=True, + 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, db_index=True, + choices=TASK_STATE_CHOICES, + verbose_name=_('Task State'), + help_text=_('Current state of the task being run')) + worker = models.CharField( + max_length=100, db_index=True, default=None, null=True, + verbose_name=_('Worker'), help_text=_('Worker that executes the task') ) - task_name = models.CharField(_('task name'), null=True, max_length=255) - task_args = models.TextField(_('task arguments'), null=True) - task_kwargs = models.TextField(_('task kwargs'), null=True) - status = models.CharField(_('state'), max_length=50, - default=states.PENDING, - choices=TASK_STATE_CHOICES - ) - content_type = models.CharField(_('content type'), max_length=128) - content_encoding = models.CharField(_('content encoding'), max_length=64) - result = models.TextField(null=True, default=None, editable=False) - date_done = models.DateTimeField(_('done at'), auto_now=True) - traceback = models.TextField(_('traceback'), blank=True, null=True) - hidden = models.BooleanField(editable=False, default=False, db_index=True) - meta = models.TextField(null=True, default=None, editable=False) + 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, db_index=True, + verbose_name=_('Created DateTime'), + help_text=_('Datetime field when the task result was created in UTC')) + date_done = models.DateTimeField( + auto_now=True, db_index=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() @@ -63,7 +103,50 @@ 'date_done': self.date_done, 'traceback': self.traceback, 'meta': self.meta, + 'worker': self.worker } def __str__(self): return ''.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, + db_index=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 GroupResult of self. + + Arguments: + --------- + app (Celery): app instance to create the GroupResult with. + + """ + return GroupResult( + self.group_id, + [result_from_tuple(r, app=app) + for r in json.loads(self.sub_tasks)], + app=app, + ) diff -Nru python-django-celery-results-1.0.4/django_celery_results/urls.py python-django-celery-results-2.0.0/django_celery_results/urls.py --- python-django-celery-results-1.0.4/django_celery_results/urls.py 1970-01-01 00:00:00.000000000 +0000 +++ python-django-celery-results-2.0.0/django_celery_results/urls.py 2020-11-19 11:02:44.000000000 +0000 @@ -0,0 +1,27 @@ +"""URLs defined for celery. + +* ``/$task_id/done/`` + URL to :func:`~celery.views.is_successful`. +* ``/$task_id/status/`` + URL to :func:`~celery.views.task_status`. +""" +from __future__ import absolute_import, unicode_literals + +from django.conf.urls import url + +from . import views + +task_pattern = r'(?P[\w\d\-\.]+)' + +urlpatterns = [ + url( + r'^%s/done/?$' % task_pattern, + views.is_task_successful, + name='celery-is_task_successful' + ), + url( + r'^%s/status/?$' % task_pattern, + views.task_status, + name='celery-task_status' + ), +] diff -Nru python-django-celery-results-1.0.4/django_celery_results/views.py python-django-celery-results-2.0.0/django_celery_results/views.py --- python-django-celery-results-1.0.4/django_celery_results/views.py 1970-01-01 00:00:00.000000000 +0000 +++ python-django-celery-results-2.0.0/django_celery_results/views.py 2020-11-19 11:02:44.000000000 +0000 @@ -0,0 +1,30 @@ +"""Views.""" +from __future__ import absolute_import, unicode_literals + +from django.http import JsonResponse + +from celery import states +from celery.result import AsyncResult +from celery.utils import get_full_cls_name +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}) diff -Nru python-django-celery-results-1.0.4/docs/includes/introduction.txt python-django-celery-results-2.0.0/docs/includes/introduction.txt --- python-django-celery-results-1.0.4/docs/includes/introduction.txt 2018-11-13 16:28:15.000000000 +0000 +++ python-django-celery-results-2.0.0/docs/includes/introduction.txt 2020-11-19 11:02:44.000000000 +0000 @@ -1,4 +1,4 @@ -:Version: 1.0.4 +:Version: 2.0.0 :Web: http://django-celery-results.readthedocs.io/ :Download: http://pypi.python.org/pypi/django-celery-results :Source: http://github.com/celery/django-celery-results diff -Nru python-django-celery-results-1.0.4/locale/es/LC_MESSAGES/django.po python-django-celery-results-2.0.0/locale/es/LC_MESSAGES/django.po --- python-django-celery-results-1.0.4/locale/es/LC_MESSAGES/django.po 1970-01-01 00:00:00.000000000 +0000 +++ python-django-celery-results-2.0.0/locale/es/LC_MESSAGES/django.po 2020-11-19 11:02:44.000000000 +0000 @@ -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. +# , 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: \n" +"Language-Team: LANGUAGE \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" diff -Nru python-django-celery-results-1.0.4/Makefile python-django-celery-results-2.0.0/Makefile --- python-django-celery-results-1.0.4/Makefile 2018-11-13 16:28:15.000000000 +0000 +++ python-django-celery-results-2.0.0/Makefile 2020-11-19 11:02:44.000000000 +0000 @@ -1,7 +1,7 @@ PROJ=django_celery_results PGPIDENT="Celery Security Team" PYTHON=python -PYTEST=py.test +PYTEST=pytest GIT=git TOX=tox ICONV=iconv @@ -141,7 +141,7 @@ $(PYTHON) setup.py test cov: covbuild - (cd $(TESTDIR); py.test -x --cov=django_celery_results --cov-report=html) + (cd $(TESTDIR); pytest -x --cov=django_celery_results --cov-report=html) build: $(PYTHON) setup.py sdist bdist_wheel diff -Nru python-django-celery-results-1.0.4/README.rst python-django-celery-results-2.0.0/README.rst --- python-django-celery-results-1.0.4/README.rst 2018-11-13 16:28:15.000000000 +0000 +++ python-django-celery-results-2.0.0/README.rst 2020-11-19 11:02:44.000000000 +0000 @@ -4,7 +4,7 @@ |build-status| |coverage| |license| |wheel| |pyversion| |pyimp| -:Version: 1.0.4 +:Version: 2.0.0 :Web: http://django-celery-results.readthedocs.io/ :Download: http://pypi.python.org/pypi/django-celery-results :Source: http://github.com/celery/django-celery-results @@ -23,10 +23,7 @@ ========== The installation instructions for this extension is available -from the `Celery documentation`_: - -http://docs.celeryproject.org/en/latest/django/first-steps-with-django.html#django-celery-results-using-the-django-orm-cache-as-a-result-backend - +from the `Celery documentation`_ .. _`Celery documentation`: http://docs.celeryproject.org/en/latest/django/first-steps-with-django.html#django-celery-results-using-the-django-orm-cache-as-a-result-backend @@ -115,3 +112,9 @@ .. |pyimp| image:: https://img.shields.io/pypi/implementation/django-celery-results.svg :alt: Support Python implementations. :target: http://pypi.python.org/pypi/django-celery-results/ + +django-celery-results as part of the Tidelift Subscription +----------------- + +The maintainers of django-celery-results and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. [Learn more.](https://tidelift.com/subscription/pkg/pypi-django-celery-results?utm_source=pypi-django-celery-results&utm_medium=referral&utm_campaign=readme&utm_term=repo) + diff -Nru python-django-celery-results-1.0.4/requirements/default.txt python-django-celery-results-2.0.0/requirements/default.txt --- python-django-celery-results-1.0.4/requirements/default.txt 2018-11-13 16:28:15.000000000 +0000 +++ python-django-celery-results-2.0.0/requirements/default.txt 2020-11-19 11:02:44.000000000 +0000 @@ -1 +1 @@ -celery>=4.0,<5.0 +celery>=4.4,<6.0 diff -Nru python-django-celery-results-1.0.4/requirements/docs.txt python-django-celery-results-2.0.0/requirements/docs.txt --- python-django-celery-results-1.0.4/requirements/docs.txt 2018-11-13 16:28:15.000000000 +0000 +++ python-django-celery-results-2.0.0/requirements/docs.txt 2020-11-19 11:02:44.000000000 +0000 @@ -1,2 +1,2 @@ sphinx_celery>=1.1 -Django>=1.10 +Django>=2.2 diff -Nru python-django-celery-results-1.0.4/requirements/pkgutils.txt python-django-celery-results-2.0.0/requirements/pkgutils.txt --- python-django-celery-results-1.0.4/requirements/pkgutils.txt 2018-11-13 16:28:15.000000000 +0000 +++ python-django-celery-results-2.0.0/requirements/pkgutils.txt 2020-11-19 11:02:44.000000000 +0000 @@ -1,6 +1,6 @@ -setuptools>=20.6.7 -wheel>=0.29.0 -flake8==3.5.0 +setuptools>=40.8.0 +wheel>=0.33.1 +flake8>=3.8.3 flakeplus>=1.1 tox>=2.3.1 sphinx2rst>=1.0 diff -Nru python-django-celery-results-1.0.4/requirements/test-django111.txt python-django-celery-results-2.0.0/requirements/test-django111.txt --- python-django-celery-results-1.0.4/requirements/test-django111.txt 2018-11-13 16:28:15.000000000 +0000 +++ python-django-celery-results-2.0.0/requirements/test-django111.txt 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -django>=1.11,<1.12 diff -Nru python-django-celery-results-1.0.4/requirements/test-django20.txt python-django-celery-results-2.0.0/requirements/test-django20.txt --- python-django-celery-results-1.0.4/requirements/test-django20.txt 2018-11-13 16:28:15.000000000 +0000 +++ python-django-celery-results-2.0.0/requirements/test-django20.txt 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -django>=2.0,<2.1 diff -Nru python-django-celery-results-1.0.4/requirements/test-django22.txt python-django-celery-results-2.0.0/requirements/test-django22.txt --- python-django-celery-results-1.0.4/requirements/test-django22.txt 1970-01-01 00:00:00.000000000 +0000 +++ python-django-celery-results-2.0.0/requirements/test-django22.txt 2020-11-19 11:02:44.000000000 +0000 @@ -0,0 +1 @@ +django>=2.2.9,<3.0 diff -Nru python-django-celery-results-1.0.4/requirements/test-django30.txt python-django-celery-results-2.0.0/requirements/test-django30.txt --- python-django-celery-results-1.0.4/requirements/test-django30.txt 1970-01-01 00:00:00.000000000 +0000 +++ python-django-celery-results-2.0.0/requirements/test-django30.txt 2020-11-19 11:02:44.000000000 +0000 @@ -0,0 +1 @@ +django>=3.0,<3.1 \ No newline at end of file diff -Nru python-django-celery-results-1.0.4/requirements/test-django31.txt python-django-celery-results-2.0.0/requirements/test-django31.txt --- python-django-celery-results-1.0.4/requirements/test-django31.txt 1970-01-01 00:00:00.000000000 +0000 +++ python-django-celery-results-2.0.0/requirements/test-django31.txt 2020-11-19 11:02:44.000000000 +0000 @@ -0,0 +1 @@ +django>=3.1,<3.2 \ No newline at end of file diff -Nru python-django-celery-results-1.0.4/requirements/test-django.txt python-django-celery-results-2.0.0/requirements/test-django.txt --- python-django-celery-results-1.0.4/requirements/test-django.txt 2018-11-13 16:28:15.000000000 +0000 +++ python-django-celery-results-2.0.0/requirements/test-django.txt 2020-11-19 11:02:44.000000000 +0000 @@ -1 +1 @@ -django +Django>=2.2,<4.0 diff -Nru python-django-celery-results-1.0.4/requirements/test.txt python-django-celery-results-2.0.0/requirements/test.txt --- python-django-celery-results-1.0.4/requirements/test.txt 2018-11-13 16:28:15.000000000 +0000 +++ python-django-celery-results-2.0.0/requirements/test.txt 2020-11-19 11:02:44.000000000 +0000 @@ -1,4 +1,6 @@ case>=1.3.1 -pytest>=3.0 -pytest-django +pytest>=4.3 +pytest-django>=2.2,<4.0 +pytest-benchmark pytz>dev +psycopg2cffi diff -Nru python-django-celery-results-1.0.4/setup.cfg python-django-celery-results-2.0.0/setup.cfg --- python-django-celery-results-1.0.4/setup.cfg 2018-11-13 16:28:15.000000000 +0000 +++ python-django-celery-results-2.0.0/setup.cfg 2020-11-19 11:02:44.000000000 +0000 @@ -1,7 +1,10 @@ [tool:pytest] -testpaths = t/unit +testpaths = t/ python_classes = test_* -DJANGO_SETTINGS_MODULE=t.proj.settings +python_files = test_* benchmark_* +DJANGO_SETTINGS_MODULE = t.proj.settings +markers = + benchmark: mark a test as a benchmark [flake8] # classes can be lowercase, arguments and variables can be uppercase diff -Nru python-django-celery-results-1.0.4/setup.py python-django-celery-results-2.0.0/setup.py --- python-django-celery-results-1.0.4/setup.py 2018-11-13 16:28:15.000000000 +0000 +++ python-django-celery-results-2.0.0/setup.py 2020-11-19 11:02:44.000000000 +0000 @@ -21,17 +21,12 @@ E_UNSUPPORTED_PYTHON = '%s 1.0 requires %%s %%s or later!' % (NAME,) PYIMP = _pyimp() -PY26_OR_LESS = sys.version_info < (2, 7) -PY3 = sys.version_info[0] == 3 -PY33_OR_LESS = PY3 and sys.version_info < (3, 4) +PY36_OR_LESS = sys.version_info < (3, 6) PYPY_VERSION = getattr(sys, 'pypy_version_info', None) -PYPY = PYPY_VERSION is not None PYPY24_ATLEAST = PYPY_VERSION and PYPY_VERSION >= (2, 4) -if PY26_OR_LESS: - raise Exception(E_UNSUPPORTED_PYTHON % (PYIMP, '2.7')) -elif PY33_OR_LESS and not PYPY24_ATLEAST: - raise Exception(E_UNSUPPORTED_PYTHON % (PYIMP, '3.4')) +if PY36_OR_LESS and not PYPY24_ATLEAST: + raise Exception(E_UNSUPPORTED_PYTHON % (PYIMP, '3.6')) # -*- Classifiers -*- @@ -39,17 +34,16 @@ Development Status :: 5 - Production/Stable License :: OSI Approved :: BSD License Programming Language :: Python - Programming Language :: Python :: 2 - Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 - Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy Framework :: Django - Framework :: Django :: 1.8 - Framework :: Django :: 1.9 - Framework :: Django :: 1.10 + Framework :: Django :: 2.2 + Framework :: Django :: 3.0 + Framework :: Django :: 3.1 Operating System :: OS Independent Topic :: Communications Topic :: System :: Distributed Computing @@ -120,7 +114,7 @@ class pytest(setuptools.command.test.test): - user_options = [('pytest-args=', 'a', 'Arguments to pass to py.test')] + user_options = [('pytest-args=', 'a', 'Arguments to pass to pytest')] def initialize_options(self): setuptools.command.test.test.initialize_options(self) @@ -136,6 +130,7 @@ version=meta['version'], description=meta['doc'], long_description=long_description, + long_description_content_type='text/markdown', keywords='celery django database result backend', author=meta['author'], author_email=meta['contact'], diff -Nru python-django-celery-results-1.0.4/t/conftest.py python-django-celery-results-2.0.0/t/conftest.py --- python-django-celery-results-1.0.4/t/conftest.py 1970-01-01 00:00:00.000000000 +0000 +++ python-django-celery-results-2.0.0/t/conftest.py 2020-11-19 11:02:44.000000000 +0000 @@ -0,0 +1,45 @@ +from __future__ import absolute_import, unicode_literals + +import pytest + +# we have to import the pytest plugin fixtures here, +# in case user did not do the `python setup.py develop` yet, +# that installs the pytest plugin into the setuptools registry. +from celery.contrib.pytest import (celery_app, celery_enable_logging, + celery_parameters, depends_on_current_app, + celery_config, use_celery_app_trap) +from celery.contrib.testing.app import TestApp, Trap + +# Tricks flake8 into silencing redefining fixtures warnings. +__all__ = ( + 'celery_app', 'celery_enable_logging', 'depends_on_current_app', + 'celery_parameters', 'celery_config', 'use_celery_app_trap' +) + + +@pytest.fixture(scope='session', autouse=True) +def setup_default_app_trap(): + from celery._state import set_default_app + set_default_app(Trap()) + + +@pytest.fixture() +def app(celery_app): + return celery_app + + +@pytest.fixture(autouse=True) +def test_cases_shortcuts(request, app, patching): + if request.instance: + @app.task + def add(x, y): + return x + y + + # IMPORTANT: We set an .app attribute for every test case class. + request.instance.app = app + request.instance.Celery = TestApp + request.instance.add = add + request.instance.patching = patching + yield + if request.instance: + request.instance.app = None diff -Nru python-django-celery-results-1.0.4/t/integration/benchmark_models.py python-django-celery-results-2.0.0/t/integration/benchmark_models.py --- python-django-celery-results-1.0.4/t/integration/benchmark_models.py 1970-01-01 00:00:00.000000000 +0000 +++ python-django-celery-results-2.0.0/t/integration/benchmark_models.py 2020-11-19 11:02:44.000000000 +0000 @@ -0,0 +1,74 @@ +from __future__ import absolute_import, unicode_literals + +import pytest + +from datetime import timedelta +import time + +from django.test import TransactionTestCase + +from celery import uuid + +from django_celery_results.models import TaskResult +from django_celery_results.utils import now + +RECORDS_COUNT = 100000 + + +@pytest.fixture() +def use_benchmark(request, benchmark): + def wrapped(a=10, b=5): + return a + b + request.cls.benchmark = benchmark + + +@pytest.mark.usefixtures('use_benchmark') +@pytest.mark.usefixtures('depends_on_current_app') +class benchmark_Models(TransactionTestCase): + + @pytest.fixture(autouse=True) + def setup_app(self, app): + self.app = app + self.app.conf.result_serializer = 'pickle' + self.app.conf.result_backend = ( + 'django_celery_results.backends:DatabaseBackend') + + def create_many_task_result(self, count): + start = time.time() + draft_results = [TaskResult(task_id=uuid()) for _ in range(count)] + drafted = time.time() + results = TaskResult.objects.bulk_create(draft_results) + done_creating = time.time() + + print(( + 'drafting time: {drafting:.2f}\n' + 'bulk_create time: {done:.2f}\n' + '------' + ).format(drafting=drafted - start, done=done_creating - drafted)) + return results + + def setup_records_to_delete(self): + self.create_many_task_result(count=RECORDS_COUNT) + mid_point = TaskResult.objects.order_by('id')[int(RECORDS_COUNT / 2)] + todelete = TaskResult.objects.filter(id__gte=mid_point.id) + todelete.update(date_done=now() - timedelta(days=10)) + + def test_taskresult_delete_expired(self): + start = time.time() + self.setup_records_to_delete() + after_setup = time.time() + self.benchmark.pedantic( + TaskResult.objects.delete_expired, + args=(self.app.conf.result_expires,), + iterations=1, + rounds=1, + ) + done = time.time() + assert TaskResult.objects.count() == int(RECORDS_COUNT / 2) + + print(( + '------' + 'setup time: {setup:.2f}\n' + 'bench time: {bench:.2f}\n' + ).format(setup=after_setup - start, bench=done - after_setup)) + assert self.benchmark.stats.stats.max < 1 diff -Nru python-django-celery-results-1.0.4/t/proj/settings.py python-django-celery-results-2.0.0/t/proj/settings.py --- python-django-celery-results-1.0.4/t/proj/settings.py 2018-11-13 16:28:15.000000000 +0000 +++ python-django-celery-results-2.0.0/t/proj/settings.py 2020-11-19 11:02:44.000000000 +0000 @@ -14,11 +14,58 @@ import os import sys + # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - sys.path.insert(0, os.path.abspath(os.path.join(BASE_DIR, os.pardir))) + +# configure psycopg2cffi for psycopg2 compatibility. We must use this package +# support pypy. +# if not installed, use sqlite as a backup (some tests may fail), +# otherwise even makemigrations won't run. +try: + from psycopg2cffi import compat + compat.register() + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': 'postgres', + 'USER': 'postgres', + 'OPTIONS': { + 'connect_timeout': 1000, + } + }, + 'secondary': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': 'postgres', + 'USER': 'postgres', + 'OPTIONS': { + 'connect_timeout': 1000, + }, + 'TEST': { + 'MIRROR': 'default', + }, + }, + } +except ImportError: + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + 'OPTIONS': { + 'timeout': 1000, + } + }, + 'secondary': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + 'OPTIONS': { + 'timeout': 1000, + } + }, + } + # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ @@ -71,20 +118,6 @@ WSGI_APPLICATION = 't.proj.wsgi.application' - -# Database -# https://docs.djangoproject.com/en/1.9/ref/settings/#databases - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), - 'OPTIONS': { - 'timeout': 1000, - }, - } -} - CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', diff -Nru python-django-celery-results-1.0.4/t/unit/backends/test_database.py python-django-celery-results-2.0.0/t/unit/backends/test_database.py --- python-django-celery-results-1.0.4/t/unit/backends/test_database.py 2018-11-13 16:28:15.000000000 +0000 +++ python-django-celery-results-2.0.0/t/unit/backends/test_database.py 2020-11-19 11:02:44.000000000 +0000 @@ -1,12 +1,15 @@ from __future__ import absolute_import, unicode_literals +import mock import celery import pytest from celery import uuid from celery import states +from celery.result import GroupResult, AsyncResult from django_celery_results.backends.database import DatabaseBackend +from django_celery_results.models import ChordCounter, TaskResult class SomeClass(object): @@ -26,18 +29,37 @@ 'django_celery_results.backends:DatabaseBackend') self.b = DatabaseBackend(app=self.app) - def test_backend__pickle_serialization(self): + def test_backend__pickle_serialization__dict_result(self): self.app.conf.result_serializer = 'pickle' self.app.conf.accept_content = {'pickle', 'json'} self.b = DatabaseBackend(app=self.app) tid2 = uuid() result = {'foo': 'baz', 'bar': SomeClass(12345)} - self.b.mark_as_done(tid2, result) - # is serialized properly. - rindb = self.b.get_result(tid2) - assert rindb.get('foo') == 'baz' - assert rindb.get('bar').data == 12345 + request = mock.MagicMock() + request.task = 'my_task' + request.args = ['a', 1, SomeClass(67)] + request.kwargs = {'c': 6, 'd': 'e', 'f': SomeClass(89)} + request.hostname = 'celery@ip-0-0-0-0' + request.chord = None + del request.argsrepr, request.kwargsrepr + + self.b.mark_as_done(tid2, result, request=request) + mindb = self.b.get_task_meta(tid2) + + assert mindb.get('result').get('foo') == 'baz' + assert mindb.get('result').get('bar').data == 12345 + assert mindb.get('task_name') == 'my_task' + + assert len(mindb.get('task_args')) == 3 + assert mindb.get('task_args')[0] == 'a' + assert mindb.get('task_args')[1] == 1 + assert mindb.get('task_args')[2].data == 67 + + assert len(mindb.get('task_kwargs')) == 3 + assert mindb.get('task_kwargs')['c'] == 6 + assert mindb.get('task_kwargs')['d'] == 'e' + assert mindb.get('task_kwargs')['f'].data == 89 tid3 = uuid() try: @@ -48,6 +70,68 @@ assert self.b.get_status(tid3) == states.FAILURE assert isinstance(self.b.get_result(tid3), KeyError) + def test_backend__pickle_serialization__str_result(self): + self.app.conf.result_serializer = 'pickle' + self.app.conf.accept_content = {'pickle', 'json'} + self.b = DatabaseBackend(app=self.app) + + tid2 = uuid() + result = 'foo' + request = mock.MagicMock() + request.task = 'my_task' + request.args = ['a', 1, SomeClass(67)] + request.kwargs = {'c': 6, 'd': 'e', 'f': SomeClass(89)} + request.hostname = 'celery@ip-0-0-0-0' + request.chord = None + del request.argsrepr, request.kwargsrepr + + self.b.mark_as_done(tid2, result, request=request) + mindb = self.b.get_task_meta(tid2) + + assert mindb.get('result') == 'foo' + assert mindb.get('task_name') == 'my_task' + + assert len(mindb.get('task_args')) == 3 + assert mindb.get('task_args')[0] == 'a' + assert mindb.get('task_args')[1] == 1 + assert mindb.get('task_args')[2].data == 67 + + assert len(mindb.get('task_kwargs')) == 3 + assert mindb.get('task_kwargs')['c'] == 6 + assert mindb.get('task_kwargs')['d'] == 'e' + assert mindb.get('task_kwargs')['f'].data == 89 + + def test_backend__pickle_serialization__bytes_result(self): + self.app.conf.result_serializer = 'pickle' + self.app.conf.accept_content = {'pickle', 'json'} + self.b = DatabaseBackend(app=self.app) + + tid2 = uuid() + result = b'foo' + request = mock.MagicMock() + request.task = 'my_task' + request.args = ['a', 1, SomeClass(67)] + request.kwargs = {'c': 6, 'd': 'e', 'f': SomeClass(89)} + request.hostname = 'celery@ip-0-0-0-0' + request.chord = None + del request.argsrepr, request.kwargsrepr + + self.b.mark_as_done(tid2, result, request=request) + mindb = self.b.get_task_meta(tid2) + + assert mindb.get('result') == b'foo' + assert mindb.get('task_name') == 'my_task' + + assert len(mindb.get('task_args')) == 3 + assert mindb.get('task_args')[0] == 'a' + assert mindb.get('task_args')[1] == 1 + assert mindb.get('task_args')[2].data == 67 + + assert len(mindb.get('task_kwargs')) == 3 + assert mindb.get('task_kwargs')['c'] == 6 + assert mindb.get('task_kwargs')['d'] == 'e' + assert mindb.get('task_kwargs')['f'].data == 89 + def xxx_backend(self): tid = uuid() @@ -77,3 +161,140 @@ # bug in 3.1.10 means result did not clear cache after forget. x._cache = None assert x.result is None + + def test_backend_secrets(self): + tid = uuid() + request = mock.MagicMock() + request.task = 'my_task' + request.args = ['a', 1, 'password'] + request.kwargs = {'c': 3, 'd': 'e', 'password': 'password'} + request.argsrepr = 'argsrepr' + request.kwargsrepr = 'kwargsrepr' + request.hostname = 'celery@ip-0-0-0-0' + request.chord = None + result = {'foo': 'baz'} + + self.b.mark_as_done(tid, result, request=request) + + mindb = self.b.get_task_meta(tid) + assert mindb.get('task_args') == 'argsrepr' + assert mindb.get('task_kwargs') == 'kwargsrepr' + assert mindb.get('worker') == 'celery@ip-0-0-0-0' + + def test_on_chord_part_return(self): + """Test if the ChordCounter is properly decremented and the callback is + triggered after all chord parts have returned""" + gid = uuid() + tid1 = uuid() + tid2 = uuid() + subtasks = [AsyncResult(tid1), AsyncResult(tid2)] + group = GroupResult(id=gid, results=subtasks) + self.b.apply_chord(group, self.add.s()) + + chord_counter = ChordCounter.objects.get(group_id=gid) + assert chord_counter.count == 2 + + request = mock.MagicMock() + request.id = subtasks[0].id + request.group = gid + request.task = "my_task" + request.args = ["a", 1, "password"] + request.kwargs = {"c": 3, "d": "e", "password": "password"} + request.argsrepr = "argsrepr" + request.kwargsrepr = "kwargsrepr" + request.hostname = "celery@ip-0-0-0-0" + result = {"foo": "baz"} + + self.b.mark_as_done(tid1, result, request=request) + + chord_counter.refresh_from_db() + assert chord_counter.count == 1 + + self.b.mark_as_done(tid2, result, request=request) + + with pytest.raises(ChordCounter.DoesNotExist): + ChordCounter.objects.get(group_id=gid) + + request.chord.delay.assert_called_once() + + def test_callback_failure(self): + """Test if a failure in the chord callback is properly handled""" + gid = uuid() + tid1 = uuid() + tid2 = uuid() + cid = uuid() + subtasks = [AsyncResult(tid1), AsyncResult(tid2)] + group = GroupResult(id=gid, results=subtasks) + self.b.apply_chord(group, self.add.s()) + + chord_counter = ChordCounter.objects.get(group_id=gid) + assert chord_counter.count == 2 + + request = mock.MagicMock() + request.id = subtasks[0].id + request.group = gid + request.task = "my_task" + request.args = ["a", 1, "password"] + request.kwargs = {"c": 3, "d": "e", "password": "password"} + request.argsrepr = "argsrepr" + request.kwargsrepr = "kwargsrepr" + request.hostname = "celery@ip-0-0-0-0" + request.chord.id = cid + result = {"foo": "baz"} + + # Trigger an exception when the callback is triggered + request.chord.delay.side_effect = ValueError() + + self.b.mark_as_done(tid1, result, request=request) + + chord_counter.refresh_from_db() + assert chord_counter.count == 1 + + self.b.mark_as_done(tid2, result, request=request) + + with pytest.raises(ChordCounter.DoesNotExist): + ChordCounter.objects.get(group_id=gid) + + request.chord.delay.assert_called_once() + + assert TaskResult.objects.get(task_id=cid).status == states.FAILURE + + def test_on_chord_part_return_failure(self): + """Test if a failure in one of the chord header tasks is properly handled + and the callback was not triggered + """ + gid = uuid() + tid1 = uuid() + tid2 = uuid() + cid = uuid() + subtasks = [AsyncResult(tid1), AsyncResult(tid2)] + group = GroupResult(id=gid, results=subtasks) + self.b.apply_chord(group, self.add.s()) + + chord_counter = ChordCounter.objects.get(group_id=gid) + assert chord_counter.count == 2 + + request = mock.MagicMock() + request.id = tid1 + request.group = gid + request.task = "my_task" + request.args = ["a", 1, "password"] + request.kwargs = {"c": 3, "d": "e", "password": "password"} + request.argsrepr = "argsrepr" + request.kwargsrepr = "kwargsrepr" + request.hostname = "celery@ip-0-0-0-0" + request.chord.id = cid + result = {"foo": "baz"} + + self.b.mark_as_done(tid1, result, request=request) + + chord_counter.refresh_from_db() + assert chord_counter.count == 1 + + request.id = tid2 + self.b.mark_as_failure(tid2, ValueError(), request=request) + + with pytest.raises(ChordCounter.DoesNotExist): + ChordCounter.objects.get(group_id=gid) + + request.chord.delay.assert_not_called() diff -Nru python-django-celery-results-1.0.4/t/unit/conftest.py python-django-celery-results-2.0.0/t/unit/conftest.py --- python-django-celery-results-1.0.4/t/unit/conftest.py 2018-11-13 16:28:15.000000000 +0000 +++ python-django-celery-results-2.0.0/t/unit/conftest.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,36 +0,0 @@ -from __future__ import absolute_import, unicode_literals - -import pytest - -from celery.contrib.pytest import depends_on_current_app -from celery.contrib.testing.app import TestApp, Trap - -__all__ = ['app', 'depends_on_current_app'] - - -@pytest.fixture(scope='session', autouse=True) -def setup_default_app_trap(): - from celery._state import set_default_app - set_default_app(Trap()) - - -@pytest.fixture() -def app(celery_app): - return celery_app - - -@pytest.fixture(autouse=True) -def test_cases_shortcuts(request, app, patching): - if request.instance: - @app.task - def add(x, y): - return x + y - - # IMPORTANT: We set an .app attribute for every test case class. - request.instance.app = app - request.instance.Celery = TestApp - request.instance.add = add - request.instance.patching = patching - yield - if request.instance: - request.instance.app = None diff -Nru python-django-celery-results-1.0.4/t/unit/test_migrations.py python-django-celery-results-2.0.0/t/unit/test_migrations.py --- python-django-celery-results-1.0.4/t/unit/test_migrations.py 1970-01-01 00:00:00.000000000 +0000 +++ python-django-celery-results-2.0.0/t/unit/test_migrations.py 2020-11-19 11:02:44.000000000 +0000 @@ -0,0 +1,51 @@ +from __future__ import absolute_import, unicode_literals +import os + +from django.test import TestCase +from django.apps import apps +from django.db.migrations.state import ProjectState +from django.db.migrations.autodetector import MigrationAutodetector +from django.db.migrations.loader import MigrationLoader +from django.db.migrations.questioner import NonInteractiveMigrationQuestioner + +from django_celery_results import migrations as result_migrations + + +class MigrationTests(TestCase): + def test_no_duplicate_migration_numbers(self): + """Verify no duplicate migration numbers. + + Migration files with the same number can cause issues with + backward migrations, so avoid them. + """ + path = os.path.dirname(result_migrations.__file__) + files = [f[:4] for f in os.listdir(path) if f.endswith('.py')] + self.assertEqual( + len(files), len(set(files)), + msg='Detected migration files with the same migration number') + + def test_models_match_migrations(self): + """Make sure that no model changes exist. + + This logic is taken from django's makemigrations.py file. + Here just detect if model changes exist that require + a migration, and if so we fail. + """ + app_labels = ['django_celery_results'] + loader = MigrationLoader(None, ignore_no_migrations=True) + questioner = NonInteractiveMigrationQuestioner( + specified_apps=app_labels, dry_run=False) + autodetector = MigrationAutodetector( + loader.project_state(), + ProjectState.from_apps(apps), + questioner, + ) + changes = autodetector.changes( + graph=loader.graph, + trim_to_apps=app_labels, + convert_apps=app_labels, + migration_name='fake_name', + ) + self.assertTrue( + not changes, + msg='Model changes exist that do not have a migration') diff -Nru python-django-celery-results-1.0.4/t/unit/test_models.py python-django-celery-results-2.0.0/t/unit/test_models.py --- python-django-celery-results-1.0.4/t/unit/test_models.py 2018-11-13 16:28:15.000000000 +0000 +++ python-django-celery-results-2.0.0/t/unit/test_models.py 2020-11-19 11:02:44.000000000 +0000 @@ -4,16 +4,18 @@ from datetime import datetime, timedelta +from django.db import transaction +from django.test import TransactionTestCase + from celery import states, uuid -from celery.five import text_t from django_celery_results.models import TaskResult from django_celery_results.utils import now -@pytest.mark.django_db() @pytest.mark.usefixtures('depends_on_current_app') -class test_Models: +class test_Models(TransactionTestCase): + databases = '__all__' @pytest.fixture(autouse=True) def setup_app(self, app): @@ -31,7 +33,7 @@ m1 = self.create_task_result() m2 = self.create_task_result() m3 = self.create_task_result() - assert text_t(m1).startswith('