├── t ├── __init__.py ├── unit │ ├── __init__.py │ ├── backends │ │ ├── __init__.py │ │ └── test_cache.py │ ├── test_migrations.py │ ├── test_admin.py │ ├── test_views.py │ └── test_models.py ├── integration │ ├── __init__.py │ └── benchmark_models.py ├── proj │ ├── __init__.py │ ├── urls.py │ ├── celery.py │ ├── wsgi.py │ └── settings.py └── conftest.py ├── docs ├── _static │ └── .keep ├── _templates │ └── .keep ├── changelog.rst ├── images │ ├── logo.png │ └── favicon.ico ├── glossary.rst ├── reference │ ├── django_celery_results.utils.rst │ ├── django_celery_results.models.rst │ ├── django_celery_results.backends.rst │ ├── django_celery_results.managers.rst │ ├── django_celery_results.backends.cache.rst │ ├── django_celery_results.backends.database.rst │ └── index.rst ├── index.rst ├── conf.py ├── copyright.rst ├── includes │ ├── introduction.txt │ └── installation.txt ├── templates │ └── readme.txt ├── injecting_metadata.rst ├── getting_started.rst ├── make.bat └── Makefile ├── django_celery_results ├── migrations │ ├── __init__.py │ ├── 0007_remove_taskresult_hidden.py │ ├── 0013_taskresult_django_cele_periodi_1993cf_idx.py │ ├── 0003_auto_20181106_1101.py │ ├── 0011_taskresult_periodic_task_name.py │ ├── 0012_taskresult_date_started.py │ ├── 0014_alter_taskresult_status.py │ ├── 0005_taskresult_worker.py │ ├── 0002_add_task_name_args_kwargs.py │ ├── 0008_chordcounter.py │ ├── 0006_taskresult_date_created.py │ ├── 0010_remove_duplicate_indices.py │ ├── 0001_initial.py │ ├── 0004_auto_20190516_0412.py │ └── 0009_groupresult.py ├── locale │ ├── es │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── ru │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── pt_BR │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ └── zh_Hans │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── backends │ ├── __init__.py │ ├── cache.py │ └── database.py ├── apps.py ├── utils.py ├── __init__.py ├── views.py ├── urls.py ├── admin.py ├── managers.py └── models.py ├── requirements ├── test-django32.txt ├── test-django41.txt ├── test-django52.txt ├── default.txt ├── test-django.txt ├── docs.txt ├── test-ci.txt ├── test.txt ├── pkgutils.txt ├── test-django42.txt ├── test-django50.txt └── test-django51.txt ├── .coveragerc ├── .readthedocs.yaml ├── .editorconfig ├── manage.py ├── .bumpversion.cfg ├── .gitignore ├── MANIFEST.in ├── setup.cfg ├── .pre-commit-config.yaml ├── .dockerignore ├── .cookiecutterrc ├── conftest.py ├── compose.yml ├── tox.ini ├── .github └── workflows │ ├── codeql-analysis.yml │ └── test.yaml ├── docker └── Dockerfile ├── LICENSE ├── AUTHORS ├── Makefile ├── setup.py ├── README.rst └── Changelog /t/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_static/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /t/unit/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_templates/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /t/integration/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /t/unit/backends/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_celery_results/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../Changelog 2 | -------------------------------------------------------------------------------- /requirements/test-django32.txt: -------------------------------------------------------------------------------- 1 | django>=3.2.25,<4.0 2 | -------------------------------------------------------------------------------- /requirements/test-django41.txt: -------------------------------------------------------------------------------- 1 | django>=4.1.13,<4.2 2 | -------------------------------------------------------------------------------- /requirements/test-django52.txt: -------------------------------------------------------------------------------- 1 | django>=5.2rc1,<6.0 2 | -------------------------------------------------------------------------------- /requirements/default.txt: -------------------------------------------------------------------------------- 1 | celery>=5.2.7,<6.0 2 | Django>=3.2.25 3 | -------------------------------------------------------------------------------- /requirements/test-django.txt: -------------------------------------------------------------------------------- 1 | Django>=3.2,<6.0 2 | psycopg>=3.1.8 3 | -------------------------------------------------------------------------------- /t/proj/__init__.py: -------------------------------------------------------------------------------- 1 | from .celery import app as celery_app # noqa 2 | -------------------------------------------------------------------------------- /requirements/docs.txt: -------------------------------------------------------------------------------- 1 | sphinx_celery>=1.1 2 | Django>=3.2.25 3 | celery>=5.2.7,<6.0 4 | -------------------------------------------------------------------------------- /requirements/test-ci.txt: -------------------------------------------------------------------------------- 1 | pytest-cov 2 | codecov 3 | importlib-metadata<5.0; python_version<"3.8" 4 | -------------------------------------------------------------------------------- /docs/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celery/django-celery-results/HEAD/docs/images/logo.png -------------------------------------------------------------------------------- /docs/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celery/django-celery-results/HEAD/docs/images/favicon.ico -------------------------------------------------------------------------------- /requirements/test.txt: -------------------------------------------------------------------------------- 1 | pytest>=6.2.5,<8 2 | pytest-django>=4.5.2 3 | pytest-benchmark 4 | pytz 5 | psycopg2cffi 6 | psycopg 7 | -------------------------------------------------------------------------------- /requirements/pkgutils.txt: -------------------------------------------------------------------------------- 1 | setuptools>=40.8.0 2 | wheel>=0.33.1 3 | flake8>=3.8.3 4 | tox>=2.3.1 5 | sphinx2rst>=1.0 6 | bumpversion 7 | pydocstyle 8 | -------------------------------------------------------------------------------- /t/proj/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path 3 | 4 | urlpatterns = [ 5 | path('admin/', admin.site.urls), 6 | ] 7 | -------------------------------------------------------------------------------- /requirements/test-django42.txt: -------------------------------------------------------------------------------- 1 | django>=4.2.11,<5.0 2 | psycopg>=3.1.8 # necessary due to https://docs.djangoproject.com/en/4.2/releases/4.2/#psycopg-3-support 3 | -------------------------------------------------------------------------------- /requirements/test-django50.txt: -------------------------------------------------------------------------------- 1 | django>=5.0.3,<5.1 2 | psycopg>=3.1.8 # necessary due to https://docs.djangoproject.com/en/4.2/releases/4.2/#psycopg-3-support 3 | -------------------------------------------------------------------------------- /requirements/test-django51.txt: -------------------------------------------------------------------------------- 1 | django>=5.1.3,<5.2 2 | psycopg>=3.1.8 # necessary due to https://docs.djangoproject.com/en/4.2/releases/4.2/#psycopg-3-support 3 | -------------------------------------------------------------------------------- /docs/glossary.rst: -------------------------------------------------------------------------------- 1 | .. _glossary: 2 | 3 | Glossary 4 | ======== 5 | 6 | .. glossary:: 7 | :sorted: 8 | 9 | term 10 | Description of term 11 | -------------------------------------------------------------------------------- /django_celery_results/locale/es/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celery/django-celery-results/HEAD/django_celery_results/locale/es/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /django_celery_results/locale/ru/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celery/django-celery-results/HEAD/django_celery_results/locale/ru/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /django_celery_results/backends/__init__.py: -------------------------------------------------------------------------------- 1 | from .cache import CacheBackend 2 | from .database import DatabaseBackend 3 | 4 | __all__ = ['CacheBackend', 'DatabaseBackend'] 5 | -------------------------------------------------------------------------------- /django_celery_results/locale/pt_BR/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celery/django-celery-results/HEAD/django_celery_results/locale/pt_BR/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /django_celery_results/locale/zh_Hans/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celery/django-celery-results/HEAD/django_celery_results/locale/zh_Hans/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = 1 3 | cover_pylib = 0 4 | include = *django_celery_results/* 5 | omit = t/* 6 | 7 | [report] 8 | omit = 9 | */python?.?/* 10 | */site-packages/* 11 | */pypy/* 12 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | build: 3 | os: ubuntu-20.04 4 | tools: 5 | python: "3.8" 6 | sphinx: 7 | configuration: docs/conf.py 8 | python: 9 | install: 10 | - requirements: requirements/docs.txt 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org/ 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [Makefile] 14 | indent_style = tab 15 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import sys 5 | 6 | if __name__ == '__main__': 7 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 't.proj.settings') 8 | 9 | from django.core.management import execute_from_command_line 10 | 11 | execute_from_command_line(sys.argv) 12 | -------------------------------------------------------------------------------- /docs/reference/django_celery_results.utils.rst: -------------------------------------------------------------------------------- 1 | ===================================================== 2 | ``django_celery_results.utils`` 3 | ===================================================== 4 | 5 | .. contents:: 6 | :local: 7 | .. currentmodule:: django_celery_results.utils 8 | 9 | .. automodule:: django_celery_results.utils 10 | :members: 11 | :undoc-members: 12 | -------------------------------------------------------------------------------- /t/proj/celery.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from celery import Celery 4 | 5 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 't.proj.settings') 6 | 7 | app = Celery('proj') 8 | 9 | # Using a string here means the worker doesn't have to serialize 10 | # the configuration object. 11 | app.config_from_object('django.conf:settings', namespace='CELERY') 12 | 13 | app.autodiscover_tasks() 14 | -------------------------------------------------------------------------------- /docs/reference/django_celery_results.models.rst: -------------------------------------------------------------------------------- 1 | ===================================================== 2 | ``django_celery_results.models`` 3 | ===================================================== 4 | 5 | .. contents:: 6 | :local: 7 | .. currentmodule:: django_celery_results.models 8 | 9 | .. automodule:: django_celery_results.models 10 | :members: 11 | :undoc-members: 12 | -------------------------------------------------------------------------------- /docs/reference/django_celery_results.backends.rst: -------------------------------------------------------------------------------- 1 | ===================================================== 2 | ``django_celery_results.backends`` 3 | ===================================================== 4 | 5 | .. contents:: 6 | :local: 7 | .. currentmodule:: django_celery_results.backends 8 | 9 | .. automodule:: django_celery_results.backends 10 | :members: 11 | :undoc-members: 12 | -------------------------------------------------------------------------------- /docs/reference/django_celery_results.managers.rst: -------------------------------------------------------------------------------- 1 | ===================================================== 2 | ``django_celery_results.managers`` 3 | ===================================================== 4 | 5 | .. contents:: 6 | :local: 7 | .. currentmodule:: django_celery_results.managers 8 | 9 | .. automodule:: django_celery_results.managers 10 | :members: 11 | :undoc-members: 12 | -------------------------------------------------------------------------------- /docs/reference/django_celery_results.backends.cache.rst: -------------------------------------------------------------------------------- 1 | ===================================================== 2 | ``django_celery_results.backends.cache`` 3 | ===================================================== 4 | 5 | .. contents:: 6 | :local: 7 | .. currentmodule:: django_celery_results.backends.cache 8 | 9 | .. automodule:: django_celery_results.backends.cache 10 | :members: 11 | :undoc-members: 12 | -------------------------------------------------------------------------------- /docs/reference/django_celery_results.backends.database.rst: -------------------------------------------------------------------------------- 1 | ===================================================== 2 | ``django_celery_results.backends.database`` 3 | ===================================================== 4 | 5 | .. contents:: 6 | :local: 7 | .. currentmodule:: django_celery_results.backends.database 8 | 9 | .. automodule:: django_celery_results.backends.database 10 | :members: 11 | :undoc-members: 12 | -------------------------------------------------------------------------------- /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 2.6.0 3 | commit = True 4 | tag = True 5 | parse = (?P\d+)\.(?P\d+)\.(?P\d+)(?P[a-z]+)? 6 | serialize = 7 | {major}.{minor}.{patch}{releaselevel} 8 | {major}.{minor}.{patch} 9 | 10 | [bumpversion:file:django_celery_results/__init__.py] 11 | 12 | [bumpversion:file:docs/includes/introduction.txt] 13 | 14 | [bumpversion:file:README.rst] 15 | 16 | -------------------------------------------------------------------------------- /docs/reference/index.rst: -------------------------------------------------------------------------------- 1 | .. _apiref: 2 | 3 | =============== 4 | API Reference 5 | =============== 6 | 7 | :Release: |version| 8 | :Date: |today| 9 | 10 | .. toctree:: 11 | :maxdepth: 1 12 | 13 | django_celery_results.backends 14 | django_celery_results.backends.database 15 | django_celery_results.backends.cache 16 | django_celery_results.models 17 | django_celery_results.managers 18 | django_celery_results.utils 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | *$py.class 4 | *~ 5 | .*.sw[pon] 6 | dist/ 7 | *.egg-info 8 | *.egg 9 | *.egg/ 10 | build/ 11 | .build/ 12 | _build/ 13 | pip-log.txt 14 | .directory 15 | erl_crash.dump 16 | *.db 17 | Documentation/ 18 | .tox/ 19 | .ropeproject/ 20 | .project 21 | .pydevproject 22 | .idea/ 23 | .coverage 24 | celery/tests/cover/ 25 | .ve* 26 | cover/ 27 | .vagrant/ 28 | *.sqlite3 29 | .cache/ 30 | htmlcov/ 31 | coverage.xml 32 | .env 33 | *.ignore -------------------------------------------------------------------------------- /t/proj/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for Test project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "t.proj.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include Changelog 2 | include LICENSE 3 | include README.rst 4 | include MANIFEST.in 5 | include setup.cfg 6 | include setup.py 7 | include manage.py 8 | recursive-include docs * 9 | recursive-include extra/* 10 | recursive-include examples * 11 | recursive-include requirements *.txt *.rst 12 | recursive-include t *.py 13 | recursive-include django_celery_results *.py *.po *.mo 14 | 15 | recursive-exclude * __pycache__ 16 | recursive-exclude * *.py[co] 17 | recursive-exclude * .*.sw* 18 | -------------------------------------------------------------------------------- /django_celery_results/apps.py: -------------------------------------------------------------------------------- 1 | """Application configuration.""" 2 | 3 | from django.apps import AppConfig 4 | from django.utils.translation import gettext_lazy as _ 5 | 6 | __all__ = ['CeleryResultConfig'] 7 | 8 | 9 | class CeleryResultConfig(AppConfig): 10 | """Default configuration for the django_celery_results app.""" 11 | 12 | name = 'django_celery_results' 13 | label = 'django_celery_results' 14 | verbose_name = _('Celery Results') 15 | default_auto_field = 'django.db.models.AutoField' 16 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | testpaths = t/ 3 | python_classes = test_* 4 | python_files = test_* benchmark_* 5 | DJANGO_SETTINGS_MODULE = t.proj.settings 6 | markers = 7 | benchmark: mark a test as a benchmark 8 | 9 | [flake8] 10 | # classes can be lowercase, arguments and variables can be uppercase 11 | # whenever it makes the code more readable. 12 | ignore = N806, N802, N801, N803 13 | 14 | [pep257] 15 | convention=google 16 | add-ignore = D102,D104,D203,D105,D213 17 | match-dir = [^migrations] 18 | 19 | [isort] 20 | profile=black 21 | -------------------------------------------------------------------------------- /django_celery_results/utils.py: -------------------------------------------------------------------------------- 1 | """Utilities.""" 2 | # -- XXX This module must not use translation as that causes 3 | # -- a recursive loader import! 4 | 5 | from django.conf import settings 6 | from django.utils import timezone 7 | 8 | # see Issue celery/django-celery#222 9 | now_localtime = getattr(timezone, 'template_localtime', timezone.localtime) 10 | 11 | 12 | def now(): 13 | """Return the current date and time.""" 14 | if getattr(settings, 'USE_TZ', False): 15 | return now_localtime(timezone.now()) 16 | else: 17 | return timezone.now() 18 | -------------------------------------------------------------------------------- /django_celery_results/migrations/0007_remove_taskresult_hidden.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.6 on 2019-10-27 11:29 2 | 3 | # this file is auto-generated so don't do flake8 on it 4 | # flake8: noqa 5 | 6 | 7 | from django.db import migrations 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | ('django_celery_results', '0006_taskresult_date_created'), 14 | ] 15 | 16 | operations = [ 17 | migrations.RemoveField( 18 | model_name='taskresult', 19 | name='hidden', 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /django_celery_results/migrations/0013_taskresult_django_cele_periodi_1993cf_idx.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.3 on 2024-11-05 13:17 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('django_celery_results', '0012_taskresult_date_started'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddIndex( 14 | model_name='taskresult', 15 | index=models.Index( 16 | fields=['periodic_task_name'], 17 | name='django_cele_periodi_1993cf_idx' 18 | ), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /django_celery_results/migrations/0003_auto_20181106_1101.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1 on 2018-11-06 11:01 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('django_celery_results', '0002_add_task_name_args_kwargs'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='taskresult', 15 | options={ 16 | 'ordering': ['-date_done'], 17 | 'verbose_name': 'task result', 18 | 'verbose_name_plural': 'task results' 19 | }, 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ======================================================================= 2 | django-celery-results - Celery Result Backends for Django 3 | ======================================================================= 4 | 5 | .. include:: includes/introduction.txt 6 | 7 | Contents 8 | ======== 9 | 10 | .. toctree:: 11 | :maxdepth: 1 12 | 13 | getting_started 14 | injecting_metadata 15 | copyright 16 | 17 | .. toctree:: 18 | :maxdepth: 2 19 | 20 | reference/index 21 | 22 | .. toctree:: 23 | :maxdepth: 1 24 | 25 | changelog 26 | glossary 27 | 28 | Indices and tables 29 | ================== 30 | 31 | * :ref:`genindex` 32 | * :ref:`modindex` 33 | * :ref:`search` 34 | 35 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/asottile/pyupgrade 3 | rev: v3.21.2 4 | hooks: 5 | - id: pyupgrade 6 | args: ["--py36-plus"] 7 | 8 | - repo: https://github.com/PyCQA/flake8 9 | rev: 7.3.0 10 | hooks: 11 | - id: flake8 12 | 13 | - repo: https://github.com/asottile/yesqa 14 | rev: v1.5.0 15 | hooks: 16 | - id: yesqa 17 | 18 | - repo: https://github.com/pre-commit/pre-commit-hooks 19 | rev: v6.0.0 20 | hooks: 21 | - id: check-merge-conflict 22 | - id: check-toml 23 | - id: check-yaml 24 | - id: mixed-line-ending 25 | 26 | - repo: https://github.com/pycqa/isort 27 | rev: 7.0.0 28 | hooks: 29 | - id: isort 30 | -------------------------------------------------------------------------------- /django_celery_results/migrations/0011_taskresult_periodic_task_name.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.8 on 2021-11-10 08:05 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('django_celery_results', '0010_remove_duplicate_indices'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='taskresult', 15 | name='periodic_task_name', 16 | field=models.CharField( 17 | help_text='Name of the Periodic Task which was run', 18 | max_length=255, 19 | null=True, 20 | verbose_name='Periodic Task Name'), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /django_celery_results/migrations/0012_taskresult_date_started.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.13 on 2024-06-02 07:56 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('django_celery_results', '0011_taskresult_periodic_task_name'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='taskresult', 15 | name='date_started', 16 | field=models.DateTimeField( 17 | default=None, 18 | help_text='Datetime field when the task was started in UTC', 19 | null=True, 20 | verbose_name='Started DateTime', 21 | ), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /django_celery_results/migrations/0014_alter_taskresult_status.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.7 on 2025-03-08 06:18 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ( 10 | "django_celery_results", 11 | "0013_taskresult_django_cele_periodi_1993cf_idx" 12 | ), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name="taskresult", 18 | name="status", 19 | field=models.CharField( 20 | default="PENDING", 21 | help_text="Current state of the task being run", 22 | max_length=50, 23 | verbose_name="Task State", 24 | ), 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /django_celery_results/migrations/0005_taskresult_worker.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.11.22 on 2019-07-24 15:38 2 | 3 | # this file is auto-generated so don't do flake8 on it 4 | # flake8: noqa 5 | 6 | 7 | from django.db import migrations, models 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | ('django_celery_results', '0004_auto_20190516_0412'), 14 | ] 15 | 16 | operations = [ 17 | migrations.AddField( 18 | model_name='taskresult', 19 | name='worker', 20 | field=models.CharField(db_index=True, default=None, 21 | help_text='Worker that executes the task', 22 | max_length=100, null=True, 23 | verbose_name='Worker'), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Version control 2 | .git 3 | .gitignore 4 | .gitattributes 5 | 6 | # Python 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | *.so 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | *.egg 26 | 27 | # Unit test / coverage reports 28 | htmlcov/ 29 | .tox/ 30 | .coverage 31 | .coverage.* 32 | .cache 33 | coverage.xml 34 | *.cover 35 | .pytest_cache/ 36 | .hypothesis/ 37 | 38 | # Django 39 | *.log 40 | local_settings.py 41 | db.sqlite3 42 | db.sqlite3-journal 43 | 44 | # Docker 45 | docker-compose*.yml 46 | Dockerfile 47 | .docker 48 | docker/ 49 | 50 | # IDE 51 | .idea/ 52 | .vscode/ 53 | *.swp 54 | *.swo 55 | *~ 56 | 57 | # Environment 58 | .env 59 | .venv 60 | env/ 61 | venv/ 62 | ENV/ 63 | 64 | # Documentation 65 | docs/ 66 | *.rst 67 | *.md 68 | LICENSE 69 | AUTHORS 70 | 71 | # OS specific 72 | .DS_Store 73 | Thumbs.db 74 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from sphinx_celery import conf 4 | 5 | globals().update(conf.build_config( 6 | 'django_celery_results', __file__, 7 | project='django_celery_results', 8 | # version_dev='2.0', 9 | # version_stable='1.4', 10 | canonical_url='https://django-celery-results.readthedocs.io', 11 | webdomain='', 12 | github_project='celery/django-celery-results', 13 | copyright='2009-2022', 14 | django_settings='proj.settings', 15 | include_intersphinx={'python', 'sphinx', 'django', 'celery'}, 16 | path_additions=[os.path.join(os.pardir, 't')], 17 | extra_extensions=['sphinx.ext.napoleon'], 18 | html_logo='images/logo.png', 19 | html_favicon='images/favicon.ico', 20 | html_prepend_sidebars=[], 21 | apicheck_ignore_modules=[ 22 | 'django_celery_results', 23 | 'django_celery_results.apps', 24 | 'django_celery_results.admin', 25 | r'django_celery_results.migrations.*', 26 | ], 27 | )) 28 | -------------------------------------------------------------------------------- /django_celery_results/migrations/0002_add_task_name_args_kwargs.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.9.1 on 2017-10-26 16:06 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('django_celery_results', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='taskresult', 15 | name='task_args', 16 | field=models.TextField(null=True, verbose_name='task arguments'), 17 | ), 18 | migrations.AddField( 19 | model_name='taskresult', 20 | name='task_kwargs', 21 | field=models.TextField(null=True, verbose_name='task kwargs'), 22 | ), 23 | migrations.AddField( 24 | model_name='taskresult', 25 | name='task_name', 26 | field=models.CharField(max_length=255, null=True, 27 | verbose_name='task name' 28 | ), 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /.cookiecutterrc: -------------------------------------------------------------------------------- 1 | # This file exists so you can easily regenerate your project. 2 | # 3 | # `cookiepatcher` is a convenient shim around `cookiecutter` 4 | # for regenerating projects (it will generate a .cookiecutterrc 5 | # automatically for any template). To use it: 6 | # 7 | # pip install cookiepatcher 8 | # cookiepatcher gh:ionelmc/cookiecutter-pylibrary project-path 9 | # 10 | # See: 11 | # https://pypi.python.org/pypi/cookiecutter 12 | # 13 | # Alternatively, you can run: 14 | # 15 | # cookiecutter --overwrite-if-exists --config-file=project-path/.cookiecutterrc gh:ionelmc/cookiecutter-pylibrary 16 | 17 | default_context: 18 | 19 | email: 'ask@celeryproject.org' 20 | full_name: 'Ask Solem' 21 | github_username: 'celery' 22 | project_name: 'django-celery-results' 23 | project_short_description: 'Celery result backends using Django' 24 | project_slug: 'django-celery-results' 25 | version: '1.0.0' 26 | year: '2016' 27 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def pytest_addoption(parser): 5 | parser.addoption( 6 | '-B', 7 | '--run-benchmarks', 8 | action='store_true', 9 | default=False, 10 | help='run benchmarks', 11 | ) 12 | 13 | 14 | def pytest_runtest_setup(item): 15 | """ 16 | Skip tests marked benchmark unless --run-benchmark is given to pytest 17 | """ 18 | run_benchmarks = item.config.getoption('--run-benchmarks') 19 | 20 | is_benchmark = any(item.iter_markers(name="benchmark")) 21 | 22 | if is_benchmark: 23 | if run_benchmarks: 24 | return 25 | 26 | pytest.skip( 27 | 'need --run-benchmarks to run benchmarks' 28 | ) 29 | 30 | 31 | def pytest_collection_modifyitems(items): 32 | """ 33 | Add the "benchmark" mark to tests that start with "benchmark_". 34 | """ 35 | for item in items: 36 | test_class_name = item.cls.__name__ 37 | if test_class_name.startswith("benchmark_"): 38 | item.add_marker(pytest.mark.benchmark) 39 | -------------------------------------------------------------------------------- /docs/copyright.rst: -------------------------------------------------------------------------------- 1 | Copyright 2 | ========= 3 | 4 | *django-celery-results User Manual* 5 | 6 | by Ask Solem 7 | 8 | .. |copy| unicode:: U+000A9 .. COPYRIGHT SIGN 9 | 10 | Copyright |copy| 2016, Ask Solem 11 | 12 | All rights reserved. This material may be copied or distributed only 13 | subject to the terms and conditions set forth in the `Creative Commons 14 | Attribution-ShareAlike 4.0 International 15 | `_ license. 16 | 17 | You may share and adapt the material, even for commercial purposes, but 18 | you must give the original author credit. 19 | If you alter, transform, or build upon this 20 | work, you may distribute the resulting work only under the same license or 21 | a license compatible to this one. 22 | 23 | .. note:: 24 | 25 | While the django-celery-results *documentation* is offered under the 26 | Creative Commons *Attribution-ShareAlike 4.0 International* license 27 | the django-celery-results *software* is offered under the 28 | `BSD License (3 Clause) `_ 29 | -------------------------------------------------------------------------------- /docs/includes/introduction.txt: -------------------------------------------------------------------------------- 1 | :Version: 2.6.0 2 | :Web: https://django-celery-results.readthedocs.io/ 3 | :Download: https://pypi.python.org/pypi/django-celery-results 4 | :Source: https://github.com/celery/django-celery-results 5 | :Keywords: django, celery, database, results 6 | 7 | About 8 | ===== 9 | 10 | This extension enables you to store Celery task and group results using the Django ORM. 11 | 12 | It defines 2 models (:class:`django_celery_results.models.TaskResult` and :class:`django_celery_results.models.GroupResult`) 13 | used to store task and group results, and you can query these database tables like 14 | any other Django model. 15 | 16 | If your :pypi:`django-celery-beat` carries ``request["properties"]["periodic_task_name"]``, 17 | it will be stored in :attr:`TaskResult.periodic_task_name ` to track the periodic task. 18 | 19 | Installing 20 | ========== 21 | 22 | .. _Celery documentation: https://docs.celeryq.dev/en/latest/django/first-steps-with-django.html#django-celery-results 23 | 24 | The installation instructions for this extension is available from the `Celery documentation`_. -------------------------------------------------------------------------------- /django_celery_results/__init__.py: -------------------------------------------------------------------------------- 1 | """Celery result backends for Django.""" 2 | # :copyright: (c) 2016, Ask Solem. 3 | # :copyright: (c) 2017-2033, Asif Saif Uddin. 4 | # All rights reserved. 5 | # :license: BSD (3 Clause), see LICENSE for more details. 6 | 7 | import re 8 | from collections import namedtuple 9 | 10 | import django 11 | 12 | __version__ = '2.6.0' 13 | __author__ = 'Asif Saif Uddin' 14 | __contact__ = 'auvipy@gmail.com' 15 | __homepage__ = 'https://github.com/celery/django-celery-results' 16 | __docformat__ = 'restructuredtext' 17 | 18 | # -eof meta- 19 | 20 | version_info_t = namedtuple('version_info_t', ( 21 | 'major', 'minor', 'micro', 'releaselevel', 'serial', 22 | )) 23 | 24 | # bumpversion can only search for {current_version} 25 | # so we have to parse the version here. 26 | _temp = re.match( 27 | r'(\d+)\.(\d+).(\d+)(.+)?', __version__).groups() 28 | VERSION = version_info = version_info_t( 29 | int(_temp[0]), int(_temp[1]), int(_temp[2]), _temp[3] or '', '') 30 | del _temp 31 | del re 32 | 33 | __all__ = [] 34 | 35 | if django.VERSION < (3, 2): 36 | default_app_config = 'django_celery_results.apps.CeleryResultConfig' 37 | -------------------------------------------------------------------------------- /docs/includes/installation.txt: -------------------------------------------------------------------------------- 1 | .. _installation: 2 | 3 | Installation 4 | ============ 5 | 6 | You can install django-celery-results either via the Python Package Index (PyPI) 7 | or from source. 8 | 9 | To install using `pip`,:: 10 | 11 | $ pip install -U django-celery-results 12 | 13 | .. _installing-from-source: 14 | 15 | Downloading and installing from source 16 | -------------------------------------- 17 | 18 | Download the latest version of django-celery-results from 19 | https://pypi.python.org/pypi/django-celery-results 20 | 21 | You can install it by doing the following,:: 22 | 23 | $ tar xvfz django-celery-results-0.0.0.tar.gz 24 | $ cd django-celery-results-0.0.0 25 | $ python setup.py build 26 | # python setup.py install 27 | 28 | The last command must be executed as a privileged user if 29 | you are not currently using a virtualenv. 30 | 31 | .. _installing-from-git: 32 | 33 | Using the development version 34 | ----------------------------- 35 | 36 | With pip 37 | ~~~~~~~~ 38 | 39 | You can install the latest snapshot of django-celery-results using the following 40 | pip command:: 41 | 42 | $ pip install https://github.com/celery/django-celery-results/zipball/master#egg=django-celery-results 43 | -------------------------------------------------------------------------------- /django_celery_results/backends/cache.py: -------------------------------------------------------------------------------- 1 | """Celery cache backend using the Django Cache Framework.""" 2 | 3 | from celery.backends.base import KeyValueStoreBackend 4 | from django.core.cache import cache as default_cache 5 | from django.core.cache import caches 6 | from kombu.utils.encoding import bytes_to_str 7 | 8 | 9 | class CacheBackend(KeyValueStoreBackend): 10 | """Backend using the Django cache framework to store task metadata.""" 11 | 12 | def __init__(self, *args, **kwargs): 13 | super().__init__(*args, **kwargs) 14 | 15 | # Must make sure backend doesn't convert exceptions to dict. 16 | self.serializer = 'pickle' 17 | 18 | def get(self, key): 19 | key = bytes_to_str(key) 20 | return self.cache_backend.get(key) 21 | 22 | def set(self, key, value): 23 | key = bytes_to_str(key) 24 | self.cache_backend.set(key, value, self.expires) 25 | 26 | def delete(self, key): 27 | key = bytes_to_str(key) 28 | self.cache_backend.delete(key) 29 | 30 | def encode(self, data): 31 | return data 32 | 33 | def decode(self, data): 34 | return data 35 | 36 | @property 37 | def cache_backend(self): 38 | backend = self.app.conf.cache_backend 39 | return caches[backend] if backend else default_cache 40 | -------------------------------------------------------------------------------- /compose.yml: -------------------------------------------------------------------------------- 1 | name: django-celeryresult 2 | 3 | volumes: 4 | postgres_data: 5 | 6 | networks: 7 | celery: 8 | 9 | services: 10 | postgres: 11 | image: postgres:${POSTGRES_VERSION:-15} 12 | container_name: djangoceleryresults-postgres 13 | cpus: 0.25 14 | mem_limit: 256m 15 | mem_reservation: 128m 16 | environment: 17 | POSTGRES_USER: ${DB_POSTGRES_USER:-postgres} 18 | POSTGRES_PASSWORD: ${DB_POSTGRES_PASSWORD:-postgres} 19 | POSTGRES_DB: ${DB_POSTGRES_DATABASE:-postgres} 20 | volumes: 21 | - postgres_data:/var/lib/postgresql/data 22 | networks: 23 | celery: 24 | aliases: 25 | - dcr-postgres 26 | 27 | app: 28 | build: 29 | context: . 30 | dockerfile: ./docker/Dockerfile 31 | container_name: djangoceleryresults-app 32 | cpus: 0.25 33 | mem_limit: 512m 34 | mem_reservation: 256m 35 | environment: 36 | DB_POSTGRES_USER: ${DB_POSTGRES_USER:-postgres} 37 | DB_POSTGRES_PASSWORD: ${DB_POSTGRES_PASSWORD:-postgres} 38 | DB_POSTGRES_DATABASE: ${DB_POSTGRES_DATABASE:-postgres} 39 | DB_POSTGRES_HOST: dcr-postgres 40 | depends_on: 41 | - postgres 42 | networks: 43 | celery: 44 | volumes: 45 | - ./:/usr/src/celery/django-celery-results 46 | -------------------------------------------------------------------------------- /t/unit/test_migrations.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.core.management import call_command 4 | from django.test import TestCase, override_settings 5 | 6 | from django_celery_results import migrations as result_migrations 7 | 8 | 9 | class MigrationTests(TestCase): 10 | def test_no_duplicate_migration_numbers(self): 11 | """Verify no duplicate migration numbers. 12 | 13 | Migration files with the same number can cause issues with 14 | backward migrations, so avoid them. 15 | """ 16 | path = os.path.dirname(result_migrations.__file__) 17 | files = [f[:4] for f in os.listdir(path) if f.endswith('.py')] 18 | self.assertEqual( 19 | len(files), len(set(files)), 20 | msg='Detected migration files with the same migration number') 21 | 22 | def test_models_match_migrations(self): 23 | """Make sure that no pending migrations exist for the app. 24 | 25 | Here just detect if model changes exist that require 26 | a migration, and if so we fail. 27 | """ 28 | call_command( 29 | "makemigrations", "django_celery_results", "--check", "--dry-run" 30 | ) 31 | 32 | @override_settings(DEFAULT_AUTO_FIELD='django.db.models.BigAutoField') 33 | def test_models_match_migrations_with_changed_default_auto_field(self): 34 | """Test with changing default_auto_field. 35 | 36 | This logic make sure that no pending migrations created even if 37 | the user changes the `DEFAULT_AUTO_FIELD`. 38 | """ 39 | self.test_models_match_migrations() 40 | -------------------------------------------------------------------------------- /django_celery_results/migrations/0008_chordcounter.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.6 on 2020-05-12 12:05 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('django_celery_results', '0007_remove_taskresult_hidden'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='ChordCounter', 16 | fields=[ 17 | ('id', models.AutoField( 18 | auto_created=True, 19 | primary_key=True, 20 | serialize=False, 21 | verbose_name='ID')), 22 | ('group_id', models.CharField( 23 | db_index=True, 24 | help_text='Celery ID for the Chord header group', 25 | max_length=getattr( 26 | settings, 27 | 'DJANGO_CELERY_RESULTS_TASK_ID_MAX_LENGTH', 28 | 255 29 | ), 30 | unique=True, 31 | verbose_name='Group ID')), 32 | ('sub_tasks', models.TextField( 33 | help_text='JSON serialized list of task result tuples. ' 34 | 'use .group_result() to decode')), 35 | ('count', models.PositiveIntegerField( 36 | help_text='Starts at len(chord header) ' 37 | 'and decrements after each task is finished')), 38 | ], 39 | ), 40 | ] 41 | -------------------------------------------------------------------------------- /docs/templates/readme.txt: -------------------------------------------------------------------------------- 1 | ===================================================================== 2 | Celery Result Backends using the Django ORM/Cache framework. 3 | ===================================================================== 4 | 5 | |build-status| |coverage| |license| |wheel| |pyversion| |pyimp| 6 | 7 | .. include:: ../includes/introduction.txt 8 | 9 | .. include:: ../includes/installation.txt 10 | 11 | .. |build-status| image:: https://secure.travis-ci.org/celery/django-celery-results.png?branch=master 12 | :alt: Build status 13 | :target: https://travis-ci.org/celery/django-celery-results 14 | 15 | .. |coverage| image:: https://codecov.io/github/celery/django-celery-results/coverage.svg?branch=master 16 | :target: https://codecov.io/github/celery/django-celery-results?branch=master 17 | 18 | .. |license| image:: https://img.shields.io/pypi/l/django-celery-results.svg 19 | :alt: BSD License 20 | :target: https://opensource.org/licenses/BSD-3-Clause 21 | 22 | .. |wheel| image:: https://img.shields.io/pypi/wheel/django-celery-results.svg 23 | :alt: django-celery-results can be installed via wheel 24 | :target: https://pypi.python.org/pypi/django-celery-results/ 25 | 26 | .. |pyversion| image:: https://img.shields.io/pypi/pyversions/django-celery-results.svg 27 | :alt: Supported Python versions. 28 | :target: https://pypi.python.org/pypi/django-celery-results/ 29 | 30 | .. |pyimp| image:: https://img.shields.io/pypi/implementation/django-celery-results.svg 31 | :alt: Support Python implementations. 32 | :target: https://pypi.python.org/pypi/django-celery-results/ 33 | -------------------------------------------------------------------------------- /django_celery_results/migrations/0006_taskresult_date_created.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.4 on 2019-08-21 19:53 2 | 3 | # this file is auto-generated so don't do flake8 on it 4 | # flake8: noqa 5 | 6 | 7 | import django.utils.timezone 8 | from django.db import migrations, models 9 | 10 | 11 | def copy_date_done_to_date_created(apps, schema_editor): 12 | TaskResult = apps.get_model('django_celery_results', 'taskresult') 13 | db_alias = schema_editor.connection.alias 14 | TaskResult.objects.using(db_alias).all().update( 15 | date_created=models.F('date_done') 16 | ) 17 | 18 | 19 | def reverse_copy_date_done_to_date_created(app, schema_editor): 20 | # the reverse of 'copy_date_done_to_date_created' is do nothing 21 | # because the 'date_created' will be removed. 22 | pass 23 | 24 | 25 | class Migration(migrations.Migration): 26 | 27 | dependencies = [ 28 | ('django_celery_results', '0005_taskresult_worker'), 29 | ] 30 | 31 | operations = [ 32 | migrations.AddField( 33 | model_name='taskresult', 34 | name='date_created', 35 | field=models.DateTimeField( 36 | auto_now_add=True, 37 | db_index=True, 38 | default=django.utils.timezone.now, 39 | help_text='Datetime field when the task result was created in UTC', 40 | verbose_name='Created DateTime' 41 | ), 42 | preserve_default=False, 43 | ), 44 | migrations.RunPython(copy_date_done_to_date_created, 45 | reverse_copy_date_done_to_date_created), 46 | ] 47 | -------------------------------------------------------------------------------- /django_celery_results/migrations/0010_remove_duplicate_indices.py: -------------------------------------------------------------------------------- 1 | """ 2 | Migration to amend the 0009 migration released on django_celery_results 2.1.0 3 | 4 | That migration introduced duplicate indexes breaking Oracle support. 5 | This migration will remove those indexes (on non-Oracle db's) 6 | while in-place changing migration 0009 7 | to not add the duplicates for new installs 8 | """ 9 | 10 | from django.db import DatabaseError, migrations 11 | 12 | 13 | class TryRemoveIndex(migrations.RemoveIndex): 14 | """Operation to remove the Index 15 | without reintroducing it on reverting the migration 16 | """ 17 | 18 | def database_forwards(self, *args, **kwargs): 19 | """Remove the index on the database if it exists""" 20 | try: 21 | super().database_forwards(*args, **kwargs) 22 | except DatabaseError: 23 | pass 24 | except Exception: 25 | # Not all DB engines throw DatabaseError when the 26 | # index does not exist. 27 | pass 28 | 29 | def database_backwards(self, *args, **kwargs): 30 | """Don't re-add the index when reverting this migration""" 31 | pass 32 | 33 | 34 | class Migration(migrations.Migration): 35 | 36 | dependencies = [ 37 | ('django_celery_results', '0009_groupresult'), 38 | ] 39 | 40 | operations = [ 41 | TryRemoveIndex( 42 | model_name='chordcounter', 43 | name='django_cele_group_i_299b0d_idx', 44 | ), 45 | TryRemoveIndex( 46 | model_name='groupresult', 47 | name='django_cele_group_i_3cddec_idx', 48 | ), 49 | TryRemoveIndex( 50 | model_name='taskresult', 51 | name='django_cele_task_id_7f8fca_idx', 52 | ), 53 | ] 54 | -------------------------------------------------------------------------------- /django_celery_results/views.py: -------------------------------------------------------------------------------- 1 | """Views.""" 2 | from celery import states 3 | from celery.result import AsyncResult, GroupResult 4 | from celery.utils import get_full_cls_name 5 | from django.http import JsonResponse 6 | from kombu.utils.encoding import safe_repr 7 | 8 | 9 | def is_task_successful(request, task_id): 10 | """Return task execution status in JSON format.""" 11 | return JsonResponse({'task': { 12 | 'id': task_id, 13 | 'executed': AsyncResult(task_id).successful(), 14 | }}) 15 | 16 | 17 | def task_status(request, task_id): 18 | """Return task status and result in JSON format.""" 19 | result = AsyncResult(task_id) 20 | state, retval = result.state, result.result 21 | response_data = {'id': task_id, 'status': state, 'result': retval} 22 | if state in states.EXCEPTION_STATES: 23 | traceback = result.traceback 24 | response_data.update({'result': safe_repr(retval), 25 | 'exc': get_full_cls_name(retval.__class__), 26 | 'traceback': traceback}) 27 | return JsonResponse({'task': response_data}) 28 | 29 | 30 | def is_group_successful(request, group_id): 31 | """Return if group was successfull as boolean.""" 32 | results = GroupResult.restore(group_id) 33 | 34 | return JsonResponse({ 35 | 'group': { 36 | 'id': group_id, 37 | 'results': [ 38 | {'id': task.id, 'executed': task.successful()} 39 | for task in results 40 | ] if results else [] 41 | } 42 | }) 43 | 44 | 45 | def group_status(request, group_id): 46 | """Return group id and its async results status & result in JSON format.""" 47 | result = GroupResult.restore(group_id) 48 | retval = [ 49 | {"result": async_result.result, "status": async_result.status} 50 | for async_result in result.results 51 | ] 52 | response_data = {'id': group_id, 'results': retval} 53 | return JsonResponse({'group': response_data}) 54 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py313-django{51,52} 4 | py312-django{51,52,42} 5 | py311-django{51,52,42} 6 | py310-django{51,52,42,32} 7 | py39-django{42,32} 8 | py38-django{42,32} 9 | pypy310-django{42,32,52} 10 | 11 | flake8 12 | apicheck 13 | pydocstyle 14 | cov 15 | integration 16 | 17 | [travis:env] 18 | DJANGO = 19 | 3.2: django32 20 | 4.2: django42 21 | 5.0: django50 22 | 5.1: django51 23 | 5.2: django52 24 | 25 | [testenv] 26 | passenv = 27 | DB_POSTGRES_* 28 | deps= 29 | -r{toxinidir}/requirements/default.txt 30 | -r{toxinidir}/requirements/test.txt 31 | -r{toxinidir}/requirements/test-ci.txt 32 | 33 | django32: -r{toxinidir}/requirements/test-django32.txt 34 | django42: -r{toxinidir}/requirements/test-django42.txt 35 | django50: -r{toxinidir}/requirements/test-django50.txt 36 | django51: -r{toxinidir}/requirements/test-django51.txt 37 | django52: -r{toxinidir}/requirements/test-django52.txt 38 | 39 | cov,integration: -r{toxinidir}/requirements/test-django.txt 40 | 41 | linkcheck,apicheck: -r{toxinidir}/requirements/docs.txt 42 | flake8,pydocstyle: -r{toxinidir}/requirements/pkgutils.txt 43 | sitepackages = False 44 | recreate = True 45 | commands = 46 | pip install celery 47 | pip install kombu 48 | pytest -xv 49 | 50 | [testenv:apicheck] 51 | commands = 52 | sphinx-build -W -b apicheck -d {envtmpdir}/doctrees docs docs/_build/apicheck 53 | 54 | [testenv:linkcheck] 55 | commands = 56 | sphinx-build -W -b linkcheck -d {envtmpdir}/doctrees docs docs/_build/linkcheck 57 | 58 | [testenv:flake8] 59 | commands = 60 | flake8 {toxinidir}/django_celery_results {toxinidir}/t 61 | 62 | [testenv:pydocstyle] 63 | commands = 64 | pydocstyle {toxinidir}/django_celery_results 65 | 66 | [testenv:cov] 67 | usedevelop = true 68 | commands = pip install celery 69 | pip install kombu 70 | pytest --cov=django_celery_results --cov-report=xml --no-cov-on-fail 71 | 72 | [testenv:integration] 73 | commands = 74 | pip install celery 75 | pip install kombu 76 | pytest -B -xv 77 | -------------------------------------------------------------------------------- /docs/injecting_metadata.rst: -------------------------------------------------------------------------------- 1 | Injecting metadata 2 | ================== 3 | 4 | 5 | To save arbitrary data on the field TaskResult.meta, the Celery Task Request must be manipulated as such: 6 | 7 | .. code-block:: python 8 | 9 | from celery import Celery 10 | 11 | app = Celery('hello', broker='amqp://guest@localhost//') 12 | 13 | @app.task(bind=True) 14 | def hello(task_instance): 15 | task_instance.request.meta = {'some_key': 'some_value'} 16 | task_instance.update_state( 17 | state='PROGRESS', 18 | meta='Task current result' 19 | ) 20 | # If TaskResult is queried from DB at this momento it will yield 21 | # TaskResult( 22 | # result='Task current result', 23 | # meta={'some_key': 'some_value'} # some discrepancies apply as I didn't document the json parse and children data 24 | # ) 25 | return 'hello world' 26 | 27 | # After task is completed, if TaskResult is queried from DB at this momento it will yield 28 | # TaskResult( 29 | # result='hello world', 30 | # meta={'some_key': 'some_value'} # some discrepancies apply as I didn't document the json parse and children data 31 | # ) 32 | 33 | This way, the value of ``task_instance.request.meta`` will be stored on ``TaskResult.meta``. 34 | 35 | Note that the `meta` arg in the method `update_state` is not really a metadata and it's not stored on ``TaskResult.meta``. 36 | This arg is used to save the CURRENT result of the task. So it's stored on ``TaskResult.result``. 37 | 38 | It works this way because while a task is executing, the `TaskResult` is used really as current task state; holding information, temporarily, until the task completes. 39 | Subsequent calls to `update_state` will update the same `TaskResult`, overwriting what was there previously. 40 | Upon completion of the task, the results of the task are stored in the same TaskResult, overwriting the previous state of the task. 41 | So the return from the function is stored in ``TaskResult.result`` and ``TaskResult.status`` is set to 'SUCCESS' (or 'FAILURE'). 42 | 43 | -------------------------------------------------------------------------------- /django_celery_results/urls.py: -------------------------------------------------------------------------------- 1 | """URLs defined for celery. 2 | 3 | * ``/$task_id/done/`` 4 | URL to :func:`~celery.views.is_successful`. 5 | * ``/$task_id/status/`` 6 | URL to :func:`~celery.views.task_status`. 7 | """ 8 | import warnings 9 | 10 | from django.conf import settings 11 | from django.urls import path, register_converter 12 | 13 | from . import views 14 | 15 | 16 | class TaskPatternConverter: 17 | """Custom path converter for task & group id's. 18 | 19 | They are slightly different from the built `uuid` 20 | """ 21 | 22 | regex = r'[\w\d\-\.]+' 23 | 24 | def to_python(self, value): 25 | """Convert url to python value.""" 26 | return str(value) 27 | 28 | def to_url(self, value): 29 | """Convert python value into url, just a string.""" 30 | return value 31 | 32 | 33 | register_converter(TaskPatternConverter, 'task_pattern') 34 | 35 | urlpatterns = [ 36 | path( 37 | 'task/done//', 38 | views.is_task_successful, 39 | name='celery-is_task_successful' 40 | ), 41 | path( 42 | 'task/status//', 43 | views.task_status, 44 | name='celery-task_status' 45 | ), 46 | path( 47 | 'group/done//', 48 | views.is_group_successful, 49 | name='celery-is_group_successful' 50 | ), 51 | path( 52 | 'group/status//', 53 | views.group_status, 54 | name='celery-group_status' 55 | ), 56 | ] 57 | 58 | if getattr(settings, 'DJANGO_CELERY_RESULTS_ID_FIRST_URLS', True): 59 | warnings.warn( 60 | "ID first urls depricated, use noun first urls instead." 61 | "Will be removed in 2022.", 62 | DeprecationWarning 63 | ) 64 | 65 | urlpatterns += [ 66 | path( 67 | '/done/', 68 | views.is_task_successful, 69 | name='celery-is_task_successful' 70 | ), 71 | path( 72 | '/status/', 73 | views.task_status, 74 | name='celery-task_status' 75 | ), 76 | path( 77 | '/group/done/', 78 | views.is_group_successful, 79 | name='celery-is_group_successful' 80 | ), 81 | path( 82 | '/group/status/', 83 | views.group_status, 84 | name='celery-group_status' 85 | ), 86 | ] 87 | -------------------------------------------------------------------------------- /t/integration/benchmark_models.py: -------------------------------------------------------------------------------- 1 | import time 2 | from datetime import timedelta 3 | 4 | import pytest 5 | from celery import uuid 6 | from django.test import TransactionTestCase 7 | 8 | from django_celery_results.models import TaskResult 9 | from django_celery_results.utils import now 10 | 11 | RECORDS_COUNT = 100000 12 | 13 | 14 | @pytest.fixture() 15 | def use_benchmark(request, benchmark): 16 | def wrapped(a=10, b=5): 17 | return a + b 18 | request.cls.benchmark = benchmark 19 | 20 | 21 | @pytest.mark.usefixtures('use_benchmark') 22 | @pytest.mark.usefixtures('depends_on_current_app') 23 | class benchmark_Models(TransactionTestCase): 24 | 25 | @pytest.fixture(autouse=True) 26 | def setup_app(self, app): 27 | self.app = app 28 | self.app.conf.result_serializer = 'pickle' 29 | self.app.conf.result_backend = ( 30 | 'django_celery_results.backends:DatabaseBackend') 31 | 32 | def create_many_task_result(self, count): 33 | start = time.time() 34 | draft_results = [TaskResult(task_id=uuid()) for _ in range(count)] 35 | drafted = time.time() 36 | results = TaskResult.objects.bulk_create(draft_results) 37 | done_creating = time.time() 38 | 39 | print(( 40 | 'drafting time: {drafting:.2f}\n' 41 | 'bulk_create time: {done:.2f}\n' 42 | '------' 43 | ).format(drafting=drafted - start, done=done_creating - drafted)) 44 | return results 45 | 46 | def setup_records_to_delete(self): 47 | self.create_many_task_result(count=RECORDS_COUNT) 48 | mid_point = TaskResult.objects.order_by('id')[int(RECORDS_COUNT / 2)] 49 | todelete = TaskResult.objects.filter(id__gte=mid_point.id) 50 | todelete.update(date_done=now() - timedelta(days=10)) 51 | 52 | def test_taskresult_delete_expired(self): 53 | start = time.time() 54 | self.setup_records_to_delete() 55 | after_setup = time.time() 56 | self.benchmark.pedantic( 57 | TaskResult.objects.delete_expired, 58 | args=(self.app.conf.result_expires,), 59 | iterations=1, 60 | rounds=1, 61 | ) 62 | done = time.time() 63 | assert TaskResult.objects.count() == int(RECORDS_COUNT / 2) 64 | 65 | print(( 66 | '------' 67 | 'setup time: {setup:.2f}\n' 68 | 'bench time: {bench:.2f}\n' 69 | ).format(setup=after_setup - start, bench=done - after_setup)) 70 | assert self.benchmark.stats.stats.max < 5 71 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '38 23 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: blacksmith-4vcpu-ubuntu-2204 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v1 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v1 71 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.13.3-slim AS base 2 | 3 | LABEL authors='Diego Castro ' \ 4 | org.opencontainers.image.description='A Docker image to run tests for Django Celery Results using Tox' \ 5 | org.opencontainers.image.documentation='https://django-celery-results.readthedocs.io/' \ 6 | org.opencontainers.image.license='BSD-3-Clause' \ 7 | org.opencontainers.image.source='https://github.com/celery/django-celery-results' \ 8 | org.opencontainers.image.title='Dockerized Tox suite for Django Celery Results' \ 9 | org.opencontainers.image.vendor='Celery' 10 | 11 | RUN <- 41 | --health-cmd pg_isready 42 | --health-interval 10s 43 | --health-timeout 5s 44 | --health-retries 5 45 | ports: 46 | # Maps tcp port 5432 on service container to the host 47 | - 5432:5432 48 | env: 49 | # Docker image requires a password to be set 50 | POSTGRES_PASSWORD: "postgres" 51 | 52 | steps: 53 | - uses: actions/checkout@v3 54 | - uses: actions/setup-python@v4 55 | with: 56 | python-version: ${{ matrix.python-version }} 57 | - run: pip install tox 58 | - run: tox -v -- -v 59 | env: 60 | TOXENV: py-django${{ matrix.django }} 61 | 62 | rest: 63 | name: Integration/Coverage/Docs/Codestyle 64 | runs-on: blacksmith-4vcpu-ubuntu-2204 65 | strategy: 66 | matrix: 67 | toxenv: [ flake8, pydocstyle, cov, integration ] 68 | services: 69 | postgres: 70 | image: postgres 71 | options: >- 72 | --health-cmd pg_isready 73 | --health-interval 10s 74 | --health-timeout 5s 75 | --health-retries 5 76 | ports: 77 | - 5432:5432 78 | env: 79 | POSTGRES_PASSWORD: "postgres" 80 | 81 | steps: 82 | - uses: actions/checkout@v3 83 | - uses: actions/setup-python@v4 84 | with: 85 | python-version: 3.9 86 | - run: pip install tox 87 | - run: tox -v -- -v 88 | env: 89 | TOXENV: ${{ matrix.toxenv }} 90 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | ========= 2 | AUTHORS 3 | ========= 4 | :order: sorted 5 | 6 | Aaron Ross 7 | Adam Endicott 8 | Alex Stapleton 9 | Alvaro Vega 10 | Andrew Frankel 11 | Andrew Watts 12 | Andrii Kostenko 13 | Anton Novosyolov 14 | Ask Solem 15 | Asif Saif Uddin 16 | Augusto Becciu 17 | Ben Firshman 18 | Brad Jasper 19 | Brett Gibson 20 | Brian Rosner 21 | Charlie DeTar 22 | Christopher Grebs 23 | Dan LaMotte 24 | Darjus Loktevic 25 | David Fischer 26 | David Ziegler 27 | Diego Andres Sanabria Martin 28 | Diego Castro 29 | Dmitriy Krasilnikov 30 | Donald Stufft 31 | Eldon Stegall 32 | Eugene Nagornyi 33 | Felix Berger 35 | Glenn Washburn 36 | Gnrhxni 37 | Greg Taylor 38 | Grégoire Cachet 39 | Hari 40 | Idan Zalzberg 41 | Ionel Maries Cristian 42 | Jannis Leidel 43 | Jason Baker 44 | Jay States 45 | Jeff Balogh 46 | Jeff Fischer 47 | Jeffrey Hu 48 | Jens Alm 49 | Jerzy Kozera 50 | Jesper Noehr 51 | John Andrews 52 | John Watson 53 | Jonas Haag 54 | Jonatan Heyman 55 | Josh Drake 56 | José Moreira 57 | Jude Nagurney 58 | Justin Quick 59 | Keith Perkins 60 | Kirill Panshin 61 | Mark Hellewell 62 | Mark Lavin 63 | Mark Stover 64 | Maxim Bodyansky 65 | Michael Elsdoerfer 66 | Michael van Tellingen 67 | Mikhail Korobov 68 | Mos Wenzy 69 | Olivier Tabone 70 | Patrick Altman 71 | Piotr Bulinski 72 | Piotr Sikora 73 | Reza Lotun 74 | Rockallite Wulf 75 | Roger Barnes 76 | Roman Imankulov 77 | Rune Halvorsen 78 | Sam Cooke 79 | Scott Rubin 80 | Sean Creeley 81 | Serj Zavadsky 82 | Simon Charette 83 | Spencer Ellinor 84 | Theo Spears 85 | Timo Sugliani 86 | Vincent Driessen 87 | Vitaly Babiy 88 | Vladislav Poluhin 89 | Weipin Xia 90 | Wes Turner 91 | Wes Winham 92 | Williams Mendez 93 | WoLpH 94 | dongweiming 95 | zeez 96 | Eduardo Oliveira 97 | -------------------------------------------------------------------------------- /t/unit/backends/test_cache.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from datetime import timedelta 3 | 4 | import pytest 5 | from billiard.einfo import ExceptionInfo 6 | from celery import result, states, uuid 7 | from kombu.utils.encoding import bytes_to_str 8 | 9 | from django_celery_results.backends.cache import CacheBackend 10 | 11 | 12 | class SomeClass: 13 | 14 | def __init__(self, data): 15 | self.data = data 16 | 17 | 18 | class test_CacheBackend: 19 | 20 | def setup_method(self): 21 | self.b = CacheBackend(app=self.app) 22 | 23 | def test_mark_as_done(self): 24 | tid = uuid() 25 | 26 | assert self.b.get_status(tid) == states.PENDING 27 | assert self.b.get_result(tid) is None 28 | 29 | self.b.mark_as_done(tid, 42) 30 | assert self.b.get_status(tid) == states.SUCCESS 31 | assert self.b.get_result(tid) == 42 32 | 33 | def test_forget(self): 34 | tid = uuid() 35 | self.b.mark_as_done(tid, {'foo': 'bar'}) 36 | assert self.b.get_result(tid).get('foo') == 'bar' 37 | self.b.forget(tid) 38 | assert tid not in self.b._cache 39 | assert self.b.get_result(tid) is None 40 | 41 | @pytest.mark.usefixtures('depends_on_current_app') 42 | def test_save_restore_delete_group(self): 43 | group_id = uuid() 44 | result_ids = [uuid() for i in range(10)] 45 | results = list(map(result.AsyncResult, result_ids)) 46 | res = result.GroupResult(group_id, results) 47 | res.save(backend=self.b) 48 | saved = result.GroupResult.restore(group_id, backend=self.b) 49 | assert saved.results == results 50 | assert saved.id == group_id 51 | saved.delete(backend=self.b) 52 | assert result.GroupResult.restore(group_id, backend=self.b) is None 53 | 54 | def test_is_pickled(self): 55 | tid2 = uuid() 56 | result = {'foo': 'baz', 'bar': SomeClass(12345)} 57 | self.b.mark_as_done(tid2, result) 58 | # is serialized properly. 59 | rindb = self.b.get_result(tid2) 60 | assert rindb.get('foo') == 'baz' 61 | assert rindb.get('bar').data == 12345 62 | 63 | def test_convert_key_from_byte_to_str(self): 64 | """ Tests that key in byte form passed into cache 65 | are succesfully completed """ 66 | tid = bytes_to_str(uuid()) 67 | 68 | assert self.b.get_status(tid) == states.PENDING 69 | assert self.b.get_result(tid) is None 70 | 71 | self.b.mark_as_done(tid, 42) 72 | assert self.b.get_status(tid) == states.SUCCESS 73 | assert self.b.get_result(tid) == 42 74 | 75 | def test_mark_as_failure(self): 76 | einfo = None 77 | tid3 = uuid() 78 | try: 79 | raise KeyError('foo') 80 | except KeyError as exception: 81 | einfo = ExceptionInfo(sys.exc_info()) 82 | self.b.mark_as_failure(tid3, exception, traceback=einfo.traceback) 83 | assert self.b.get_status(tid3) == states.FAILURE 84 | assert isinstance(self.b.get_result(tid3), KeyError) 85 | assert self.b.get_traceback(tid3) == einfo.traceback 86 | 87 | def test_process_cleanup(self): 88 | self.b.process_cleanup() 89 | 90 | def test_set_expires(self): 91 | cb1 = CacheBackend(app=self.app, expires=timedelta(seconds=16)) 92 | assert cb1.expires == 16 93 | cb2 = CacheBackend(app=self.app, expires=32) 94 | assert cb2.expires == 32 95 | 96 | 97 | class test_custom_CacheBackend: 98 | 99 | def test_custom_cache_backend(self): 100 | self.app.conf.cache_backend = 'dummy' 101 | b = CacheBackend(app=self.app) 102 | assert ( 103 | b.cache_backend.__class__.__module__ == 'django.core.cache.backends.dummy' # noqa 104 | ) 105 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJ=django_celery_results 2 | PGPIDENT="Celery Security Team" 3 | PYTHON=python3 4 | PYTEST=pytest 5 | GIT=git 6 | TOX=tox 7 | ICONV=iconv 8 | FLAKE8=flake8 9 | PYDOCSTYLE=pydocstyle 10 | SPHINX2RST=sphinx2rst 11 | 12 | TESTDIR=t 13 | SPHINX_DIR=docs/ 14 | SPHINX_BUILDDIR="${SPHINX_DIR}/_build" 15 | README=README.rst 16 | README_SRC="docs/templates/readme.txt" 17 | CONTRIBUTING=CONTRIBUTING.rst 18 | CONTRIBUTING_SRC="docs/contributing.rst" 19 | SPHINX_HTMLDIR="${SPHINX_BUILDDIR}/html" 20 | DOCUMENTATION=Documentation 21 | 22 | 23 | all: help 24 | 25 | help: 26 | @echo "docs - Build documentation." 27 | @echo "test-all - Run tests for all supported python versions." 28 | @echo "distcheck ---------- - Check distribution for problems." 29 | @echo " test - Run unittests using current python." 30 | @echo " lint ------------ - Check codebase for problems." 31 | @echo " apicheck - Check API reference coverage." 32 | @echo " configcheck - Check configuration reference coverage." 33 | @echo " readmecheck - Check README.rst encoding." 34 | @echo " contribcheck - Check CONTRIBUTING.rst encoding" 35 | @echo " flakes -------- - Check code for syntax and style errors." 36 | @echo " flakecheck - Run flake8 on the source code." 37 | @echo " pep257check - Run pydocstyle on the source code." 38 | @echo "readme - Regenerate README.rst file." 39 | @echo "contrib - Regenerate CONTRIBUTING.rst file" 40 | @echo "clean-dist --------- - Clean all distribution build artifacts." 41 | @echo " clean-git-force - Remove all uncomitted files." 42 | @echo " clean ------------ - Non-destructive clean" 43 | @echo " clean-pyc - Remove .pyc/__pycache__ files" 44 | @echo " clean-docs - Remove documentation build artifacts." 45 | @echo " clean-build - Remove setup artifacts." 46 | @echo "bump - Bump patch version number." 47 | @echo "bump-minor - Bump minor version number." 48 | @echo "bump-major - Bump major version number." 49 | @echo "release - Make PyPI release." 50 | 51 | clean: clean-docs clean-pyc clean-build 52 | 53 | clean-dist: clean clean-git-force 54 | 55 | bump: 56 | bumpversion patch 57 | 58 | bump-minor: 59 | bumpversion minor 60 | 61 | bump-major: 62 | bumpversion major 63 | 64 | release: 65 | python setup.py register sdist bdist_wheel upload --sign --identity="$(PGPIDENT)" 66 | 67 | Documentation: 68 | (cd "$(SPHINX_DIR)"; $(MAKE) html) 69 | mv "$(SPHINX_HTMLDIR)" $(DOCUMENTATION) 70 | 71 | docs: Documentation 72 | 73 | clean-docs: 74 | -rm -rf "$(SPHINX_BUILDDIR)" 75 | 76 | lint: flakecheck apicheck configcheck readmecheck 77 | 78 | apicheck: 79 | (cd "$(SPHINX_DIR)"; $(MAKE) apicheck) 80 | 81 | configcheck: 82 | true 83 | 84 | flakecheck: 85 | $(FLAKE8) "$(PROJ)" "$(TESTDIR)" 86 | 87 | flakediag: 88 | -$(MAKE) flakecheck 89 | 90 | pep257check: 91 | $(PYDOCSTYLE) "$(PROJ)" 92 | 93 | 94 | flakes: flakediag pep257check 95 | 96 | clean-readme: 97 | -rm -f $(README) 98 | 99 | readmecheck: 100 | $(ICONV) -f ascii -t ascii $(README) >/dev/null 101 | 102 | $(README): 103 | $(SPHINX2RST) "$(README_SRC)" --ascii > $@ 104 | 105 | readme: clean-readme $(README) readmecheck 106 | 107 | clean-contrib: 108 | -rm -f "$(CONTRIBUTING)" 109 | 110 | $(CONTRIBUTING): 111 | $(SPHINX2RST) "$(CONTRIBUTING_SRC)" > $@ 112 | 113 | contrib: clean-contrib $(CONTRIBUTING) 114 | 115 | clean-pyc: 116 | -find . -type f -a \( -name "*.pyc" -o -name "*$$py.class" \) | xargs rm 117 | -find . -type d -name "__pycache__" | xargs rm -r 118 | 119 | removepyc: clean-pyc 120 | 121 | clean-build: 122 | rm -rf build/ dist/ .eggs/ *.egg-info/ .tox/ .coverage cover/ 123 | 124 | clean-git: 125 | $(GIT) clean -xdn 126 | 127 | clean-git-force: 128 | $(GIT) clean -xdf 129 | 130 | test-all: clean-pyc 131 | $(TOX) 132 | 133 | test: 134 | $(PYTHON) setup.py test 135 | 136 | cov: covbuild 137 | (cd $(TESTDIR); pytest -x --cov=django_celery_results --cov-report=html) 138 | 139 | build: 140 | $(PYTHON) setup.py sdist bdist_wheel 141 | 142 | distcheck: lint test clean 143 | 144 | dist: readme contrib clean-dist build 145 | -------------------------------------------------------------------------------- /t/unit/test_views.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | from celery import states, uuid 5 | from celery.result import AsyncResult 6 | from celery.result import GroupResult as CeleryGroupResult 7 | from django.test import TestCase 8 | from django.test.client import RequestFactory 9 | 10 | from django_celery_results.models import GroupResult, TaskResult 11 | from django_celery_results.views import ( 12 | group_status, 13 | is_group_successful, 14 | is_task_successful, 15 | task_status, 16 | ) 17 | 18 | 19 | @pytest.mark.usefixtures('depends_on_current_app') 20 | class test_Views(TestCase): 21 | @pytest.fixture(autouse=True) 22 | def setup_app(self, app): 23 | self.app = app 24 | self.app.conf.result_serializer = 'json' 25 | self.app.conf.result_backend = ( 26 | 'django_celery_results.backends:DatabaseBackend' 27 | ) 28 | 29 | def setUp(self): 30 | self.factory = RequestFactory() 31 | 32 | def create_task_result(self): 33 | id = uuid() 34 | taskmeta, created = TaskResult.objects.get_or_create(task_id=id) 35 | return taskmeta 36 | 37 | def test_is_task_successful(self): 38 | taskmeta = self.create_task_result() 39 | request = self.factory.get(f'/done/{taskmeta.task_id}') 40 | response = is_task_successful(request, taskmeta.task_id) 41 | assert response 42 | result = json.loads(response.content.decode('utf-8')) 43 | assert result['task']['executed'] is False 44 | 45 | TaskResult.objects.store_result( 46 | 'application/json', 47 | 'utf-8', 48 | taskmeta.task_id, 49 | json.dumps({'result': True}), 50 | status=states.SUCCESS 51 | ) 52 | 53 | request = self.factory.get(f'/done/{taskmeta.task_id}') 54 | response = is_task_successful(request, taskmeta.task_id) 55 | assert response 56 | result = json.loads(response.content.decode('utf-8')) 57 | assert result['task']['executed'] is True 58 | 59 | def test_task_status(self): 60 | taskmeta = self.create_task_result() 61 | request = self.factory.get(f'/status/{taskmeta.task_id}') 62 | response = task_status(request, taskmeta.task_id) 63 | assert response 64 | result = json.loads(response.content.decode('utf-8')) 65 | assert result['task']['status'] is not states.SUCCESS 66 | 67 | TaskResult.objects.store_result( 68 | 'application/json', 69 | 'utf-8', 70 | taskmeta.task_id, 71 | json.dumps({'result': True}), 72 | status=states.SUCCESS 73 | ) 74 | 75 | request = self.factory.get(f'/status/{taskmeta.task_id}') 76 | response = task_status(request, taskmeta.task_id) 77 | assert response 78 | result = json.loads(response.content.decode('utf-8')) 79 | assert result['task']['status'] == states.SUCCESS 80 | 81 | def create_group_result(self): 82 | """Return a GroupResult model instance 83 | with a single, successful result""" 84 | id = uuid() 85 | task_result = self.create_task_result() 86 | task_result.status = states.SUCCESS 87 | task_result.save() 88 | results = [AsyncResult(id=task_result.task_id)] 89 | group = CeleryGroupResult(id=id, results=results) 90 | group.save() 91 | meta = GroupResult.objects.get(group_id=id) 92 | return meta 93 | 94 | def test_is_group_successful(self): 95 | meta = self.create_group_result() 96 | request = self.factory.get(f'/group/done/{meta.group_id}') 97 | response = is_group_successful(request, meta.group_id) 98 | assert response 99 | 100 | result = json.loads(response.content.decode('utf-8')) 101 | assert len(result['group']['results']) == 1 102 | result = json.loads(response.content.decode('utf-8')) 103 | assert result['group']['results'][0]['executed'] is True 104 | 105 | def test_group_status(self): 106 | meta = self.create_group_result() 107 | request = self.factory.get(f'/group/status/{meta.group_id}') 108 | response = group_status(request, meta.group_id) 109 | assert response 110 | 111 | result = json.loads(response.content.decode('utf-8')) 112 | assert len(result["group"]["results"]) == 1 113 | assert result["group"]["results"][0]["status"] == states.SUCCESS 114 | -------------------------------------------------------------------------------- /django_celery_results/migrations/0004_auto_20190516_0412.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.11.20 on 2019-05-16 04:12 2 | 3 | # this file is auto-generated so don't do flake8 on it 4 | # flake8: noqa 5 | 6 | 7 | from django.conf import settings 8 | from django.db import migrations, models 9 | 10 | 11 | class Migration(migrations.Migration): 12 | 13 | dependencies = [ 14 | ('django_celery_results', '0003_auto_20181106_1101'), 15 | ] 16 | 17 | operations = [ 18 | migrations.AlterField( 19 | model_name='taskresult', 20 | name='content_encoding', 21 | field=models.CharField(help_text='The encoding used to save the task result data', max_length=64, verbose_name='Result Encoding'), 22 | ), 23 | migrations.AlterField( 24 | model_name='taskresult', 25 | name='content_type', 26 | field=models.CharField(help_text='Content type of the result data', max_length=128, verbose_name='Result Content Type'), 27 | ), 28 | migrations.AlterField( 29 | model_name='taskresult', 30 | name='date_done', 31 | field=models.DateTimeField(auto_now=True, db_index=True, help_text='Datetime field when the task was completed in UTC', verbose_name='Completed DateTime'), 32 | ), 33 | migrations.AlterField( 34 | model_name='taskresult', 35 | name='hidden', 36 | 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'), 37 | ), 38 | migrations.AlterField( 39 | model_name='taskresult', 40 | name='meta', 41 | 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'), 42 | ), 43 | migrations.AlterField( 44 | model_name='taskresult', 45 | name='result', 46 | 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'), 47 | ), 48 | migrations.AlterField( 49 | model_name='taskresult', 50 | name='status', 51 | 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'), 52 | ), 53 | migrations.AlterField( 54 | model_name='taskresult', 55 | name='task_args', 56 | field=models.TextField(help_text='JSON representation of the positional arguments used with the task', null=True, verbose_name='Task Positional Arguments'), 57 | ), 58 | migrations.AlterField( 59 | model_name='taskresult', 60 | name='task_id', 61 | field=models.CharField( 62 | db_index=True, 63 | help_text='Celery ID for the Task that was run', 64 | max_length=getattr( 65 | settings, 66 | 'DJANGO_CELERY_RESULTS_TASK_ID_MAX_LENGTH', 67 | 255 68 | ), 69 | unique=True, 70 | verbose_name='Task ID' 71 | ), 72 | ), 73 | migrations.AlterField( 74 | model_name='taskresult', 75 | name='task_kwargs', 76 | field=models.TextField(help_text='JSON representation of the named arguments used with the task', null=True, verbose_name='Task Named Arguments'), 77 | ), 78 | migrations.AlterField( 79 | model_name='taskresult', 80 | name='task_name', 81 | field=models.CharField( 82 | db_index=True, 83 | help_text='Name of the Task which was run', 84 | max_length=getattr( 85 | settings, 86 | 'DJANGO_CELERY_RESULTS_TASK_ID_MAX_LENGTH', 87 | 255 88 | ), 89 | null=True, 90 | verbose_name='Task Name'), 91 | ), 92 | migrations.AlterField( 93 | model_name='taskresult', 94 | name='traceback', 95 | field=models.TextField(blank=True, help_text='Text of the traceback if the task generated one', null=True, verbose_name='Traceback'), 96 | ), 97 | ] 98 | -------------------------------------------------------------------------------- /django_celery_results/locale/es/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # Spanish translation strings for django-celery-results. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as django-celery-results. 4 | # , 2020. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version:\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2020-02-26 18:34+0100\n" 12 | "PO-Revision-Date: 2020-02-26 20:25-0015\n" 13 | "Last-Translator: \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: es\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 20 | 21 | #: django_celery_results/admin.py:39 22 | msgid "Parameters" 23 | msgstr "Parámetros" 24 | 25 | #: django_celery_results/admin.py:46 26 | msgid "Result" 27 | msgstr "Resultado" 28 | 29 | #: django_celery_results/apps.py:15 30 | msgid "Celery Results" 31 | msgstr "Resultados Celery" 32 | 33 | #: django_celery_results/models.py:28 34 | msgid "Task ID" 35 | msgstr "ID de Tarea" 36 | 37 | #: django_celery_results/models.py:29 38 | msgid "Celery ID for the Task that was run" 39 | msgstr "ID de Celery para la tarea que fue ejecutada" 40 | 41 | #: django_celery_results/models.py:32 42 | msgid "Task Name" 43 | msgstr "Nombre de Tarea" 44 | 45 | #: django_celery_results/models.py:33 46 | msgid "Name of the Task which was run" 47 | msgstr "Nombre de la Tarea que fue ejecutada" 48 | 49 | #: django_celery_results/models.py:36 50 | msgid "Task Positional Arguments" 51 | msgstr "Argumentos posicionales de la Tarea" 52 | 53 | #: django_celery_results/models.py:37 54 | msgid "JSON representation of the positional arguments used with the task" 55 | msgstr "Representación JSON de los argumentos posicionales usados en la tarea" 56 | 57 | #: django_celery_results/models.py:41 58 | msgid "Task Named Arguments" 59 | msgstr "Argumentos opcionales de la tarea" 60 | 61 | #: django_celery_results/models.py:42 62 | msgid "JSON representation of the named arguments used with the task" 63 | msgstr "Representación JSON de los argumentos opcionales usados en la tarea" 64 | 65 | #: django_celery_results/models.py:47 66 | msgid "Task State" 67 | msgstr "Estado de la Tarea" 68 | 69 | #: django_celery_results/models.py:48 70 | msgid "Current state of the task being run" 71 | msgstr "Estado actual en el que se encuentra la tarea en ejecución" 72 | 73 | #: django_celery_results/models.py:51 74 | msgid "Worker" 75 | msgstr "Worker" 76 | 77 | #: django_celery_results/models.py:51 78 | msgid "Worker that executes the task" 79 | msgstr "Worker que ejecuta la tarea" 80 | 81 | #: django_celery_results/models.py:55 82 | msgid "Result Content Type" 83 | msgstr "Content Type del resultado" 84 | 85 | #: django_celery_results/models.py:56 86 | msgid "Content type of the result data" 87 | msgstr "Atributo Content type de los datos del resultado" 88 | 89 | #: django_celery_results/models.py:59 90 | msgid "Result Encoding" 91 | msgstr "Codificación del resultado" 92 | 93 | #: django_celery_results/models.py:60 94 | msgid "The encoding used to save the task result data" 95 | msgstr "La codificación usada para guardar los datos del resultado" 96 | 97 | #: django_celery_results/models.py:63 98 | msgid "Result Data" 99 | msgstr "Datos del resultado" 100 | 101 | #: django_celery_results/models.py:64 102 | msgid "" 103 | "The data returned by the task. Use content_encoding and content_type fields" 104 | " to read." 105 | msgstr "" 106 | "Datos devueltos por la tarea. Usa los campos content_encoding y content_type" 107 | " para leerlos." 108 | 109 | #: django_celery_results/models.py:68 110 | msgid "Created DateTime" 111 | msgstr "Fecha de creación" 112 | 113 | #: django_celery_results/models.py:69 114 | msgid "Datetime field when the task result was created in UTC" 115 | msgstr "Fecha de creación de la tarea en UTC" 116 | 117 | #: django_celery_results/models.py:72 118 | msgid "Completed DateTime" 119 | msgstr "Fecha de terminación" 120 | 121 | #: django_celery_results/models.py:73 122 | msgid "Datetime field when the task was completed in UTC" 123 | msgstr "Fecha de completitud de la tarea en UTC" 124 | 125 | #: django_celery_results/models.py:76 126 | msgid "Traceback" 127 | msgstr "Traceback" 128 | 129 | #: django_celery_results/models.py:77 130 | msgid "Text of the traceback if the task generated one" 131 | msgstr "Texto del traceback si la tarea generó uno" 132 | 133 | #: django_celery_results/models.py:80 134 | msgid "Task Meta Information" 135 | msgstr "Metadatos de la tarea" 136 | 137 | #: django_celery_results/models.py:81 138 | msgid "" 139 | "JSON meta information about the task, such as information on child tasks" 140 | msgstr "" 141 | "Metainformación sobre la tarea en formato JSON, como la información de las " 142 | "tareas hijas" 143 | 144 | #: django_celery_results/models.py:91 145 | msgid "task result" 146 | msgstr "resultado de la tarea" 147 | 148 | #: django_celery_results/models.py:92 149 | msgid "task results" 150 | msgstr "resultados de tareas" 151 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import codecs 4 | import os 5 | import re 6 | import sys 7 | 8 | import setuptools 9 | import setuptools.command.test 10 | 11 | try: 12 | import platform 13 | _pyimp = platform.python_implementation 14 | except (AttributeError, ImportError): 15 | def _pyimp(): 16 | return 'Python' 17 | 18 | NAME = 'django_celery_results' 19 | 20 | E_UNSUPPORTED_PYTHON = f'{NAME} 1.0 requires %s %s or later!' 21 | 22 | PYIMP = _pyimp() 23 | PY37_OR_LESS = sys.version_info < (3, 7) 24 | PYPY_VERSION = getattr(sys, 'pypy_version_info', None) 25 | PYPY73_ATLEAST = PYPY_VERSION and PYPY_VERSION >= (7, 3) 26 | 27 | if PY37_OR_LESS and not PYPY73_ATLEAST: 28 | raise Exception(E_UNSUPPORTED_PYTHON % (PYIMP, '3.7')) 29 | 30 | # -*- Classifiers -*- 31 | 32 | classes = """ 33 | Development Status :: 5 - Production/Stable 34 | License :: OSI Approved :: BSD License 35 | Programming Language :: Python 36 | Programming Language :: Python :: 3 37 | Programming Language :: Python :: 3.8 38 | Programming Language :: Python :: 3.9 39 | Programming Language :: Python :: 3.10 40 | Programming Language :: Python :: 3.11 41 | Programming Language :: Python :: 3.12 42 | Programming Language :: Python :: 3.13 43 | Programming Language :: Python :: Implementation :: CPython 44 | Programming Language :: Python :: Implementation :: PyPy 45 | Framework :: Django 46 | Framework :: Django :: 3.2 47 | Framework :: Django :: 4.1 48 | Framework :: Django :: 4.2 49 | Framework :: Django :: 5.0 50 | Framework :: Django :: 5.1 51 | Framework :: Django :: 5.2 52 | Operating System :: OS Independent 53 | Topic :: Communications 54 | Topic :: System :: Distributed Computing 55 | Topic :: Software Development :: Libraries :: Python Modules 56 | """ 57 | classifiers = [s.strip() for s in classes.split('\n') if s] 58 | 59 | # -*- Distribution Meta -*- 60 | 61 | re_meta = re.compile(r'__(\w+?)__\s*=\s*(.*)') 62 | re_doc = re.compile(r'^"""(.+?)"""') 63 | 64 | 65 | def add_default(m): 66 | attr_name, attr_value = m.groups() 67 | return ((attr_name, attr_value.strip("\"'")),) 68 | 69 | 70 | def add_doc(m): 71 | return (('doc', m.groups()[0]),) 72 | 73 | 74 | pats = {re_meta: add_default, 75 | re_doc: add_doc} 76 | here = os.path.abspath(os.path.dirname(__file__)) 77 | with open(os.path.join(here, NAME, '__init__.py')) as meta_fh: 78 | meta = {} 79 | for line in meta_fh: 80 | if line.strip() == '# -eof meta-': 81 | break 82 | for pattern, handler in pats.items(): 83 | m = pattern.match(line.strip()) 84 | if m: 85 | meta.update(handler(m)) 86 | 87 | # -*- Installation Requires -*- 88 | 89 | 90 | def strip_comments(line): 91 | return line.split('#', 1)[0].strip() 92 | 93 | 94 | def _pip_requirement(req): 95 | if req.startswith('-r '): 96 | _, path = req.split() 97 | return reqs(*path.split('/')) 98 | return [req] 99 | 100 | 101 | def _reqs(*f): 102 | with open(os.path.join(os.getcwd(), 'requirements', *f)) as fp: 103 | return [ 104 | _pip_requirement(r) 105 | for r in (strip_comments(line) for line in fp) 106 | if r 107 | ] 108 | 109 | 110 | def reqs(*f): 111 | return [req for subreq in _reqs(*f) for req in subreq] 112 | 113 | # -*- Long Description -*- 114 | 115 | 116 | if os.path.exists('README.rst'): 117 | long_description = codecs.open('README.rst', 'r', 'utf-8').read() 118 | else: 119 | long_description = f'See https://pypi.python.org/pypi/{NAME}' 120 | 121 | # -*- %%% -*- 122 | 123 | 124 | class pytest(setuptools.command.test.test): 125 | user_options = [('pytest-args=', 'a', 'Arguments to pass to pytest')] 126 | 127 | def initialize_options(self): 128 | super().initialize_options() 129 | self.pytest_args = [] 130 | 131 | def run_tests(self): 132 | import pytest 133 | sys.exit(pytest.main(self.pytest_args)) 134 | 135 | 136 | setuptools.setup( 137 | name=NAME, 138 | packages=setuptools.find_packages(exclude=['ez_setup', 't', 't.*']), 139 | version=meta['version'], 140 | description=meta['doc'], 141 | long_description=long_description, 142 | long_description_content_type='text/x-rst', 143 | keywords='celery django database result backend', 144 | author=meta['author'], 145 | author_email=meta['contact'], 146 | url=meta['homepage'], 147 | project_urls={ 148 | 'Documentation': ( 149 | 'https://django-celery-results.readthedocs.io/en/latest/' 150 | ), 151 | 'Changelog': ( 152 | 'https://django-celery-results.readthedocs.io/en/latest/' 153 | 'changelog.html' 154 | ), 155 | 'Repository': 'https://github.com/celery/django-celery-results', 156 | }, 157 | platforms=['any'], 158 | license='BSD', 159 | classifiers=classifiers, 160 | install_requires=reqs('default.txt'), 161 | tests_require=reqs('test.txt') + reqs('test-django.txt'), 162 | cmdclass={'test': pytest}, 163 | entry_points={ 164 | 'celery.result_backends': [ 165 | 'django-db = django_celery_results.backends:DatabaseBackend', 166 | 'django-cache = django_celery_results.backends:CacheBackend', 167 | ], 168 | }, 169 | zip_safe=False, 170 | include_package_data=True, 171 | ) 172 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ===================================================================== 2 | Celery Result Backends using the Django ORM/Cache framework. 3 | ===================================================================== 4 | 5 | |build-status| |coverage| |license| |wheel| |pyversion| |pyimp| 6 | 7 | :Version: 2.6.0 8 | :Web: https://django-celery-results.readthedocs.io/ 9 | :Download: https://pypi.python.org/pypi/django-celery-results 10 | :Source: https://github.com/celery/django-celery-results 11 | :DeepWiki: |deepwiki| 12 | :Keywords: django, celery, database, results 13 | 14 | About 15 | ===== 16 | 17 | This extension enables you to store Celery task results using the Django ORM. 18 | 19 | It defines a single model (``django_celery_results.models.TaskResult``) 20 | used to store task results, and you can query this database table like 21 | any other Django model. 22 | 23 | Installing 24 | ========== 25 | 26 | The installation instructions for this extension is available 27 | from the `Celery documentation`_ 28 | 29 | .. _`Celery documentation`: 30 | https://docs.celeryq.dev/en/latest/django/first-steps-with-django.html#django-celery-results-using-the-django-orm-cache-as-a-result-backend 31 | 32 | .. _installation: 33 | 34 | Installation 35 | ============ 36 | 37 | You can install django-celery-results either via the Python Package Index (PyPI) 38 | or from source. 39 | 40 | To install using `pip`,:: 41 | 42 | $ pip install -U django-celery-results 43 | 44 | .. _installing-from-source: 45 | 46 | Downloading and installing from source 47 | -------------------------------------- 48 | 49 | Download the latest version of django-celery-results from 50 | https://pypi.python.org/pypi/django-celery-results 51 | 52 | You can install it by doing the following,:: 53 | 54 | $ tar xvfz django-celery-results-0.0.0.tar.gz 55 | $ cd django-celery-results-0.0.0 56 | $ python setup.py build 57 | # python setup.py install 58 | 59 | The last command must be executed as a privileged user if 60 | you are not currently using a virtualenv. 61 | 62 | .. _installing-from-git: 63 | 64 | Using the development version 65 | ----------------------------- 66 | 67 | With pip 68 | ~~~~~~~~ 69 | 70 | You can install the latest snapshot of django-celery-results using the following 71 | pip command:: 72 | 73 | $ pip install https://github.com/celery/django-celery-results/zipball/master#egg=django-celery-results 74 | 75 | 76 | Running with Docker Compose 77 | =========================== 78 | 79 | To run the project using Docker Compose, ensure you have Docker and Docker Compose installed. Then, execute the following command in the project root directory:: 80 | 81 | $ docker-compose up --build 82 | 83 | This will start a containerized PostgreSQL database and run the tests using `tox`. 84 | 85 | 86 | 87 | Issues with mysql 88 | ----------------- 89 | 90 | If you want to run ``django-celery-results`` with MySQL, you might run into some issues. 91 | 92 | One such issue is when you try to run ``python manage.py migrate django_celery_results``, you might get the following error:: 93 | 94 | django.db.utils.OperationalError: (1071, 'Specified key was too long; max key length is 767 bytes') 95 | 96 | To get around this issue, you can set:: 97 | 98 | DJANGO_CELERY_RESULTS_TASK_ID_MAX_LENGTH=191 99 | 100 | (or any other value if any other db other than MySQL is causing similar issues.) 101 | 102 | max_length of **191** seems to work for MySQL. 103 | 104 | 105 | .. |build-status| image:: https://secure.travis-ci.org/celery/django-celery-results.svg?branch=master 106 | :alt: Build status 107 | :target: https://travis-ci.org/celery/django-celery-results 108 | 109 | .. |coverage| image:: https://codecov.io/github/celery/django-celery-results/coverage.svg?branch=master 110 | :target: https://codecov.io/github/celery/django-celery-results?branch=master 111 | 112 | .. |license| image:: https://img.shields.io/pypi/l/django-celery-results.svg 113 | :alt: BSD License 114 | :target: https://opensource.org/licenses/BSD-3-Clause 115 | 116 | .. |wheel| image:: https://img.shields.io/pypi/wheel/django-celery-results.svg 117 | :alt: django-celery-results can be installed via wheel 118 | :target: https://pypi.python.org/pypi/django-celery-results/ 119 | 120 | .. |pyversion| image:: https://img.shields.io/pypi/pyversions/django-celery-results.svg 121 | :alt: Supported Python versions. 122 | :target: https://pypi.python.org/pypi/django-celery-results/ 123 | 124 | .. |pyimp| image:: https://img.shields.io/pypi/implementation/django-celery-results.svg 125 | :alt: Support Python implementations. 126 | :target: https://pypi.python.org/pypi/django-celery-results/ 127 | 128 | .. |deepwiki| image:: https://devin.ai/assets/deepwiki-badge.png 129 | :alt: Ask http://DeepWiki.com 130 | :target: https://deepwiki.com/celery/django-celery-results 131 | :width: 125px 132 | 133 | django-celery-results for enterprise 134 | ------------------------------------ 135 | 136 | Available as part of the Tidelift Subscription. 137 | 138 | The maintainer of django-celery-results and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source packages you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainer of the exact packages you use. `Learn more. `_ 139 | 140 | -------------------------------------------------------------------------------- /t/conftest.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import types 3 | from contextlib import contextmanager 4 | from unittest.mock import MagicMock, Mock, patch 5 | 6 | import pytest 7 | 8 | # we have to import the pytest plugin fixtures here, 9 | # in case user did not do the `python setup.py develop` yet, 10 | # that installs the pytest plugin into the setuptools registry. 11 | from celery.contrib.pytest import ( 12 | celery_app, 13 | celery_config, 14 | celery_enable_logging, 15 | celery_parameters, 16 | depends_on_current_app, 17 | use_celery_app_trap, 18 | ) 19 | from celery.contrib.testing.app import TestApp, Trap 20 | 21 | # Tricks flake8 into silencing redefining fixtures warnings. 22 | __all__ = ( 23 | 'celery_app', 'celery_enable_logging', 'depends_on_current_app', 24 | 'celery_parameters', 'celery_config', 'use_celery_app_trap' 25 | ) 26 | 27 | 28 | SENTINEL = object() 29 | 30 | 31 | @pytest.fixture(scope='session', autouse=True) 32 | def setup_default_app_trap(): 33 | from celery._state import set_default_app 34 | set_default_app(Trap()) 35 | 36 | 37 | @pytest.fixture() 38 | def app(celery_app): 39 | return celery_app 40 | 41 | 42 | @contextmanager 43 | def module_context_manager(*names): 44 | """Mock one or modules such that every attribute is a :class:`Mock`.""" 45 | yield from _module(*names) 46 | 47 | 48 | def _module(*names): 49 | prev = {} 50 | 51 | class MockModule(types.ModuleType): 52 | 53 | def __getattr__(self, attr): 54 | setattr(self, attr, Mock()) 55 | return types.ModuleType.__getattribute__(self, attr) 56 | 57 | mods = [] 58 | for name in names: 59 | try: 60 | prev[name] = sys.modules[name] 61 | except KeyError: 62 | pass 63 | mod = sys.modules[name] = MockModule(name) 64 | mods.append(mod) 65 | try: 66 | yield mods 67 | finally: 68 | for name in names: 69 | try: 70 | sys.modules[name] = prev[name] 71 | except KeyError: 72 | try: 73 | del sys.modules[name] 74 | except KeyError: 75 | pass 76 | 77 | 78 | class _patching: 79 | 80 | def __init__(self, monkeypatch, request): 81 | self.monkeypatch = monkeypatch 82 | self.request = request 83 | 84 | def __getattr__(self, name): 85 | return getattr(self.monkeypatch, name) 86 | 87 | def __call__(self, path, value=SENTINEL, name=None, 88 | new=MagicMock, **kwargs): 89 | value = self._value_or_mock(value, new, name, path, **kwargs) 90 | self.monkeypatch.setattr(path, value) 91 | return value 92 | 93 | def object(self, target, attribute, *args, **kwargs): 94 | return _wrap_context( 95 | patch.object(target, attribute, *args, **kwargs), 96 | self.request) 97 | 98 | def _value_or_mock(self, value, new, name, path, **kwargs): 99 | if value is SENTINEL: 100 | value = new(name=name or path.rpartition('.')[2]) 101 | for k, v in kwargs.items(): 102 | setattr(value, k, v) 103 | return value 104 | 105 | def setattr(self, target, name=SENTINEL, value=SENTINEL, **kwargs): 106 | # alias to __call__ with the interface of pytest.monkeypatch.setattr 107 | if value is SENTINEL: 108 | value, name = name, None 109 | return self(target, value, name=name) 110 | 111 | def setitem(self, dic, name, value=SENTINEL, new=MagicMock, **kwargs): 112 | # same as pytest.monkeypatch.setattr but default value is MagicMock 113 | value = self._value_or_mock(value, new, name, dic, **kwargs) 114 | self.monkeypatch.setitem(dic, name, value) 115 | return value 116 | 117 | def modules(self, *mods): 118 | modules = [] 119 | for mod in mods: 120 | mod = mod.split('.') 121 | modules.extend(reversed([ 122 | '.'.join(mod[:-i] if i else mod) for i in range(len(mod)) 123 | ])) 124 | modules = sorted(set(modules)) 125 | return _wrap_context(module_context_manager(*modules), self.request) 126 | 127 | 128 | def _wrap_context(context, request): 129 | ret = context.__enter__() 130 | 131 | def fin(): 132 | context.__exit__(*sys.exc_info()) 133 | request.addfinalizer(fin) 134 | return ret 135 | 136 | 137 | @pytest.fixture() 138 | def patching(monkeypatch, request): 139 | """Monkeypath.setattr shortcut. 140 | Example: 141 | .. code-block:: python 142 | >>> def test_foo(patching): 143 | >>> # execv value here will be mock.MagicMock by default. 144 | >>> execv = patching('os.execv') 145 | >>> patching('sys.platform', 'darwin') # set concrete value 146 | >>> patching.setenv('DJANGO_SETTINGS_MODULE', 'x.settings') 147 | >>> # val will be of type mock.MagicMock by default 148 | >>> val = patching.setitem('path.to.dict', 'KEY') 149 | """ 150 | return _patching(monkeypatch, request) 151 | 152 | 153 | @pytest.fixture(autouse=True) 154 | def test_cases_shortcuts(request, app, patching): 155 | if request.instance: 156 | @app.task 157 | def add(x, y): 158 | return x + y 159 | 160 | # IMPORTANT: We set an .app attribute for every test case class. 161 | request.instance.app = app 162 | request.instance.Celery = TestApp 163 | request.instance.add = add 164 | request.instance.patching = patching 165 | yield 166 | if request.instance: 167 | request.instance.app = None 168 | -------------------------------------------------------------------------------- /django_celery_results/locale/pt_BR/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # Brazilian portuguese translation strings for django-celery-results. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as django-celery-results. 4 | # Eduardo Oliveira , 2022. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: \n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2022-01-04 19:52-0300\n" 12 | "PO-Revision-Date: 2022-01-04 19:52-0300\n" 13 | "Last-Translator: Eduardo Oliveira \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: pt_BR\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 20 | 21 | #: admin.py:40 22 | msgid "Parameters" 23 | msgstr "Parâmetros" 24 | 25 | #: admin.py:47 26 | msgid "Result" 27 | msgstr "Resultado" 28 | 29 | #: apps.py:14 30 | msgid "Celery Results" 31 | msgstr "Resultados do celery" 32 | 33 | #: models.py:28 34 | msgid "Task ID" 35 | msgstr "Id da tarefa" 36 | 37 | #: models.py:29 38 | msgid "Celery ID for the Task that was run" 39 | msgstr "Id do celery em que a tarefa foi executada" 40 | 41 | #: models.py:32 42 | msgid "Periodic Task Name" 43 | msgstr "Nome da tarefa periódica" 44 | 45 | #: models.py:33 46 | msgid "Name of the Periodic Task which was run" 47 | msgstr "Nome da tarefa periódica que foi executada" 48 | 49 | #: models.py:36 50 | msgid "Task Name" 51 | msgstr "Nome da tarefa" 52 | 53 | #: models.py:37 54 | msgid "Name of the Task which was run" 55 | msgstr "Nome da tarefa que foi executada" 56 | 57 | #: models.py:40 58 | msgid "Task Positional Arguments" 59 | msgstr "Argumentos posicionais da tarefa" 60 | 61 | #: models.py:41 62 | msgid "JSON representation of the positional arguments used with the task" 63 | msgstr "Representação JSON dos argumentos posicionais usados pela tarefa" 64 | 65 | #: models.py:45 66 | msgid "Task Named Arguments" 67 | msgstr "Argumentos nomeados da tarefa" 68 | 69 | #: models.py:46 70 | msgid "JSON representation of the named arguments used with the task" 71 | msgstr "Representação JSON dos argumentos nomeados usados pela tarefa" 72 | 73 | #: models.py:51 74 | msgid "Task State" 75 | msgstr "Status da tarefa" 76 | 77 | #: models.py:52 78 | msgid "Current state of the task being run" 79 | msgstr "Status atual da tarefa em execução" 80 | 81 | #: models.py:55 82 | msgid "Worker" 83 | msgstr "Worker" 84 | 85 | #: models.py:55 86 | msgid "Worker that executes the task" 87 | msgstr "Worker que executa a tarefa" 88 | 89 | #: models.py:59 models.py:190 90 | msgid "Result Content Type" 91 | msgstr "Tipo de conteúdo do resultado" 92 | 93 | #: models.py:60 models.py:191 94 | msgid "Content type of the result data" 95 | msgstr "Tipo de conteúdo dos dados do resultado" 96 | 97 | #: models.py:63 models.py:195 98 | msgid "Result Encoding" 99 | msgstr "Codificação do resultado" 100 | 101 | #: models.py:64 models.py:196 102 | msgid "The encoding used to save the task result data" 103 | msgstr "A codificação usada para salvar os dados de resultado da tarefa" 104 | 105 | #: models.py:67 models.py:200 106 | msgid "Result Data" 107 | msgstr "Dados do resultado" 108 | 109 | #: models.py:68 models.py:201 110 | msgid "" 111 | "The data returned by the task. Use content_encoding and content_type fields " 112 | "to read." 113 | msgstr "Os dados retornados pela tarefa. Use os campos content_encoding e content_type para ler." 114 | 115 | #: models.py:72 models.py:180 116 | msgid "Created DateTime" 117 | msgstr "Data/Horário de criação" 118 | 119 | #: models.py:73 120 | msgid "Datetime field when the task result was created in UTC" 121 | msgstr "Data/Horário em que o resultado da tarefa foi criado (em UTC)" 122 | 123 | #: models.py:76 models.py:185 124 | msgid "Completed DateTime" 125 | msgstr "Data/Horário em que foi concluída" 126 | 127 | #: models.py:77 128 | msgid "Datetime field when the task was completed in UTC" 129 | msgstr "Data/Horário em que a tarefa foi concluída (em UTC)" 130 | 131 | #: models.py:80 132 | msgid "Traceback" 133 | msgstr "Traceback" 134 | 135 | #: models.py:81 136 | msgid "Text of the traceback if the task generated one" 137 | msgstr "Texto de traceback se a tarefa gerou um" 138 | 139 | #: models.py:84 140 | msgid "Task Meta Information" 141 | msgstr "Meta informação da tarefa" 142 | 143 | #: models.py:85 144 | msgid "" 145 | "JSON meta information about the task, such as information on child tasks" 146 | msgstr "Meta informação JSON sobre a tarefa, como informações sobre as subtarefas" 147 | 148 | #: models.py:95 149 | msgid "task result" 150 | msgstr "resultado da tarefa" 151 | 152 | #: models.py:96 153 | msgid "task results" 154 | msgstr "resultados das tarefas" 155 | 156 | #: models.py:133 models.py:175 157 | msgid "Group ID" 158 | msgstr "Id do grupo" 159 | 160 | #: models.py:134 161 | msgid "Celery ID for the Chord header group" 162 | msgstr "Id do celery para o grupo de cabeçalho Chord" 163 | 164 | #: models.py:138 165 | msgid "" 166 | "JSON serialized list of task result tuples. use .group_result() to decode" 167 | msgstr "lista de tuplas de resultados de tarefas serializadas como JSON. Use .group_result() para decodificar" 168 | 169 | #: models.py:144 170 | msgid "Starts at len(chord header) and decrements after each task is finished" 171 | msgstr "Começa em len(chord header) e decaí após o término de cada tarefa" 172 | 173 | #: models.py:176 174 | msgid "Celery ID for the Group that was run" 175 | msgstr "Id do celery para o grupo que foi executado" 176 | 177 | #: models.py:181 178 | msgid "Datetime field when the group result was created in UTC" 179 | msgstr "Data/Horário em que o resultado do grupo foi criado (em UTC)" 180 | 181 | #: models.py:186 182 | msgid "Datetime field when the group was completed in UTC" 183 | msgstr "Data/Horário em que o grupo foi concluída (em UTC)" 184 | 185 | #: models.py:221 186 | msgid "group result" 187 | msgstr "resultado do grupo" 188 | 189 | #: models.py:222 190 | msgid "group results" 191 | msgstr "resultados dos grupos" 192 | -------------------------------------------------------------------------------- /django_celery_results/locale/zh_Hans/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # Simplified Chinese translation strings for django-celery-results. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as django-celery-results. 4 | # , 2021. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version:\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2023-03-19 22:23+0800\n" 12 | "PO-Revision-Date: 2021-11-20 23:00+0800\n" 13 | "Last-Translator: ifmos \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: zh-hans\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 20 | 21 | #: django_celery_results/admin.py:41 22 | msgid "Parameters" 23 | msgstr "参数" 24 | 25 | #: django_celery_results/admin.py:48 26 | msgid "Result" 27 | msgstr "结果" 28 | 29 | #: django_celery_results/apps.py:14 30 | msgid "Celery Results" 31 | msgstr "Celery 结果" 32 | 33 | #: django_celery_results/models.py:28 34 | msgid "Task ID" 35 | msgstr "任务 ID" 36 | 37 | #: django_celery_results/models.py:29 38 | msgid "Celery ID for the Task that was run" 39 | msgstr "已运行任务 Celery ID" 40 | 41 | #: django_celery_results/models.py:32 42 | msgid "Periodic Task Name" 43 | msgstr "周期任务名称" 44 | 45 | #: django_celery_results/models.py:33 46 | msgid "Name of the Periodic Task which was run" 47 | msgstr "已运行周期任务名称" 48 | 49 | #: django_celery_results/models.py:40 50 | msgid "Task Name" 51 | msgstr "任务名称" 52 | 53 | #: django_celery_results/models.py:41 54 | msgid "Name of the Task which was run" 55 | msgstr "已运行任务名称" 56 | 57 | #: django_celery_results/models.py:44 58 | msgid "Task Positional Arguments" 59 | msgstr "任务位置参数" 60 | 61 | #: django_celery_results/models.py:45 62 | msgid "JSON representation of the positional arguments used with the task" 63 | msgstr "该任务位置参数的 JSON 字符串" 64 | 65 | #: django_celery_results/models.py:49 66 | msgid "Task Named Arguments" 67 | msgstr "任务具名参数" 68 | 69 | #: django_celery_results/models.py:50 70 | msgid "JSON representation of the named arguments used with the task" 71 | msgstr "该任务具名参数的 JSON 字符串" 72 | 73 | #: django_celery_results/models.py:55 74 | msgid "Task State" 75 | msgstr "任务状态" 76 | 77 | #: django_celery_results/models.py:56 78 | msgid "Current state of the task being run" 79 | msgstr "运行中任务的当前状态" 80 | 81 | #: django_celery_results/models.py:59 82 | msgid "Worker" 83 | msgstr "Worker" 84 | 85 | #: django_celery_results/models.py:59 86 | msgid "Worker that executes the task" 87 | msgstr "执行该任务的 Worker" 88 | 89 | #: django_celery_results/models.py:63 django_celery_results/models.py:200 90 | msgid "Result Content Type" 91 | msgstr "结果内容类型" 92 | 93 | #: django_celery_results/models.py:64 django_celery_results/models.py:201 94 | msgid "Content type of the result data" 95 | msgstr "结果数据的内容类型" 96 | 97 | #: django_celery_results/models.py:67 django_celery_results/models.py:205 98 | msgid "Result Encoding" 99 | msgstr "结果编码格式" 100 | 101 | #: django_celery_results/models.py:68 django_celery_results/models.py:206 102 | msgid "The encoding used to save the task result data" 103 | msgstr "保存结果数据的编码格式" 104 | 105 | #: django_celery_results/models.py:71 django_celery_results/models.py:210 106 | msgid "Result Data" 107 | msgstr "结果数据" 108 | 109 | #: django_celery_results/models.py:72 django_celery_results/models.py:211 110 | msgid "" 111 | "The data returned by the task. Use content_encoding and content_type fields " 112 | "to read." 113 | msgstr "该任务返回数据,根据 content_encoding 和 content_type 字段读取。" 114 | 115 | #: django_celery_results/models.py:76 django_celery_results/models.py:190 116 | msgid "Created DateTime" 117 | msgstr "创建时间" 118 | 119 | #: django_celery_results/models.py:77 120 | msgid "Datetime field when the task result was created in UTC" 121 | msgstr "UTC格式的任务创建时间字段" 122 | 123 | #: django_celery_results/models.py:80 django_celery_results/models.py:195 124 | msgid "Completed DateTime" 125 | msgstr "完成时间" 126 | 127 | #: django_celery_results/models.py:81 128 | msgid "Datetime field when the task was completed in UTC" 129 | msgstr "UTC格式的任务完成时间字段" 130 | 131 | #: django_celery_results/models.py:84 132 | msgid "Traceback" 133 | msgstr "Traceback" 134 | 135 | #: django_celery_results/models.py:85 136 | msgid "Text of the traceback if the task generated one" 137 | msgstr "任务生成报错时的 traceback 文本" 138 | 139 | #: django_celery_results/models.py:88 140 | msgid "Task Meta Information" 141 | msgstr "任务元信息" 142 | 143 | #: django_celery_results/models.py:89 144 | msgid "" 145 | "JSON meta information about the task, such as information on child tasks" 146 | msgstr "关于该任务的 JSON 元信息,如子任务的信息" 147 | 148 | #: django_celery_results/models.py:99 149 | msgid "task result" 150 | msgstr "任务结果" 151 | 152 | #: django_celery_results/models.py:100 153 | msgid "task results" 154 | msgstr "任务结果" 155 | 156 | #: django_celery_results/models.py:143 django_celery_results/models.py:185 157 | msgid "Group ID" 158 | msgstr "分组 ID" 159 | 160 | #: django_celery_results/models.py:144 161 | msgid "Celery ID for the Chord header group" 162 | msgstr "Chord header 分组的 Celery ID" 163 | 164 | #: django_celery_results/models.py:148 165 | msgid "" 166 | "JSON serialized list of task result tuples. use .group_result() to decode" 167 | msgstr "任务结果元组的 JSON 序列化列表。使用 .group_result() 进行解码" 168 | 169 | #: django_celery_results/models.py:154 170 | msgid "Starts at len(chord header) and decrements after each task is finished" 171 | msgstr "在 len(chord header) 处开始并且会在每个任务结束后递减" 172 | 173 | #: django_celery_results/models.py:186 174 | msgid "Celery ID for the Group that was run" 175 | msgstr "已运行分组的 Celery ID" 176 | 177 | #: django_celery_results/models.py:191 178 | msgid "Datetime field when the group result was created in UTC" 179 | msgstr "分组结果创建时的 UTC 格式 datetime 字段" 180 | 181 | #: django_celery_results/models.py:196 182 | msgid "Datetime field when the group was completed in UTC" 183 | msgstr "分组结果完成时的 UTC 格式 datetime 字段" 184 | 185 | #: django_celery_results/models.py:231 186 | msgid "group result" 187 | msgstr "分组结果" 188 | 189 | #: django_celery_results/models.py:232 190 | msgid "group results" 191 | msgstr "分组结果" 192 | -------------------------------------------------------------------------------- /t/proj/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for Test project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.9.1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.9/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.9/ref/settings/ 11 | """ 12 | 13 | import os 14 | import sys 15 | 16 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 17 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 18 | sys.path.insert(0, os.path.abspath(os.path.join(BASE_DIR, os.pardir))) 19 | 20 | 21 | # configure psycopg2cffi for psycopg2 compatibility. We must use this package 22 | # support pypy. 23 | # if not installed, use sqlite as a backup (some tests may fail), 24 | # otherwise even makemigrations won't run. 25 | try: 26 | from psycopg2cffi import compat 27 | compat.register() 28 | DATABASES = { 29 | 'default': { 30 | 'ENGINE': 'django.db.backends.postgresql', 31 | 'HOST': os.getenv('DB_POSTGRES_HOST', 'localhost'), 32 | 'PORT': os.getenv('DB_POSTGRES_PORT', '5432'), 33 | 'NAME': os.getenv('DB_POSTGRES_DATABASE', 'postgres'), 34 | 'USER': os.getenv('DB_POSTGRES_USER', 'postgres'), 35 | 'PASSWORD': os.getenv('DB_POSTGRES_PASSWORD', 'postgres'), 36 | 'OPTIONS': { 37 | 'connect_timeout': 1000, 38 | }, 39 | }, 40 | 'secondary': { 41 | 'ENGINE': 'django.db.backends.postgresql', 42 | 'HOST': os.getenv('DB_POSTGRES_HOST', 'localhost'), 43 | 'PORT': os.getenv('DB_POSTGRES_PORT', '5432'), 44 | 'NAME': os.getenv('DB_POSTGRES_DATABASE', 'postgres'), 45 | 'USER': os.getenv('DB_POSTGRES_USER', 'postgres'), 46 | 'PASSWORD': os.getenv('DB_POSTGRES_PASSWORD', 'postgres'), 47 | 'OPTIONS': { 48 | 'connect_timeout': 1000, 49 | }, 50 | 'TEST': { 51 | 'MIRROR': 'default', 52 | }, 53 | }, 54 | 'read-only': { 55 | 'ENGINE': 'django.db.backends.postgresql', 56 | 'HOST': os.getenv('DB_POSTGRES_HOST', 'localhost'), 57 | 'PORT': os.getenv('DB_POSTGRES_PORT', '5432'), 58 | 'NAME': 'read-only-database', 59 | 'USER': os.getenv('DB_POSTGRES_USER', 'postgres'), 60 | 'PASSWORD': os.getenv('DB_POSTGRES_PASSWORD', 'postgres'), 61 | 'OPTIONS': { 62 | 'connect_timeout': 1000, 63 | 'options': '-c default_transaction_read_only=on', 64 | }, 65 | 'TEST': { 66 | 'MIRROR': 'default', 67 | }, 68 | }, 69 | } 70 | except ImportError: 71 | DATABASES = { 72 | 'default': { 73 | 'ENGINE': 'django.db.backends.sqlite3', 74 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 75 | 'OPTIONS': { 76 | 'timeout': 1000, 77 | } 78 | }, 79 | 'secondary': { 80 | 'ENGINE': 'django.db.backends.sqlite3', 81 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 82 | 'OPTIONS': { 83 | 'timeout': 1000, 84 | } 85 | }, 86 | 'read-only': { 87 | 'ENGINE': 'django.db.backends.sqlite3', 88 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 89 | 'OPTIONS': { 90 | 'timeout': 1000, 91 | } 92 | } 93 | } 94 | 95 | # Quick-start development settings - unsuitable for production 96 | # See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ 97 | 98 | # SECURITY WARNING: keep the secret key used in production secret! 99 | SECRET_KEY = 'u($kbs9$irs0)436gbo9%!b&#zyd&70tx!n7!i&fl6qun@z1_l' 100 | 101 | # SECURITY WARNING: don't run with debug turned on in production! 102 | DEBUG = True 103 | 104 | ALLOWED_HOSTS = [] 105 | 106 | # Application definition 107 | 108 | INSTALLED_APPS = [ 109 | 'django.contrib.admin', 110 | 'django.contrib.auth', 111 | 'django.contrib.contenttypes', 112 | 'django.contrib.sessions', 113 | 'django.contrib.messages', 114 | 'django.contrib.staticfiles', 115 | 'django_celery_results', 116 | ] 117 | 118 | MIDDLEWARE = [ 119 | 'django.middleware.security.SecurityMiddleware', 120 | 'django.contrib.sessions.middleware.SessionMiddleware', 121 | 'django.middleware.common.CommonMiddleware', 122 | 'django.middleware.csrf.CsrfViewMiddleware', 123 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 124 | 'django.contrib.messages.middleware.MessageMiddleware', 125 | ] 126 | 127 | ROOT_URLCONF = 't.proj.urls' 128 | 129 | TEMPLATES = [ 130 | { 131 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 132 | 'DIRS': [], 133 | 'APP_DIRS': True, 134 | 'OPTIONS': { 135 | 'context_processors': [ 136 | 'django.template.context_processors.debug', 137 | 'django.template.context_processors.request', 138 | 'django.contrib.auth.context_processors.auth', 139 | 'django.contrib.messages.context_processors.messages', 140 | ], 141 | }, 142 | }, 143 | ] 144 | 145 | WSGI_APPLICATION = 't.proj.wsgi.application' 146 | 147 | CACHES = { 148 | 'default': { 149 | 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', 150 | }, 151 | 'dummy': { 152 | 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', 153 | }, 154 | } 155 | 156 | # Password validation 157 | # https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators 158 | 159 | django_auth = 'django.contrib.auth.password_validation.' 160 | 161 | AUTH_PASSWORD_VALIDATORS = [ 162 | ] 163 | 164 | 165 | # Internationalization 166 | # https://docs.djangoproject.com/en/1.9/topics/i18n/ 167 | 168 | LANGUAGE_CODE = 'en-us' 169 | 170 | TIME_ZONE = 'UTC' 171 | 172 | USE_I18N = True 173 | 174 | USE_L10N = True 175 | 176 | USE_TZ = True 177 | DJANGO_CELERY_RESULTS_TASK_ID_MAX_LENGTH = 191 178 | 179 | 180 | # Static files (CSS, JavaScript, Images) 181 | # https://docs.djangoproject.com/en/1.9/howto/static-files/ 182 | 183 | STATIC_URL = '/static/' 184 | -------------------------------------------------------------------------------- /django_celery_results/locale/ru/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # Russian translation strings for django-celery-results. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # ILDAR MINNAKHMETOV , 2021. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: \n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2021-11-09 19:16+0000\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: ILDAR MINNAKHMETOV \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: ru\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 20 | 21 | #: django_celery_results/admin.py:38 22 | msgid "Parameters" 23 | msgstr "Параметры" 24 | 25 | #: django_celery_results/admin.py:45 26 | msgid "Result" 27 | msgstr "Результаты" 28 | 29 | #: django_celery_results/apps.py:14 30 | msgid "Celery Results" 31 | msgstr "Результаты Celery" 32 | 33 | #: django_celery_results/models.py:28 34 | msgid "Task ID" 35 | msgstr "ID Задачи" 36 | 37 | #: django_celery_results/models.py:29 38 | msgid "Celery ID for the Task that was run" 39 | msgstr "Celery ID задачи" 40 | 41 | #: django_celery_results/models.py:32 42 | msgid "Task Name" 43 | msgstr "Название задачи" 44 | 45 | #: django_celery_results/models.py:33 46 | msgid "Name of the Task which was run" 47 | msgstr "Название задачи которая была запущена" 48 | 49 | #: django_celery_results/models.py:36 50 | msgid "Task Positional Arguments" 51 | msgstr "Аргументы задачи" 52 | 53 | #: django_celery_results/models.py:37 54 | msgid "JSON representation of the positional arguments used with the task" 55 | msgstr "JSON с позиционными аргументами задачи (*args)" 56 | 57 | #: django_celery_results/models.py:41 58 | msgid "Task Named Arguments" 59 | msgstr "Именованные аргументы задачи" 60 | 61 | #: django_celery_results/models.py:42 62 | msgid "JSON representation of the named arguments used with the task" 63 | msgstr "JSON с именованными аргументами задачи (**kwargs)" 64 | 65 | #: django_celery_results/models.py:47 66 | msgid "Task State" 67 | msgstr "Статус задачи" 68 | 69 | #: django_celery_results/models.py:48 70 | msgid "Current state of the task being run" 71 | msgstr "Текущий статус запущенной задачи" 72 | 73 | #: django_celery_results/models.py:51 74 | msgid "Worker" 75 | msgstr "Воркер" 76 | 77 | #: django_celery_results/models.py:51 78 | msgid "Worker that executes the task" 79 | msgstr "Воркер который выполняет задачу" 80 | 81 | #: django_celery_results/models.py:55 django_celery_results/models.py:186 82 | msgid "Result Content Type" 83 | msgstr "Тип контента результата" 84 | 85 | #: django_celery_results/models.py:56 django_celery_results/models.py:187 86 | msgid "Content type of the result data" 87 | msgstr "Тип контента данных результата" 88 | 89 | #: django_celery_results/models.py:59 django_celery_results/models.py:191 90 | msgid "Result Encoding" 91 | msgstr "Кодировка результата" 92 | 93 | #: django_celery_results/models.py:60 django_celery_results/models.py:192 94 | msgid "The encoding used to save the task result data" 95 | msgstr "Кодировка использованная для сохранения данных результата" 96 | 97 | #: django_celery_results/models.py:63 django_celery_results/models.py:196 98 | msgid "Result Data" 99 | msgstr "Данные результата" 100 | 101 | #: django_celery_results/models.py:64 django_celery_results/models.py:197 102 | msgid "" 103 | "The data returned by the task. Use content_encoding and content_type fields " 104 | "to read." 105 | msgstr "Данные, которые вернула задача. Используйте content_encoding и content_type для чтения." 106 | 107 | #: django_celery_results/models.py:68 django_celery_results/models.py:176 108 | msgid "Created DateTime" 109 | msgstr "Дата и время создания" 110 | 111 | #: django_celery_results/models.py:69 112 | msgid "Datetime field when the task result was created in UTC" 113 | msgstr "Дата и время когда результат был создан (UTC)" 114 | 115 | #: django_celery_results/models.py:72 django_celery_results/models.py:181 116 | msgid "Completed DateTime" 117 | msgstr "Дата и время завершения" 118 | 119 | #: django_celery_results/models.py:73 120 | msgid "Datetime field when the task was completed in UTC" 121 | msgstr "Дата и время когда задача была завершена (UTC)" 122 | 123 | #: django_celery_results/models.py:76 124 | msgid "Traceback" 125 | msgstr "Traceback" 126 | 127 | #: django_celery_results/models.py:77 128 | msgid "Text of the traceback if the task generated one" 129 | msgstr "Текст traceback, если есть" 130 | 131 | #: django_celery_results/models.py:80 132 | msgid "Task Meta Information" 133 | msgstr "Метаинформация задачи" 134 | 135 | #: django_celery_results/models.py:81 136 | msgid "" 137 | "JSON meta information about the task, such as information on child tasks" 138 | msgstr "" 139 | "JSON мета-информация о задаче, к примеру о дочерних задачах" 140 | 141 | #: django_celery_results/models.py:91 142 | msgid "task result" 143 | msgstr "результат задачи" 144 | 145 | #: django_celery_results/models.py:92 146 | msgid "task results" 147 | msgstr "результаты задач" 148 | 149 | #: django_celery_results/models.py:129 django_celery_results/models.py:171 150 | msgid "Group ID" 151 | msgstr "ID группы" 152 | 153 | #: django_celery_results/models.py:130 154 | msgid "Celery ID for the Chord header group" 155 | msgstr "Celery ID для заголовка группы" 156 | 157 | #: django_celery_results/models.py:134 158 | msgid "" 159 | "JSON serialized list of task result tuples. use .group_result() to decode" 160 | msgstr "" 161 | "JSON-список кортежей результата. Используйте .group_result() для декодирования" 162 | 163 | #: django_celery_results/models.py:140 164 | msgid "Starts at len(chord header) and decrements after each task is finished" 165 | msgstr "Начинается в len(chord header) и уменьшается после каждого завершенного задания" 166 | 167 | #: django_celery_results/models.py:172 168 | msgid "Celery ID for the Group that was run" 169 | msgstr "Celery ID для группы которая была запущена" 170 | 171 | #: django_celery_results/models.py:177 172 | msgid "Datetime field when the group result was created in UTC" 173 | msgstr "Дата и время если результат группы был создан (UTC)" 174 | 175 | #: django_celery_results/models.py:182 176 | msgid "Datetime field when the group was completed in UTC" 177 | msgstr "Дата и время, когда группа была завершена (UTC)" 178 | 179 | #: django_celery_results/models.py:217 180 | msgid "group result" 181 | msgstr "результат группы" 182 | 183 | #: django_celery_results/models.py:218 184 | msgid "group results" 185 | msgstr "результаты групп" 186 | -------------------------------------------------------------------------------- /Changelog: -------------------------------------------------------------------------------- 1 | .. _changelog: 2 | 3 | ================ 4 | Change history 5 | ================ 6 | 7 | .. _version-2.6.0: 8 | 9 | 2.6.0 10 | ===== 11 | :release-date: 2025-04-10 2:15 P.M. UTC+6:00 12 | :release-by: Asif Saif Uddin 13 | 14 | - Fix atomic transaction not routing to the the correct DB in DatabaseBackend (#427). 15 | - Store current time when TaskResult started (#432). 16 | - Added extra except clause to TryRemoveIndex. 17 | - Fix for missing periodic task name (#445). 18 | - Update behavior of app.result_expires to match celery docs. 19 | - Added index to periodic_task_name. 20 | - Added support for Django up to 5.2. 21 | - Added support for python up to 3.13. 22 | - fix: allow for custom states in status field (#407). 23 | 24 | 25 | .. _version-2.5.1: 26 | 27 | 2.5.1 28 | ===== 29 | :release-date: 2023-05-08 8:15 P.M. UTC+6:00 30 | :release-by: Asif Saif Uddin 31 | 32 | - Revert "feat: raw delete expired instead of Queryset.delete (#235)" partially. 33 | 34 | 35 | .. _version-2.5.0: 36 | 37 | 2.5.0 38 | ===== 39 | :release-date: 2023-03-13 5:45 P.M. UTC+6:00 40 | :release-by: Asif Saif Uddin 41 | 42 | - try possible fix to avoid a oracle regression (#325). 43 | - Added periodic_task_name to admin fieldset for parity with list view. 44 | - Only update the ChordCounter.count field when saving. 45 | - Meta injection (#366). 46 | 47 | 48 | .. _version-2.4.0: 49 | 50 | 2.4.0 51 | ===== 52 | :release-date: 2022-06-29 4:30 P.M. UTC+6:00 53 | :release-by: Asif Saif Uddin 54 | 55 | - Fix [#315] Save args, kwargs and other extended props only when result_extended config is set to True. 56 | - Fix atomic transaction not routing to the the correct DB (#324). 57 | - Drop django 2.2 from matrix 58 | 59 | 60 | .. _version-2.3.1: 61 | 62 | 2.3.1 63 | ===== 64 | :release-date: 2022-04-17 12:50 P.M. UTC+6:00 65 | :release-by: Asif Saif Uddin 66 | 67 | - Remove hard dependency on psycopg2. 68 | - Fix #296 Stop producing a universal wheel, python 2 is unspported. 69 | - fix: The description content type for setuptools needs to be rst to markdown. 70 | 71 | 72 | .. _version-2.3.0: 73 | 74 | 2.3.0 75 | ===== 76 | :release-date: 2022-03-01 1:45 p.m. UTC+6:00 77 | :release-by: Asif Saif Uddin 78 | 79 | - Fix default_app_config deprecation (#221) 80 | - Use string values for django-cache keys #230 (#242) 81 | - feat: raw delete expired instead of Queryset.delete (#235) 82 | - Fix ``pydoc.ErrorDuringImport`` problem in django_celery_results url 83 | - Russian language support (#255) 84 | - Add Simplified Chinese translation strings. 85 | - Minor code clean up 86 | - feat: add periodic_task_name (#261) 87 | - Update CI with django 4.0 (#272) 88 | - Add translation of the messages to brazilian portuguese (#278) 89 | - Fix properties default value (#281) 90 | - Work around Oracle migration instability 91 | - Fix field size for MySQL (#285) 92 | - Update python & pypy min version (#291) 93 | - bum min pytest versions 94 | 95 | 96 | .. _version-2.2.0: 97 | 98 | 2.2.0 99 | ===== 100 | :release-date: 2021-07-02 11:00 a.m. UTC+6:00 101 | :release-by: Asif Saif Uddin 102 | 103 | - add new urls with nouns first structure (#216) 104 | - Remove duplicate indexes 105 | - fix group status view return data, add tests for it (#215) 106 | - typo fix (#218) 107 | - Use the DJANGO_CELERY_RESULTS_TASK_ID_MAX_LENGTH for group_id/task_id 108 | - Minor code clean up 109 | 110 | .. _version-2.1.0: 111 | 112 | 2.1.0 113 | ===== 114 | :release-date: 2021-06-14 09:00 p.m. UTC+6:00 115 | :release-by: Asif Saif Uddin 116 | 117 | - Don't raise an error when ChordCounter is not found 118 | - add default_auto_field to apps.py 119 | - Use the provided chord_size when available 120 | - Match apply_chord call signature to Celery 5.1 121 | - Add support for GroupResult (#161) 122 | - Minor code clean up 123 | 124 | .. _version-2.0.1: 125 | 126 | 2.0.1 127 | ===== 128 | :release-date: 2021-01-19 07:30 p.m. UTC+6:00 129 | :release-by: 130 | 131 | - Fix backward compatibility in DatabaseBackend._store_result function 132 | - Fix 'args' and 'kwargs' propiertes of AsyncResult for DatabaseBackend 133 | - Fix task_args/task_kwargs in task_protocol=1 134 | - Test refactors 135 | - Add task_args and task_kwargs to admins searchable fields (#182) 136 | 137 | .. _version-2.0.0: 138 | 139 | 2.0.0 140 | ===== 141 | :release-date: 142 | :release-by: 143 | 144 | - Add Spanish translations (#134) 145 | - Add support for Django 3.0 and 3.1 (#145, #163) 146 | - Add support for Celery 5 (#163) 147 | - Drop support for Django < 2.2 (#147, #152) 148 | - Drop support for Python < 3.6 (#146, #147, #152) 149 | - Add Chord syncronisation from the database (#144) 150 | - Encode `task_args` and `task_kwargs` of `TaskResult` using `json.dumps` instead of using `str` (#78) 151 | 152 | .. _version-1.1.2: 153 | 154 | 1.1.2 155 | ===== 156 | :release-date: 2019-06-06 00:00 a.m. UTC+6:00 157 | :release-by: Asif Saif Uddin 158 | 159 | 160 | - Fixed few regressions 161 | 162 | .. _version-1.1.0: 163 | 164 | 1.1.0 165 | ===== 166 | :release-date: 2019-05-21 17:00 p.m. UTC+6:00 167 | :release-by: Asif Saif Uddin 168 | 169 | 170 | - Django 2.2+. 171 | - Drop python 3.4 and django 2.0 172 | - Support specifying the database to use for the store_result method (#63) 173 | - Fix MySQL8 system variable tx_isolation issue (#84) 174 | 175 | 176 | .. _version-1.0.4: 177 | 178 | 1.0.4 179 | ===== 180 | :release-date: 2018-11-12 19:00 p.m. UTC+2:00 181 | :release-by: Omer Katz 182 | 183 | 1.0.3 is broken. Use 1.0.4 184 | 185 | - Revert renaming label as it is a breaking change. 186 | 187 | .. _version-1.0.3: 188 | 189 | 1.0.3 190 | ===== 191 | :release-date: 2018-11-12 18:00 p.m. UTC+2:00 192 | :release-by: Omer Katz 193 | 194 | - Revert renaming label as it is a breaking change. 195 | 196 | .. _version-1.0.2: 197 | 198 | 1.0.2 199 | ===== 200 | :release-date: 2018-11-12 18:00 p.m. UTC+2:00 201 | :release-by: Omer Katz 202 | 203 | - Store task name, args, kwargs as part of the task results in database. 204 | Contributed by :github_user: `wardal`. 205 | 206 | - Admin screen changes - task name filter, search on task_name, task_id, status. 207 | Contributed by :github_user: `jaylynch`. 208 | 209 | - Added default_app_config. 210 | - Added missing migration. 211 | - Fix MySQL max length issue. 212 | - Drop support for Django<1.11. 213 | 214 | .. _version-1.0.1: 215 | 216 | 1.0.1 217 | ===== 218 | :release-date: 2016-11-07 02:00 p.m. PST 219 | :release-by: Ask Solem 220 | 221 | - Migrations were not being installed as part of the distribution (Issue #4). 222 | 223 | - Now includes simple task result admin interface. 224 | 225 | Contributed by :github_user:`zeezdev`. 226 | 227 | - Now depends on Celery 4.0.0. 228 | 229 | .. _version-1.0.0: 230 | 231 | 1.0.0 232 | ===== 233 | :release-date: 2016-09-08 03:19 p.m. PDT 234 | :release-by: Ask Solem 235 | 236 | - Initial release 237 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. epub3 to make an epub3 31 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 32 | echo. text to make text files 33 | echo. man to make manual pages 34 | echo. texinfo to make Texinfo files 35 | echo. gettext to make PO message catalogs 36 | echo. changes to make an overview over all changed/added/deprecated items 37 | echo. xml to make Docutils-native XML files 38 | echo. pseudoxml to make pseudoxml-XML files for display purposes 39 | echo. linkcheck to check all external links for integrity 40 | echo. doctest to run all doctests embedded in the documentation if enabled 41 | echo. coverage to run coverage check of the documentation if enabled 42 | goto end 43 | ) 44 | 45 | if "%1" == "clean" ( 46 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 47 | del /q /s %BUILDDIR%\* 48 | goto end 49 | ) 50 | 51 | 52 | REM Check if sphinx-build is available and fallback to Python version if any 53 | %SPHINXBUILD% 1>NUL 2>NUL 54 | if errorlevel 9009 goto sphinx_python 55 | goto sphinx_ok 56 | 57 | :sphinx_python 58 | 59 | set SPHINXBUILD=python -m sphinx.__init__ 60 | %SPHINXBUILD% 2> nul 61 | if errorlevel 9009 ( 62 | echo. 63 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 64 | echo.installed, then set the SPHINXBUILD environment variable to point 65 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 66 | echo.may add the Sphinx directory to PATH. 67 | echo. 68 | echo.If you don't have Sphinx installed, grab it from 69 | echo.https://www.sphinx-doc.org/ 70 | exit /b 1 71 | ) 72 | 73 | :sphinx_ok 74 | 75 | 76 | if "%1" == "html" ( 77 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 78 | if errorlevel 1 exit /b 1 79 | echo. 80 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 81 | goto end 82 | ) 83 | 84 | if "%1" == "dirhtml" ( 85 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 86 | if errorlevel 1 exit /b 1 87 | echo. 88 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 89 | goto end 90 | ) 91 | 92 | if "%1" == "singlehtml" ( 93 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 94 | if errorlevel 1 exit /b 1 95 | echo. 96 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 97 | goto end 98 | ) 99 | 100 | if "%1" == "pickle" ( 101 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 102 | if errorlevel 1 exit /b 1 103 | echo. 104 | echo.Build finished; now you can process the pickle files. 105 | goto end 106 | ) 107 | 108 | if "%1" == "json" ( 109 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 110 | if errorlevel 1 exit /b 1 111 | echo. 112 | echo.Build finished; now you can process the JSON files. 113 | goto end 114 | ) 115 | 116 | if "%1" == "htmlhelp" ( 117 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 118 | if errorlevel 1 exit /b 1 119 | echo. 120 | echo.Build finished; now you can run HTML Help Workshop with the ^ 121 | .hhp project file in %BUILDDIR%/htmlhelp. 122 | goto end 123 | ) 124 | 125 | if "%1" == "qthelp" ( 126 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 127 | if errorlevel 1 exit /b 1 128 | echo. 129 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 130 | .qhcp project file in %BUILDDIR%/qthelp, like this: 131 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\PROJ.qhcp 132 | echo.To view the help file: 133 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\PROJ.ghc 134 | goto end 135 | ) 136 | 137 | if "%1" == "devhelp" ( 138 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 139 | if errorlevel 1 exit /b 1 140 | echo. 141 | echo.Build finished. 142 | goto end 143 | ) 144 | 145 | if "%1" == "epub" ( 146 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 147 | if errorlevel 1 exit /b 1 148 | echo. 149 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 150 | goto end 151 | ) 152 | 153 | if "%1" == "epub3" ( 154 | %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 155 | if errorlevel 1 exit /b 1 156 | echo. 157 | echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. 158 | goto end 159 | ) 160 | 161 | if "%1" == "latex" ( 162 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 163 | if errorlevel 1 exit /b 1 164 | echo. 165 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 166 | goto end 167 | ) 168 | 169 | if "%1" == "latexpdf" ( 170 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 171 | cd %BUILDDIR%/latex 172 | make all-pdf 173 | cd %~dp0 174 | echo. 175 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 176 | goto end 177 | ) 178 | 179 | if "%1" == "latexpdfja" ( 180 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 181 | cd %BUILDDIR%/latex 182 | make all-pdf-ja 183 | cd %~dp0 184 | echo. 185 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 186 | goto end 187 | ) 188 | 189 | if "%1" == "text" ( 190 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 191 | if errorlevel 1 exit /b 1 192 | echo. 193 | echo.Build finished. The text files are in %BUILDDIR%/text. 194 | goto end 195 | ) 196 | 197 | if "%1" == "man" ( 198 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 199 | if errorlevel 1 exit /b 1 200 | echo. 201 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 202 | goto end 203 | ) 204 | 205 | if "%1" == "texinfo" ( 206 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 207 | if errorlevel 1 exit /b 1 208 | echo. 209 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 210 | goto end 211 | ) 212 | 213 | if "%1" == "gettext" ( 214 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/django_celery_results/locale 215 | if errorlevel 1 exit /b 1 216 | echo. 217 | echo.Build finished. The message catalogs are in %BUILDDIR%/django_celery_results/locale. 218 | goto end 219 | ) 220 | 221 | if "%1" == "changes" ( 222 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 223 | if errorlevel 1 exit /b 1 224 | echo. 225 | echo.The overview file is in %BUILDDIR%/changes. 226 | goto end 227 | ) 228 | 229 | if "%1" == "linkcheck" ( 230 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 231 | if errorlevel 1 exit /b 1 232 | echo. 233 | echo.Link check complete; look for any errors in the above output ^ 234 | or in %BUILDDIR%/linkcheck/output.txt. 235 | goto end 236 | ) 237 | 238 | if "%1" == "doctest" ( 239 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 240 | if errorlevel 1 exit /b 1 241 | echo. 242 | echo.Testing of doctests in the sources finished, look at the ^ 243 | results in %BUILDDIR%/doctest/output.txt. 244 | goto end 245 | ) 246 | 247 | if "%1" == "coverage" ( 248 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 249 | if errorlevel 1 exit /b 1 250 | echo. 251 | echo.Testing of coverage in the sources finished, look at the ^ 252 | results in %BUILDDIR%/coverage/python.txt. 253 | goto end 254 | ) 255 | 256 | if "%1" == "xml" ( 257 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 258 | if errorlevel 1 exit /b 1 259 | echo. 260 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 261 | goto end 262 | ) 263 | 264 | if "%1" == "pseudoxml" ( 265 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 266 | if errorlevel 1 exit /b 1 267 | echo. 268 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 269 | goto end 270 | ) 271 | 272 | :end 273 | -------------------------------------------------------------------------------- /django_celery_results/migrations/0009_groupresult.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-04-19 14:55 2 | from django.conf import settings 3 | from django.db import migrations, models 4 | 5 | 6 | class FakeAddIndex(migrations.AddIndex): 7 | """Fake AddIndex to correct for duplicate index 8 | added in the original 0009 migration 9 | """ 10 | def database_forwards(self, *args, **kwargs): 11 | """Don't do anything""" 12 | 13 | def database_backwards(self, *args, **kwargs): 14 | """Also don't do anything on reverting this migration 15 | 16 | The duplicate index will be cleaned up when migrating from the 17 | original 0009 to the cleanup 0010 18 | """ 19 | 20 | 21 | class Migration(migrations.Migration): 22 | 23 | dependencies = [ 24 | ('django_celery_results', '0008_chordcounter'), 25 | ] 26 | 27 | operations = [ 28 | migrations.CreateModel( 29 | name='GroupResult', 30 | fields=[ 31 | ('id', models.AutoField( 32 | auto_created=True, 33 | primary_key=True, 34 | serialize=False, 35 | verbose_name='ID')), 36 | ('group_id', models.CharField( 37 | help_text='Celery ID for the Group that was run', 38 | max_length=getattr( 39 | settings, 40 | 'DJANGO_CELERY_RESULTS_TASK_ID_MAX_LENGTH', 41 | 255 42 | ), 43 | unique=True, 44 | verbose_name='Group ID')), 45 | ('date_created', models.DateTimeField( 46 | auto_now_add=True, 47 | help_text='Datetime field when the group result ' 48 | 'was created in UTC', 49 | verbose_name='Created DateTime')), 50 | ('date_done', models.DateTimeField( 51 | auto_now=True, 52 | help_text='Datetime field when the group was ' 53 | 'completed in UTC', 54 | verbose_name='Completed DateTime')), 55 | ('content_type', models.CharField( 56 | help_text='Content type of the result data', 57 | max_length=128, 58 | verbose_name='Result Content Type')), 59 | ('content_encoding', models.CharField( 60 | help_text='The encoding used to save the task ' 61 | 'result data', 62 | max_length=64, 63 | verbose_name='Result Encoding')), 64 | ('result', models.TextField( 65 | default=None, 66 | editable=False, 67 | help_text='The data returned by the task. Use ' 68 | 'content_encoding and content_type ' 69 | 'fields to read.', 70 | null=True, 71 | verbose_name='Result Data')), 72 | ], 73 | options={ 74 | 'verbose_name': 'group result', 75 | 'verbose_name_plural': 'group results', 76 | 'ordering': ['-date_done'], 77 | }, 78 | ), 79 | migrations.AlterField( 80 | model_name='chordcounter', 81 | name='group_id', 82 | field=models.CharField( 83 | help_text='Celery ID for the Chord header group', 84 | max_length=getattr( 85 | settings, 86 | 'DJANGO_CELERY_RESULTS_TASK_ID_MAX_LENGTH', 87 | 255 88 | ), 89 | unique=True, 90 | verbose_name='Group ID'), 91 | ), 92 | migrations.AlterField( 93 | model_name='taskresult', 94 | name='date_created', 95 | field=models.DateTimeField( 96 | auto_now_add=True, 97 | help_text='Datetime field when the task result ' 98 | 'was created in UTC', 99 | verbose_name='Created DateTime'), 100 | ), 101 | migrations.AlterField( 102 | model_name='taskresult', 103 | name='date_done', 104 | field=models.DateTimeField( 105 | auto_now=True, 106 | help_text='Datetime field when the task was completed in UTC', 107 | verbose_name='Completed DateTime'), 108 | ), 109 | migrations.AlterField( 110 | model_name='taskresult', 111 | name='status', 112 | field=models.CharField( 113 | choices=[ 114 | ('FAILURE', 'FAILURE'), 115 | ('PENDING', 'PENDING'), 116 | ('RECEIVED', 'RECEIVED'), 117 | ('RETRY', 'RETRY'), 118 | ('REVOKED', 'REVOKED'), 119 | ('STARTED', 'STARTED'), 120 | ('SUCCESS', 'SUCCESS')], 121 | default='PENDING', 122 | help_text='Current state of the task being run', 123 | max_length=50, 124 | verbose_name='Task State'), 125 | ), 126 | migrations.AlterField( 127 | model_name='taskresult', 128 | name='task_id', 129 | field=models.CharField( 130 | help_text='Celery ID for the Task that was run', 131 | max_length=getattr( 132 | settings, 133 | 'DJANGO_CELERY_RESULTS_TASK_ID_MAX_LENGTH', 134 | 255 135 | ), 136 | unique=True, 137 | verbose_name='Task ID'), 138 | ), 139 | migrations.AlterField( 140 | model_name='taskresult', 141 | name='task_name', 142 | field=models.CharField( 143 | help_text='Name of the Task which was run', 144 | max_length=getattr( 145 | settings, 146 | 'DJANGO_CELERY_RESULTS_TASK_ID_MAX_LENGTH', 147 | 255 148 | ), 149 | null=True, 150 | verbose_name='Task Name'), 151 | ), 152 | migrations.AlterField( 153 | model_name='taskresult', 154 | name='worker', 155 | field=models.CharField( 156 | default=None, 157 | help_text='Worker that executes the task', 158 | max_length=100, 159 | null=True, 160 | verbose_name='Worker'), 161 | ), 162 | FakeAddIndex( 163 | model_name='chordcounter', 164 | index=models.Index( 165 | fields=['group_id'], 166 | name='django_cele_group_i_299b0d_idx'), 167 | ), 168 | FakeAddIndex( 169 | model_name='taskresult', 170 | index=models.Index( 171 | fields=['task_id'], 172 | name='django_cele_task_id_7f8fca_idx'), 173 | ), 174 | migrations.AddIndex( 175 | model_name='taskresult', 176 | index=models.Index( 177 | fields=['task_name'], 178 | name='django_cele_task_na_08aec9_idx'), 179 | ), 180 | migrations.AddIndex( 181 | model_name='taskresult', 182 | index=models.Index( 183 | fields=['status'], 184 | name='django_cele_status_9b6201_idx'), 185 | ), 186 | migrations.AddIndex( 187 | model_name='taskresult', 188 | index=models.Index( 189 | fields=['worker'], 190 | name='django_cele_worker_d54dd8_idx'), 191 | ), 192 | migrations.AddIndex( 193 | model_name='taskresult', 194 | index=models.Index( 195 | fields=['date_created'], 196 | name='django_cele_date_cr_f04a50_idx'), 197 | ), 198 | migrations.AddIndex( 199 | model_name='taskresult', 200 | index=models.Index( 201 | fields=['date_done'], 202 | name='django_cele_date_do_f59aad_idx'), 203 | ), 204 | FakeAddIndex( 205 | model_name='groupresult', 206 | index=models.Index( 207 | fields=['group_id'], 208 | name='django_cele_group_i_3cddec_idx'), 209 | ), 210 | migrations.AddIndex( 211 | model_name='groupresult', 212 | index=models.Index( 213 | fields=['date_created'], 214 | name='django_cele_date_cr_bd6c1d_idx'), 215 | ), 216 | migrations.AddIndex( 217 | model_name='groupresult', 218 | index=models.Index( 219 | fields=['date_done'], 220 | name='django_cele_date_do_caae0e_idx'), 221 | ), 222 | ] 223 | -------------------------------------------------------------------------------- /django_celery_results/managers.py: -------------------------------------------------------------------------------- 1 | """Model managers.""" 2 | 3 | import warnings 4 | from functools import wraps 5 | from itertools import count 6 | 7 | from celery.utils.time import maybe_timedelta 8 | from django.conf import settings 9 | from django.db import connections, models, router, transaction 10 | 11 | from .utils import now 12 | 13 | W_ISOLATION_REP = """ 14 | Polling results with transaction isolation level 'repeatable-read' 15 | within the same transaction may give outdated results. 16 | 17 | Be sure to commit the transaction for each poll iteration. 18 | """ 19 | 20 | 21 | class TxIsolationWarning(UserWarning): 22 | """Warning emitted if the transaction isolation level is suboptimal.""" 23 | 24 | 25 | def transaction_retry(max_retries=1): 26 | """Decorate a function to retry database operations. 27 | 28 | For functions doing database operations, adding 29 | retrying if the operation fails. 30 | 31 | Keyword Arguments: 32 | max_retries (int): Maximum number of retries. Default one retry. 33 | 34 | """ 35 | def _outer(fun): 36 | 37 | @wraps(fun) 38 | def _inner(*args, **kwargs): 39 | _max_retries = kwargs.pop('exception_retry_count', max_retries) 40 | for retries in count(0): 41 | try: 42 | return fun(*args, **kwargs) 43 | except Exception: # pragma: no cover 44 | # Depending on the database backend used we can experience 45 | # various exceptions. E.g. psycopg2 raises an exception 46 | # if some operation breaks the transaction, so saving 47 | # the task result won't be possible until we rollback 48 | # the transaction. 49 | if retries >= _max_retries: 50 | raise 51 | return _inner 52 | 53 | return _outer 54 | 55 | 56 | class ResultManager(models.Manager): 57 | """Generic manager for celery results.""" 58 | 59 | def warn_if_repeatable_read(self): 60 | if 'mysql' in self.current_engine().lower(): 61 | cursor = self.connection_for_read().cursor() 62 | # MariaDB and MySQL since 8.0 have different transaction isolation 63 | # variables: the former has tx_isolation, while the latter has 64 | # transaction_isolation 65 | if cursor.execute("SHOW VARIABLES WHERE variable_name IN " 66 | "('tx_isolation', 'transaction_isolation');"): 67 | isolation = cursor.fetchone()[1] 68 | if isolation == 'REPEATABLE-READ': 69 | warnings.warn(TxIsolationWarning(W_ISOLATION_REP.strip())) 70 | 71 | def connection_for_write(self): 72 | return connections[router.db_for_write(self.model)] 73 | 74 | def connection_for_read(self): 75 | return connections[self.db] 76 | 77 | def current_engine(self): 78 | try: 79 | return settings.DATABASES[self.db]['ENGINE'] 80 | except AttributeError: 81 | return settings.DATABASE_ENGINE 82 | 83 | def get_all_expired(self, expires): 84 | """Get all expired results.""" 85 | return self.filter(date_done__lt=now() - maybe_timedelta(expires)) 86 | 87 | def delete_expired(self, expires, batch_size=100000): 88 | """Delete all expired results.""" 89 | qs = self.get_all_expired(expires).order_by("id") 90 | 91 | while True: 92 | ids = list(qs.values_list("id", flat=True)[:batch_size]) 93 | if not ids: 94 | break 95 | with transaction.atomic(using=self.db): 96 | self.filter(id__in=ids).delete() 97 | 98 | 99 | class TaskResultManager(ResultManager): 100 | """Manager for :class:`~.models.TaskResult` models.""" 101 | 102 | _last_id = None 103 | 104 | def get_task(self, task_id): 105 | """Get result for task by ``task_id``. 106 | 107 | Keyword Arguments: 108 | exception_retry_count (int): How many times to retry by 109 | transaction rollback on exception. This could 110 | happen in a race condition if another worker is trying to 111 | create the same task. The default is to retry once. 112 | 113 | """ 114 | try: 115 | return self.get(task_id=task_id) 116 | except self.model.DoesNotExist: 117 | if self._last_id == task_id: 118 | self.warn_if_repeatable_read() 119 | self._last_id = task_id 120 | return self.model(task_id=task_id) 121 | 122 | @transaction_retry(max_retries=2) 123 | def store_result(self, content_type, content_encoding, 124 | task_id, result, status, 125 | traceback=None, meta=None, 126 | periodic_task_name=None, 127 | task_name=None, task_args=None, task_kwargs=None, 128 | worker=None, using=None, **kwargs): 129 | """Store the result and status of a task. 130 | 131 | Arguments: 132 | content_type (str): Mime-type of result and meta content. 133 | content_encoding (str): Type of encoding (e.g. binary/utf-8). 134 | task_id (str): Id of task. 135 | periodic_task_name (str): Celery Periodic task name. 136 | task_name (str): Celery task name. 137 | task_args (str): Task arguments. 138 | task_kwargs (str): Task kwargs. 139 | result (str): The serialized return value of the task, 140 | or an exception instance raised by the task. 141 | status (str): Task status. See :mod:`celery.states` for a list of 142 | possible status values. 143 | worker (str): Worker that executes the task. 144 | using (str): Django database connection to use. 145 | traceback (str): The traceback string taken at the point of 146 | exception (only passed if the task failed). 147 | meta (str): Serialized result meta data (this contains e.g. 148 | children). 149 | 150 | Keyword Arguments: 151 | exception_retry_count (int): How many times to retry by 152 | transaction rollback on exception. This could 153 | happen in a race condition if another worker is trying to 154 | create the same task. The default is to retry twice. 155 | 156 | """ 157 | fields = { 158 | 'status': status, 159 | 'result': result, 160 | 'traceback': traceback, 161 | 'meta': meta, 162 | 'content_encoding': content_encoding, 163 | 'content_type': content_type, 164 | 'periodic_task_name': periodic_task_name, 165 | 'task_name': task_name, 166 | 'task_args': task_args, 167 | 'task_kwargs': task_kwargs, 168 | 'worker': worker 169 | } 170 | if 'date_started' in kwargs: 171 | fields['date_started'] = kwargs['date_started'] 172 | 173 | obj, created = self.using(using).get_or_create(task_id=task_id, 174 | defaults=fields) 175 | if not created: 176 | for k, v in fields.items(): 177 | setattr(obj, k, v) 178 | obj.save(using=using) 179 | return obj 180 | 181 | 182 | class GroupResultManager(ResultManager): 183 | """Manager for :class:`~.models.GroupResult` models.""" 184 | 185 | _last_id = None 186 | 187 | def get_group(self, group_id): 188 | """Get result for group by ``group_id``. 189 | 190 | Keyword Arguments: 191 | exception_retry_count (int): How many times to retry by 192 | transaction rollback on exception. This could 193 | happen in a race condition if another worker is trying to 194 | create the same task. The default is to retry once. 195 | 196 | """ 197 | try: 198 | return self.get(group_id=group_id) 199 | except self.model.DoesNotExist: 200 | if self._last_id == group_id: 201 | self.warn_if_repeatable_read() 202 | self._last_id = group_id 203 | return self.model(group_id=group_id) 204 | 205 | @transaction_retry(max_retries=2) 206 | def store_group_result(self, content_type, content_encoding, 207 | group_id, result, using=None): 208 | fields = { 209 | 'result': result, 210 | 'content_encoding': content_encoding, 211 | 'content_type': content_type, 212 | } 213 | 214 | if not using: 215 | using = self.db 216 | 217 | obj, created = self.using(using).get_or_create(group_id=group_id, 218 | defaults=fields) 219 | if not created: 220 | for k, v in fields.items(): 221 | setattr(obj, k, v) 222 | obj.save(using=self.db) 223 | return obj 224 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don\'t have Sphinx installed, grab it from https://www.sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help 23 | help: 24 | @echo "Please use \`make ' where is one of" 25 | @echo " html to make standalone HTML files" 26 | @echo " dirhtml to make HTML files named index.html in directories" 27 | @echo " singlehtml to make a single large HTML file" 28 | @echo " pickle to make pickle files" 29 | @echo " json to make JSON files" 30 | @echo " htmlhelp to make HTML files and a HTML help project" 31 | @echo " qthelp to make HTML files and a qthelp project" 32 | @echo " applehelp to make an Apple Help Book" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " epub3 to make an epub3" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | @echo " apicheck to verify that all modules are present in autodoc" 51 | @echo " configcheck to verify that all modules are present in autodoc" 52 | @echo " spelling to run a spell checker on the documentation" 53 | 54 | .PHONY: clean 55 | clean: 56 | rm -rf $(BUILDDIR)/* 57 | 58 | .PHONY: html 59 | html: 60 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 61 | @echo 62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 63 | 64 | .PHONY: dirhtml 65 | dirhtml: 66 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 67 | @echo 68 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 69 | 70 | .PHONY: singlehtml 71 | singlehtml: 72 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 73 | @echo 74 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 75 | 76 | .PHONY: pickle 77 | pickle: 78 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 79 | @echo 80 | @echo "Build finished; now you can process the pickle files." 81 | 82 | .PHONY: json 83 | json: 84 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 85 | @echo 86 | @echo "Build finished; now you can process the JSON files." 87 | 88 | .PHONY: htmlhelp 89 | htmlhelp: 90 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 91 | @echo 92 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 93 | ".hhp project file in $(BUILDDIR)/htmlhelp." 94 | 95 | .PHONY: qthelp 96 | qthelp: 97 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 98 | @echo 99 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 100 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 101 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PROJ.qhcp" 102 | @echo "To view the help file:" 103 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PROJ.qhc" 104 | 105 | .PHONY: applehelp 106 | applehelp: 107 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 108 | @echo 109 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 110 | @echo "N.B. You won't be able to view it unless you put it in" \ 111 | "~/Library/Documentation/Help or install it in your application" \ 112 | "bundle." 113 | 114 | .PHONY: devhelp 115 | devhelp: 116 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 117 | @echo 118 | @echo "Build finished." 119 | @echo "To view the help file:" 120 | @echo "# mkdir -p $$HOME/.local/share/devhelp/PROJ" 121 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PROJ" 122 | @echo "# devhelp" 123 | 124 | .PHONY: epub 125 | epub: 126 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 127 | @echo 128 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 129 | 130 | .PHONY: epub3 131 | epub3: 132 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 133 | @echo 134 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." 135 | 136 | .PHONY: latex 137 | latex: 138 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 139 | @echo 140 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 141 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 142 | "(use \`make latexpdf' here to do that automatically)." 143 | 144 | .PHONY: latexpdf 145 | latexpdf: 146 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 147 | @echo "Running LaTeX files through pdflatex..." 148 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 149 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 150 | 151 | .PHONY: latexpdfja 152 | latexpdfja: 153 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 154 | @echo "Running LaTeX files through platex and dvipdfmx..." 155 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 156 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 157 | 158 | .PHONY: text 159 | text: 160 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 161 | @echo 162 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 163 | 164 | .PHONY: man 165 | man: 166 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 167 | @echo 168 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 169 | 170 | .PHONY: texinfo 171 | texinfo: 172 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 173 | @echo 174 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 175 | @echo "Run \`make' in that directory to run these through makeinfo" \ 176 | "(use \`make info' here to do that automatically)." 177 | 178 | .PHONY: info 179 | info: 180 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 181 | @echo "Running Texinfo files through makeinfo..." 182 | make -C $(BUILDDIR)/texinfo info 183 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 184 | 185 | .PHONY: gettext 186 | gettext: 187 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/django_celery_results/locale 188 | @echo 189 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/django_celery_results/locale." 190 | 191 | .PHONY: changes 192 | changes: 193 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 194 | @echo 195 | @echo "The overview file is in $(BUILDDIR)/changes." 196 | 197 | .PHONY: linkcheck 198 | linkcheck: 199 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 200 | @echo 201 | @echo "Link check complete; look for any errors in the above output " \ 202 | "or in $(BUILDDIR)/linkcheck/output.txt." 203 | 204 | .PHONY: doctest 205 | doctest: 206 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 207 | @echo "Testing of doctests in the sources finished, look at the " \ 208 | "results in $(BUILDDIR)/doctest/output.txt." 209 | 210 | .PHONY: coverage 211 | coverage: 212 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 213 | @echo "Testing of coverage in the sources finished, look at the " \ 214 | "results in $(BUILDDIR)/coverage/python.txt." 215 | 216 | .PHONY: apicheck 217 | apicheck: 218 | $(SPHINXBUILD) -b apicheck $(ALLSPHINXOPTS) $(BUILDDIR)/apicheck 219 | 220 | .PHONY: configcheck 221 | configcheck: 222 | $(SPHINXBUILD) -b configcheck $(ALLSPHINXOPTS) $(BUILDDIR)/configcheck 223 | 224 | .PHONY: spelling 225 | spelling: 226 | SPELLCHECK=1 $(SPHINXBUILD) -b spelling $(ALLSPHINXOPTS) $(BUILDDIR)/spelling 227 | 228 | .PHONY: xml 229 | xml: 230 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 231 | @echo 232 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 233 | 234 | .PHONY: pseudoxml 235 | pseudoxml: 236 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 237 | @echo 238 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 239 | -------------------------------------------------------------------------------- /django_celery_results/models.py: -------------------------------------------------------------------------------- 1 | """Database models.""" 2 | 3 | import json 4 | 5 | from celery import states 6 | from celery.result import GroupResult as CeleryGroupResult 7 | from celery.result import result_from_tuple 8 | from django.conf import settings 9 | from django.db import models 10 | from django.utils.translation import gettext_lazy as _ 11 | 12 | from . import managers 13 | 14 | ALL_STATES = sorted(states.ALL_STATES) 15 | TASK_STATE_CHOICES = sorted(zip(ALL_STATES, ALL_STATES)) 16 | 17 | 18 | class TaskResult(models.Model): 19 | """Task result/status.""" 20 | 21 | task_id = models.CharField( 22 | max_length=getattr( 23 | settings, 24 | 'DJANGO_CELERY_RESULTS_TASK_ID_MAX_LENGTH', 25 | 255 26 | ), 27 | unique=True, 28 | verbose_name=_('Task ID'), 29 | help_text=_('Celery ID for the Task that was run')) 30 | periodic_task_name = models.CharField( 31 | null=True, max_length=255, 32 | verbose_name=_('Periodic Task Name'), 33 | help_text=_('Name of the Periodic Task which was run')) 34 | task_name = models.CharField( 35 | null=True, max_length=getattr( 36 | settings, 37 | 'DJANGO_CELERY_RESULTS_TASK_ID_MAX_LENGTH', 38 | 255 39 | ), 40 | verbose_name=_('Task Name'), 41 | help_text=_('Name of the Task which was run')) 42 | task_args = models.TextField( 43 | null=True, 44 | verbose_name=_('Task Positional Arguments'), 45 | help_text=_('JSON representation of the positional arguments ' 46 | 'used with the task')) 47 | task_kwargs = models.TextField( 48 | null=True, 49 | verbose_name=_('Task Named Arguments'), 50 | help_text=_('JSON representation of the named arguments ' 51 | 'used with the task')) 52 | status = models.CharField( 53 | max_length=50, default=states.PENDING, 54 | verbose_name=_('Task State'), 55 | help_text=_('Current state of the task being run')) 56 | worker = models.CharField( 57 | max_length=100, default=None, null=True, 58 | verbose_name=_('Worker'), help_text=_('Worker that executes the task') 59 | ) 60 | content_type = models.CharField( 61 | max_length=128, 62 | verbose_name=_('Result Content Type'), 63 | help_text=_('Content type of the result data')) 64 | content_encoding = models.CharField( 65 | max_length=64, 66 | verbose_name=_('Result Encoding'), 67 | help_text=_('The encoding used to save the task result data')) 68 | result = models.TextField( 69 | null=True, default=None, editable=False, 70 | verbose_name=_('Result Data'), 71 | help_text=_('The data returned by the task. ' 72 | 'Use content_encoding and content_type fields to read.')) 73 | date_created = models.DateTimeField( 74 | auto_now_add=True, 75 | verbose_name=_('Created DateTime'), 76 | help_text=_('Datetime field when the task result was created in UTC')) 77 | date_started = models.DateTimeField( 78 | null=True, default=None, 79 | verbose_name=_('Started DateTime'), 80 | help_text=_('Datetime field when the task was started in UTC')) 81 | date_done = models.DateTimeField( 82 | auto_now=True, 83 | verbose_name=_('Completed DateTime'), 84 | help_text=_('Datetime field when the task was completed in UTC')) 85 | traceback = models.TextField( 86 | blank=True, null=True, 87 | verbose_name=_('Traceback'), 88 | help_text=_('Text of the traceback if the task generated one')) 89 | meta = models.TextField( 90 | null=True, default=None, editable=False, 91 | verbose_name=_('Task Meta Information'), 92 | help_text=_('JSON meta information about the task, ' 93 | 'such as information on child tasks')) 94 | 95 | objects = managers.TaskResultManager() 96 | 97 | class Meta: 98 | """Table information.""" 99 | 100 | ordering = ['-date_done'] 101 | 102 | verbose_name = _('task result') 103 | verbose_name_plural = _('task results') 104 | 105 | # Explicit names to solve https://code.djangoproject.com/ticket/33483 106 | indexes = [ 107 | models.Index(fields=['task_name'], 108 | name='django_cele_task_na_08aec9_idx'), 109 | models.Index(fields=['status'], 110 | name='django_cele_status_9b6201_idx'), 111 | models.Index(fields=['worker'], 112 | name='django_cele_worker_d54dd8_idx'), 113 | models.Index(fields=['date_created'], 114 | name='django_cele_date_cr_f04a50_idx'), 115 | models.Index(fields=['date_done'], 116 | name='django_cele_date_do_f59aad_idx'), 117 | models.Index(fields=['periodic_task_name'], 118 | name='django_cele_periodi_1993cf_idx'), 119 | ] 120 | 121 | def as_dict(self): 122 | return { 123 | 'task_id': self.task_id, 124 | 'task_name': self.task_name, 125 | 'task_args': self.task_args, 126 | 'task_kwargs': self.task_kwargs, 127 | 'status': self.status, 128 | 'result': self.result, 129 | 'date_done': self.date_done, 130 | 'traceback': self.traceback, 131 | 'meta': self.meta, 132 | 'worker': self.worker 133 | } 134 | 135 | def __str__(self): 136 | return ''.format(self) 137 | 138 | 139 | class ChordCounter(models.Model): 140 | """Chord synchronisation.""" 141 | 142 | group_id = models.CharField( 143 | max_length=getattr( 144 | settings, 145 | "DJANGO_CELERY_RESULTS_TASK_ID_MAX_LENGTH", 146 | 255), 147 | unique=True, 148 | verbose_name=_("Group ID"), 149 | help_text=_("Celery ID for the Chord header group"), 150 | ) 151 | sub_tasks = models.TextField( 152 | help_text=_( 153 | "JSON serialized list of task result tuples. " 154 | "use .group_result() to decode" 155 | ) 156 | ) 157 | count = models.PositiveIntegerField( 158 | help_text=_( 159 | "Starts at len(chord header) and decrements after each task is " 160 | "finished" 161 | ) 162 | ) 163 | 164 | def group_result(self, app=None): 165 | """Return the :class:`celery.result.GroupResult` of self. 166 | 167 | Arguments: 168 | app (celery.app.base.Celery): app instance to create the 169 | :class:`celery.result.GroupResult` with. 170 | 171 | """ 172 | return CeleryGroupResult( 173 | self.group_id, 174 | [result_from_tuple(r, app=app) 175 | for r in json.loads(self.sub_tasks)], 176 | app=app 177 | ) 178 | 179 | 180 | class GroupResult(models.Model): 181 | """Task Group result/status.""" 182 | 183 | group_id = models.CharField( 184 | max_length=getattr( 185 | settings, 186 | "DJANGO_CELERY_RESULTS_TASK_ID_MAX_LENGTH", 187 | 255 188 | ), 189 | unique=True, 190 | verbose_name=_("Group ID"), 191 | help_text=_("Celery ID for the Group that was run"), 192 | ) 193 | date_created = models.DateTimeField( 194 | auto_now_add=True, 195 | verbose_name=_("Created DateTime"), 196 | help_text=_("Datetime field when the group result was created in UTC"), 197 | ) 198 | date_done = models.DateTimeField( 199 | auto_now=True, 200 | verbose_name=_("Completed DateTime"), 201 | help_text=_("Datetime field when the group was completed in UTC"), 202 | ) 203 | content_type = models.CharField( 204 | max_length=128, 205 | verbose_name=_("Result Content Type"), 206 | help_text=_("Content type of the result data"), 207 | ) 208 | content_encoding = models.CharField( 209 | max_length=64, 210 | verbose_name=_("Result Encoding"), 211 | help_text=_("The encoding used to save the task result data"), 212 | ) 213 | result = models.TextField( 214 | null=True, default=None, editable=False, 215 | verbose_name=_('Result Data'), 216 | help_text=_('The data returned by the task. ' 217 | 'Use content_encoding and content_type fields to read.')) 218 | 219 | def as_dict(self): 220 | return { 221 | 'group_id': self.group_id, 222 | 'result': self.result, 223 | 'date_done': self.date_done, 224 | } 225 | 226 | def __str__(self): 227 | return f'' 228 | 229 | objects = managers.GroupResultManager() 230 | 231 | class Meta: 232 | """Table information.""" 233 | 234 | ordering = ['-date_done'] 235 | 236 | verbose_name = _('group result') 237 | verbose_name_plural = _('group results') 238 | 239 | # Explicit names to solve https://code.djangoproject.com/ticket/33483 240 | indexes = [ 241 | models.Index(fields=['date_created'], 242 | name='django_cele_date_cr_bd6c1d_idx'), 243 | models.Index(fields=['date_done'], 244 | name='django_cele_date_do_caae0e_idx'), 245 | ] 246 | -------------------------------------------------------------------------------- /t/unit/test_models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from unittest.mock import patch 3 | 4 | import pytest 5 | from celery import states, uuid 6 | from django.db import transaction 7 | from django.db.utils import InterfaceError 8 | from django.test import TransactionTestCase 9 | 10 | from django_celery_results.backends import DatabaseBackend 11 | from django_celery_results.models import GroupResult, TaskResult 12 | from django_celery_results.utils import now 13 | 14 | 15 | @pytest.mark.usefixtures('depends_on_current_app') 16 | class test_Models(TransactionTestCase): 17 | databases = '__all__' 18 | 19 | @pytest.fixture(autouse=True) 20 | def setup_app(self, app): 21 | self.app = app 22 | self.app.conf.result_serializer = 'pickle' 23 | self.app.conf.result_backend = ( 24 | 'django_celery_results.backends:DatabaseBackend') 25 | 26 | def create_task_result(self): 27 | id = uuid() 28 | taskmeta, created = TaskResult.objects.get_or_create(task_id=id) 29 | return taskmeta 30 | 31 | def test_taskmeta(self, ctype='application/json', cenc='utf-8'): 32 | m1 = self.create_task_result() 33 | m2 = self.create_task_result() 34 | m3 = self.create_task_result() 35 | assert str(m1).startswith('