pax_global_header00006660000000000000000000000064137554503240014522gustar00rootroot0000000000000052 comment=e29586b4445268906b737426345d6a003fff162f django-celery-results-2.0.0/000077500000000000000000000000001375545032400157435ustar00rootroot00000000000000django-celery-results-2.0.0/.bumpversion.cfg000066400000000000000000000005451375545032400210570ustar00rootroot00000000000000[bumpversion] current_version = 2.0.0 commit = True tag = True parse = (?P\d+)\.(?P\d+)\.(?P\d+)(?P[a-z]+)? serialize = {major}.{minor}.{patch}{releaselevel} {major}.{minor}.{patch} [bumpversion:file:django_celery_results/__init__.py] [bumpversion:file:docs/includes/introduction.txt] [bumpversion:file:README.rst] django-celery-results-2.0.0/.cookiecutterrc000066400000000000000000000016721375545032400207770ustar00rootroot00000000000000# This file exists so you can easily regenerate your project. # # `cookiepatcher` is a convenient shim around `cookiecutter` # for regenerating projects (it will generate a .cookiecutterrc # automatically for any template). To use it: # # pip install cookiepatcher # cookiepatcher gh:ionelmc/cookiecutter-pylibrary project-path # # See: # https://pypi.python.org/pypi/cookiecutter # # Alternatively, you can run: # # cookiecutter --overwrite-if-exists --config-file=project-path/.cookiecutterrc gh:ionelmc/cookiecutter-pylibrary default_context: email: 'ask@celeryproject.org' full_name: 'Ask Solem' github_username: 'celery' project_name: 'django-celery-results' project_short_description: 'Celery result backends using Django' project_slug: 'django-celery-results' version: '1.0.0' year: '2016' django-celery-results-2.0.0/.coveragerc000066400000000000000000000002251375545032400200630ustar00rootroot00000000000000[run] branch = 1 cover_pylib = 0 include = *django_celery_results/* omit = t/* [report] omit = */python?.?/* */site-packages/* */pypy/* django-celery-results-2.0.0/.editorconfig000066400000000000000000000003151375545032400204170ustar00rootroot00000000000000# http://editorconfig.org root = true [*] indent_style = space indent_size = 4 trim_trailing_whitespace = true insert_final_newline = true charset = utf-8 end_of_line = lf [Makefile] indent_style = tab django-celery-results-2.0.0/.gitignore000066400000000000000000000004461375545032400177370ustar00rootroot00000000000000.DS_Store *.pyc *$py.class *~ .*.sw[pon] dist/ *.egg-info *.egg *.egg/ build/ .build/ _build/ pip-log.txt .directory erl_crash.dump *.db Documentation/ .tox/ .ropeproject/ .project .pydevproject .idea/ .coverage celery/tests/cover/ .ve* cover/ .vagrant/ *.sqlite3 .cache/ htmlcov/ coverage.xml django-celery-results-2.0.0/.travis.yml000066400000000000000000000014001375545032400200470ustar00rootroot00000000000000language: python cache: pip os: - linux services: - postgresql addons: postgresql: "13" dist: focal python: - 3.6 - 3.7 - 3.8 env: - DJANGO=2.2 - DJANGO=3.0 - DJANGO=3.1 matrix: include: - { python: 3.8, env: TOXENV=flake8 } - { python: 3.8, env: TOXENV=pydocstyle } - { python: 3.8, env: TOXENV=cov } - { python: 3.8, env: TOXENV=integration } # disabled temporarily due to upstream bug # https://github.com/celery/sphinx_celery/issues/9 # - { python: 3.8, env: TOXENV=apicheck } # - { python: 3.8, env: TOXENV=linkcheck } install: travis_retry pip install -U tox-travis script: tox -v -- -v after_success: - .tox/$TRAVIS_PYTHON_VERSION/bin/coverage xml - .tox/$TRAVIS_PYTHON_VERSION/bin/codecov -e TOXENV django-celery-results-2.0.0/AUTHORS000066400000000000000000000060151375545032400170150ustar00rootroot00000000000000========= AUTHORS ========= :order: sorted Aaron Ross Adam Endicott Alex Stapleton Alvaro Vega Andrew Frankel Andrew Watts Andrii Kostenko Anton Novosyolov Ask Solem Augusto Becciu Ben Firshman Brad Jasper Brett Gibson Brian Rosner Charlie DeTar Christopher Grebs Dan LaMotte Darjus Loktevic David Fischer David Ziegler Diego Andres Sanabria Martin Dmitriy Krasilnikov Donald Stufft Eldon Stegall Eugene Nagornyi Felix Berger Glenn Washburn Gnrhxni Greg Taylor Grégoire Cachet Hari Idan Zalzberg Ionel Maries Cristian Jannis Leidel Jason Baker Jay States Jeff Balogh Jeff Fischer Jeffrey Hu Jens Alm Jerzy Kozera Jesper Noehr John Andrews John Watson Jonas Haag Jonatan Heyman Josh Drake José Moreira Jude Nagurney Justin Quick Keith Perkins Kirill Panshin Mark Hellewell Mark Lavin Mark Stover Maxim Bodyansky Michael Elsdoerfer Michael van Tellingen Mikhail Korobov Olivier Tabone Patrick Altman Piotr Bulinski Piotr Sikora Reza Lotun Rockallite Wulf Roger Barnes Roman Imankulov Rune Halvorsen Sam Cooke Scott Rubin Sean Creeley Serj Zavadsky Simon Charette Spencer Ellinor Theo Spears Timo Sugliani Vincent Driessen Vitaly Babiy Vladislav Poluhin Weipin Xia Wes Turner Wes Winham Williams Mendez WoLpH dongweiming zeez django-celery-results-2.0.0/Changelog000066400000000000000000000041371375545032400175620ustar00rootroot00000000000000.. _changelog: ================ Change history ================ .. _version-2.0.0: 2.0.0 ===== :release-date: :release-by: - Add Spanish translations (#134) - Add support for Django 3.0 and 3.1 (#145, #163) - Add support for Celery 5 (#163) - Drop support for Django < 2.2 (#147, #152) - Drop support for Python < 3.6 (#146, #147, #152) - Add Chord syncronisation from the database (#144) - Encode `task_args` and `task_kwargs` of `TaskResult` using `json.dumps` instead of using `str` (#78) .. _version-1.1.2: 1.1.2 ===== :release-date: 2019-06-06 00:00 a.m. UTC+6:00 :release-by: Asif Saif Uddin - Fixed few regressions .. _version-1.1.0: 1.1.0 ===== :release-date: 2019-05-21 17:00 p.m. UTC+6:00 :release-by: Asif Saif Uddin - Django 2.2+. - Drop python 3.4 and django 2.0 - Support specifying the database to use for the store_result method (#63) - Fix MySQL8 system variable tx_isolation issue (#84) .. _version-1.0.4: 1.0.4 ===== :release-date: 2018-11-12 19:00 p.m. UTC+2:00 :release-by: Omer Katz 1.0.3 is broken. Use 1.0.4 - Revert renaming label as it is a breaking change. .. _version-1.0.3: 1.0.3 ===== :release-date: 2018-11-12 18:00 p.m. UTC+2:00 :release-by: Omer Katz - Revert renaming label as it is a breaking change. .. _version-1.0.2: 1.0.2 ===== :release-date: 2018-11-12 18:00 p.m. UTC+2:00 :release-by: Omer Katz - Store task name, args, kwargs as part of the task results in database. Contributed by :github_user: `wardal`. - Admin screen changes - task name filter, search on task_name, task_id, status. Contributed by :github_user: `jaylynch`. - Added default_app_config. - Added missing migration. - Fix MySQL max length issue. - Drop support for Django<1.11. .. _version-1.0.1: 1.0.1 ===== :release-date: 2016-11-07 02:00 p.m. PST :release-by: Ask Solem - Migrations were not being installed as part of the distribution (Issue #4). - Now includes simple task result admin interface. Contributed by :github_user:`zeezdev`. - Now depends on Celery 4.0.0. .. _version-1.0.0: 1.0.0 ===== :release-date: 2016-09-08 03:19 p.m. PDT :release-by: Ask Solem - Initial release django-celery-results-2.0.0/LICENSE000066400000000000000000000051101375545032400167450ustar00rootroot00000000000000Copyright (c) 2015-2016 Ask Solem. All Rights Reserved. Copyright (c) 2012-2014 GoPivotal, Inc. All Rights Reserved. Copyright (c) 2009-2012 Ask Solem. All Rights Reserved. django-celery-results is licensed under The BSD License (3 Clause, also known as the new BSD license). The license is an OSI approved Open Source license and is GPL-compatible(1). The license text can also be found here: http://www.opensource.org/licenses/BSD-3-Clause License ======= Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Ask Solem nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Ask Solem OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Documentation License ===================== The documentation portion of django-celery-results (the rendered contents of the "docs" directory of a software distribution or checkout) is supplied under the "Creative Commons Attribution-ShareAlike 4.0 International" (CC BY-SA 4.0) License as described by http://creativecommons.org/licenses/by-sa/4.0/ Footnotes ========= (1) A GPL-compatible license makes it possible to combine django-celery-results with other software that is released under the GPL, it does not mean that we're distributing django-celery-results under the GPL license. The BSD license, unlike the GPL, let you distribute a modified version without making your changes open source. django-celery-results-2.0.0/MANIFEST.in000066400000000000000000000006301375545032400175000ustar00rootroot00000000000000include Changelog include LICENSE include README.rst include MANIFEST.in include setup.cfg include setup.py include manage.py recursive-include docs * recursive-include extra/* recursive-include examples * recursive-include requirements *.txt *.rst recursive-include t *.py recursive-include django_celery_results *.py recursive-exclude * __pycache__ recursive-exclude * *.py[co] recursive-exclude * .*.sw* django-celery-results-2.0.0/Makefile000066400000000000000000000073231375545032400174100ustar00rootroot00000000000000PROJ=django_celery_results PGPIDENT="Celery Security Team" PYTHON=python PYTEST=pytest GIT=git TOX=tox ICONV=iconv FLAKE8=flake8 FLAKEPLUS=flakeplus PYDOCSTYLE=pydocstyle SPHINX2RST=sphinx2rst TESTDIR=t SPHINX_DIR=docs/ SPHINX_BUILDDIR="${SPHINX_DIR}/_build" README=README.rst README_SRC="docs/templates/readme.txt" CONTRIBUTING=CONTRIBUTING.rst CONTRIBUTING_SRC="docs/contributing.rst" SPHINX_HTMLDIR="${SPHINX_BUILDDIR}/html" DOCUMENTATION=Documentation FLAKEPLUSTARGET=2.7 all: help help: @echo "docs - Build documentation." @echo "test-all - Run tests for all supported python versions." @echo "distcheck ---------- - Check distribution for problems." @echo " test - Run unittests using current python." @echo " lint ------------ - Check codebase for problems." @echo " apicheck - Check API reference coverage." @echo " configcheck - Check configuration reference coverage." @echo " readmecheck - Check README.rst encoding." @echo " contribcheck - Check CONTRIBUTING.rst encoding" @echo " flakes -------- - Check code for syntax and style errors." @echo " flakecheck - Run flake8 on the source code." @echo " flakepluscheck - Run flakeplus on the source code." @echo " pep257check - Run pydocstyle on the source code." @echo "readme - Regenerate README.rst file." @echo "contrib - Regenerate CONTRIBUTING.rst file" @echo "clean-dist --------- - Clean all distribution build artifacts." @echo " clean-git-force - Remove all uncomitted files." @echo " clean ------------ - Non-destructive clean" @echo " clean-pyc - Remove .pyc/__pycache__ files" @echo " clean-docs - Remove documentation build artifacts." @echo " clean-build - Remove setup artifacts." @echo "bump - Bump patch version number." @echo "bump-minor - Bump minor version number." @echo "bump-major - Bump major version number." @echo "release - Make PyPI release." clean: clean-docs clean-pyc clean-build clean-dist: clean clean-git-force bump: bumpversion patch bump-minor: bumpversion minor bump-major: bumpversion major release: python setup.py register sdist bdist_wheel upload --sign --identity="$(PGPIDENT)" Documentation: (cd "$(SPHINX_DIR)"; $(MAKE) html) mv "$(SPHINX_HTMLDIR)" $(DOCUMENTATION) docs: Documentation clean-docs: -rm -rf "$(SPHINX_BUILDDIR)" lint: flakecheck apicheck configcheck readmecheck apicheck: (cd "$(SPHINX_DIR)"; $(MAKE) apicheck) configcheck: true flakecheck: $(FLAKE8) "$(PROJ)" "$(TESTDIR)" flakediag: -$(MAKE) flakecheck pep257check: $(PYDOCSTYLE) "$(PROJ)" flakepluscheck: $(FLAKEPLUS) --$(FLAKEPLUSTARGET) "$(PROJ)" "$(TESTDIR)" flakeplusdiag: -$(MAKE) flakepluscheck flakes: flakediag flakeplusdiag pep257check clean-readme: -rm -f $(README) readmecheck: $(ICONV) -f ascii -t ascii $(README) >/dev/null $(README): $(SPHINX2RST) "$(README_SRC)" --ascii > $@ readme: clean-readme $(README) readmecheck clean-contrib: -rm -f "$(CONTRIBUTING)" $(CONTRIBUTING): $(SPHINX2RST) "$(CONTRIBUTING_SRC)" > $@ contrib: clean-contrib $(CONTRIBUTING) clean-pyc: -find . -type f -a \( -name "*.pyc" -o -name "*$$py.class" \) | xargs rm -find . -type d -name "__pycache__" | xargs rm -r removepyc: clean-pyc clean-build: rm -rf build/ dist/ .eggs/ *.egg-info/ .tox/ .coverage cover/ clean-git: $(GIT) clean -xdn clean-git-force: $(GIT) clean -xdf test-all: clean-pyc $(TOX) test: $(PYTHON) setup.py test cov: covbuild (cd $(TESTDIR); pytest -x --cov=django_celery_results --cov-report=html) build: $(PYTHON) setup.py sdist bdist_wheel distcheck: lint test clean dist: readme contrib clean-dist build django-celery-results-2.0.0/README.rst000066400000000000000000000104311375545032400174310ustar00rootroot00000000000000===================================================================== Celery Result Backends using the Django ORM/Cache framework. ===================================================================== |build-status| |coverage| |license| |wheel| |pyversion| |pyimp| :Version: 2.0.0 :Web: http://django-celery-results.readthedocs.io/ :Download: http://pypi.python.org/pypi/django-celery-results :Source: http://github.com/celery/django-celery-results :Keywords: django, celery, database, results About ===== This extension enables you to store Celery task results using the Django ORM. It defines a single model (``django_celery_results.models.TaskResult``) used to store task results, and you can query this database table like any other Django model. Installing ========== The installation instructions for this extension is available from the `Celery documentation`_ .. _`Celery documentation`: http://docs.celeryproject.org/en/latest/django/first-steps-with-django.html#django-celery-results-using-the-django-orm-cache-as-a-result-backend .. _installation: Installation ============ You can install django-celery-results either via the Python Package Index (PyPI) or from source. To install using `pip`,:: $ pip install -U django-celery-results .. _installing-from-source: Downloading and installing from source -------------------------------------- Download the latest version of django-celery-results from http://pypi.python.org/pypi/django-celery-results You can install it by doing the following,:: $ tar xvfz django-celery-results-0.0.0.tar.gz $ cd django-celery-results-0.0.0 $ python setup.py build # python setup.py install The last command must be executed as a privileged user if you are not currently using a virtualenv. .. _installing-from-git: Using the development version ----------------------------- With pip ~~~~~~~~ You can install the latest snapshot of django-celery-results using the following pip command:: $ pip install https://github.com/celery/django-celery-results/zipball/master#egg=django-celery-results Issues with mysql ----------------- If you want to run ``django-celery-results`` with MySQL, you might run into some issues. One such issue is when you try to run ``python manage.py migrate django_celery_results``, you might get the following error:: django.db.utils.OperationalError: (1071, 'Specified key was too long; max key length is 767 bytes') To get around this issue, you can set:: DJANGO_CELERY_RESULTS_TASK_ID_MAX_LENGTH=191 (or any other value if any other db other than MySQL is causing similar issues.) max_length of **191** seems to work for MySQL. .. |build-status| image:: https://secure.travis-ci.org/celery/django-celery-results.svg?branch=master :alt: Build status :target: https://travis-ci.org/celery/django-celery-results .. |coverage| image:: https://codecov.io/github/celery/django-celery-results/coverage.svg?branch=master :target: https://codecov.io/github/celery/django-celery-results?branch=master .. |license| image:: https://img.shields.io/pypi/l/django-celery-results.svg :alt: BSD License :target: https://opensource.org/licenses/BSD-3-Clause .. |wheel| image:: https://img.shields.io/pypi/wheel/django-celery-results.svg :alt: django-celery-results can be installed via wheel :target: http://pypi.python.org/pypi/django-celery-results/ .. |pyversion| image:: https://img.shields.io/pypi/pyversions/django-celery-results.svg :alt: Supported Python versions. :target: http://pypi.python.org/pypi/django-celery-results/ .. |pyimp| image:: https://img.shields.io/pypi/implementation/django-celery-results.svg :alt: Support Python implementations. :target: http://pypi.python.org/pypi/django-celery-results/ django-celery-results as part of the Tidelift Subscription ----------------- The maintainers of django-celery-results and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. [Learn more.](https://tidelift.com/subscription/pkg/pypi-django-celery-results?utm_source=pypi-django-celery-results&utm_medium=referral&utm_campaign=readme&utm_term=repo) django-celery-results-2.0.0/appveyor.yml000066400000000000000000000024511375545032400203350ustar00rootroot00000000000000environment: global: # SDK v7.0 MSVC Express 2008's SetEnv.cmd script will fail if the # /E:ON and /V:ON options are not enabled in the batch script intepreter # See: http://stackoverflow.com/a/13751649/163740 WITH_COMPILER: "cmd /E:ON /V:ON /C .\\extra\\appveyor\\run_with_compiler.cmd" matrix: # Pre-installed Python versions, which Appveyor may upgrade to # a later point release. # See: http://www.appveyor.com/docs/installed-software#python - PYTHON: "C:\\Python27" PYTHON_VERSION: "2.7.x" PYTHON_ARCH: "32" - PYTHON: "C:\\Python34" PYTHON_VERSION: "3.4.x" PYTHON_ARCH: "32" - PYTHON: "C:\\Python27-x64" PYTHON_VERSION: "2.7.x" PYTHON_ARCH: "64" WINDOWS_SDK_VERSION: "v7.0" - PYTHON: "C:\\Python34-x64" PYTHON_VERSION: "3.4.x" PYTHON_ARCH: "64" WINDOWS_SDK_VERSION: "v7.1" init: - "ECHO %PYTHON% %PYTHON_VERSION% %PYTHON_ARCH%" install: - "powershell extra\\appveyor\\install.ps1" - "%PYTHON%/Scripts/pip.exe install -U setuptools" build: off test_script: - "%WITH_COMPILER% %PYTHON%/python setup.py test" after_test: - "%WITH_COMPILER% %PYTHON%/python setup.py bdist_wheel" artifacts: - path: dist\* #on_success: # - TODO: upload the content of dist/*.whl to a public wheelhouse django-celery-results-2.0.0/conftest.py000066400000000000000000000016041375545032400201430ustar00rootroot00000000000000import pytest def pytest_addoption(parser): parser.addoption( '-B', '--run-benchmarks', action='store_true', default=False, help='run benchmarks', ) def pytest_runtest_setup(item): """ Skip tests marked benchmark unless --run-benchmark is given to pytest """ run_benchmarks = item.config.getoption('--run-benchmarks') is_benchmark = any(item.iter_markers(name="benchmark")) if is_benchmark: if run_benchmarks: return pytest.skip( 'need --run-benchmarks to run benchmarks' ) def pytest_collection_modifyitems(items): """ Add the "benchmark" mark to tests that start with "benchmark_". """ for item in items: test_class_name = item.cls.__name__ if test_class_name.startswith("benchmark_"): item.add_marker(pytest.mark.benchmark) django-celery-results-2.0.0/django_celery_results/000077500000000000000000000000001375545032400223315ustar00rootroot00000000000000django-celery-results-2.0.0/django_celery_results/__init__.py000066400000000000000000000017751375545032400244540ustar00rootroot00000000000000# -*- coding: utf-8 -*- """Celery result backends for Django.""" # :copyright: (c) 2016, Ask Solem. # All rights reserved. # :license: BSD (3 Clause), see LICENSE for more details. from __future__ import absolute_import, unicode_literals import re from collections import namedtuple __version__ = '2.0.0' __author__ = 'Asif Saif Uddin, Ask Solem' __contact__ = 'auvipy@gmai.com, ask@celeryproject.org' __homepage__ = 'https://github.com/celery/django-celery-results' __docformat__ = 'restructuredtext' # -eof meta- version_info_t = namedtuple('version_info_t', ( 'major', 'minor', 'micro', 'releaselevel', 'serial', )) # bumpversion can only search for {current_version} # so we have to parse the version here. _temp = re.match( r'(\d+)\.(\d+).(\d+)(.+)?', __version__).groups() VERSION = version_info = version_info_t( int(_temp[0]), int(_temp[1]), int(_temp[2]), _temp[3] or '', '') del(_temp) del(re) __all__ = [] default_app_config = 'django_celery_results.apps.CeleryResultConfig' django-celery-results-2.0.0/django_celery_results/admin.py000066400000000000000000000035001375545032400237710ustar00rootroot00000000000000"""Result Task Admin interface.""" from __future__ import absolute_import, unicode_literals from django.contrib import admin from django.conf import settings from django.utils.translation import gettext_lazy as _ try: ALLOW_EDITS = settings.DJANGO_CELERY_RESULTS['ALLOW_EDITS'] except (AttributeError, KeyError): ALLOW_EDITS = False pass from .models import TaskResult class TaskResultAdmin(admin.ModelAdmin): """Admin-interface for results of tasks.""" model = TaskResult date_hierarchy = 'date_done' list_display = ('task_id', 'task_name', 'date_done', 'status', 'worker') list_filter = ('status', 'date_done', 'task_name', 'worker') readonly_fields = ('date_created', 'date_done', 'result', 'meta') search_fields = ('task_name', 'task_id', 'status') fieldsets = ( (None, { 'fields': ( 'task_id', 'task_name', 'status', 'worker', 'content_type', 'content_encoding', ), 'classes': ('extrapretty', 'wide') }), (_('Parameters'), { 'fields': ( 'task_args', 'task_kwargs', ), 'classes': ('extrapretty', 'wide') }), (_('Result'), { 'fields': ( 'result', 'date_created', 'date_done', 'traceback', 'meta', ), 'classes': ('extrapretty', 'wide') }), ) def get_readonly_fields(self, request, obj=None): if ALLOW_EDITS: return self.readonly_fields else: return list(set( [field.name for field in self.opts.local_fields] )) admin.site.register(TaskResult, TaskResultAdmin) django-celery-results-2.0.0/django_celery_results/apps.py000066400000000000000000000006571375545032400236560ustar00rootroot00000000000000"""Application configuration.""" from __future__ import absolute_import, unicode_literals from django.apps import AppConfig from django.utils.translation import gettext_lazy as _ __all__ = ['CeleryResultConfig'] class CeleryResultConfig(AppConfig): """Default configuration for the django_celery_results app.""" name = 'django_celery_results' label = 'django_celery_results' verbose_name = _('Celery Results') django-celery-results-2.0.0/django_celery_results/backends/000077500000000000000000000000001375545032400241035ustar00rootroot00000000000000django-celery-results-2.0.0/django_celery_results/backends/__init__.py000066400000000000000000000002571375545032400262200ustar00rootroot00000000000000from __future__ import absolute_import, unicode_literals from .cache import CacheBackend from .database import DatabaseBackend __all__ = ['CacheBackend', 'DatabaseBackend'] django-celery-results-2.0.0/django_celery_results/backends/cache.py000066400000000000000000000020121375545032400255130ustar00rootroot00000000000000"""Celery cache backend using the Django Cache Framework.""" from __future__ import absolute_import, unicode_literals from django.core.cache import cache as default_cache, caches from celery.backends.base import KeyValueStoreBackend class CacheBackend(KeyValueStoreBackend): """Backend using the Django cache framework to store task metadata.""" def __init__(self, *args, **kwargs): super(CacheBackend, self).__init__(*args, **kwargs) # Must make sure backend doesn't convert exceptions to dict. self.serializer = 'pickle' def get(self, key): return self.cache_backend.get(key) def set(self, key, value): self.cache_backend.set(key, value, self.expires) def delete(self, key): self.cache_backend.delete(key) def encode(self, data): return data def decode(self, data): return data @property def cache_backend(self): backend = self.app.conf.cache_backend return caches[backend] if backend else default_cache django-celery-results-2.0.0/django_celery_results/backends/database.py000066400000000000000000000134741375545032400262320ustar00rootroot00000000000000from __future__ import absolute_import, unicode_literals import json from celery import maybe_signature from celery.backends.base import BaseDictBackend from celery.exceptions import ChordError from celery.result import allow_join_result from celery.utils.serialization import b64encode, b64decode from celery.utils.log import get_logger from django.db import transaction from ..models import TaskResult, ChordCounter logger = get_logger(__name__) class DatabaseBackend(BaseDictBackend): """The Django database backend, using models to store task state.""" TaskModel = TaskResult subpolling_interval = 0.5 def _store_result(self, task_id, result, status, traceback=None, request=None, using=None): """Store return value and status of an executed task.""" content_type, content_encoding, result = self.encode_content(result) _, _, meta = self.encode_content({ 'children': self.current_task_children(request), }) task_name = getattr(request, 'task', None) _, _, task_args = self.encode_content( getattr(request, 'argsrepr', getattr(request, 'args', None)) ) _, _, task_kwargs = self.encode_content( getattr(request, 'kwargsrepr', getattr(request, 'kwargs', None)) ) worker = getattr(request, 'hostname', None) self.TaskModel._default_manager.store_result( content_type, content_encoding, task_id, result, status, traceback=traceback, meta=meta, task_name=task_name, task_args=task_args, task_kwargs=task_kwargs, worker=worker, using=using, ) return result def _get_task_meta_for(self, task_id): """Get task metadata for a task by id.""" obj = self.TaskModel._default_manager.get_task(task_id) res = obj.as_dict() meta = self.decode_content(obj, res.pop('meta', None)) or {} result = self.decode_content(obj, res.get('result')) task_args = self.decode_content(obj, res.get('task_args')) task_kwargs = self.decode_content(obj, res.get('task_kwargs')) res.update( meta, result=result, task_args=task_args, task_kwargs=task_kwargs, ) return self.meta_from_decoded(res) def encode_content(self, data): content_type, content_encoding, content = self._encode(data) if content_encoding == 'binary': content = b64encode(content) return content_type, content_encoding, content def decode_content(self, obj, content): if content: if obj.content_encoding == 'binary': content = b64decode(content) return self.decode(content) def _forget(self, task_id): try: self.TaskModel._default_manager.get(task_id=task_id).delete() except self.TaskModel.DoesNotExist: pass def cleanup(self): """Delete expired metadata.""" self.TaskModel._default_manager.delete_expired(self.expires) def apply_chord(self, header_result, body, **kwargs): """Add a ChordCounter with the expected number of results""" results = [r.as_tuple() for r in header_result] data = json.dumps(results) ChordCounter.objects.create( group_id=header_result.id, sub_tasks=data, count=len(results) ) def on_chord_part_return(self, request, state, result, **kwargs): """Called on finishing each part of a Chord header""" tid, gid = request.id, request.group if not gid or not tid: return call_callback = False with transaction.atomic(): # We need to know if `count` hits 0. # wrap the update in a transaction # with a `select_for_update` lock to prevent race conditions. # SELECT FOR UPDATE is not supported on all databases chord_counter = ( ChordCounter.objects.select_for_update() .get(group_id=gid) ) chord_counter.count -= 1 if chord_counter.count != 0: chord_counter.save() else: # Last task in the chord header has finished call_callback = True chord_counter.delete() if call_callback: deps = chord_counter.group_result(app=self.app) if deps.ready(): callback = maybe_signature(request.chord, app=self.app) trigger_callback( app=self.app, callback=callback, group_result=deps ) def trigger_callback(app, callback, group_result): """Add the callback to the queue or mark the callback as failed Implementation borrowed from `celery.app.builtins.unlock_chord` """ j = ( group_result.join_native if group_result.supports_native_join else group_result.join ) try: with allow_join_result(): ret = j(timeout=app.conf.result_chord_join_timeout, propagate=True) except Exception as exc: # pylint: disable=broad-except try: culprit = next(group_result._failed_join_report()) reason = "Dependency {0.id} raised {1!r}".format(culprit, exc) except StopIteration: reason = repr(exc) logger.exception("Chord %r raised: %r", group_result.id, exc) app.backend.chord_error_from_stack(callback, ChordError(reason)) else: try: callback.delay(ret) except Exception as exc: # pylint: disable=broad-except logger.exception("Chord %r raised: %r", group_result.id, exc) app.backend.chord_error_from_stack( callback, exc=ChordError("Callback error: {0!r}".format(exc)) ) django-celery-results-2.0.0/django_celery_results/managers.py000066400000000000000000000141561375545032400245070ustar00rootroot00000000000000"""Model managers.""" from __future__ import absolute_import, unicode_literals import warnings from functools import wraps from itertools import count from celery.utils.time import maybe_timedelta from django.db import connections, router, transaction from django.db import models from django.conf import settings from .utils import now W_ISOLATION_REP = """ Polling results with transaction isolation level 'repeatable-read' within the same transaction may give outdated results. Be sure to commit the transaction for each poll iteration. """ class TxIsolationWarning(UserWarning): """Warning emitted if the transaction isolation level is suboptimal.""" def transaction_retry(max_retries=1): """Decorate a function to retry database operations. For functions doing database operations, adding retrying if the operation fails. Keyword Arguments: ----------------- max_retries (int): Maximum number of retries. Default one retry. """ def _outer(fun): @wraps(fun) def _inner(*args, **kwargs): _max_retries = kwargs.pop('exception_retry_count', max_retries) for retries in count(0): try: return fun(*args, **kwargs) except Exception: # pragma: no cover # Depending on the database backend used we can experience # various exceptions. E.g. psycopg2 raises an exception # if some operation breaks the transaction, so saving # the task result won't be possible until we rollback # the transaction. if retries >= _max_retries: raise return _inner return _outer class TaskResultManager(models.Manager): """Manager for :class:`celery.models.TaskResult` models.""" _last_id = None def get_task(self, task_id): """Get result for task by ``task_id``. Keyword Arguments: ----------------- exception_retry_count (int): How many times to retry by transaction rollback on exception. This could happen in a race condition if another worker is trying to create the same task. The default is to retry once. """ try: return self.get(task_id=task_id) except self.model.DoesNotExist: if self._last_id == task_id: self.warn_if_repeatable_read() self._last_id = task_id return self.model(task_id=task_id) @transaction_retry(max_retries=2) def store_result(self, content_type, content_encoding, task_id, result, status, traceback=None, meta=None, task_name=None, task_args=None, task_kwargs=None, worker=None, using=None): """Store the result and status of a task. Arguments: --------- content_type (str): Mime-type of result and meta content. content_encoding (str): Type of encoding (e.g. binary/utf-8). task_id (str): Id of task. task_name (str): Celery task name. task_args (str): Task arguments. task_kwargs (str): Task kwargs. result (str): The serialized return value of the task, or an exception instance raised by the task. status (str): Task status. See :mod:`celery.states` for a list of possible status values. worker (str): Worker that executes the task. using (str): Django database connection to use. traceback (str): The traceback string taken at the point of exception (only passed if the task failed). meta (str): Serialized result meta data (this contains e.g. children). Keyword Arguments: ----------------- exception_retry_count (int): How many times to retry by transaction rollback on exception. This could happen in a race condition if another worker is trying to create the same task. The default is to retry twice. """ fields = { 'status': status, 'result': result, 'traceback': traceback, 'meta': meta, 'content_encoding': content_encoding, 'content_type': content_type, 'task_name': task_name, 'task_args': task_args, 'task_kwargs': task_kwargs, 'worker': worker } obj, created = self.using(using).get_or_create(task_id=task_id, defaults=fields) if not created: for k, v in fields.items(): setattr(obj, k, v) obj.save(using=using) return obj def warn_if_repeatable_read(self): if 'mysql' in self.current_engine().lower(): cursor = self.connection_for_read().cursor() # MariaDB and MySQL since 8.0 have different transaction isolation # variables: the former has tx_isolation, while the latter has # transaction_isolation if cursor.execute("SHOW VARIABLES WHERE variable_name IN " "('tx_isolation', 'transaction_isolation');"): isolation = cursor.fetchone()[1] if isolation == 'REPEATABLE-READ': warnings.warn(TxIsolationWarning(W_ISOLATION_REP.strip())) def connection_for_write(self): return connections[router.db_for_write(self.model)] def connection_for_read(self): return connections[self.db] def current_engine(self): try: return settings.DATABASES[self.db]['ENGINE'] except AttributeError: return settings.DATABASE_ENGINE def get_all_expired(self, expires): """Get all expired task results.""" return self.filter(date_done__lt=now() - maybe_timedelta(expires)) def delete_expired(self, expires): """Delete all expired results.""" with transaction.atomic(): self.get_all_expired(expires).delete() django-celery-results-2.0.0/django_celery_results/migrations/000077500000000000000000000000001375545032400245055ustar00rootroot00000000000000django-celery-results-2.0.0/django_celery_results/migrations/0001_initial.py000066400000000000000000000051531375545032400271540ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import absolute_import, unicode_literals from django.conf import settings from django.db import migrations, models class Migration(migrations.Migration): initial = True dependencies = [ ] operations = [ migrations.CreateModel( name='TaskResult', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('task_id', models.CharField( max_length=getattr( settings, 'DJANGO_CELERY_RESULTS_TASK_ID_MAX_LENGTH', 255 ), unique=True, verbose_name='task id' )), ('status', models.CharField(choices=[('FAILURE', 'FAILURE'), ('PENDING', 'PENDING'), ('RECEIVED', 'RECEIVED'), ('RETRY', 'RETRY'), ('REVOKED', 'REVOKED'), ('STARTED', 'STARTED'), ('SUCCESS', 'SUCCESS')], default='PENDING', max_length=50, verbose_name='state')), ('content_type', models.CharField( max_length=128, verbose_name='content type')), ('content_encoding', models.CharField( max_length=64, verbose_name='content encoding')), ('result', models.TextField(default=None, editable=False, null=True)), ('date_done', models.DateTimeField( auto_now=True, verbose_name='done at')), ('traceback', models.TextField( blank=True, null=True, verbose_name='traceback')), ('hidden', models.BooleanField( db_index=True, default=False, editable=False)), ('meta', models.TextField(default=None, editable=False, null=True)), ], options={ 'verbose_name': 'task result', 'verbose_name_plural': 'task results', }, ), ] django-celery-results-2.0.0/django_celery_results/migrations/0002_add_task_name_args_kwargs.py000066400000000000000000000016711375545032400326710ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Generated by Django 1.9.1 on 2017-10-26 16:06 from __future__ import absolute_import, unicode_literals from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('django_celery_results', '0001_initial'), ] operations = [ migrations.AddField( model_name='taskresult', name='task_args', field=models.TextField(null=True, verbose_name='task arguments'), ), migrations.AddField( model_name='taskresult', name='task_kwargs', field=models.TextField(null=True, verbose_name='task kwargs'), ), migrations.AddField( model_name='taskresult', name='task_name', field=models.CharField(max_length=255, null=True, verbose_name='task name' ), ), ] django-celery-results-2.0.0/django_celery_results/migrations/0003_auto_20181106_1101.py000066400000000000000000000011131375545032400301110ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Generated by Django 2.1 on 2018-11-06 11:01 from __future__ import absolute_import, unicode_literals from django.db import migrations class Migration(migrations.Migration): dependencies = [ ('django_celery_results', '0002_add_task_name_args_kwargs'), ] operations = [ migrations.AlterModelOptions( name='taskresult', options={ 'ordering': ['-date_done'], 'verbose_name': 'task result', 'verbose_name_plural': 'task results' }, ), ] django-celery-results-2.0.0/django_celery_results/migrations/0004_auto_20190516_0412.py000066400000000000000000000100211375545032400301210ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Generated by Django 1.11.20 on 2019-05-16 04:12 # this file is auto-generated so don't do flake8 on it # flake8: noqa from __future__ import absolute_import, unicode_literals from django.conf import settings from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('django_celery_results', '0003_auto_20181106_1101'), ] operations = [ migrations.AlterField( model_name='taskresult', name='content_encoding', field=models.CharField(help_text='The encoding used to save the task result data', max_length=64, verbose_name='Result Encoding'), ), migrations.AlterField( model_name='taskresult', name='content_type', field=models.CharField(help_text='Content type of the result data', max_length=128, verbose_name='Result Content Type'), ), migrations.AlterField( model_name='taskresult', name='date_done', field=models.DateTimeField(auto_now=True, db_index=True, help_text='Datetime field when the task was completed in UTC', verbose_name='Completed DateTime'), ), migrations.AlterField( model_name='taskresult', name='hidden', field=models.BooleanField(db_index=True, default=False, editable=False, help_text='Soft Delete flag that can be used instead of full delete', verbose_name='Hidden'), ), migrations.AlterField( model_name='taskresult', name='meta', field=models.TextField(default=None, editable=False, help_text='JSON meta information about the task, such as information on child tasks', null=True, verbose_name='Task Meta Information'), ), migrations.AlterField( model_name='taskresult', name='result', field=models.TextField(default=None, editable=False, help_text='The data returned by the task. Use content_encoding and content_type fields to read.', null=True, verbose_name='Result Data'), ), migrations.AlterField( model_name='taskresult', name='status', field=models.CharField(choices=[('FAILURE', 'FAILURE'), ('PENDING', 'PENDING'), ('RECEIVED', 'RECEIVED'), ('RETRY', 'RETRY'), ('REVOKED', 'REVOKED'), ('STARTED', 'STARTED'), ('SUCCESS', 'SUCCESS')], db_index=True, default='PENDING', help_text='Current state of the task being run', max_length=50, verbose_name='Task State'), ), migrations.AlterField( model_name='taskresult', name='task_args', field=models.TextField(help_text='JSON representation of the positional arguments used with the task', null=True, verbose_name='Task Positional Arguments'), ), migrations.AlterField( model_name='taskresult', name='task_id', field=models.CharField( db_index=True, help_text='Celery ID for the Task that was run', max_length=getattr( settings, 'DJANGO_CELERY_RESULTS_TASK_ID_MAX_LENGTH', 255 ), unique=True, verbose_name='Task ID' ), ), migrations.AlterField( model_name='taskresult', name='task_kwargs', field=models.TextField(help_text='JSON representation of the named arguments used with the task', null=True, verbose_name='Task Named Arguments'), ), migrations.AlterField( model_name='taskresult', name='task_name', field=models.CharField(db_index=True, help_text='Name of the Task which was run', max_length=255, null=True, verbose_name='Task Name'), ), migrations.AlterField( model_name='taskresult', name='traceback', field=models.TextField(blank=True, help_text='Text of the traceback if the task generated one', null=True, verbose_name='Traceback'), ), ] django-celery-results-2.0.0/django_celery_results/migrations/0005_taskresult_worker.py000066400000000000000000000014011375545032400313110ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Generated by Django 1.11.22 on 2019-07-24 15:38 # this file is auto-generated so don't do flake8 on it # flake8: noqa from __future__ import absolute_import, unicode_literals from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('django_celery_results', '0004_auto_20190516_0412'), ] operations = [ migrations.AddField( model_name='taskresult', name='worker', field=models.CharField(db_index=True, default=None, help_text='Worker that executes the task', max_length=100, null=True, verbose_name='Worker'), ), ] django-celery-results-2.0.0/django_celery_results/migrations/0006_taskresult_date_created.py000066400000000000000000000027101375545032400324110ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Generated by Django 2.2.4 on 2019-08-21 19:53 # this file is auto-generated so don't do flake8 on it # flake8: noqa from __future__ import absolute_import, unicode_literals from django.db import migrations, models import django.utils.timezone def copy_date_done_to_date_created(apps, schema_editor): TaskResult = apps.get_model('django_celery_results', 'taskresult') db_alias = schema_editor.connection.alias TaskResult.objects.using(db_alias).all().update( date_created=models.F('date_done') ) def reverse_copy_date_done_to_date_created(app, schema_editor): # the reverse of 'copy_date_done_to_date_created' is do nothing # because the 'date_created' will be removed. pass class Migration(migrations.Migration): dependencies = [ ('django_celery_results', '0005_taskresult_worker'), ] operations = [ migrations.AddField( model_name='taskresult', name='date_created', field=models.DateTimeField( auto_now_add=True, db_index=True, default=django.utils.timezone.now, help_text='Datetime field when the task result was created in UTC', verbose_name='Created DateTime' ), preserve_default=False, ), migrations.RunPython(copy_date_done_to_date_created, reverse_copy_date_done_to_date_created), ] django-celery-results-2.0.0/django_celery_results/migrations/0007_remove_taskresult_hidden.py000066400000000000000000000007701375545032400326220ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Generated by Django 2.2.6 on 2019-10-27 11:29 # this file is auto-generated so don't do flake8 on it # flake8: noqa from __future__ import absolute_import, unicode_literals from django.db import migrations class Migration(migrations.Migration): dependencies = [ ('django_celery_results', '0006_taskresult_date_created'), ] operations = [ migrations.RemoveField( model_name='taskresult', name='hidden', ), ] django-celery-results-2.0.0/django_celery_results/migrations/0008_chordcounter.py000066400000000000000000000026641375545032400302350ustar00rootroot00000000000000# Generated by Django 3.0.6 on 2020-05-12 12:05 from __future__ import unicode_literals, absolute_import from django.conf import settings from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('django_celery_results', '0007_remove_taskresult_hidden'), ] operations = [ migrations.CreateModel( name='ChordCounter', fields=[ ('id', models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('group_id', models.CharField( db_index=True, help_text='Celery ID for the Chord header group', max_length=getattr( settings, 'DJANGO_CELERY_RESULTS_TASK_ID_MAX_LENGTH', 255 ), unique=True, verbose_name='Group ID')), ('sub_tasks', models.TextField( help_text='JSON serialized list of task result tuples. ' 'use .group_result() to decode')), ('count', models.PositiveIntegerField( help_text='Starts at len(chord header) ' 'and decrements after each task is finished')), ], ), ] django-celery-results-2.0.0/django_celery_results/migrations/__init__.py000066400000000000000000000000001375545032400266040ustar00rootroot00000000000000django-celery-results-2.0.0/django_celery_results/models.py000066400000000000000000000117001375545032400241650ustar00rootroot00000000000000"""Database models.""" from __future__ import absolute_import, unicode_literals import json from django.conf import settings from django.db import models from django.utils.translation import gettext_lazy as _ from celery import states from celery.result import GroupResult, result_from_tuple from . import managers ALL_STATES = sorted(states.ALL_STATES) TASK_STATE_CHOICES = sorted(zip(ALL_STATES, ALL_STATES)) class TaskResult(models.Model): """Task result/status.""" task_id = models.CharField( max_length=getattr( settings, 'DJANGO_CELERY_RESULTS_TASK_ID_MAX_LENGTH', 255 ), unique=True, db_index=True, verbose_name=_('Task ID'), help_text=_('Celery ID for the Task that was run')) task_name = models.CharField( null=True, max_length=255, db_index=True, verbose_name=_('Task Name'), help_text=_('Name of the Task which was run')) task_args = models.TextField( null=True, verbose_name=_('Task Positional Arguments'), help_text=_('JSON representation of the positional arguments ' 'used with the task')) task_kwargs = models.TextField( null=True, verbose_name=_('Task Named Arguments'), help_text=_('JSON representation of the named arguments ' 'used with the task')) status = models.CharField( max_length=50, default=states.PENDING, db_index=True, choices=TASK_STATE_CHOICES, verbose_name=_('Task State'), help_text=_('Current state of the task being run')) worker = models.CharField( max_length=100, db_index=True, default=None, null=True, verbose_name=_('Worker'), help_text=_('Worker that executes the task') ) content_type = models.CharField( max_length=128, verbose_name=_('Result Content Type'), help_text=_('Content type of the result data')) content_encoding = models.CharField( max_length=64, verbose_name=_('Result Encoding'), help_text=_('The encoding used to save the task result data')) result = models.TextField( null=True, default=None, editable=False, verbose_name=_('Result Data'), help_text=_('The data returned by the task. ' 'Use content_encoding and content_type fields to read.')) date_created = models.DateTimeField( auto_now_add=True, db_index=True, verbose_name=_('Created DateTime'), help_text=_('Datetime field when the task result was created in UTC')) date_done = models.DateTimeField( auto_now=True, db_index=True, verbose_name=_('Completed DateTime'), help_text=_('Datetime field when the task was completed in UTC')) traceback = models.TextField( blank=True, null=True, verbose_name=_('Traceback'), help_text=_('Text of the traceback if the task generated one')) meta = models.TextField( null=True, default=None, editable=False, verbose_name=_('Task Meta Information'), help_text=_('JSON meta information about the task, ' 'such as information on child tasks')) objects = managers.TaskResultManager() class Meta: """Table information.""" ordering = ['-date_done'] verbose_name = _('task result') verbose_name_plural = _('task results') def as_dict(self): return { 'task_id': self.task_id, 'task_name': self.task_name, 'task_args': self.task_args, 'task_kwargs': self.task_kwargs, 'status': self.status, 'result': self.result, 'date_done': self.date_done, 'traceback': self.traceback, 'meta': self.meta, 'worker': self.worker } def __str__(self): return ''.format(self) class ChordCounter(models.Model): """Chord synchronisation.""" group_id = models.CharField( max_length=getattr( settings, "DJANGO_CELERY_RESULTS_TASK_ID_MAX_LENGTH", 255), unique=True, db_index=True, verbose_name=_("Group ID"), help_text=_("Celery ID for the Chord header group"), ) sub_tasks = models.TextField( help_text=_( "JSON serialized list of task result tuples. " "use .group_result() to decode" ) ) count = models.PositiveIntegerField( help_text=_( "Starts at len(chord header) and decrements after each task is " "finished" ) ) def group_result(self, app=None): """Return the GroupResult of self. Arguments: --------- app (Celery): app instance to create the GroupResult with. """ return GroupResult( self.group_id, [result_from_tuple(r, app=app) for r in json.loads(self.sub_tasks)], app=app, ) django-celery-results-2.0.0/django_celery_results/urls.py000066400000000000000000000011251375545032400236670ustar00rootroot00000000000000"""URLs defined for celery. * ``/$task_id/done/`` URL to :func:`~celery.views.is_successful`. * ``/$task_id/status/`` URL to :func:`~celery.views.task_status`. """ from __future__ import absolute_import, unicode_literals from django.conf.urls import url from . import views task_pattern = r'(?P[\w\d\-\.]+)' urlpatterns = [ url( r'^%s/done/?$' % task_pattern, views.is_task_successful, name='celery-is_task_successful' ), url( r'^%s/status/?$' % task_pattern, views.task_status, name='celery-task_status' ), ] django-celery-results-2.0.0/django_celery_results/utils.py000066400000000000000000000010261375545032400240420ustar00rootroot00000000000000"""Utilities.""" # -- XXX This module must not use translation as that causes # -- a recursive loader import! from __future__ import absolute_import, unicode_literals from django.conf import settings from django.utils import timezone # see Issue celery/django-celery#222 now_localtime = getattr(timezone, 'template_localtime', timezone.localtime) def now(): """Return the current date and time.""" if getattr(settings, 'USE_TZ', False): return now_localtime(timezone.now()) else: return timezone.now() django-celery-results-2.0.0/django_celery_results/views.py000066400000000000000000000020201375545032400240320ustar00rootroot00000000000000"""Views.""" from __future__ import absolute_import, unicode_literals from django.http import JsonResponse from celery import states from celery.result import AsyncResult from celery.utils import get_full_cls_name from kombu.utils.encoding import safe_repr def is_task_successful(request, task_id): """Return task execution status in JSON format.""" return JsonResponse({'task': { 'id': task_id, 'executed': AsyncResult(task_id).successful(), }}) def task_status(request, task_id): """Return task status and result in JSON format.""" result = AsyncResult(task_id) state, retval = result.state, result.result response_data = {'id': task_id, 'status': state, 'result': retval} if state in states.EXCEPTION_STATES: traceback = result.traceback response_data.update({'result': safe_repr(retval), 'exc': get_full_cls_name(retval.__class__), 'traceback': traceback}) return JsonResponse({'task': response_data}) django-celery-results-2.0.0/docs/000077500000000000000000000000001375545032400166735ustar00rootroot00000000000000django-celery-results-2.0.0/docs/Makefile000066400000000000000000000202511375545032400203330ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(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 http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " applehelp to make an Apple Help Book" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " epub3 to make an epub3" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" @echo " apicheck to verify that all modules are present in autodoc" @echo " configcheck to verify that all modules are present in autodoc" @echo " spelling to run a spell checker on the documentation" .PHONY: clean clean: rm -rf $(BUILDDIR)/* .PHONY: html html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." .PHONY: dirhtml dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." .PHONY: singlehtml singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." .PHONY: pickle pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." .PHONY: json json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." .PHONY: htmlhelp htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." .PHONY: qthelp qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PROJ.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PROJ.qhc" .PHONY: applehelp applehelp: $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp @echo @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." @echo "N.B. You won't be able to view it unless you put it in" \ "~/Library/Documentation/Help or install it in your application" \ "bundle." .PHONY: devhelp devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/PROJ" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PROJ" @echo "# devhelp" .PHONY: epub epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." .PHONY: epub3 epub3: $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 @echo @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." .PHONY: latex latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." .PHONY: latexpdf latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." .PHONY: latexpdfja latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." .PHONY: text text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." .PHONY: man man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." .PHONY: texinfo texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." .PHONY: info info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." .PHONY: gettext gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." .PHONY: changes changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." .PHONY: linkcheck linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." .PHONY: doctest doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." .PHONY: coverage coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." .PHONY: apicheck apicheck: $(SPHINXBUILD) -b apicheck $(ALLSPHINXOPTS) $(BUILDDIR)/apicheck .PHONY: configcheck configcheck: $(SPHINXBUILD) -b configcheck $(ALLSPHINXOPTS) $(BUILDDIR)/configcheck .PHONY: spelling spelling: SPELLCHECK=1 $(SPHINXBUILD) -b spelling $(ALLSPHINXOPTS) $(BUILDDIR)/spelling .PHONY: xml xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." .PHONY: pseudoxml pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." django-celery-results-2.0.0/docs/_static/000077500000000000000000000000001375545032400203215ustar00rootroot00000000000000django-celery-results-2.0.0/docs/_static/.keep000066400000000000000000000000001375545032400212340ustar00rootroot00000000000000django-celery-results-2.0.0/docs/_templates/000077500000000000000000000000001375545032400210305ustar00rootroot00000000000000django-celery-results-2.0.0/docs/_templates/.keep000066400000000000000000000000001375545032400217430ustar00rootroot00000000000000django-celery-results-2.0.0/docs/changelog.rst000066400000000000000000000000321375545032400213470ustar00rootroot00000000000000.. include:: ../Changelog django-celery-results-2.0.0/docs/conf.py000066400000000000000000000016661375545032400202030ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import absolute_import, unicode_literals import os from sphinx_celery import conf globals().update(conf.build_config( 'django_celery_results', __file__, project='django_celery_results', # version_dev='2.0', # version_stable='1.4', canonical_url='http://django-celery-results.readthedocs.io', webdomain='', github_project='celery/django-celery-results', copyright='2009-2016', django_settings='proj.settings', include_intersphinx={'python', 'sphinx', 'django', 'celery'}, path_additions=[os.path.join(os.pardir, 't')], extra_extensions=['sphinx.ext.napoleon'], html_logo='images/logo.png', html_favicon='images/favicon.ico', html_prepend_sidebars=[], apicheck_ignore_modules=[ 'django_celery_results', 'django_celery_results.apps', 'django_celery_results.admin', r'django_celery_results.migrations.*', ], )) django-celery-results-2.0.0/docs/copyright.rst000066400000000000000000000017121375545032400214360ustar00rootroot00000000000000Copyright ========= *django-celery-results User Manual* by Ask Solem .. |copy| unicode:: U+000A9 .. COPYRIGHT SIGN Copyright |copy| 2016, Ask Solem All rights reserved. This material may be copied or distributed only subject to the terms and conditions set forth in the `Creative Commons Attribution-ShareAlike 4.0 International` `_ license. You may share and adapt the material, even for commercial purposes, but you must give the original author credit. If you alter, transform, or build upon this work, you may distribute the resulting work only under the same license or a license compatible to this one. .. note:: While the django-celery-results *documentation* is offered under the Creative Commons *Attribution-ShareAlike 4.0 International* license the django-celery-results *software* is offered under the `BSD License (3 Clause) `_ django-celery-results-2.0.0/docs/glossary.rst000066400000000000000000000001431375545032400212660ustar00rootroot00000000000000.. _glossary: Glossary ======== .. glossary:: :sorted: term Description of term django-celery-results-2.0.0/docs/images/000077500000000000000000000000001375545032400201405ustar00rootroot00000000000000django-celery-results-2.0.0/docs/images/favicon.ico000066400000000000000000000064441375545032400222710ustar00rootroot00000000000000‰PNG  IHDR szzôÿiCCPICC Profile8TÍOA-Ëbñ""6Æl1)4¼ƒ …FA£$z$lŒ ¿µí7`ãzûþþ¿¶Æ1d€ÀÄåœ!—ÏÔÉ:5¸5´wO›:âzÆi¦˜ âc l,2œµqÌâŒdú#>(¥â"ⶬÏ^ða;«5'ˆF¨" ¬iZÎ+*ñ¥û÷¶’ZqãÂÞ¤›Ñ Ž'±ËÓþQÃX‡Ð\qäâÄQÅLŽ8öq-›ºjó·§Êƒ‡ó_ym1ä)° ÉÔv Ú<«øVü,nˆÅgâ§?Ô¨ðK"¾øÌ³_ö9ØÝ ÷g±8 Œ¢Wi+¶Ú4˜E¾¾ç,¬ÕÜq®“KrÝ\Ü%î2×ËÅpvr|œñøÓ|ßÉ0ìî„?…¾üÆ÷œ€ìÛ«ÿ‰E|i‡¬Uó)kuÕòø“̘ìRõ•õYªЦÐ%Šç…>KDHjrG› ©ª`¹ ƒÐ*Éu{óì븕±Þ²@˪g3¯\ü‚ÿðšg›¬¼0Žœõla¼S‡,“+´êÜï@à5€‘?ÓeÏš¢øæ¼¯Õ¶ð~4<ع_«}R«í 1 LÂ'YfIDATX ½WKl\WþîcžöØãÔNÇm“ª„&TIP¨J „¡ªRP»B,Úª v,€R%V,*+VTB ‚„x…g- Ð¥UÄNb9~df)à¤@dÀÊ® b]S,$Cá À6Å?I·Ç×>°(‡†IJ€>2wa½‰,c¦“¼¸0hO!è$1U,v‡CƪÉ)×@@Ý…§àâG¦t…œX">—¬ÓËÕ`uµù¤âX´‘ŽoõãXM?Gpóžõ,£ÒyŠþp`ãÀæÝM4· Ô‚êõzZu.7Õc&°À–XI—Ó@žg$áɈ¤séžKj.J¼g mÊx§Ö¥<(ªÛýþ>˜®Jf[Ê,ãT©@Ã5½-ç²jÐÀƒáób‹SoöÓbå—\zI`ÉY̽ü"çURÒÐ1ÉËâ§!+^è‰ÔZfEŠ­ì¬¼$†W€†mÃQß‹´BR1iu§,áeãe[˜Öì•Òó8<½óFü]F½¬Þ¸Œ—c&íôŠk¯àó\}&»àd¬Tð¤DTá²îÁg²˜zÒK ¬¸jb2a'Éd¬~¼”È’4d6˘¾>|’YvÔ‡ÉG{º,'ö ÖôØ'Àþ¾G:®!¥•|İX–œ»fLòó’ñ ¡Ó¶÷€ž´¯ï“˜¨¢YQ†á‹W€u P»<4U)¯÷ÚKç2z¥÷" lçÝ0[ WÁ“Sû¾¶™c$ÊFáøP  Áz!o´F”ô^@RA̵hÙÊÂÔ24StX©ï²hUlpKFš¦~;¶A,Ä„1›ÖûÀ,P#ÉŸ½–Á”K©t? ›¸³Ñ§a-²å[«aX‹Q(&÷…tâêßîbîha‹¡ÒÂ#'xù¼`7?Í®…@¦YeȈÞóãèzEI¦¦š¸ÍMefv7Ö®áµ_uñðc‡9Qs¸èõ¶Ö¬^¹ƒëooá­K·ðÄ3gqæ£Óèõ7 D›TÔä7”Þ1#`‰!ïõe|u&T"*fVNÎãµ?ÿ33!ºÝ9üöâë¸ôÊ<¦; ìRln 8MtçZ˜áuêñ^þñXzðƒˆÚ’DÇ4I/%™ÚŽÍQ³¢Xiˆ¥¿K.¦¶ïnãØ©{äþzyr=6ö—ÙªÆl¥û8ºRgFX¿½…¸èâ³O<ŽZ›ÒxUÜKF óA{?‹W€Íc£hâs'ãdÆKôFo~ú>:ÜÁßÿr;ë;”´ RÏ ¦yhY\8ŽÅ¥.Ï`öHem€udܘPɯK0ö#9X<“Gì­ mie8˜ÕC@×òwpâKxàÜYdCJ™ûw7¬¸Á³d› 즛Xëíðà’2Œ5BœÙïšjRìœJ¡N(¶¹ŒÖuíÅb«<ÐÆ¡ýýÚÍ+¨7´[6·yzç;Ç£U>à.Çl—ÌÚ²Cžì£¨aÏ•‘P?#´N0ÛYLÝ^¾Ö˜ãa4ŽÚúS¦ÕJwû'Äã™–ý(ª#M*ÎqƒYì ¢;?ྩ›<Õ>!PÕnÕLµ$NzXÓ³F²k}èsýßOχ﷿`”ݧ|#«[OÍ>‹…ÌïªêøfÀcÚ DH‹5ÃàNuéÕïuΫ#Y’Ô®Û=û™êÙ¨^Þ_•Aé·zýuÓÆlæÅ†]}Ù¯ךô;n%š‚±W(§>¬ŠðÊ? ¾ììðýx¨ì úßW ü?r›HIEND®B`‚django-celery-results-2.0.0/docs/images/logo.png000066400000000000000000000632121375545032400216120ustar00rootroot00000000000000‰PNG  IHDR€€Ã>aËiCCPICC Profilex…TßkÓPþÚe°á‹:g >h‘ndStCœ¶kWºÍZê6·!H›¦m\šÆ$í~°Ù‹o:Åwñ>ù Ùƒo{’ Æaø¬ˆ"Lö"³ž›4M'S¹÷»ßùî9'çä^ ùqZÓ/USOÅÂüÄäßò^C+ühM‹†J&G@Ó²yï³óÆltîoß«þcÕš• ð ¾”5Ä"áY i\ÔtàÖ‰ï15ÂÍLsX§ g8ocáŒ#–f45@š ÂÅB:K¸@8˜iàó ØÎä'&©’.‹<«ER/ådE² öðsƒò_°¨”é›­çmšNÑ|ŠÞ9}pŒæÕÁ?_½A¸pX6ã£5~BÍ$®&½çîti˜íeš—Y)%$¼bT®3liæ ‰šæÓíôP’°Ÿ4¿43YóãíP•ë1ÅõöKFôº½×Û‘“ã5>§)Ö@þ½÷õrŠåy’ðë´Õô[’:VÛÛäͦ#ÃÄwQ?HB‚Žd(à‘B ašcĪøL"J¤ÒitTy²8Ö;(“–íGxÉ_¸^õ[²¸öàûžÝ%׎¼…Å·£ØQíµéº²šua¥£ná7¹å›m« QþŠå±H^eÊO‚Q×u6æS—üu Ï2”î%vX º¬ð^ø*l O…—¿ÔÈÎÞ­Ë€q,>«žSÍÆì%ÒLÒëd¸¿ŠõBÆù1CZ¾$MœŠ9òÚP 'w‚ëæâ\/מ»Ì]áú¹­.r#ŽÂõE|!ð¾3¾>_·oˆa§Û¾Ódë£1Zë»Ó‘º¢±z”Û'ö=Žª²±¾±~V+´¢cjJ³tO%mN—ó“ï„ |ˆ®-‰«bWO+ o™ ^— I¯HÙ.°;í¶SÖ]æi_s9ó*péýÃë.7U^ÀÑs. 3uä °|^,ëÛ<ž·€‘;Ûc­=maº‹>V«Ût.[»«ÕŸÏªÕÝçä x£ü©# Ö¡_2 pHYs  šœ IDATxí½ néYß÷öÞ}»û®sçΦÑ,ÍH#ÍhA ÂaYl±(»›*ÇqÀ$¸’ؘpˆSÁ’”q‚1•ì  %V!$!‰Ñ®Ñhf_ï¾ô½½wç÷û?çí¾3÷~W,&U.ôvß9ç]žýyÞå¼ç|cÛÛÛíËé/¯Æÿò²þeΕÀ— à/¹Lþyù#ýya|¹ýŸ]tá®>üOmQøµ±v_Ûþå¶õç%àÏÎú—[ûö±ñvgÛn?ÖTÇŸÊ Æþ4õQþ8õ·.=y\9 \,”ÿÿÎÕ÷æÅè.§£‹Ë_|þ'2€x=-ÅÆù §_Éçk^ÿ7Ýuý¡›ONNc_Âð³yõìPcÛnC/†Cþ8y6ì#–‹Ïw`¼ðĪ—ÇUõ¾Tù ¡ý)®.B:>>Ö¶v®/æñòðÆ¢Â^'Ðã ’rÚnøb[ß\Ý~æøÇîýõ?þ,™àóT´z±¾È»bú’p±Eqþî7ó?ümïü«÷¼þõw¶ƒGæÚÜÌž611Ù¶· Jbá³³Ú;º¥\޽À\ºêw)¥°Â*m ÉÀX]•2òØ.a 5…¾ bÃÅy»ÅÛ”WÉåË/®Y罞\õ4äy0òB”t#C?ùýº·èÇ‚Rß‘‡íšn¨°œo)ßßLó@´º¶ÒN]n÷~âóíWý7>öá_½ÿç{,¿Xw^_.]Ñ:€±«ÇÚ¹öÏ~ê§ÿþ÷ã7½©Í¬´Ó«G7Ï­œjë[k0,Ç¥V‰4iý¦0ÀéäD ‡Š)êÙN»)o)&µîÝbÄ¢¢K¾2¸jOŒ¶ÞÆ×ÍïÂJÉP ¹IE›¦ó2­@Úë'ú©<ÅKšR«e·¥ÒdnZÐ^#ÂËÅï9H;§©³¹µcðÊ|Ë4qnPЏZk›EŸ¢“6ë6xߤž¸Lõíu¡ivr¶-Ìîoûg¯žØ^n¿ñkoÿåþ3?׿Ûº}t{©ëЗK# €†–mGùGÚ/ýß?óOßõò»o=zâ ÛÇN£ßWIãmzj*a®+\Kš´¬~ñQò»âeÒ²‰‰l¯§Æñx¯ÔÔÄ­¶Û&Âñ:õw<ÅNRŽý WE(9Û+á(ˆ6:9ʰã–iBJ¶U`F…­áX®2Cÿ8øÈ¦ù¶—g†ÄUÖmŠf N:̳MHç xµ9ŽclmrBÚ¤Îç¡-ÕáùX_þ…¡œ= Zøò³±¹Æq«]µïðæ-‡î{ð³ÇÆÿÃü/ÞÛžoß5At$/úúÒ°ìçßó+?ù}7ßµoýS|ŒX?56=9=¹„òe$A*I&&øŠH––¯ì;#“(ØTÖN›´-oµÞÊT!ŒëiêwG‘ÂTX~YøuŸ5Y.=Ö©º¥íO¨¥Pê´i;NËJè¥PK¥¢h*(¶'.XF3Îqck£`Sg ØQÞ +k{ì#FÂ%ø„ïµü‚)4Wy–›Ýi4kŸ«Âøúöë^öÆÇ>{vêÝßöÿÕöéí¿Œ‘Pî'Ä‹õ' Ôß·ÿøOþíï»ñ• [Ÿxø£“[ÓQ¾Œl1øTh›››ùÈz„¯@cùÑ=_‚U •J…p#íëÆâ=ip¨[a[a€|00z8Õ3»b‚Ÿ/ì¹!UÏTÉ Vü^ó±Üsõ,,ák Ö“/ËýÔ¹‹†ÐŸüj³±¾Yxœ–‡`«üÀ5¢)³D1C’ÆD®Åm‰Ê7?´õê¡…J¶¯ˆ â‘¿ôó7ÅXl|{vìÞ?6yÃ+ölýãÿÞï£î·Cƒº,O äݯK €ŠZË&‡¹·¼ëU?òÖoxYûâÓŸÛÛšÓ;dN‹‹ ‚RQ–9Xƒê”P¥\¦¼¶ÜóëV 2I§®¾±­Ç¿ò„-ƒ‚AÁÞPÌê.z¤HªÚ†¦m¤ NO“½õöj¡Idâ±DÙg@K^7•þhù€_ *VØÒçÑD••g–‡~ÚÎuÛ‡*ÚF°½eð €$MÊe}c3Ç.# Ö$+£ÃÚÞœ{àéÏoÕ;^ÞÞò®;D]:-4i]_Šõ…éÝ;“­·}Ý×ßý†•±³íÌÙ%Àïö{i0^vR¸Q2ÈEU­×~&âU€âîŒ×Ѿ;^‚8dSôLÔÎöŸy0Ûáp]øË#…j¹¸loŠr…A{£åÊ·çùPW¦‘ôIu´oÆÛÄÝÒ$­5Ö’áŸ8=Ú´GAi×.4B½u‚.OÞL‘€$ò1G:ÅÑiT¥\¹èx‘¯uÁ8ʤ¡s£‡ÎpêôÒÄòö™öö¿òš7PýmÂj»ºÍ¥_—À»{ÑÁöu·Ü½·:w|s¼M €1*„вäª^ÌIˆ„mè õð*S~Ê y 3‚T0´ÞÀÈxÆ(ÃöÔÛ¤«Q¶0q£,é!wÈ«ò]XÂ4¯”îÀ®ãWÞÖ“V°ø­« 1Pv!k ´` ¸4ª?\#©WòPy[œË?€ø.•§Ñ ;òàhD'i•Fò"? 'õî¡óoÞmF3¤€“ Ú3(#ÆÐÆ&Û©ó'6oE‡ ]ZsG·^ é’¥à_fyË{Ã;nºkva¼]X]¯ª©Å) 1z0™'Q5WWƒ•’_caÛè5^PÁÖïE™“á3 †Y2²I_ªÒ‘—É‚ÏêÔÃÞÀIK§™UJ裱ÂÈ9¨*:ÁY•ípQp€˜2Ë«vûÛâWΔ”òоiJ½®+6üÛ:*µø‚=‚¼ð¯£Øž5¥Äï5z¼)0dк†¯¡¾p’§HÒVÞv®WVÖÚâât{Ã=7ÝQÐj’om'Y)P[›Û·ï~-}uŒ #y?žWx…A˜°OÛ G@Ú¤¿êIf"2œ® Þ0èQåÄÚe( –7Z^I±¨\=Kc€T™%[…¤ý0…Ò8,B´m{Ãs®Â‰j÷t Ï®1Qźé7¡C+"ȹ¼)l*úµÆ ¡U¢Nð—Ñ„–Þe/‹ŠœFùŠ@ &'šŒœ†~N4}ĭׇډ×ùÊZ ˜"CÎ9å'Ùœ´‹õõrÞýû÷í'sNŽJÇB©tIò§‘×Ì6ž·±¾Ê‹!”Á\7ÄQ&T’É)ºŠð:ŠºÓº& •1 Œ‚0'oi“**©$®r,1»„Yp7Q´I 3žé±Ó5´±­ÉÚãóeEY­ýF¹WΠ øN^€W´‘øÍ3íâ—> ùe^…5˜åQˆ°!1Oá Ôxâ'/¼°è$ j•o=£¨†R<’ç¶Q®ÝÔSŒæk7Þ*Àv\¾ŸæsÏ Ò( ¹ÆÂ·H€ ‚Kù„o v’"RªK”i(Çñ–È@å8±nÂðÀ(H†£åL•87œŠÁS·ÉàWÅØÞ¤P*yîG¥ÑÂHž–Y‘ëUÁ:–QàB¯n†ØCdmm¤ÛQíåQ Ê¢0:LÑ~Uªy†ö²ËPEQÖfE$œA”À·QTäþYÏÅ¡r:j_òW¸Q–`}ót’š:‚0p3çb#" !û¹$4ÄjÅNàĂ㲟Dدõ.ƒ¬VF! ²¥'I!q/‘z*·`gOÆøóÝGi«©©›´p§eou>â—FOOS¦¢…ONà9WƒP¬9Þ¦R¿ ¦F)ôÞɳ\øl y¬jz.¼ô"îºÆœÖ)“·Îh`”áfÝ#áP‚Ø.R纎%o)Nhà‚i^8·Ë r·ì86Á¯ö›²ÊXAÂp£°d¤ .M£ @TÀ1†@ÈÀ\1¡‚‹á g1BÀ£ã} ‘›´Qá–k©”°§¸o¡4ö˜%š± cn~¶¼j±MO·= ³¹ž™¦ÜðÜX„Y£íf[ÛXåö+K­tá^ƒöœp½I·aíý5GnÅu‰>üF(ï½ÿÿŒµÒ&hà©xê¤h2øG‚ÒaB²à*µ”®nìJ0NŒ]akÔq7CÅÒHÐóã1k€WFAsN•˜rÂ0R)«çÚéˆ(pñ•*Á”žfL›(Þö&aœ† ÎâÓ¶Æ”ÆÇ1š…ùE®fÚÑgε/|æÁöÙ?z¢=ð…§Ú}÷=ÔÎ<»XÚ¯—½úºväšýíÆ[·ëo¸ªÝpÓÕíšë÷µÃ×ìk{÷-´¹=Åv[ÛÄa3@FÐŒ´cö´„c• ïÁò)£ˆí˜c+¯v)/¥š©#VŒŽzΛjëÈ¿©¤ÀHÎdLJ¬¯,*?q*T£G§Fëñ§Õ ¿F<ÅŠ%Ð.dp­yy± Î ¤cxd)V†!|»}º’Ö¬ÇIM}À¦6m`j †»òËú1$ë(™íɨa~ž3Ûþ±ö›¿ú©öoÿõïµ¥£¥ð=×·¶wq¾:‚qk’1Ëæºc—bZÒ›’€»¾¶•éÙ&½²²ÞέoOÝ÷Lûðo u†Ã‘—îo¯~Í-í寸¾ÝöÊkÛKn9ÔŽ\·¿Íƒk|RïÛhË+Ëm}›‘ø€‘GÑ(åÍð ÿ*Ö1ƒJI>"‘ù®r€hŠÜ7éâ” /Q¸ò6z8÷˜ƒ]4Ĩ[ø(£M˜W0‘yØ(bœ¡[„ãP.«1Ž¢· ïˆS":CⵋÃi„*7fm1ˆqG˜ (êƒÂÕOd·oï><þ|{Ï¿ú@û—?]Zºá•‹íú›ö…EØ4¢”õu”1C»Y`jÍü#¦Ð¹º¶Ù¦èƦQ”ÓZêÏϵÅñŶ¸²Ý¦o¦&ù~ÖYQ¹¿ý‘O¶ßþµOvêÛk¾òæv×ëniwÅÍíÖWÁ öb‹C,//·6jxÃLçØäþƘ3Zo5”ƒìî,oK©Ï$$ÖœŒ±”ë0Ö‚ PlxèÑ6åø%!`Ñ lsUðS𢯑 ²%¨Fü¥D O$P¦(u .È x­®'­¹ææŒðÜ8°Äi<½ ]í3*DùäÇr1x½WêYA¶í[ÜÛ>þÁGÚõ¿ÐN>u¡ÝúÚ«ðpM‚cl½Mчs3 bð|„=—MM×®Ë*‚î8ž?37ÞÐQ Ü‘îËò2>µ…q°¶ŠÒ …áEŸ™i·`å–šU,Ÿ_oO<ûhûôÿòh禽ý]wµ{¾êŽöª×ÝØn¼õP;¸w±­­­¶•Õ mcl Óä®!±•‡rh©Œ»rÊkaÛ––ê󭳫 #Šeʲdn¹™|8-cenæÒHHèE9Ø„‰#@µPJ$"F öNÈ¡ݦ?‡:·žm†hírx'ëäöm긓jší‡“ôóû´÷ý_Ô~ôïýR;ò²=íŽ{®Æ{ë®áì•ÌfFE4šÁ›7·ð~ Á Û™™²¹Ádr–î„2…6K”Èš;¸6ô&Y‡ ’8\_TSwyi=d–ˆ1·÷P»ê0Æö»¿ýÙö»ïuk^koùúW´·}ëÛë‰×ßpˆè¹Õί°³¹ŒÒW#Ì!:K´Ä³U*§ÙyG4NÝ …Y“ N.ƒEÙé`¹,ëé˜ÑQšy-LÍæòi¤€*-üE9lQŸÖ©h±Ûß:ʹƒ Ç:FtŸ¾È£†•¾ÉÂSÙNÍÀíwù¼09>Õöí;ÐÞûo>ÙþñüR{Ùë·É -Í¡x›Î-¢xŒf¯µo™Gñ(î<^:˹1Ä¥[£ÍžY§…ãméíg§ ™u ò'Pè,i·ïžÃ(&g kÇN'¨Kë4ekkЋa9vÐKÓ^~÷ÁD«åóíÞÏßÏ8âþÈïÛ¾÷Íí¯¼ë5íŽWÞÐö´å‰³íÂ…%â;ÓTp% €‡hA¨NR‰Lbsü¨ÐK~œ+Û"×£r/ÁK—©@F¤‘ b³¬Jà Âá¤BIÌ•ÐêóÇÈ–£L‚Á–Ý…{ÿTÐ Ÿ¤N1a$©UÅb,–jø¥þ8÷³f@‘ì÷÷µýæƒ(ÿÿl·¿ñ0›‘6ÚÞmG²Å;,2ܯÓG.ÌMeöaèžió À€®5®÷,L·)à_X^o ÎÞý5m\g,°¸ošñƒ@Êf1Šù Á?¦{æ§Ãë<vÏÆÞ7‚âFÙê2Qe­f(+«(ÍiòžívÓKµÍkÙ ]¿ò ˜Ï›¾îŽö×þúÚÝoº©í½ê`[:{2÷YTŸÞiÐcÀP|ñdÊ”—³ %æB˜ÇŒò=碶ˆü¡Á<õ¢Ž´d/ÜQi¤Äkâ<:qH¹eަh¤¯<ÙЙ%I*¸še%Û$pÒGº£18È´ÜOºFÛÁÈQcqÏþöèOµü®Ÿm7ÝͽŒ©Í6M7<=;Ö¦0ÆÙyz~pOWAG±Gj‡ (Íð~þìFÛ¿Ž5B´«e¸×Âj²XuöÜJÛ»w&Ôuøs0¶¸·VO³vp€þ~·^[Õ÷ÛÂ>”ok(tŽîcr ±ÉÝÌÌœæHD€ð)ò]‹ˆ¤ˆRÛÌo{ã!46Þ>ùù/¶þÎÛËﺮ½ó;_×¾æ]·Óî‡>G\ïÏ=CšÌ eÌYŒY+¯·®]…Ëé¶Ä`)S¦®ÕH³Î¥nÔ¾ú•ʤG•$¡žòŒü!HË”á̃]ˆ§"€Gë$3aJ‹õRB¨³¡…Ó/1cH Nvìj¦ò|Џ»¾:ÞþÅO¿¿5t?¿wªMãY³{ì!ŠL êÎÍ£ŽNçfŒP(û PÙzòÂÓë Î00:¬^÷àÍD¬eËh3‹Rí2ÒrqþÂáž±Ÿu Áh2O»Ò ÊÞÌ<èYÐ(çézf|rÛEãœYÀH‰ÖÙ_m/½ñ`{ům~ö™öài‹ÓWÇ!d ‹IÊ6òF˜ Ê^ˆ†£ð”+ˆØÄu¨RtÙPäêBú †0 ´WC—O# …Y§q†èMõTÛíLÜ„™%ìÏãi _AÞ'é_ l;­S¡z±¾Eúv¢K‚Äýûæ»ÂŒbq~w²?vjÇX5\ :ø\ÃL;xpŒ5¢ÄTàa–¹¯ LïœGñ®½‹ˆèøaƒ†fTÒTfŒp/]`„œžÁηö­ýëÛòæqè½€«· 0TJ–4 d¨#9äæ˜IdzŠòç蟲äcªÐ_å¶·F©p™/sd à‚¤ôб>³\òfùÔ* sqGój½¬N¢´Né1Ĺέ‚b”Éõ6žãúþÔä$ kþÀ}­aj9Þ•Zc$¼¸8ËÖôŽ'#|Ë ùz²¡}ß¾9H˜nüñ#¹QsŽiøs§”1¦8|h_{ò±§ÛçŸmíðÐC´§Ž·v뵇+ˆR§g«pàð¢ 2h¼ß»o&Š_ç Ùè0;;!X@bMÀzëÐ{~ÉÉT[Ø;×òÉöÍïúºvÛ×¶'O}4Ó½ÍuV Q°’Q]™ëcà.ï:m5);˺’7C©tó\*æ<]pnŒ…nysÌ(aÄN'—¤‘`¿++ÍÌnÀÕ6—1˪5ÈÇu¥‚¼æQb‚‹%Ôˆ6ß !—åe lÂåäÔl;{b½ÝûñûÛ­7ímô¥20M˜Ãs×ðH]5b//ß‹âõB#€iïœ`oÀ“O=ßžfDÿö;ßÖ¾òµ_ßn{Ù­íúë¯ÃøœŠB‚F€'OŸj'Oœj§—N´'Ÿx¦=ôǶçN=Üž9û™öáŸgD×Ú›_Š1N!âLµC‡fXÓÎ^8Q÷†PFò…DP´ë.+é2éX\DÎ]ž:,$YMU”žm+Œ¡´Wê¾ø0ÒâÑâ €‚¯[vQ–¹JÃ`œ&Ñ0æ‚lر3¨Ê†Dºia‚1Áø´dÈðx{ö©“­Ýh£S>úP•¶Bߺ€!ú–ί&ìk L;&X[kŸøÂj{ÿÿöSíÐísOü*pVñ4=Ü.œti¯’Þ§±kçbÐçVžnÏŸñ”µ…q6‰Ìl 3W·—Ý}M{Õë¿¶}Ë·~Cûá?Üf|ñû¿ÿ±öÁ{ßß<ýh[`qx~;rä t­¶³gWÚ…s·¯zç«ÛkßðòöÜÉOEvÛðO…ÿ®§’‡ßÞvW6Ub Í‘/kÄëFìX¡W {Cõ¡²¥þ›2y™4ÒÒ—ÄQ@ûö/¡Ç“†@vnªg ¶ÉÐÎZK,Ó6*ŠáC?Z§7y%Vè éLÏœ>ßYñseájßÁC³mœ¥^GùÞ¸™ãÜØwni…Õ¾Ù ÀÖV'Ûo~úñö?ÿ'?ÚnáñýOý¿ Aóøú»y"iº™íÁ¤ôcŒàĹdÖÛ{P¾£pæ ü–ÚÉ3À¡}Œ®gæPÛ¿pC{í›^ÚÞô–»Ú|ê{Úç?÷PûÐÇþ€Èð‹í7~ç‘vÓKì&îhûü‰öÏ~诵‰=KíôÉc𩱩xy—„¢A…©Ð_ ÞM‘ؤáê”Êή#¢·mºPÅ[œ E9´Ë|4És©Q!­#o°07kTZ(€@‰cī³óVÙAHT*òAT–…¥Š¥RÇ©#²©¤l IDAT¸¦Àr.pŽ=դ硆)nÞô)—Û¼ö1ºžÄð\pLJ¼ÛçȆ{û¯=|u{Û×¾¶=sæ3‚…¿µÅ=_x©mmråTtˆRm–+<·P—wÉ–:k¦Œ¶ÇVÚ™u—[z¼=A×83u XĸówÏ[ÿNûžçþFûä§>ÝÞ÷›ïmÿög«í½…»ƒoe{þôÕ\ œ@¬/•ç˜ÈgÕ©Ø¥)až iŠCJçT[Í|H«gt¾¡ý V}2ëáÄ—¦‘Ë”#B)3+Ài’)1nάùl”®SßsÍ#_Cº§NÕ²˜$, '™úUM'™³må¼Cß,ì8¬XY]CñôÏ´[Y©2|â8Gˆu½||¡=øèãíwÿͶçÀF{òäsÀ–JŒ YwŒ9UÏy¤o 9rÀ•:\ɾo?19°b¸Æ5D o1[#ïéãGÛÓ'>ÓöLl‡omoÿ†»x&ïMío~ûw·cÇŸƒ™3íÄsÏc” rP‰ÀqàgŠl!Å$Q%Îe·ëS îpä¥úÈú€¼Jl‡I×u3%—ÿi%Œ> #p‘d„©‚ù(‰m—(=•b(O³„òìë ô›¼v½Câ­“q€aP¶vµxiwiÕ>†€á^/÷z£kip•Îü=ÜÜqAfùÖs T©ê¹ÛH¹I›åýhH:Iº¡Åå# @ŠU´¢ôÛ‹R‘Xs¼kðþx=rÕ0$À›:ÊP8.äÈ”¤g¸eé86î ͺâÑC˜.(vUºÙ(ß›9†Êy*Ïà@p†U¶g¿ÍLs“gûBúAX‡á–â pm?„ÿb/«q"WÆ¥<)d¯¥=¼µ¨Sçt_œ;&²RîÞqô$…|XÌ×sŒž=õ±ÆSpƒškí]˜Âã¿ ªµ¢-dÚ¦Ëßz‰æC£í«ÛÚBÆ©AÔÑÙÞt…4ÒD(0#³;{âÑ UE’ôf0 ‹CˆLèM.… +R!OÚ”ÿ‡2q³Ä1†å«<”:ÃMv¼ëÊÚæþYZZÍ€ ëܱ 3ÕKˆ7dw:"t ÒhdPY;Ê.e”q'ÊaŒâNè‡Ç®0)Î 'BC¡ K1ŒS`•#œm»\;àJÅ©œÂ}œWÔÀlS Lm+ò©E1’‹ÃuÕdàåvw&£™ô)o#«ù‰´)½ü—ŽNb2q4¤8©t)ÐèdAÈQá–À”Ö±±fÀxsÈ~là)L ¾>,ƒ,.9Ÿ'ßÅ냜lïK­ªL¶­ØB,Y £úw”š‡\«~Ú¡vpQq×H€Êµû+„ìô]•J# ÀËÊ*;a6–Ùt1(ÈÒ¹¹;«zƒ°3JB*Åc}Бþ,\J†1ÀÁ£Äm`(SñŒ´„ªtŸát#†+Žd8÷ùYùó|9;yØš…òõPgn9 }rŸ.G!+wéRÄds´½xl—Ä…SÝÜpÇLw!°êõ²2páQe'hÐ äuÌŒ6JqÖë3 ìâ¤GÚ̃œ%-4.ùЊ¬l¼ Õu€”|#—|QεƒÐŒAJ1*gQ[Û+íèæR;ÏbÚ¨4Òέ¬¶§>ÑV×­Â(Fá3Ó³Ù-³É`Ðî 0‰˜k£‹=²(°f<‡àñ†Þ Ðh’'€€žºÀÈÑ1út߾޹Ä}wïù»ðã~ag:˜óË«™û; 0” ‡Ð0…j¨*ývÍë|Ìsuœ'’V¦ÙóÈbÙ¨4ÒìW¼ùQ„ œž1&X[7ìâ-gžÏÒhêº.ÎcÕ“,*4oÊó ˜/ùé2¡È$½#x„¯ Êýz>‹à\_ïZåÚY€ëv nÔt·ãƒeV óÞàk\¥­¦QGè~|P°˜ø7Êy¢à»ç…_ðÉ Œri6÷ˆf]yTù]qn’­÷!ô—‰,´µMiÅ7€³ Áá™Jk®s¿Y‘@”A9…9Æ0ƾ´5ãù HÉ\Y«û fºÁrŠ7\OO³gá,`³É*¡u:sûw{X T8«ÌìÄá,ÀAnóöÚ*9&Š ôÎôrÊû8ã?!Ç\<¯¿’E2^f|@•Ý-kr%ÿixu Ÿd)sËÊ Ù£ 爇VÈ7D+ÃR°]‚ 1Ú€û•‘øÍÊÞIà/Î"Ä Õš\ªðPǹΧšgû©è¤"k^æk¤(¤êCK`ÕB3_Õ¥VÖýµnßó+QRì!–Pˆñ¼n%K Ä*—h i.謲9ó\ÞıÍ7>#°%nõîç6°ÓÁìú·† pΜ¥+`ÿþœ[°±³ók'=VyS!Á¦‚QðF"9C2Ob«Þnu»Û8Ýíä§+RžWœ!™º‡ [¯­é2\£B)DåjŒ¥4 9W§ÊÒ¨ãgÛÆð0zw‰^ú4®H[røguQFZ»£‹FAå$õ'_b®`½Qì‚‹€¢hÝ?Ȭc¿4ô©°-‹¥#Œ~ÑPeQgh'bÀ°á’îÆz®._`ÞÏÞ{ŸÆÉ½žÓ·ßGNlYÏÀ<¼·ÙM°%‡#y% Oàæí[l»&ˆs‹Å#£²¿ø¢x°¦û’p7ÐA~?/§QM™µØØ:*­”SÊ­ä@Äy싟{X+ ˆ •—§ˆog=ðºWXž™H=½Ti¤Õ“ã”ù¯ò8%+u#\2廸ç$)‘O§ 5U¥8x‡såÜS …‹ žû1òfÕŠü9…Üà57yfxžïÎW..û5Ú€P4YEp¨KdÆT’áÆ ÑEØäG¹ä]ÌŒM½N:ôS ËnC¸ÁçC·ÂÖs³ÇÇnüèÞ\ek¸#}o[#€k 3¬±HÈ¢IÑFahªÁ_y‡Jµ_’4 hay¥õK¡½Ë†å’¨œä^<ÖÑ’‰axGø*7áYÑüÂYÓ¹^/†X]Â*££eá¼øDxävÙ*këçcEÙÊ«1õ‹æ:»ü÷H(à,`6sP§2ƒ ÔàÀd¦# ké2hŠr¦Õ[?ŒDkCItà¤"LFW÷ì»XÊf ˜÷ö\`½[Ã}VïÂRM= ZÇ]ÁÜ8RE ±E˜Àó¨§K¯´¿4H„Bå£q¤Žç/à­èô±páØVö¬34,j`çÜ|×㬜¡¡pÛ6¼p„rå¼Öº RL35¬îœÔXh R¢³!ù_ÅVøåÓH°¡cˆî5e¹¥|1<íHk‰y„ó^[Ï’°ÿuAÈG¿\W…ý(bÉ9À_‚Êk*§ Jz¥@$M!*ti·ÌÇÉ-èü«(ùÉ8 c ®0c‘Ô9/ÞÍî%ñXêÄ™8ZS üÁ@¬_‘² GùÅQ .4À³mæ%è¤x´@:wñSå²i¤XÛæ!ó; ãÚó„Àqx‚*ª6]à]ÀÂÑ{ÊÓcñi¯àØg@#g”q>ïS5*C/÷¸ÂÚö4w}ÄûÿN U¼÷éø2'Å[=“ž8ŒÄ+;ôÉt‹Û‘3èwøëÆ!í–=—âŸlÊJNa»ñDNÀÚi+ íàPT”ýÂQr0åç°2Ɉ .ùñ#/GÏâ4rŸI¸Ò`]³Ì·÷#†*)½Ü×hpd‹E9ý J˜7‚D×ýa±[]Md™’DÛyæ1†T°Z‰w>]]‡¦duñ(»‚Qö2ï¼7å{Hz/Àª V|oëM¼®`1hã¯+Daúéø»À*"™?y K#•…˜ŸœáZƒ^ø'¿ 9‚¦®ŠíÞ jÿwG6´•gá/ùØÞ:ý˜”­4éåŽp4 ’Äv¡+ŒŸJU®ñø°¤»|Àšé Jw:¸ÊâÏä8ø³Ë0Zp:0lD‘ÖºÅZ¡I襪\ûû¢Iì -i ±+Ë<ù-a—Bé¢úÖIû0(-ÅpõÕàtÇ/–’øÁ¥l#7š kD©[8ŒA"ˆGz¤¼§:·~¡/×9¯KO½î £ `¨—9pÄ$0oJ@¥-©U„ÃVÍüU‰Q?J¦²uÈÀ †Ð¥õ‰å]XÒ*Áî@V¹zН±ž÷ü]ðõqޤ£ÆîbH»0 *Ý&áKlz|êhÐ ­m®Ã—žôq!>ï:F ]9€‘¾ê „,\¾9¥ÕriˆÌâ!FÈ`NËÄg½r¨ŽG@6V¹þ¥AüËr (y¦2—5€¶¨h®u”¿ðM6 Â\]ú5ÚÄ×KCØ h}@‰GÝU)\"•¨úì¨LzŠíõâÀ‰M‘—|„<Œòæ Gúà›c?‚ûÜ¢WÀÁ•KÀŽdO døà ;0ÉT¡žXyÕŸÕ,“4#BuKR©7âaWØ6OÑjX¾øzò”¢àÿ\TžxJ¹^+·l¬áb8äµL# þ‚-Í ~ò#7óD¶“Š6²SWax¡ìÔyáIWñ sûÕ[!í Ùÿ„!êBÏaY¯*'“ö^»cº5›_,fs¨ÆÄê iX®Úú8–»€U˜³_ò´ÄÊ ]/€òÜú¾Öc ÃA5 ÔK‹p¿³´<žÂ-¡ƒ*Q˸Düxõëë([~T~)¶”­—é¥Ðο‚Pàé6c&ݨ…6x¦uDl/Mˆ=OÈE©”M†ÙC™ã¯à'KºM1ÊͯäeÛiYp÷ïH Τ³Â¶gz¤Âµ]y×Ê©ø©V”%R}©„ºrz¿+­ÖOÆGpKe$Há ¹·n•©£Ã@u:<ÛéÁ­¸º¼…•x ½*Wü{Œ!HÍ/?‘Õ×\IÉHntÚQ k„]aM#p`(Q+£=Ô†‰ºÖJèT€`ÁãŽÐÈœ( #Ó0”í,@ÅK¼á]ؾÏBÖvGëns•Ч†ÜÚ(¹ÃR0Þ- ÓzÒëŠ èò‹' S:ýXÇ´³¯ß×Å I8åeeìvÙ¾I…ŠS8]Q"•Oo¦%¸ö\ÓðXã[ øØ#¡bêÇ2‚2Ló•s` x{è×lêQç_6‘®daÕR€&•¯`Ó¿ÉzqXUÉP½„Ui;‰¹8yU!Ú+%0ÇëìTñâYbÀ×ïøõ1€»…¢ B®?¸@ÔÜJÒP«¯-å{î K)ÌNA±Sa]:¥-†™zå­ÂS±6²¾°bÄÔ©Y„íë6l‡+ ay;‰œ$dÓ¦pH¤ ÁÇGãÎV.Úôz5®û 3ëÇO"jp”Ã({Ç'âwjµ")Q¶Ns}¹¯+@œÁš£óÁ3d"›A€&h£Šù±\ %¸¬X•õy Öº¶/(ÂÓ¼0ÌÀåó¾¶Ý!3<)ä”P£Ðpœ*Vt 0 èÝ“pœDÈ +]$ØAeS>€oê22/ Açg9*ä‡ô£0? ÊÀ±žp㜘enEÆêÅåP.]â‘FÑ\§Ó£åzø§Mo_r‹QR(.q«›Rþ€_ØWÞFÑ+¥+@Hë04x‰HM*¶÷o2£Õv$ܘ -“È> Ìší…“Oέ+`•…€Ig€†ðdb†1€ŒúÂHÿø½ Ìi#(h?âí²¾B)mÞ@ª®lð¬¾ÐϹBìÊ‚˜¤B2Ð vyÅ“õK‘À¶,3ù BôYƒu¹~d’mf‘‡FR‹uê¼ ÿàlò¡ð¬']ŸrRé‘¿0"¾ÒA/¯hg!Õ¯FŽ´ ŸÚÙôñ$’ÇY‰M(-AI_/wU‹•:>]ˆ!˜úZ¢©÷Á€"|­Ù_Ôª(Q€-™Yôñgä :Èu Ðߤ!ø^~_ £Á¬1=t%PaùÑû<êùy¸Et$VÊ.cÍ5Äȃ´jLxCÚœÐ:À,0…¼]¶êDå˧$TÄ·ÝY)MžËøEB¢b¯m‚‹ e—&Ú[n=ej—õí󻲩•pXOÃð—Ø"[ ;¼â P=_6‰Wd.$ Ê¿\ƒÌT°+D)fSªh›Üháakî ñtCR¡¼]C¯6¬:ôè˜ ïýgYxÅkL.9hš¦kh¬š^Ý\(胤„/’…Ž Åk¹Çj[†®àë–¯|•’¬ɨv%ŸàÊÒNþ5ÀoA.™)åSòÞ:T”¿u•SIKX7=i±©xÐjK×m´µ‚ÔŒJ#  70 HˆÖ«0$V$‚¤ Š°"F&9KêS/ì»M)·*ÀTlšFV‘ߣï,@C±+У4„©i|¾¶}¸à¬ÀQ¾Ñ¡VK¸Ò«áÙ>p¡:чlC³)¦,~è-£¥-4ô{ý 9F ŒN³íÒÈÿ@¿ãΰõ ~I ŒQH5#RK^NŸ5ÂN³ø*zt€…_‡‚fð™¬ùs-Ï5’‚I™V€òMY·°ÁÒh´XB,üu^ éMa6„”7)T‰/”uôÁ;:œÄ‹3¨c”–«ò2ˆ © ýò¡ ¥%!›“Dóàl‚WÜiÑòêöìœo.f|²Úüôó³ÜùF~"n‰é*¯l±^ÉEœ]FAƒ6ñˆWº"G.}Q¸­Ì—”ŽÕdÌ}é†ÒÖ²4N][J# @oÕ#LeédNä|PAÁH`Å2f GÒZãÚÙØH‘HC& ÌŸ¢Élƒ«þVÜé‹qùsqþÆŸ/ˆ2ˆíÂ9"{4WÖŒ×ÙßîýØCí¿ûL güZ^T‘ýÿJ ä#® Œêš"™-1$#–4É–ï/^]³ÿjçx>QqNE}kÙ44OA‹¿e°ÝN<»Ü¾øÉ'ÚÇ?ðh{àüä "üÉ_z'U~ jIç gƒÌ‚³#vdJý(’:޵2+Sš!¶„ü¨ZÃ…\«æC)×Ü]ÍUñÈÙåÒH0Ô¤ˆ­pgHVR¢W{T¨nÕª†u“g%Û);…©a˜×C¬°%6Æ`%~Â8¾nßùvüsüxÓaC¸„ñ[€üpä̰'PèÐÞ{þ>ð ‘oâñÏ=|º}âo_û-·2PtiEä;tÀ‰tûÞ¾Þ+9 R^òfpNT|uþþtûª \wkº¿U8Kˆg)‰Ÿ=¹Ö¾ðñgÛg?úLûÐû’Ä6ÃS½äöíÉNQv¢½ñ?¸Ž­¾ÞÅ›Cna6Ä9 ©ÈIùT”)¹vCž²¢fõŒ8åûŠ~yq•¥¬+9óð9K»ØQi¤ˆM¤*Ïé^æñäí„`Æãa †"zÌã?}µˆÐÉ—È^_b2"ß°6É/ŽKVû¯âgÞÏ «{®ïd?sýuúù9~‚ÅÕ=g*ÞB¼ o Ÿæ÷ n|Õ¾öóÿ݇ÚËî<Ü®»e;ÉoLM͵1~¨y‚ŸŸS€ÛLJÐéu¨¿ b—#>’6Zî \ŠöÌÔÖ*¶õü“Ú“?ßøôsü¾ñÃZZÒ5·,ð ¢.Z±lÍÂÞ0×~÷W¾Øî~Ëu¬dòŽ#žoç—Du_Ÿ›nGYÒZå—–wK ;k l;é ­”éxÛÈǃəòWù8HjËéåÓh áÆ6OÚxŸ¡tvÀC×,…{V£ÛPocâÜíÓ½­3Ø AsL@Í0ï áu1Çð ÛÑ>ŠJW°ˆÿî4å'ãQ–÷ œ 6ÙKÈú?3Ûþ÷Ÿú`ûŸx;°´¥3K,+ó䆦GÔÀJo«ðª7;• yp3Š¿ñgøu•ÑA³3'×Û1~ÏèÑŽ·ûïEñŸz6´øuäæ…¬E8¦ÉJF6ůˆj¬{çfÛcŸ?ÕúÔñv×ÛŽ°í΂!3@-‡/¼ ÒEI‡"æ«¢Au«FEäÝúæIwxH?$O#C_dL£ h“ºfˆq Q"Úf@Ã(—1(`‰rÞkh•h‡®¡’ï6&_%Â04Ë5Ü»üàá…»êHߟ]E¹:†³€,ùRêÔÐßv(KÇL Çã„Á½fÛÑçNµŸú¡_oßóC_Õn»ë0‘#V;ˆ5Z™ºQº®Ec*Ïåæeúê3'–Û±gηg=ÝyèùöùóÃCZ`ÆqÝmü¬÷Fä)º+Ç®Ð=‹îi€Í€ZÙwíTûý÷=ØnÿŠÃéÊ67ÝÐVŠò;ä’§ü*·ŠžvM“jôS©ºÐ«˜c‰†#I†9ÈÇNP§‘F€MJ黀3hGy¦X:G‰¬q€Š*‚ºp%ÁsS–Œíø³¾+q2á¹–>Îoœ¿°Ü®ºöêvíWÁ¡ÒÛÂ<À¯†ªìúÝ@¢}o]É`ПsÁÛ\ÎÊä>²·-^mÿý¿½æ-7%ü^wÓ¾¶pÀ'ŠˆiLXú{È­Ÿå÷O]Æpζçž<ÛžøâÙvòy^?ÞAéš[3­÷§sœþ" Êú$ËÓ¾‹ÇÇ×U€û“JNåoÿ¡yfÏÑ]<ß^yÏÕü õr Ljc¨ÖˆLÊ/B2=MÖðe„(Ù&z’k·ìX šçZ=ÔàQ}éõ|ò–Wœ×Ð0"4Ó+d€&|úûwu Vª°š!¨H4Až}¹Œh­…õt+ˆò/º%ŸöâÁ{&Æù1Æ¥óíÈSí5o¼µ½ï½kw¾öªšÐçºMÌ_èÝ2C#CÖ–ËȳIé‘á9 㥯ši÷ßÿXûô‡“Фù«|ëÈ^î n¢t~Ëõ2éª÷´n߇‚)D›zC”¬âO°ê¨r-´[ñgl'ˆ$ƒÑÎ1€m|Œ­¢ FpýTûÈoüq»ýµ×ÐmíA†,co^`ÔŽ<…K[DýJ¥.z¸8j‰3Τ’‘…2¯vÞùSÎGºˆ §éÃ2Î^œF€Þ™_Á¨€ÀñV.´Þ K”²ê °ŒãA¾Êi¾<7ϤÅʸ kZ¸Œ®n\hwÝó’ö¾ó1ê²ùƒ÷¸à´Ï{âTùÚÒÞ$ÂÀÐk×ô oÉ»?@¥—^};6q¸ðûør·xGn«ùÉ÷ µÐ¬M«ì6×ÒëžDúۿôò¨HžÁ£â¥Å©è´¿Zêkcå 8vYå ÝÏA~†ö¾=Ûù܉vëk÷1#X&º@T]’2—òsÀmÔ6ÊæuïJ(2V¹ é8|i [É3 H{å#×>:Áe’f<"©(E `ªÅy®@–¨Õ‰‹õR^a°!!±8²žÂQ€ýXá’—Nn´“§N¶[_Åoô½â†væÌ9|¾€¹>]?a?«° Q|Ê|“x•¥+€+…¯ÒƧ — p€6ÉšÍì/œ²ÛÀø­Èôßþ(“}ŠƒÈ|XXšà# û†á@O3Ý”w9šb%Ò² ŸF ßi¨Ò³‰•è0A™³Ê¿pT ?0Ö>üþπ˦råcTt@¬mÐC H£qÀé@Øè3M] oË>žœšê®Ò¢"w¢u=G<5>Áˆ®½v_û̇žn?p:}¹E ®•Mä¦Òà«<˜3p& §<±D=»S7„¸„ÑóbXÔQÒèÙ¨4Òl‡Ê¢|•à@)EJfYnB%„k†z¶ó¡‘04SÖ8j¹z¯ðzèµ¾FPïâYoÏ}¦½æ­7µ¯zǫډ3Ì ¹)µB¿o¿«<7T+`‡cGä–k$²k?\IoÅŒQ¸£õÞ©ríËÍ·ÔÊõV=Þ_GÕÃ8–×â“ÖU=X/†‘I^4<#U§EXŽ3”]" ÓM”Lïý¥ÅX©Ä0-WN;róBé"ÏrŒRlÉ*ÁÕeÚÑTWü/¿z Üè$h/û5ÒPdŽ$¹2!r.1¦ÐÚÁL¤çY¹¢B)YC¨6²¹ŒË !Õ°Üâí€ðìÙÓíÜêñö]ç«Ûɇ¨Ç Ô·„¹õË®§àù"H‹ïñ§i‹×´öãÿë÷¶?~ª­q£/ý1ôôÝB [® ê©*ÕP,J/…G2v@¡>Š­‘Äh˜ŒÊvé×È!õ…«‹®5¦}9Ædÿ.>œƒ$:0ByH?o€w*Íñ Û%~Rþs_äw8ÑÞt÷ëÚ?ùÙïo?òß SÁ#™5¹¡Ä¥Z•,Í&²ô<ÝF ¬üSæ–÷¾Ÿ+< å¨ìÌÒ ”¯…:b”dÁeR“—ѯ CºB¢ø8€Žhe™¨ªVÙ(!Í2àhH¹ŠsK™†.±:¥ aÛ¾×/\ÅãM<ñP{Å=¯iÿÙO¼»ýô?|O{ùƒ!¶ɮ`va"×Q 0U’ÉÝBþì¬ÞšnAø#T&™ì»gæ1 ¹<&pÔ`\~öþ‚I#¦G_E3Ϫ¤^ŸUIíVγ…·UŽ3ýâÃGÛÚ“,½bº}ç7¿½½é«_Ñ^zûÁ¶Â»}ö¾vôè1žrZ7ÅAû8ÜÙUªL²*2FP]E„òÒ±ÔO G¦TŠ€H‘u²èñ¤äÒ¯‘ ÊŒŽäUš‘ÃëÜ_ žKÎ.1«|½]¯wŠ©wåî#0½3)LËö:8&:ð«¥Û,b:ª=ò)„ÀBæ7}ó=íu÷¼œ™Ìõmñª©¶´zº=øôg™áœ z¬¦›#n h»×Ñ2QÊ#¤òå‰ *Û¼äÞi5М*~šºq*Cœ38ëwX\½84ÛT?nÈöÎ\)Ç|Ï»åõü!â|¼>©èÚ!²ú5!Jà„vjÄ à°Vôì>ð*¼dëÄñöÐØ}í­ïze»êÈßn?ùþv~j½½ê–#,±\ŒÀ]Ë·Oª}¾h5„9¢ƒÊÞ-«`;Ÿ.RÙâÛd—û 0qž‡NzYº @CÐxW¸§¯ÁŒOl´çž9ÖŽ=¢E<ý%Óí+ï¾§}Åß»½ÝöÊëÛÞC¼¼ŠG•Nž=Ö}èéŒi\Lsäûÿëuö¥T Dz~ård n“çV²¼®u>¼ˆTŽÈy‰´ê¦¾zK•+~4€´ P pÔ¾Bä0P‰7CD7«:BMD ¶Öêø/‡Y>a„| “IëÈ”×»Æ!S¶AX"Fp'Om÷qg펯xuûç¿øÃY$úåŸÿ=î·¶vÛÔÁ¬vyßÀ±„û õŽ”‘ËÇznî"2b7Ôk ÐAc¦”Ó5÷&o$—ÎuÏæèB4ÓÇùÝ!§ù}â6ÜxÛ;_Ù¾å›nl·Ýñ’vÃ-WµÅƒ(]?§Îžh_xüYÞdvšÈäϹðR x¨5ûjÇP%+e¬LÊ(2à<²å˜žŸb«¸žïíøDGºKi´­ípG¦jjHÔQ¶WJW0{Ž]«Ë#Èv@L5>®ÒUD–vÂUÝéãF -§¸ú5ÎM·ÔåÕ>ˆÑÈ8bPa:ë'N´Ï¬~¼ÝxÃ-í»à­í«ßñêö_ÿlû½~ ?Nð]}„»W‚žš6λ‚ÈTÏ-Ìß)ýËç ýeˆÝÁ,ã k癧϶1ˆÎû¬á3|Hw¼áêöê;ïh·¾ì†ö’[®n×Üp í;į–c8k›+íôÙçÛãã%Ög1ÀóÌÙõnyæã_¼¾<>Ò _zü˜TÒ gMUà·Kò:•rPΦ܇áÔö‚Ùq$N:®Ô¡Ü6»‘æ/øº‚T=QjµãÌcÝÌ pµÐD•.!µç½ˆ²\µ|tð¡ lÛ[??âÄ1‘C#¼@ðu˺ì©Ã ½}»uv«=øƒ¨ýϵ—Þpsû?ô¶ö®ï¼§=ôùgÚÃ>Ùþøá§ÚÇÿèa4r»¿K²»„sI>ÊÇ´Ï™œ%„ûLÛÊo»ýšöú»¯o×\sU;rÍ¡vÝKrci_[Ü?ßöpblJZVÚÙó§Û£Gmgðò~œb•_Ws›º‹cñt§c‰aų£Dø—?Îå=BÁLå¶§Ý KN#/«ä d¸À”ÕÈœ–u¹Gé”Ç`@¡ƒ9»Rm"à Cø^Jè¤ Ë•´Nú.ʬ§‚£L¼¸R)ÛüšŽ@\¬To(Ëõšâ|Œ]41Z¿úíþI[üöq\þôéSüžà>¶‚ÝÐ^óµ×¶7¼ýfFôx.¿#´Îö±cÏÉàð¯–?sr)ø:å›dßü>~ª†ûÞÿßw`1L¦ÙuT«ƒÐá­n¬0áNá…“íñ'Nµ¥ ü¢ Ï,¬Ó?d ‹²ö†R–G"^œ î]Z”à4 åçµenøÃ0v$ , ò­¾Ztü ñ¸OAEwõÀ‹WFa ñG}`ƒ»ŸWJ£ ú­ÐÀ“AêÞ/®¥öRn½x=G=½Bx‘ÈÁiˆ£vapÎQâ³+Xñ£OEÅáJ<¬ nlòÊ á†àbËž¹ù¶¸°—M¢ (sO»ùÚEÞ ^¿5œ1'ü‹ nck~¸¯€å¬1Æ8³¼ÔÖxíÌòò…ܬY#ßš×­À^æàƒr¤'¯h‘híô‹ƒKRÿÎû¼Ž“ì:‹ Óp™è'}Ô1U—hT *ìlÌÍu5Õ±Äç3–_ü)g+xz™¯Ñ@eiƒBNÓàÝe…T £ixRH¦#Ô¨~]B åeLj’ñC‚…Ø]Á™.`#‚F°‰ò01žÎaEÙÂy:íÌ„ár±ž“[·às³HŒ¸¼·Q8ô¬Rž†àºF<:yÑbŒYškô^Ï(¨8ÐQÛíp¥[Øs+–cn‘“[Š(^30&¯¸—¿Ÿx-nË5šJà¡ ^E×:eÒâ\ ¿¸hÏ 3KÆð9ˆ¹À]æ{´€XFHœ Œ6HØÚZƒ !x1V8ƺxÑ­Uk‚Ë× ÄZ+XseXjBGC³«8ɨÇҭ˵uT|ò€“¢Â§°2Šr¹0b'Ï,ÃzªÓ0ñ{?Ü­Z&ñVx4t~…(Únx–‘vðShUy O”©Ä ‡Âð·Z5Ì áÒ<(¢ÐÉ“¼)C{§]9©¿å ·ËO 2 ×è¼ ÕV2ùêŸê6h–qÙ‘Y9HÝ…LÃ}•«½(Ó¶0JWĶ%jôW»‰½,­#r‰W¸ý(B¯e ç ^#èý–BTP•/2E\p"2¼ÁR`0š(¢p„±Ð G•Sž%Q£ð—€l¢@¥Mz°íM9|hœ–gÏð! —<ù‰‚h”ææy2x£ž/Žt•…ÖŠ…ßÙîèÍ“ÖÐÂ×àLâ·œ³:æ;‹JnÚUûjçJ#mÓXŽ'êÑ«hã‹Ò.C&„HÉêúÊö¹´e4w80Wƒ ª›T3…Ó0Ò°+=Ó´9†6*KÅp͆z“-#,´ªRÄ•üQ %TT(Î׮ଫ@"<8ª3;<(3ô—·Y¥…Žj¯`Ã._e´Õ¦ðWγ]cÎmç+κâO@yŽE,Ïú=¸Ó™E§4ìº4„°…³"§çÀ!_X¾FOGïÜÔ¾µµmw¸®îBàjH/0l·s}f{™E­û×—±²q§B50±’Á"ÔmYšv­ÏsAT*OÊ5Y%ˆ¡ÄëÀ,¸zµ)u:Lká^«°\;Þ ~¯Ý!ã§’Þo›‚8=·}>”…î¡¶¥ó¢ÁR5i§½°üÚ=À·*Š;£ü¡±y¶·.àZŠžÊÓ°‹Q¯õReî°ã()ú…7Ðþ}Wf•_/ÒgêiÁ Oà›dzËn-Ÿn÷Sg¹ý7Ñmg7X/ÜWT^8¹õÑ¥ãÓwñº3ãÛ,¾l0ºÞf‹Ø|ð+m$­N"Í/"Š seå\`÷Ëô>Xfíö*­X WÀz¦L™‘Á'e‚É6Ž”•u0G¶ôq´~2Ó üz†uêÚ¶f¨ü‚2ä[eêM…¯ã¯Z*9‘N%>tK?)‚¾¸TjåU~®ƒŸü* @ùW©iÓqxQƒmùO¡^‘S”,Û—ì©<Ï5pgÊÏ»ŒbnZŸ~vû¶>hƒn r}_jïqŒÌªéÒ™\ýÌÁ‡ö^7wÛä$Ûئ?Æþ8‚©_\°a"ûëõB”Àˆ;a Õ8!Õ”i’üxjßßÇ >•Sžk©‚‘Ÿ°–6~•’#¢­fãâˆÒâÙœ;å+üÀ<²‚T¼Ý Šp€g¢ãç<±„B¤ð¥ÂÿÒkE‰ê·ÿÂË~ D|–E±f„$ù*Ü¡&xœ¥„¿¥Š¤EÇßÛs¬(Q‘VX ‹ÁWXw‹V @cÄ&&×éù'ÆÏ<3õЙ³Ï~ÄvmÐm·¯K ‚¶Ç¾gÁ®nOœ~zó÷_»ðcûo<ÆÍ˜½ yãÎS*’q’©ÐЇ:Wwñ%CA®A¶t„«25­±—T¢Ì”e;pá,O¡h(SˆÀÐ;…ͽ]"Gy—¢,/Q1–'ò× `e}J¥øxvB3ýµØFèÂTõÂ,ó„A øƒtâ׸‹nñ—Ò… ªxføž©º£ åÀ}´×ƒ·Ù¢fR‘ÂÏù@Ÿ°” ç<4+K’p„7»ÀÝË'®n§ŸÛüÅUt©NiW•R³¾ìï/º2¥êÝm|ï'Û͇ù¹›ß¼öÕSó§6—NíË$ÞõåŒâ‘ƒkéç"V5Ân-f˜Wž]Ó±è¹ÖsQȲâ³héFC³0¨§FКÁ CYïÐynØ¢ž¬ø·£LÊL Úˆ‘º MxÒAJˆ@®øŠaªñóç¿ÉëÂE%Îí†,ªA™y¹ OW¹õ½Î`üq$ñDñ ›*Li*Ç(Å:õ­BQ¦VxÉr5»Œ%`þà™Íõ¥|tú÷OœzþûϾ®=ª÷S¿{H—5ËÜøM7µésÓ³o=xhßÏÜøºõWL-œÝZ]šåf ®8ÄY¹£¢þÓÿ£lš¨$#%äRHñà¹a,d…kave1ÖÀ¸ÊPì=¨A_„Ó¦!š OÔ”LÊ $â‘fØÏ­¹„Ž åñ"*L€_CQ¨Ýˆ¤¹f%vY*Ó¤±ÐÒ7èM½>ËîÐ&Öç«*)ç(YÊ.Æ Æû"1Ô¡M"|ïøñÆñ”š˜ÞØš[Xn«çöŽ?õééûO?ñƒ‹kzì1ñøeÒ• @´·¿¥Í99ó¶=‹ûÿÑ5/ߺgñšó(€Û >ȃWã¼-*o†Ë\3H„GÒXø¢,â¤+!GÔUF×ìÍ·ßcŒÁ#:F-ÙN/XB…Û0|OÕÓ8"­(Caš,ÓüÆÇ|οÂváVશî|ÂOia7G—âS¼0%±¨$éÖõ¾«|€åMüÖ_Æ$(OÔy ˜wÈ»0í<Ú…WÝòÎgÙƒGåÇÇý‘DZå™rŽ=ùÂÏSHŸšf 8¹Ò­4îIDATïL}v¾=÷ÀøÇ.œ;÷ßn¼ðÁãÎ[”¹Ó,†KÓH°*Œ‹uâ–×·ù£'ÛÝ Ó¿÷ÐõSïÜ{ÝæÕóXŸññ+CSÔÁÛ¨„eør$0\«¢«–%Iº‚EÑ–¾Ä;x¾Ì+´ÞМ\êºMMÃÒãJ¸¥Â^áØ{C{*Öàpž;—HS˜c(Ç8sse~ï9¨pͨªøàªJ•þ2Ú¡%j ­˜ ÍÁ/½ÖS¶bÂÜT&繎,¤Ã.²Ž;Ê·~äUükÁÍÛU6ÖØszªyfâ艧×ÿŸ¥µ“¿põÁö™G>Ñ|øq¤òÅsEH%ð5mâð±Ã³mùصl yÃôä¯ZÜ7yçÜâøõìŠYÔà‹\‰«A‰ÀŒ~1”:\\­7+ÅXÒsªuA† ²ðbœ¨ü$6•ËAö97?×ÖÝ¡~À“æu.<7jHG™Ÿ”t<ÅWä[q庲z… â ²F^¼¸ùµpÐj y{cmìÜʹ­§/œÙ¸oeãÔ°ãíÚÜág>¶Ò>peå‹üK@*WcwÞÙ&—gÛÜù¥vÍ­G¸…~ZânÙ¬/ÈÐì2MNÕMÜÈ+%\¬‰*ºä»,мnWÖÅ º'Y¨/Z¿çõHÒ¯)™¬{I=ò:.Ëxt |x4ISY'U¾CïÀ§!õ:°ª~é·m^TçbYØ@¹vüVW®‹{&VX78ÁéŽónªççÚɹ•¶|ß}Ù E€èž „˧?‘ØÐMc_ó5mü±vÓäùcM.ΘX=j’èØË«Ö—¿ÿB%@°=3`ãÜÊ©ÍùÃ7mÜÔÛøÀbn åKàŸØ.ææ"c ¾^±.®þåó¿8 ôž!þ¤J¿˜œ?“\ ÀóÄ‹‹¿|ý$?‹Â_LÊ¿x1Ð/_ÿû# =þý!üË”þ»‘ÀÿýPMˆ(%IEND®B`‚django-celery-results-2.0.0/docs/includes/000077500000000000000000000000001375545032400205015ustar00rootroot00000000000000django-celery-results-2.0.0/docs/includes/installation.txt000066400000000000000000000020121375545032400237360ustar00rootroot00000000000000.. _installation: Installation ============ You can install django-celery-results either via the Python Package Index (PyPI) or from source. To install using `pip`,:: $ pip install -U django-celery-results .. _installing-from-source: Downloading and installing from source -------------------------------------- Download the latest version of django-celery-results from http://pypi.python.org/pypi/django-celery-results You can install it by doing the following,:: $ tar xvfz django-celery-results-0.0.0.tar.gz $ cd django-celery-results-0.0.0 $ python setup.py build # python setup.py install The last command must be executed as a privileged user if you are not currently using a virtualenv. .. _installing-from-git: Using the development version ----------------------------- With pip ~~~~~~~~ You can install the latest snapshot of django-celery-results using the following pip command:: $ pip install https://github.com/celery/django-celery-results/zipball/master#egg=django-celery-results django-celery-results-2.0.0/docs/includes/introduction.txt000066400000000000000000000016461375545032400237720ustar00rootroot00000000000000:Version: 2.0.0 :Web: http://django-celery-results.readthedocs.io/ :Download: http://pypi.python.org/pypi/django-celery-results :Source: http://github.com/celery/django-celery-results :Keywords: django, celery, database, results About ===== This extension enables you to store Celery task results using the Django ORM. It defines a single model (``django_celery_results.models.TaskResult``) used to store task results, and you can query this database table like any other Django model. Installing ========== The installation instructions for this extension is available from the `Celery documentation`_: http://docs.celeryproject.org/en/latest/django/first-steps-with-django.html#django-celery-results-using-the-django-orm-cache-as-a-result-backend .. _`Celery documentation`: http://docs.celeryproject.org/en/latest/django/first-steps-with-django.html#django-celery-results-using-the-django-orm-cache-as-a-result-backend django-celery-results-2.0.0/docs/index.rst000066400000000000000000000010001375545032400205230ustar00rootroot00000000000000======================================================================= django-celery-results - Celery Result Backends for Django ======================================================================= .. include:: includes/introduction.txt Contents ======== .. toctree:: :maxdepth: 1 copyright .. toctree:: :maxdepth: 2 reference/index .. toctree:: :maxdepth: 1 changelog glossary Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` django-celery-results-2.0.0/docs/make.bat000066400000000000000000000160441375545032400203050ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. epub3 to make an epub3 echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. xml to make Docutils-native XML files echo. pseudoxml to make pseudoxml-XML files for display purposes echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled echo. coverage to run coverage check of the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) REM Check if sphinx-build is available and fallback to Python version if any %SPHINXBUILD% 1>NUL 2>NUL if errorlevel 9009 goto sphinx_python goto sphinx_ok :sphinx_python set SPHINXBUILD=python -m sphinx.__init__ %SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) :sphinx_ok if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\PROJ.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\PROJ.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "epub3" ( %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %~dp0 echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %~dp0 echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) if "%1" == "coverage" ( %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage if errorlevel 1 exit /b 1 echo. echo.Testing of coverage in the sources finished, look at the ^ results in %BUILDDIR%/coverage/python.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%/xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. goto end ) :end django-celery-results-2.0.0/docs/reference/000077500000000000000000000000001375545032400206315ustar00rootroot00000000000000django-celery-results-2.0.0/docs/reference/django_celery_results.backends.cache.rst000066400000000000000000000005011375545032400305600ustar00rootroot00000000000000===================================================== ``django_celery_results.backends.cache`` ===================================================== .. contents:: :local: .. currentmodule:: django_celery_results.backends.cache .. automodule:: django_celery_results.backends.cache :members: :undoc-members: django-celery-results-2.0.0/docs/reference/django_celery_results.backends.database.rst000066400000000000000000000005121375545032400312630ustar00rootroot00000000000000===================================================== ``django_celery_results.backends.database`` ===================================================== .. contents:: :local: .. currentmodule:: django_celery_results.backends.database .. automodule:: django_celery_results.backends.database :members: :undoc-members: django-celery-results-2.0.0/docs/reference/django_celery_results.backends.rst000066400000000000000000000004571375545032400275300ustar00rootroot00000000000000===================================================== ``django_celery_results.backends`` ===================================================== .. contents:: :local: .. currentmodule:: django_celery_results.backends .. automodule:: django_celery_results.backends :members: :undoc-members: django-celery-results-2.0.0/docs/reference/django_celery_results.managers.rst000066400000000000000000000004571375545032400275530ustar00rootroot00000000000000===================================================== ``django_celery_results.managers`` ===================================================== .. contents:: :local: .. currentmodule:: django_celery_results.managers .. automodule:: django_celery_results.managers :members: :undoc-members: django-celery-results-2.0.0/docs/reference/django_celery_results.models.rst000066400000000000000000000004511375545032400272330ustar00rootroot00000000000000===================================================== ``django_celery_results.models`` ===================================================== .. contents:: :local: .. currentmodule:: django_celery_results.models .. automodule:: django_celery_results.models :members: :undoc-members: django-celery-results-2.0.0/docs/reference/django_celery_results.utils.rst000066400000000000000000000004461375545032400271140ustar00rootroot00000000000000===================================================== ``django_celery_results.utils`` ===================================================== .. contents:: :local: .. currentmodule:: django_celery_results.utils .. automodule:: django_celery_results.utils :members: :undoc-members: django-celery-results-2.0.0/docs/reference/index.rst000066400000000000000000000005341375545032400224740ustar00rootroot00000000000000.. _apiref: =============== API Reference =============== :Release: |version| :Date: |today| .. toctree:: :maxdepth: 1 django_celery_results.backends django_celery_results.backends.database django_celery_results.backends.cache django_celery_results.models django_celery_results.managers django_celery_results.utils django-celery-results-2.0.0/docs/templates/000077500000000000000000000000001375545032400206715ustar00rootroot00000000000000django-celery-results-2.0.0/docs/templates/readme.txt000066400000000000000000000027041375545032400226720ustar00rootroot00000000000000===================================================================== Celery Result Backends using the Django ORM/Cache framework. ===================================================================== |build-status| |coverage| |license| |wheel| |pyversion| |pyimp| .. include:: ../includes/introduction.txt .. include:: ../includes/installation.txt .. |build-status| image:: https://secure.travis-ci.org/celery/django-celery-results.png?branch=master :alt: Build status :target: https://travis-ci.org/celery/django-celery-results .. |coverage| image:: https://codecov.io/github/celery/django-celery-results/coverage.svg?branch=master :target: https://codecov.io/github/celery/django-celery-results?branch=master .. |license| image:: https://img.shields.io/pypi/l/django-celery-results.svg :alt: BSD License :target: https://opensource.org/licenses/BSD-3-Clause .. |wheel| image:: https://img.shields.io/pypi/wheel/django-celery-results.svg :alt: django-celery-results can be installed via wheel :target: http://pypi.python.org/pypi/django-celery-results/ .. |pyversion| image:: https://img.shields.io/pypi/pyversions/django-celery-results.svg :alt: Supported Python versions. :target: http://pypi.python.org/pypi/django-celery-results/ .. |pyimp| image:: https://img.shields.io/pypi/implementation/django-celery-results.svg :alt: Support Python implementations. :target: http://pypi.python.org/pypi/django-celery-results/ django-celery-results-2.0.0/extra/000077500000000000000000000000001375545032400170665ustar00rootroot00000000000000django-celery-results-2.0.0/extra/appveyor/000077500000000000000000000000001375545032400207335ustar00rootroot00000000000000django-celery-results-2.0.0/extra/appveyor/install.ps1000066400000000000000000000053421375545032400230320ustar00rootroot00000000000000# Sample script to install Python and pip under Windows # Authors: Olivier Grisel and Kyle Kastner # License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/ $BASE_URL = "https://www.python.org/ftp/python/" $GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py" $GET_PIP_PATH = "C:\get-pip.py" function DownloadPython ($python_version, $platform_suffix) { $webclient = New-Object System.Net.WebClient $filename = "python-" + $python_version + $platform_suffix + ".msi" $url = $BASE_URL + $python_version + "/" + $filename $basedir = $pwd.Path + "\" $filepath = $basedir + $filename if (Test-Path $filename) { Write-Host "Reusing" $filepath return $filepath } # Download and retry up to 5 times in case of network transient errors. Write-Host "Downloading" $filename "from" $url $retry_attempts = 3 for($i=0; $i -lt $retry_attempts; $i++){ try { $webclient.DownloadFile($url, $filepath) break } Catch [Exception]{ Start-Sleep 1 } } Write-Host "File saved at" $filepath return $filepath } function InstallPython ($python_version, $architecture, $python_home) { Write-Host "Installing Python" $python_version "for" $architecture "bit architecture to" $python_home if (Test-Path $python_home) { Write-Host $python_home "already exists, skipping." return $false } if ($architecture -eq "32") { $platform_suffix = "" } else { $platform_suffix = ".amd64" } $filepath = DownloadPython $python_version $platform_suffix Write-Host "Installing" $filepath "to" $python_home $args = "/qn /i $filepath TARGETDIR=$python_home" Write-Host "msiexec.exe" $args Start-Process -FilePath "msiexec.exe" -ArgumentList $args -Wait -Passthru Write-Host "Python $python_version ($architecture) installation complete" return $true } function InstallPip ($python_home) { $pip_path = $python_home + "/Scripts/pip.exe" $python_path = $python_home + "/python.exe" if (-not(Test-Path $pip_path)) { Write-Host "Installing pip..." $webclient = New-Object System.Net.WebClient $webclient.DownloadFile($GET_PIP_URL, $GET_PIP_PATH) Write-Host "Executing:" $python_path $GET_PIP_PATH Start-Process -FilePath "$python_path" -ArgumentList "$GET_PIP_PATH" -Wait -Passthru } else { Write-Host "pip already installed." } } function InstallPackage ($python_home, $pkg) { $pip_path = $python_home + "/Scripts/pip.exe" & $pip_path install $pkg } function main () { InstallPython $env:PYTHON_VERSION $env:PYTHON_ARCH $env:PYTHON InstallPip $env:PYTHON InstallPackage $env:PYTHON wheel } main django-celery-results-2.0.0/extra/appveyor/run_with_compiler.cmd000066400000000000000000000034621375545032400251560ustar00rootroot00000000000000:: To build extensions for 64 bit Python 3, we need to configure environment :: variables to use the MSVC 2010 C++ compilers from GRMSDKX_EN_DVD.iso of: :: MS Windows SDK for Windows 7 and .NET Framework 4 (SDK v7.1) :: :: To build extensions for 64 bit Python 2, we need to configure environment :: variables to use the MSVC 2008 C++ compilers from GRMSDKX_EN_DVD.iso of: :: MS Windows SDK for Windows 7 and .NET Framework 3.5 (SDK v7.0) :: :: 32 bit builds do not require specific environment configurations. :: :: Note: this script needs to be run with the /E:ON and /V:ON flags for the :: cmd interpreter, at least for (SDK v7.0) :: :: More details at: :: https://github.com/cython/cython/wiki/64BitCythonExtensionsOnWindows :: http://stackoverflow.com/a/13751649/163740 :: :: Author: Olivier Grisel :: License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/ @ECHO OFF SET COMMAND_TO_RUN=%* SET WIN_SDK_ROOT=C:\Program Files\Microsoft SDKs\Windows SET MAJOR_PYTHON_VERSION="%PYTHON_VERSION:~0,1%" IF %MAJOR_PYTHON_VERSION% == "2" ( SET WINDOWS_SDK_VERSION="v7.0" ) ELSE IF %MAJOR_PYTHON_VERSION% == "3" ( SET WINDOWS_SDK_VERSION="v7.1" ) ELSE ( ECHO Unsupported Python version: "%MAJOR_PYTHON_VERSION%" EXIT 1 ) IF "%PYTHON_ARCH%"=="64" ( ECHO Configuring Windows SDK %WINDOWS_SDK_VERSION% for Python %MAJOR_PYTHON_VERSION% on a 64 bit architecture SET DISTUTILS_USE_SDK=1 SET MSSdk=1 "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Setup\WindowsSdkVer.exe" -q -version:%WINDOWS_SDK_VERSION% "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Bin\SetEnv.cmd" /x64 /release ECHO Executing: %COMMAND_TO_RUN% call %COMMAND_TO_RUN% || EXIT 1 ) ELSE ( ECHO Using default MSVC build environment for 32 bit architecture ECHO Executing: %COMMAND_TO_RUN% call %COMMAND_TO_RUN% || EXIT 1 ) django-celery-results-2.0.0/locale/000077500000000000000000000000001375545032400172025ustar00rootroot00000000000000django-celery-results-2.0.0/locale/es/000077500000000000000000000000001375545032400176115ustar00rootroot00000000000000django-celery-results-2.0.0/locale/es/LC_MESSAGES/000077500000000000000000000000001375545032400213765ustar00rootroot00000000000000django-celery-results-2.0.0/locale/es/LC_MESSAGES/django.po000066400000000000000000000104351375545032400232030ustar00rootroot00000000000000# Spanish translation strings for django-celery-results. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as django-celery-results. # , 2020. # #, fuzzy msgid "" msgstr "" "Project-Id-Version:\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-02-26 18:34+0100\n" "PO-Revision-Date: 2020-02-26 20:25-0015\n" "Last-Translator: \n" "Language-Team: LANGUAGE \n" "Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: django_celery_results/admin.py:39 msgid "Parameters" msgstr "Parámetros" #: django_celery_results/admin.py:46 msgid "Result" msgstr "Resultado" #: django_celery_results/apps.py:15 msgid "Celery Results" msgstr "Resultados Celery" #: django_celery_results/models.py:28 msgid "Task ID" msgstr "ID de Tarea" #: django_celery_results/models.py:29 msgid "Celery ID for the Task that was run" msgstr "ID de Celery para la tarea que fue ejecutada" #: django_celery_results/models.py:32 msgid "Task Name" msgstr "Nombre de Tarea" #: django_celery_results/models.py:33 msgid "Name of the Task which was run" msgstr "Nombre de la Tarea que fue ejecutada" #: django_celery_results/models.py:36 msgid "Task Positional Arguments" msgstr "Argumentos posicionales de la Tarea" #: django_celery_results/models.py:37 msgid "JSON representation of the positional arguments used with the task" msgstr "Representación JSON de los argumentos posicionales usados en la tarea" #: django_celery_results/models.py:41 msgid "Task Named Arguments" msgstr "Argumentos opcionales de la tarea" #: django_celery_results/models.py:42 msgid "JSON representation of the named arguments used with the task" msgstr "Representación JSON de los argumentos opcionales usados en la tarea" #: django_celery_results/models.py:47 msgid "Task State" msgstr "Estado de la Tarea" #: django_celery_results/models.py:48 msgid "Current state of the task being run" msgstr "Estado actual en el que se encuentra la tarea en ejecución" #: django_celery_results/models.py:51 msgid "Worker" msgstr "Worker" #: django_celery_results/models.py:51 msgid "Worker that executes the task" msgstr "Worker que ejecuta la tarea" #: django_celery_results/models.py:55 msgid "Result Content Type" msgstr "Content Type del resultado" #: django_celery_results/models.py:56 msgid "Content type of the result data" msgstr "Atributo Content type de los datos del resultado" #: django_celery_results/models.py:59 msgid "Result Encoding" msgstr "Codificación del resultado" #: django_celery_results/models.py:60 msgid "The encoding used to save the task result data" msgstr "La codificación usada para guardar los datos del resultado" #: django_celery_results/models.py:63 msgid "Result Data" msgstr "Datos del resultado" #: django_celery_results/models.py:64 msgid "" "The data returned by the task. Use content_encoding and content_type fields" " to read." msgstr "" "Datos devueltos por la tarea. Usa los campos content_encoding y content_type" " para leerlos." #: django_celery_results/models.py:68 msgid "Created DateTime" msgstr "Fecha de creación" #: django_celery_results/models.py:69 msgid "Datetime field when the task result was created in UTC" msgstr "Fecha de creación de la tarea en UTC" #: django_celery_results/models.py:72 msgid "Completed DateTime" msgstr "Fecha de terminación" #: django_celery_results/models.py:73 msgid "Datetime field when the task was completed in UTC" msgstr "Fecha de completitud de la tarea en UTC" #: django_celery_results/models.py:76 msgid "Traceback" msgstr "Traceback" #: django_celery_results/models.py:77 msgid "Text of the traceback if the task generated one" msgstr "Texto del traceback si la tarea generó uno" #: django_celery_results/models.py:80 msgid "Task Meta Information" msgstr "Metadatos de la tarea" #: django_celery_results/models.py:81 msgid "" "JSON meta information about the task, such as information on child tasks" msgstr "" "Metainformación sobre la tarea en formato JSON, como la información de las " "tareas hijas" #: django_celery_results/models.py:91 msgid "task result" msgstr "resultado de la tarea" #: django_celery_results/models.py:92 msgid "task results" msgstr "resultados de tareas" django-celery-results-2.0.0/manage.py000077500000000000000000000004621375545032400175520ustar00rootroot00000000000000#!/usr/bin/env python from __future__ import absolute_import, unicode_literals import os import sys if __name__ == '__main__': os.environ.setdefault('DJANGO_SETTINGS_MODULE', 't.proj.settings') from django.core.management import execute_from_command_line execute_from_command_line(sys.argv) django-celery-results-2.0.0/requirements/000077500000000000000000000000001375545032400204665ustar00rootroot00000000000000django-celery-results-2.0.0/requirements/default.txt000066400000000000000000000000211375545032400226440ustar00rootroot00000000000000celery>=4.4,<6.0 django-celery-results-2.0.0/requirements/docs.txt000066400000000000000000000000371375545032400221570ustar00rootroot00000000000000sphinx_celery>=1.1 Django>=2.2 django-celery-results-2.0.0/requirements/pkgutils.txt000066400000000000000000000001601375545032400230660ustar00rootroot00000000000000setuptools>=40.8.0 wheel>=0.33.1 flake8>=3.8.3 flakeplus>=1.1 tox>=2.3.1 sphinx2rst>=1.0 bumpversion pydocstyle django-celery-results-2.0.0/requirements/test-ci.txt000066400000000000000000000000231375545032400225720ustar00rootroot00000000000000pytest-cov codecov django-celery-results-2.0.0/requirements/test-django.txt000066400000000000000000000000211375545032400234370ustar00rootroot00000000000000Django>=2.2,<4.0 django-celery-results-2.0.0/requirements/test-django22.txt000066400000000000000000000000231375545032400236050ustar00rootroot00000000000000django>=2.2.9,<3.0 django-celery-results-2.0.0/requirements/test-django30.txt000066400000000000000000000000201375545032400236010ustar00rootroot00000000000000django>=3.0,<3.1django-celery-results-2.0.0/requirements/test-django31.txt000066400000000000000000000000201375545032400236020ustar00rootroot00000000000000django>=3.1,<3.2django-celery-results-2.0.0/requirements/test.txt000066400000000000000000000001271375545032400222060ustar00rootroot00000000000000case>=1.3.1 pytest>=4.3 pytest-django>=2.2,<4.0 pytest-benchmark pytz>dev psycopg2cffi django-celery-results-2.0.0/setup.cfg000066400000000000000000000006541375545032400175710ustar00rootroot00000000000000[tool:pytest] testpaths = t/ python_classes = test_* python_files = test_* benchmark_* DJANGO_SETTINGS_MODULE = t.proj.settings markers = benchmark: mark a test as a benchmark [flake8] # classes can be lowercase, arguments and variables can be uppercase # whenever it makes the code more readable. ignore = N806, N802, N801, N803 [pep257] ignore = D102,D104,D203,D105,D213 match-dir = [^migrations] [wheel] universal = 1 django-celery-results-2.0.0/setup.py000066400000000000000000000100531375545032400174540ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import os import re import sys import codecs import setuptools import setuptools.command.test try: import platform _pyimp = platform.python_implementation except (AttributeError, ImportError): def _pyimp(): return 'Python' NAME = 'django_celery_results' E_UNSUPPORTED_PYTHON = '%s 1.0 requires %%s %%s or later!' % (NAME,) PYIMP = _pyimp() PY36_OR_LESS = sys.version_info < (3, 6) PYPY_VERSION = getattr(sys, 'pypy_version_info', None) PYPY24_ATLEAST = PYPY_VERSION and PYPY_VERSION >= (2, 4) if PY36_OR_LESS and not PYPY24_ATLEAST: raise Exception(E_UNSUPPORTED_PYTHON % (PYIMP, '3.6')) # -*- Classifiers -*- classes = """ Development Status :: 5 - Production/Stable License :: OSI Approved :: BSD License Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy Framework :: Django Framework :: Django :: 2.2 Framework :: Django :: 3.0 Framework :: Django :: 3.1 Operating System :: OS Independent Topic :: Communications Topic :: System :: Distributed Computing Topic :: Software Development :: Libraries :: Python Modules """ classifiers = [s.strip() for s in classes.split('\n') if s] # -*- Distribution Meta -*- re_meta = re.compile(r'__(\w+?)__\s*=\s*(.*)') re_doc = re.compile(r'^"""(.+?)"""') def add_default(m): attr_name, attr_value = m.groups() return ((attr_name, attr_value.strip("\"'")),) def add_doc(m): return (('doc', m.groups()[0]),) pats = {re_meta: add_default, re_doc: add_doc} here = os.path.abspath(os.path.dirname(__file__)) with open(os.path.join(here, NAME, '__init__.py')) as meta_fh: meta = {} for line in meta_fh: if line.strip() == '# -eof meta-': break for pattern, handler in pats.items(): m = pattern.match(line.strip()) if m: meta.update(handler(m)) # -*- Installation Requires -*- def strip_comments(l): return l.split('#', 1)[0].strip() def _pip_requirement(req): if req.startswith('-r '): _, path = req.split() return reqs(*path.split('/')) return [req] def _reqs(*f): return [ _pip_requirement(r) for r in ( strip_comments(l) for l in open( os.path.join(os.getcwd(), 'requirements', *f)).readlines() ) if r] def reqs(*f): return [req for subreq in _reqs(*f) for req in subreq] # -*- Long Description -*- if os.path.exists('README.rst'): long_description = codecs.open('README.rst', 'r', 'utf-8').read() else: long_description = 'See http://pypi.python.org/pypi/%s' % (NAME,) # -*- %%% -*- class pytest(setuptools.command.test.test): user_options = [('pytest-args=', 'a', 'Arguments to pass to pytest')] def initialize_options(self): setuptools.command.test.test.initialize_options(self) self.pytest_args = [] def run_tests(self): import pytest sys.exit(pytest.main(self.pytest_args)) setuptools.setup( name=NAME, packages=setuptools.find_packages(exclude=['ez_setup', 't', 't.*']), version=meta['version'], description=meta['doc'], long_description=long_description, long_description_content_type='text/markdown', keywords='celery django database result backend', author=meta['author'], author_email=meta['contact'], url=meta['homepage'], platforms=['any'], license='BSD', classifiers=classifiers, install_requires=reqs('default.txt'), tests_require=reqs('test.txt') + reqs('test-django.txt'), cmdclass={'test': pytest}, entry_points={ 'celery.result_backends': [ 'django-db = django_celery_results.backends:DatabaseBackend', 'django-cache = django_celery_results.backends:CacheBackend', ], }, zip_safe=False, include_package_data=False, ) django-celery-results-2.0.0/t/000077500000000000000000000000001375545032400162065ustar00rootroot00000000000000django-celery-results-2.0.0/t/__init__.py000066400000000000000000000000001375545032400203050ustar00rootroot00000000000000django-celery-results-2.0.0/t/conftest.py000066400000000000000000000026171375545032400204130ustar00rootroot00000000000000from __future__ import absolute_import, unicode_literals import pytest # we have to import the pytest plugin fixtures here, # in case user did not do the `python setup.py develop` yet, # that installs the pytest plugin into the setuptools registry. from celery.contrib.pytest import (celery_app, celery_enable_logging, celery_parameters, depends_on_current_app, celery_config, use_celery_app_trap) from celery.contrib.testing.app import TestApp, Trap # Tricks flake8 into silencing redefining fixtures warnings. __all__ = ( 'celery_app', 'celery_enable_logging', 'depends_on_current_app', 'celery_parameters', 'celery_config', 'use_celery_app_trap' ) @pytest.fixture(scope='session', autouse=True) def setup_default_app_trap(): from celery._state import set_default_app set_default_app(Trap()) @pytest.fixture() def app(celery_app): return celery_app @pytest.fixture(autouse=True) def test_cases_shortcuts(request, app, patching): if request.instance: @app.task def add(x, y): return x + y # IMPORTANT: We set an .app attribute for every test case class. request.instance.app = app request.instance.Celery = TestApp request.instance.add = add request.instance.patching = patching yield if request.instance: request.instance.app = None django-celery-results-2.0.0/t/integration/000077500000000000000000000000001375545032400205315ustar00rootroot00000000000000django-celery-results-2.0.0/t/integration/__init__.py000066400000000000000000000000001375545032400226300ustar00rootroot00000000000000django-celery-results-2.0.0/t/integration/benchmark_models.py000066400000000000000000000044141375545032400244030ustar00rootroot00000000000000from __future__ import absolute_import, unicode_literals import pytest from datetime import timedelta import time from django.test import TransactionTestCase from celery import uuid from django_celery_results.models import TaskResult from django_celery_results.utils import now RECORDS_COUNT = 100000 @pytest.fixture() def use_benchmark(request, benchmark): def wrapped(a=10, b=5): return a + b request.cls.benchmark = benchmark @pytest.mark.usefixtures('use_benchmark') @pytest.mark.usefixtures('depends_on_current_app') class benchmark_Models(TransactionTestCase): @pytest.fixture(autouse=True) def setup_app(self, app): self.app = app self.app.conf.result_serializer = 'pickle' self.app.conf.result_backend = ( 'django_celery_results.backends:DatabaseBackend') def create_many_task_result(self, count): start = time.time() draft_results = [TaskResult(task_id=uuid()) for _ in range(count)] drafted = time.time() results = TaskResult.objects.bulk_create(draft_results) done_creating = time.time() print(( 'drafting time: {drafting:.2f}\n' 'bulk_create time: {done:.2f}\n' '------' ).format(drafting=drafted - start, done=done_creating - drafted)) return results def setup_records_to_delete(self): self.create_many_task_result(count=RECORDS_COUNT) mid_point = TaskResult.objects.order_by('id')[int(RECORDS_COUNT / 2)] todelete = TaskResult.objects.filter(id__gte=mid_point.id) todelete.update(date_done=now() - timedelta(days=10)) def test_taskresult_delete_expired(self): start = time.time() self.setup_records_to_delete() after_setup = time.time() self.benchmark.pedantic( TaskResult.objects.delete_expired, args=(self.app.conf.result_expires,), iterations=1, rounds=1, ) done = time.time() assert TaskResult.objects.count() == int(RECORDS_COUNT / 2) print(( '------' 'setup time: {setup:.2f}\n' 'bench time: {bench:.2f}\n' ).format(setup=after_setup - start, bench=done - after_setup)) assert self.benchmark.stats.stats.max < 1 django-celery-results-2.0.0/t/proj/000077500000000000000000000000001375545032400171605ustar00rootroot00000000000000django-celery-results-2.0.0/t/proj/__init__.py000066400000000000000000000000561375545032400212720ustar00rootroot00000000000000from .celery import app as celery_app # noqa django-celery-results-2.0.0/t/proj/celery.py000066400000000000000000000005641375545032400210220ustar00rootroot00000000000000from __future__ import absolute_import, unicode_literals import os from celery import Celery os.environ.setdefault('DJANGO_SETTINGS_MODULE', 't.proj.settings') app = Celery('proj') # Using a string here means the worker doesn't have to serialize # the configuration object. app.config_from_object('django.conf:settings', namespace='CELERY') app.autodiscover_tasks() django-celery-results-2.0.0/t/proj/settings.py000066400000000000000000000101361375545032400213730ustar00rootroot00000000000000""" Django settings for Test project. Generated by 'django-admin startproject' using Django 1.9.1. For more information on this file, see https://docs.djangoproject.com/en/1.9/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/1.9/ref/settings/ """ from __future__ import absolute_import, unicode_literals import os import sys # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, os.path.abspath(os.path.join(BASE_DIR, os.pardir))) # configure psycopg2cffi for psycopg2 compatibility. We must use this package # support pypy. # if not installed, use sqlite as a backup (some tests may fail), # otherwise even makemigrations won't run. try: from psycopg2cffi import compat compat.register() DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', 'NAME': 'postgres', 'USER': 'postgres', 'OPTIONS': { 'connect_timeout': 1000, } }, 'secondary': { 'ENGINE': 'django.db.backends.postgresql', 'NAME': 'postgres', 'USER': 'postgres', 'OPTIONS': { 'connect_timeout': 1000, }, 'TEST': { 'MIRROR': 'default', }, }, } except ImportError: DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 'OPTIONS': { 'timeout': 1000, } }, 'secondary': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 'OPTIONS': { 'timeout': 1000, } }, } # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = 'u($kbs9$irs0)436gbo9%!b&#zyd&70tx!n7!i&fl6qun@z1_l' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'django_celery_results', ] MIDDLEWARE_CLASSES = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', ] ROOT_URLCONF = 't.proj.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] WSGI_APPLICATION = 't.proj.wsgi.application' CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', }, 'dummy': { 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', }, } # Password validation # https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators django_auth = 'django.contrib.auth.password_validation.' AUTH_PASSWORD_VALIDATORS = [ ] # Internationalization # https://docs.djangoproject.com/en/1.9/topics/i18n/ LANGUAGE_CODE = 'en-us' TIME_ZONE = 'UTC' USE_I18N = True USE_L10N = True USE_TZ = True DJANGO_CELERY_RESULTS_TASK_ID_MAX_LENGTH = 191 # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.9/howto/static-files/ STATIC_URL = '/static/' django-celery-results-2.0.0/t/proj/urls.py000066400000000000000000000002651375545032400205220ustar00rootroot00000000000000from __future__ import absolute_import, unicode_literals from django.conf.urls import url from django.contrib import admin urlpatterns = [ url(r'^admin/', admin.site.urls), ] django-celery-results-2.0.0/t/proj/wsgi.py000066400000000000000000000006741375545032400205120ustar00rootroot00000000000000""" WSGI config for Test project. It exposes the WSGI callable as a module-level variable named ``application``. For more information on this file, see https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/ """ from __future__ import absolute_import, unicode_literals import os from django.core.wsgi import get_wsgi_application os.environ.setdefault("DJANGO_SETTINGS_MODULE", "t.proj.settings") application = get_wsgi_application() django-celery-results-2.0.0/t/unit/000077500000000000000000000000001375545032400171655ustar00rootroot00000000000000django-celery-results-2.0.0/t/unit/__init__.py000066400000000000000000000000001375545032400212640ustar00rootroot00000000000000django-celery-results-2.0.0/t/unit/backends/000077500000000000000000000000001375545032400207375ustar00rootroot00000000000000django-celery-results-2.0.0/t/unit/backends/__init__.py000066400000000000000000000000001375545032400230360ustar00rootroot00000000000000django-celery-results-2.0.0/t/unit/backends/test_cache.py000066400000000000000000000056461375545032400234260ustar00rootroot00000000000000from __future__ import absolute_import, unicode_literals import pytest import sys from datetime import timedelta from billiard.einfo import ExceptionInfo from celery import result from celery import states from celery import uuid from django_celery_results.backends.cache import CacheBackend class SomeClass(object): def __init__(self, data): self.data = data class test_CacheBackend: def setup(self): self.b = CacheBackend(app=self.app) def test_mark_as_done(self): tid = uuid() assert self.b.get_status(tid) == states.PENDING assert self.b.get_result(tid) is None self.b.mark_as_done(tid, 42) assert self.b.get_status(tid) == states.SUCCESS assert self.b.get_result(tid) == 42 def test_forget(self): tid = uuid() self.b.mark_as_done(tid, {'foo': 'bar'}) assert self.b.get_result(tid).get('foo') == 'bar' self.b.forget(tid) assert tid not in self.b._cache assert self.b.get_result(tid) is None @pytest.mark.usefixtures('depends_on_current_app') def test_save_restore_delete_group(self): group_id = uuid() result_ids = [uuid() for i in range(10)] results = list(map(result.AsyncResult, result_ids)) res = result.GroupResult(group_id, results) res.save(backend=self.b) saved = result.GroupResult.restore(group_id, backend=self.b) assert saved.results == results assert saved.id == group_id saved.delete(backend=self.b) assert result.GroupResult.restore(group_id, backend=self.b) is None def test_is_pickled(self): tid2 = uuid() result = {'foo': 'baz', 'bar': SomeClass(12345)} self.b.mark_as_done(tid2, result) # is serialized properly. rindb = self.b.get_result(tid2) assert rindb.get('foo') == 'baz' assert rindb.get('bar').data == 12345 def test_mark_as_failure(self): einfo = None tid3 = uuid() try: raise KeyError('foo') except KeyError as exception: einfo = ExceptionInfo(sys.exc_info()) self.b.mark_as_failure(tid3, exception, traceback=einfo.traceback) assert self.b.get_status(tid3) == states.FAILURE assert isinstance(self.b.get_result(tid3), KeyError) assert self.b.get_traceback(tid3) == einfo.traceback def test_process_cleanup(self): self.b.process_cleanup() def test_set_expires(self): cb1 = CacheBackend(app=self.app, expires=timedelta(seconds=16)) assert cb1.expires == 16 cb2 = CacheBackend(app=self.app, expires=32) assert cb2.expires == 32 class test_custom_CacheBackend: def test_custom_cache_backend(self): self.app.conf.cache_backend = 'dummy' b = CacheBackend(app=self.app) assert ( b.cache_backend.__class__.__module__ == 'django.core.cache.backends.dummy' # noqa ) django-celery-results-2.0.0/t/unit/backends/test_database.py000066400000000000000000000242511375545032400241200ustar00rootroot00000000000000from __future__ import absolute_import, unicode_literals import mock import celery import pytest from celery import uuid from celery import states from celery.result import GroupResult, AsyncResult from django_celery_results.backends.database import DatabaseBackend from django_celery_results.models import ChordCounter, TaskResult class SomeClass(object): def __init__(self, data): self.data = data @pytest.mark.django_db() @pytest.mark.usefixtures('depends_on_current_app') class test_DatabaseBackend: @pytest.fixture(autouse=True) def setup_backend(self): self.app.conf.result_serializer = 'json' self.app.conf.result_backend = ( 'django_celery_results.backends:DatabaseBackend') self.b = DatabaseBackend(app=self.app) def test_backend__pickle_serialization__dict_result(self): self.app.conf.result_serializer = 'pickle' self.app.conf.accept_content = {'pickle', 'json'} self.b = DatabaseBackend(app=self.app) tid2 = uuid() result = {'foo': 'baz', 'bar': SomeClass(12345)} request = mock.MagicMock() request.task = 'my_task' request.args = ['a', 1, SomeClass(67)] request.kwargs = {'c': 6, 'd': 'e', 'f': SomeClass(89)} request.hostname = 'celery@ip-0-0-0-0' request.chord = None del request.argsrepr, request.kwargsrepr self.b.mark_as_done(tid2, result, request=request) mindb = self.b.get_task_meta(tid2) assert mindb.get('result').get('foo') == 'baz' assert mindb.get('result').get('bar').data == 12345 assert mindb.get('task_name') == 'my_task' assert len(mindb.get('task_args')) == 3 assert mindb.get('task_args')[0] == 'a' assert mindb.get('task_args')[1] == 1 assert mindb.get('task_args')[2].data == 67 assert len(mindb.get('task_kwargs')) == 3 assert mindb.get('task_kwargs')['c'] == 6 assert mindb.get('task_kwargs')['d'] == 'e' assert mindb.get('task_kwargs')['f'].data == 89 tid3 = uuid() try: raise KeyError('foo') except KeyError as exception: self.b.mark_as_failure(tid3, exception) assert self.b.get_status(tid3) == states.FAILURE assert isinstance(self.b.get_result(tid3), KeyError) def test_backend__pickle_serialization__str_result(self): self.app.conf.result_serializer = 'pickle' self.app.conf.accept_content = {'pickle', 'json'} self.b = DatabaseBackend(app=self.app) tid2 = uuid() result = 'foo' request = mock.MagicMock() request.task = 'my_task' request.args = ['a', 1, SomeClass(67)] request.kwargs = {'c': 6, 'd': 'e', 'f': SomeClass(89)} request.hostname = 'celery@ip-0-0-0-0' request.chord = None del request.argsrepr, request.kwargsrepr self.b.mark_as_done(tid2, result, request=request) mindb = self.b.get_task_meta(tid2) assert mindb.get('result') == 'foo' assert mindb.get('task_name') == 'my_task' assert len(mindb.get('task_args')) == 3 assert mindb.get('task_args')[0] == 'a' assert mindb.get('task_args')[1] == 1 assert mindb.get('task_args')[2].data == 67 assert len(mindb.get('task_kwargs')) == 3 assert mindb.get('task_kwargs')['c'] == 6 assert mindb.get('task_kwargs')['d'] == 'e' assert mindb.get('task_kwargs')['f'].data == 89 def test_backend__pickle_serialization__bytes_result(self): self.app.conf.result_serializer = 'pickle' self.app.conf.accept_content = {'pickle', 'json'} self.b = DatabaseBackend(app=self.app) tid2 = uuid() result = b'foo' request = mock.MagicMock() request.task = 'my_task' request.args = ['a', 1, SomeClass(67)] request.kwargs = {'c': 6, 'd': 'e', 'f': SomeClass(89)} request.hostname = 'celery@ip-0-0-0-0' request.chord = None del request.argsrepr, request.kwargsrepr self.b.mark_as_done(tid2, result, request=request) mindb = self.b.get_task_meta(tid2) assert mindb.get('result') == b'foo' assert mindb.get('task_name') == 'my_task' assert len(mindb.get('task_args')) == 3 assert mindb.get('task_args')[0] == 'a' assert mindb.get('task_args')[1] == 1 assert mindb.get('task_args')[2].data == 67 assert len(mindb.get('task_kwargs')) == 3 assert mindb.get('task_kwargs')['c'] == 6 assert mindb.get('task_kwargs')['d'] == 'e' assert mindb.get('task_kwargs')['f'].data == 89 def xxx_backend(self): tid = uuid() assert self.b.get_status(tid) == states.PENDING assert self.b.get_result(tid) is None self.b.mark_as_done(tid, 42) assert self.b.get_status(tid) == states.SUCCESS assert self.b.get_result(tid) == 42 tid2 = uuid() try: raise KeyError('foo') except KeyError as exception: self.b.mark_as_failure(tid2, exception) assert self.b.get_status(tid2) == states.FAILURE assert isinstance(self.b.get_result(tid2), KeyError) def test_forget(self): tid = uuid() self.b.mark_as_done(tid, {'foo': 'bar'}) x = self.app.AsyncResult(tid) assert x.result.get('foo') == 'bar' x.forget() if celery.VERSION[0:3] == (3, 1, 10): # bug in 3.1.10 means result did not clear cache after forget. x._cache = None assert x.result is None def test_backend_secrets(self): tid = uuid() request = mock.MagicMock() request.task = 'my_task' request.args = ['a', 1, 'password'] request.kwargs = {'c': 3, 'd': 'e', 'password': 'password'} request.argsrepr = 'argsrepr' request.kwargsrepr = 'kwargsrepr' request.hostname = 'celery@ip-0-0-0-0' request.chord = None result = {'foo': 'baz'} self.b.mark_as_done(tid, result, request=request) mindb = self.b.get_task_meta(tid) assert mindb.get('task_args') == 'argsrepr' assert mindb.get('task_kwargs') == 'kwargsrepr' assert mindb.get('worker') == 'celery@ip-0-0-0-0' def test_on_chord_part_return(self): """Test if the ChordCounter is properly decremented and the callback is triggered after all chord parts have returned""" gid = uuid() tid1 = uuid() tid2 = uuid() subtasks = [AsyncResult(tid1), AsyncResult(tid2)] group = GroupResult(id=gid, results=subtasks) self.b.apply_chord(group, self.add.s()) chord_counter = ChordCounter.objects.get(group_id=gid) assert chord_counter.count == 2 request = mock.MagicMock() request.id = subtasks[0].id request.group = gid request.task = "my_task" request.args = ["a", 1, "password"] request.kwargs = {"c": 3, "d": "e", "password": "password"} request.argsrepr = "argsrepr" request.kwargsrepr = "kwargsrepr" request.hostname = "celery@ip-0-0-0-0" result = {"foo": "baz"} self.b.mark_as_done(tid1, result, request=request) chord_counter.refresh_from_db() assert chord_counter.count == 1 self.b.mark_as_done(tid2, result, request=request) with pytest.raises(ChordCounter.DoesNotExist): ChordCounter.objects.get(group_id=gid) request.chord.delay.assert_called_once() def test_callback_failure(self): """Test if a failure in the chord callback is properly handled""" gid = uuid() tid1 = uuid() tid2 = uuid() cid = uuid() subtasks = [AsyncResult(tid1), AsyncResult(tid2)] group = GroupResult(id=gid, results=subtasks) self.b.apply_chord(group, self.add.s()) chord_counter = ChordCounter.objects.get(group_id=gid) assert chord_counter.count == 2 request = mock.MagicMock() request.id = subtasks[0].id request.group = gid request.task = "my_task" request.args = ["a", 1, "password"] request.kwargs = {"c": 3, "d": "e", "password": "password"} request.argsrepr = "argsrepr" request.kwargsrepr = "kwargsrepr" request.hostname = "celery@ip-0-0-0-0" request.chord.id = cid result = {"foo": "baz"} # Trigger an exception when the callback is triggered request.chord.delay.side_effect = ValueError() self.b.mark_as_done(tid1, result, request=request) chord_counter.refresh_from_db() assert chord_counter.count == 1 self.b.mark_as_done(tid2, result, request=request) with pytest.raises(ChordCounter.DoesNotExist): ChordCounter.objects.get(group_id=gid) request.chord.delay.assert_called_once() assert TaskResult.objects.get(task_id=cid).status == states.FAILURE def test_on_chord_part_return_failure(self): """Test if a failure in one of the chord header tasks is properly handled and the callback was not triggered """ gid = uuid() tid1 = uuid() tid2 = uuid() cid = uuid() subtasks = [AsyncResult(tid1), AsyncResult(tid2)] group = GroupResult(id=gid, results=subtasks) self.b.apply_chord(group, self.add.s()) chord_counter = ChordCounter.objects.get(group_id=gid) assert chord_counter.count == 2 request = mock.MagicMock() request.id = tid1 request.group = gid request.task = "my_task" request.args = ["a", 1, "password"] request.kwargs = {"c": 3, "d": "e", "password": "password"} request.argsrepr = "argsrepr" request.kwargsrepr = "kwargsrepr" request.hostname = "celery@ip-0-0-0-0" request.chord.id = cid result = {"foo": "baz"} self.b.mark_as_done(tid1, result, request=request) chord_counter.refresh_from_db() assert chord_counter.count == 1 request.id = tid2 self.b.mark_as_failure(tid2, ValueError(), request=request) with pytest.raises(ChordCounter.DoesNotExist): ChordCounter.objects.get(group_id=gid) request.chord.delay.assert_not_called() django-celery-results-2.0.0/t/unit/test_migrations.py000066400000000000000000000036401375545032400227550ustar00rootroot00000000000000from __future__ import absolute_import, unicode_literals import os from django.test import TestCase from django.apps import apps from django.db.migrations.state import ProjectState from django.db.migrations.autodetector import MigrationAutodetector from django.db.migrations.loader import MigrationLoader from django.db.migrations.questioner import NonInteractiveMigrationQuestioner from django_celery_results import migrations as result_migrations class MigrationTests(TestCase): def test_no_duplicate_migration_numbers(self): """Verify no duplicate migration numbers. Migration files with the same number can cause issues with backward migrations, so avoid them. """ path = os.path.dirname(result_migrations.__file__) files = [f[:4] for f in os.listdir(path) if f.endswith('.py')] self.assertEqual( len(files), len(set(files)), msg='Detected migration files with the same migration number') def test_models_match_migrations(self): """Make sure that no model changes exist. This logic is taken from django's makemigrations.py file. Here just detect if model changes exist that require a migration, and if so we fail. """ app_labels = ['django_celery_results'] loader = MigrationLoader(None, ignore_no_migrations=True) questioner = NonInteractiveMigrationQuestioner( specified_apps=app_labels, dry_run=False) autodetector = MigrationAutodetector( loader.project_state(), ProjectState.from_apps(apps), questioner, ) changes = autodetector.changes( graph=loader.graph, trim_to_apps=app_labels, convert_apps=app_labels, migration_name='fake_name', ) self.assertTrue( not changes, msg='Model changes exist that do not have a migration') django-celery-results-2.0.0/t/unit/test_models.py000066400000000000000000000071121375545032400220620ustar00rootroot00000000000000from __future__ import absolute_import, unicode_literals import pytest from datetime import datetime, timedelta from django.db import transaction from django.test import TransactionTestCase from celery import states, uuid from django_celery_results.models import TaskResult from django_celery_results.utils import now @pytest.mark.usefixtures('depends_on_current_app') class test_Models(TransactionTestCase): databases = '__all__' @pytest.fixture(autouse=True) def setup_app(self, app): self.app = app self.app.conf.result_serializer = 'pickle' self.app.conf.result_backend = ( 'django_celery_results.backends:DatabaseBackend') def create_task_result(self): id = uuid() taskmeta, created = TaskResult.objects.get_or_create(task_id=id) return taskmeta def test_taskmeta(self, ctype='application/json', cenc='utf-8'): m1 = self.create_task_result() m2 = self.create_task_result() m3 = self.create_task_result() assert str(m1).startswith('