pax_global_header00006660000000000000000000000064132422712530014513gustar00rootroot0000000000000052 comment=60b7fc624528ac213ff2c6aa77e0063c63f727ea django-celery-beat-1.1.1/000077500000000000000000000000001324227125300151475ustar00rootroot00000000000000django-celery-beat-1.1.1/.bumpversion.cfg000066400000000000000000000005421324227125300202600ustar00rootroot00000000000000[bumpversion] current_version = 1.1.1 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_beat/__init__.py] [bumpversion:file:docs/includes/introduction.txt] [bumpversion:file:README.rst] django-celery-beat-1.1.1/.cookiecutterrc000066400000000000000000000016571324227125300202060ustar00rootroot00000000000000# 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-beat' project_short_description: 'Database-backed Periodic Tasks' project_slug: 'django-celery-beat' version: '1.0.0' year: '2016' django-celery-beat-1.1.1/.coveragerc000066400000000000000000000002511324227125300172660ustar00rootroot00000000000000[run] branch = 1 cover_pylib = 0 include = *django_celery_beat/* omit = django_celery_beat.tests.* [report] omit = */python?.?/* */site-packages/* */pypy/* django-celery-beat-1.1.1/.editorconfig000066400000000000000000000003151324227125300176230ustar00rootroot00000000000000# 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-beat-1.1.1/.gitignore000066400000000000000000000004551324227125300171430ustar00rootroot00000000000000.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 .eggs/ django-celery-beat-1.1.1/.travis.yml000066400000000000000000000013631324227125300172630ustar00rootroot00000000000000language: python sudo: false cache: false python: - 2.7 - 3.4 - 3.5 - pypy env: - DJANGO=1.8 - DJANGO=1.9 - DJANGO=1.10 - DJANGO=1.11 - DJANGO=2.0 os: - linux matrix: include: - { python: 2.7, env: TOXENV=flake8 } - { python: 2.7, env: TOXENV=flakeplus } - { python: 2.7, env: TOXENV=pydocstyle } - { python: 3.5, env: TOXENV=apicheck } - { python: 3.5, env: TOXENV=linkcheck } - { python: 3.5, env: TOXENV=cov } exclude: - { python: 2.7, env: DJANGO=2.0 } - { python: pypy, env: DJANGO=2.0 } install: travis_retry pip install -U tox-travis services: rabbitmq script: tox -v -- -v after_success: - .tox/$TRAVIS_PYTHON_VERSION/bin/coverage xml - .tox/$TRAVIS_PYTHON_VERSION/bin/codecov -e TOXENV django-celery-beat-1.1.1/AUTHORS000066400000000000000000000061661324227125300162300ustar00rootroot00000000000000========= 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 Jimmy Bradshaw Joey Wilhelm John Andrews John Watson Jonas Haag Jonatan Heyman Josh Drake José Moreira Jude Nagurney Justin Quick Keith Perkins Kirill Panshin Mark Hellewell Mark Heppner 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 django-celery-beat-1.1.1/Changelog000066400000000000000000000035661324227125300167730ustar00rootroot00000000000000.. _changelog: ================ Change history ================ .. _version-1.1.1: 1.1.1 ===== :release-date: 2018-2-18 2:30 p.m. UTC+3:00 :release-by: Omer Katz - Fix interval schedules by providing nowfun. - Removing code that forced last_run_at to be timezone naive for no reason, made timezone aware. Fixes crontab schedules after celery/celery#4173. - Entry.last_run_at is no-longer timezone naive. - Use a localized PyTZ timezone object for now() otherwise conversions fail scheduling breaks resulting in constant running of tasks or possibly not running ever. - Fix endless migrations creation for solar schedules events. - Prevent MySQL has gone away errors. - Added support for Django 2.0. - Adjust CrontabSchedule's minutes, hour & day_of_month fields max length .. _version-1.1.0: 1.1.0 ===== :release-date: 2017-10-31 2:30 p.m. UTC+3:00 :release-by: Omer Katz - Adds default_app_config (Issue celery/celery#3567) - Adds "run now" admin action for tasks. - Adds admin actions to toggle tasks. - Add solar schedules (Issue #8) - Notify beat of changes when Interval/Crontab models change. (Issue celery/celery#3683) - Fix PeriodicTask.enable sync issues - Notify beat of changes when Solar model changes. - Resolve CSS class conflict with django-adminlte2 package. - We now support Django 1.11 - Deletes are now performed cascadingly. - Return schedule for solar periodic tasks so that Celery Beat does not crash when one is scheduled. - Adding nowfun to solar and crontab schedulers so that the Django timezone is used. .. _version-1.0.1: 1.0.1 ===== :release-date: 2016-11-07 02:28 p.m. PST :release-by: Ask Solem - Now depends on Celery 4.0.0. - Migration modules were not included in the distribution. - Adds documentation: http://django-celery-beat.readthedocs.io/ .. _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-beat-1.1.1/LICENSE000066400000000000000000000050741324227125300161620ustar00rootroot00000000000000Copyright (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-beat 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-beat (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-beat with other software that is released under the GPL, it does not mean that we're distributing django-celery-beat under the GPL license. The BSD license, unlike the GPL, let you distribute a modified version without making your changes open source. django-celery-beat-1.1.1/MANIFEST.in000066400000000000000000000006401324227125300167050ustar00rootroot00000000000000include 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 django_celery_beat *.py *.html recursive-include t *.py recursive-exclude * __pycache__ recursive-exclude * *.py[co] recursive-exclude * .*.sw[a-z] django-celery-beat-1.1.1/Makefile000066400000000000000000000072771324227125300166240ustar00rootroot00000000000000PROJ=django_celery_beat PGPIDENT="Celery Security Team" PYTHON=python PYTEST=py.test GIT=git TOX=tox ICONV=iconv FLAKE8=flake8 FLAKEPLUS=flakeplus PYDOCSTYLE=pydocstyle SPHINX2RST=sphinx2rst 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 TESTDIR=t 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 flakeplus 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 flakepluscheck: $(FLAKEPLUS) --$(FLAKEPLUSTARGET) "$(PROJ)" "$(TESTDIR)" flakeplusdiag: -$(MAKE) flakepluscheck pep257check: $(PYDOCSTYLE) "$(PROJ)" 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: (cd $(TESTDIR); $(PYTEST) -x --cov="$(PROJ)" --cov-report=html) build: $(PYTHON) setup.py sdist bdist_wheel distcheck: lint test clean dist: readme contrib clean-dist build django-celery-beat-1.1.1/README.rst000066400000000000000000000177241324227125300166510ustar00rootroot00000000000000===================================================================== Database-backed Periodic Tasks ===================================================================== |build-status| |coverage| |license| |wheel| |pyversion| |pyimp| :Version: 1.1.1 :Web: http://django-celery-beat.readthedocs.io/ :Download: http://pypi.python.org/pypi/django-celery-beat :Source: http://github.com/celery/django-celery-beat :Keywords: django, celery, beat, periodic task, cron, scheduling About ===== This extension enables you to store the periodic task schedule in the database. The periodic tasks can be managed from the Django Admin interface, where you can create, edit and delete periodic tasks and how often they should run. Using the Extension =================== Usage and installation instructions for this extension are available from the `Celery documentation`_: http://docs.celeryproject.org/en/latest/userguide/periodic-tasks.html#using-custom-scheduler-classes .. _`Celery documentation`: http://docs.celeryproject.org/en/latest/userguide/periodic-tasks.html#using-custom-scheduler-classes Important Warning about Time Zones ================================== .. warning:: If you change the Django ``TIME_ZONE`` setting your periodic task schedule will still be based on the old timezone. To fix that you would have to reset the "last run time" for each periodic task:: >>> from django_celery_beat.models import PeriodicTask, PeriodicTasks >>> PeriodicTask.objects.all().update(last_run_at=None) >>> for task in PeriodicTask.objects.all(): >>> PeriodicTasks.changed(task) Note that this will reset the state as if the periodic tasks have never run before. Models ====== - ``django_celery_beat.models.PeriodicTask`` This model defines a single periodic task to be run. It must be associated with a schedule, which defines how often the task should run. - ``django_celery_beat.models.IntervalSchedule`` A schedule that runs at a specific interval (e.g. every 5 seconds). - ``django_celery_beat.models.CrontabSchedule`` A schedule with fields like entries in cron: ``minute hour day-of-week day_of_month month_of_year``. - ``django_celery_beat.models.PeriodicTasks`` This model is only used as an index to keep track of when the schedule has changed. Whenever you update a ``PeriodicTask`` a counter in this table is also incremented, which tells the ``celery beat`` service to reload the schedule from the database. If you update periodic tasks in bulk, you will need to update the counter manually:: >>> from django_celery_beat.models import PeriodicTasks >>> PeriodicTasks.changed() Example creating interval-based periodic task --------------------------------------------- To create a periodic task executing at an interval you must first create the interval object:: >>> from django_celery_beat.models import PeriodicTask, IntervalSchedule # executes every 10 seconds. >>> schedule, created = IntervalSchedule.objects.get_or_create( ... every=10, ... period=IntervalSchedule.SECONDS, ... ) That's all the fields you need: a period type and the frequency. You can choose between a specific set of periods: - ``IntervalSchedule.DAYS`` - ``IntervalSchedule.HOURS`` - ``IntervalSchedule.MINUTES`` - ``IntervalSchedule.SECONDS`` - ``IntervalSchedule.MICROSECONDS`` .. note:: If you have multiple periodic tasks executing every 10 seconds, then they should all point to the same schedule object. There's also a "choices tuple" available should you need to present this to the user:: >>> IntervalSchedule.PERIOD_CHOICES Now that we have defined the schedule object, we can create the periodic task entry:: >>> PeriodicTask.objects.create( ... interval=schedule, # we created this above. ... name='Importing contacts', # simply describes this periodic task. ... task='proj.tasks.import_contacts', # name of task. ... ) Note that this is a very basic example, you can also specify the arguments and keyword arguments used to execute the task, the ``queue`` to send it to[*], and set an expiry time. Here's an example specifying the arguments, note how JSON serialization is required:: >>> import json >>> from datetime import datetime, timedelta >>> PeriodicTask.objects.create( ... interval=schedule, # we created this above. ... name='Importing contacts', # simply describes this periodic task. ... task='proj.tasks.import_contacts', # name of task. ... args=json.dumps(['arg1', 'arg2']), ... kwargs=json.dumps({ ... 'be_careful': True, ... }), ... expires=datetime.utcnow() + timedelta(seconds=30) ... ) .. [*] you can also use low-level AMQP routing using the ``exchange`` and ``routing_key`` fields. Example creating crontab-based periodic task -------------------------------------------- A crontab schedule has the fields: ``minute``, ``hour``, ``day_of_week``, ``day_of_month`` and ``month_of_year`, so if you want the equivalent of a ``30 * * * *`` (execute every 30 minutes) crontab entry you specify:: >>> from django_celery_beat.models import CrontabSchedule, PeriodicTask >>> schedule, _ = CrontabSchedule.objects.get_or_create( ... minute='30', ... hour='*', ... day_of_week='*', ... day_of_month='*', ... month_of_year='*', ... ) Then to create a periodic task using this schedule, use the same approach as the interval-based periodic task earlier in this document, but instead of ``interval=schedule``, specify ``crontab=schedule``:: >>> PeriodicTask.objects.create( ... crontab=schedule, ... name='Importing contacts', ... task='proj.tasks.import_contacts', ... ) Temporarily disable a periodic task ----------------------------------- You can use the ``enabled`` flag to temporarily disable a periodic task:: >>> periodic_task.enabled = False >>> periodic_task.save() Installation ============ You can install django-celery-beat either via the Python Package Index (PyPI) or from source. To install using `pip`,:: $ pip install -U django-celery-beat Downloading and installing from source -------------------------------------- Download the latest version of django-celery-beat from http://pypi.python.org/pypi/django-celery-beat You can install it by doing the following,:: $ tar xvfz django-celery-beat-0.0.0.tar.gz $ cd django-celery-beat-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. Using the development version ----------------------------- With pip ~~~~~~~~ You can install the latest snapshot of django-celery-beat using the following pip command:: $ pip install https://github.com/celery/django-celery-beat/zipball/master#egg=django-celery-beat .. |build-status| image:: https://secure.travis-ci.org/celery/django-celery-beat.svg?branch=master :alt: Build status :target: https://travis-ci.org/celery/django-celery-beat .. |coverage| image:: https://codecov.io/github/celery/django-celery-beat/coverage.svg?branch=master :target: https://codecov.io/github/celery/django-celery-beat?branch=master .. |license| image:: https://img.shields.io/pypi/l/django-celery-beat.svg :alt: BSD License :target: https://opensource.org/licenses/BSD-3-Clause .. |wheel| image:: https://img.shields.io/pypi/wheel/django-celery-beat.svg :alt: django-celery-beat can be installed via wheel :target: http://pypi.python.org/pypi/django-celery-beat/ .. |pyversion| image:: https://img.shields.io/pypi/pyversions/django-celery-beat.svg :alt: Supported Python versions. :target: http://pypi.python.org/pypi/django-celery-beat/ .. |pyimp| image:: https://img.shields.io/pypi/implementation/django-celery-beat.svg :alt: Support Python implementations. :target: http://pypi.python.org/pypi/django-celery-beat/ django-celery-beat-1.1.1/appveyor.yml000066400000000000000000000024511324227125300175410ustar00rootroot00000000000000environment: 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-beat-1.1.1/django_celery_beat/000077500000000000000000000000001324227125300207475ustar00rootroot00000000000000django-celery-beat-1.1.1/django_celery_beat/__init__.py000066400000000000000000000017121324227125300230610ustar00rootroot00000000000000# -*- coding: utf-8 -*- """Database-backed Periodic Tasks.""" # :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__ = '1.1.1' __author__ = 'Ask Solem' __contact__ = 'ask@celeryproject.org' __homepage__ = 'https://github.com/celery/django-celery-beat' __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_beat.apps.BeatConfig' django-celery-beat-1.1.1/django_celery_beat/admin.py000066400000000000000000000140561324227125300224170ustar00rootroot00000000000000"""Periodic Task Admin interface.""" from __future__ import absolute_import, unicode_literals from django import forms from django.conf import settings from django.contrib import admin from django.forms.widgets import Select from django.template.defaultfilters import pluralize from django.utils.translation import ugettext_lazy as _ from celery import current_app from celery.utils import cached_property from kombu.utils.json import loads from .models import ( PeriodicTask, PeriodicTasks, IntervalSchedule, CrontabSchedule, SolarSchedule ) from .utils import is_database_scheduler try: from django.utils.encoding import force_text except ImportError: from django.utils.encoding import force_unicode as force_text # noqa class TaskSelectWidget(Select): """Widget that lets you choose between task names.""" celery_app = current_app _choices = None def tasks_as_choices(self): _ = self._modules # noqa tasks = list(sorted(name for name in self.celery_app.tasks if not name.startswith('celery.'))) return (('', ''), ) + tuple(zip(tasks, tasks)) @property def choices(self): if self._choices is None: self._choices = self.tasks_as_choices() return self._choices @choices.setter def choices(self, _): # ChoiceField.__init__ sets ``self.choices = choices`` # which would override ours. pass @cached_property def _modules(self): self.celery_app.loader.import_default_modules() class TaskChoiceField(forms.ChoiceField): """Field that lets you choose between task names.""" widget = TaskSelectWidget def valid_value(self, value): return True class PeriodicTaskForm(forms.ModelForm): """Form that lets you create and modify periodic tasks.""" regtask = TaskChoiceField( label=_('Task (registered)'), required=False, ) task = forms.CharField( label=_('Task (custom)'), required=False, max_length=200, ) class Meta: """Form metadata.""" model = PeriodicTask exclude = () def clean(self): data = super(PeriodicTaskForm, self).clean() regtask = data.get('regtask') if regtask: data['task'] = regtask if not data['task']: exc = forms.ValidationError(_('Need name of task')) self._errors['task'] = self.error_class(exc.messages) raise exc return data def _clean_json(self, field): value = self.cleaned_data[field] try: loads(value) except ValueError as exc: raise forms.ValidationError( _('Unable to parse JSON: %s') % exc, ) return value def clean_args(self): return self._clean_json('args') def clean_kwargs(self): return self._clean_json('kwargs') class PeriodicTaskAdmin(admin.ModelAdmin): """Admin-interface for periodic tasks.""" form = PeriodicTaskForm model = PeriodicTask celery_app = current_app list_display = ('__str__', 'enabled') actions = ('enable_tasks', 'disable_tasks', 'run_tasks') fieldsets = ( (None, { 'fields': ('name', 'regtask', 'task', 'enabled'), 'classes': ('extrapretty', 'wide'), }), ('Schedule', { 'fields': ('interval', 'crontab', 'solar'), 'classes': ('extrapretty', 'wide', ), }), ('Arguments', { 'fields': ('args', 'kwargs'), 'classes': ('extrapretty', 'wide', 'collapse', 'in'), }), ('Execution Options', { 'fields': ('expires', 'queue', 'exchange', 'routing_key'), 'classes': ('extrapretty', 'wide', 'collapse', 'in'), }), ) def changelist_view(self, request, extra_context=None): extra_context = extra_context or {} scheduler = getattr(settings, 'CELERY_BEAT_SCHEDULER', None) extra_context['wrong_scheduler'] = not is_database_scheduler(scheduler) return super(PeriodicTaskAdmin, self).changelist_view( request, extra_context) def get_queryset(self, request): qs = super(PeriodicTaskAdmin, self).get_queryset(request) return qs.select_related('interval', 'crontab', 'solar') def enable_tasks(self, request, queryset): rows_updated = queryset.update(enabled=True) PeriodicTasks.update_changed() self.message_user( request, _('{0} task{1} {2} successfully enabled').format( rows_updated, pluralize(rows_updated), pluralize(rows_updated, _('was,were')), ), ) enable_tasks.short_description = _('Enable selected tasks') def disable_tasks(self, request, queryset): rows_updated = queryset.update(enabled=False) PeriodicTasks.update_changed() self.message_user( request, _('{0} task{1} {2} successfully disabled').format( rows_updated, pluralize(rows_updated), pluralize(rows_updated, _('was,were')), ), ) disable_tasks.short_description = _('Disable selected tasks') def run_tasks(self, request, queryset): self.celery_app.loader.import_default_modules() tasks = [(self.celery_app.tasks.get(task.task), loads(task.args), loads(task.kwargs)) for task in queryset] task_ids = [task.delay(*args, **kwargs) for task, args, kwargs in tasks] tasks_run = len(task_ids) self.message_user( request, _('{0} task{1} {2} successfully run').format( tasks_run, pluralize(tasks_run), pluralize(tasks_run, _('was,were')), ), ) run_tasks.short_description = _('Run selected tasks') admin.site.register(IntervalSchedule) admin.site.register(CrontabSchedule) admin.site.register(SolarSchedule) admin.site.register(PeriodicTask, PeriodicTaskAdmin) django-celery-beat-1.1.1/django_celery_beat/apps.py000066400000000000000000000006321324227125300222650ustar00rootroot00000000000000"""Django Application configuration.""" from __future__ import absolute_import, unicode_literals from django.apps import AppConfig from django.utils.translation import ugettext_lazy as _ __all__ = ['BeatConfig'] class BeatConfig(AppConfig): """Default configuration for django_celery_beat app.""" name = 'django_celery_beat' label = 'django_celery_beat' verbose_name = _('Periodic Tasks') django-celery-beat-1.1.1/django_celery_beat/managers.py000066400000000000000000000017061324227125300231220ustar00rootroot00000000000000"""Model managers.""" from __future__ import absolute_import, unicode_literals from celery.five import items from django.db import models from django.db.models.query import QuerySet class ExtendedQuerySet(QuerySet): """Base class for query sets.""" def update_or_create(self, defaults=None, **kwargs): obj, created = self.get_or_create(defaults=defaults, **kwargs) if not created: self._update_model_with_dict(obj, dict(defaults or {}, **kwargs)) return obj def _update_model_with_dict(self, obj, fields): [setattr(obj, attr_name, attr_value) for attr_name, attr_value in items(fields)] obj.save() return obj class ExtendedManager(models.Manager.from_queryset(ExtendedQuerySet)): """Manager with common utilities.""" class PeriodicTaskManager(ExtendedManager): """Manager for PeriodicTask model.""" def enabled(self): return self.filter(enabled=True) django-celery-beat-1.1.1/django_celery_beat/migrations/000077500000000000000000000000001324227125300231235ustar00rootroot00000000000000django-celery-beat-1.1.1/django_celery_beat/migrations/0001_initial.py000066400000000000000000000130111324227125300255620ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Generated by Django 1.9.5 on 2016-08-04 02:13 from __future__ import absolute_import, unicode_literals from django.db import migrations, models import django.db.models.deletion class Migration(migrations.Migration): initial = True dependencies = [ ] operations = [ migrations.CreateModel( name='CrontabSchedule', fields=[ ('id', models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('minute', models.CharField( default='*', max_length=64, verbose_name='minute')), ('hour', models.CharField( default='*', max_length=64, verbose_name='hour')), ('day_of_week', models.CharField( default='*', max_length=64, verbose_name='day of week')), ('day_of_month', models.CharField( default='*', max_length=64, verbose_name='day of month')), ('month_of_year', models.CharField( default='*', max_length=64, verbose_name='month of year')), ], options={ 'ordering': [ 'month_of_year', 'day_of_month', 'day_of_week', 'hour', 'minute', ], 'verbose_name': 'crontab', 'verbose_name_plural': 'crontabs', }, ), migrations.CreateModel( name='IntervalSchedule', fields=[ ('id', models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('every', models.IntegerField(verbose_name='every')), ('period', models.CharField( choices=[ ('days', 'Days'), ('hours', 'Hours'), ('minutes', 'Minutes'), ('seconds', 'Seconds'), ('microseconds', 'Microseconds'), ], max_length=24, verbose_name='period')), ], options={ 'ordering': ['period', 'every'], 'verbose_name': 'interval', 'verbose_name_plural': 'intervals', }, ), migrations.CreateModel( name='PeriodicTask', fields=[ ('id', models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField( help_text='Useful description', max_length=200, unique=True, verbose_name='name')), ('task', models.CharField( max_length=200, verbose_name='task name')), ('args', models.TextField( blank=True, default='[]', help_text='JSON encoded positional arguments', verbose_name='Arguments')), ('kwargs', models.TextField( blank=True, default='{}', help_text='JSON encoded keyword arguments', verbose_name='Keyword arguments')), ('queue', models.CharField( blank=True, default=None, help_text='Queue defined in CELERY_TASK_QUEUES', max_length=200, null=True, verbose_name='queue')), ('exchange', models.CharField( blank=True, default=None, max_length=200, null=True, verbose_name='exchange')), ('routing_key', models.CharField( blank=True, default=None, max_length=200, null=True, verbose_name='routing key')), ('expires', models.DateTimeField( blank=True, null=True, verbose_name='expires')), ('enabled', models.BooleanField( default=True, verbose_name='enabled')), ('last_run_at', models.DateTimeField( blank=True, editable=False, null=True)), ('total_run_count', models.PositiveIntegerField( default=0, editable=False)), ('date_changed', models.DateTimeField(auto_now=True)), ('description', models.TextField( blank=True, verbose_name='description')), ('crontab', models.ForeignKey( blank=True, help_text='Use one of interval/crontab', null=True, on_delete=django.db.models.deletion.CASCADE, to='django_celery_beat.CrontabSchedule', verbose_name='crontab')), ('interval', models.ForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='django_celery_beat.IntervalSchedule', verbose_name='interval')), ], options={ 'verbose_name': 'periodic task', 'verbose_name_plural': 'periodic tasks', }, ), migrations.CreateModel( name='PeriodicTasks', fields=[ ('ident', models.SmallIntegerField( default=1, primary_key=True, serialize=False, unique=True)), ('last_update', models.DateTimeField()), ], ), ] django-celery-beat-1.1.1/django_celery_beat/migrations/0002_auto_20161118_0346.py000066400000000000000000000040421324227125300265450ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Generated by Django 1.10.3 on 2016-11-18 03:46 from __future__ import absolute_import, unicode_literals from django.db import migrations, models import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ ('django_celery_beat', '0001_initial'), ] operations = [ migrations.CreateModel( name='SolarSchedule', fields=[ ('id', models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('event', models.CharField( choices=[('dusk_nautical', 'dusk_nautical'), ('dawn_astronomical', 'dawn_astronomical'), ('dawn_nautical', 'dawn_nautical'), ('dawn_civil', 'dawn_civil'), ('sunset', 'sunset'), ('solar_noon', 'solar_noon'), ('dusk_astronomical', 'dusk_astronomical'), ('sunrise', 'sunrise'), ('dusk_civil', 'dusk_civil')], max_length=24, verbose_name='event')), ('latitude', models.DecimalField( decimal_places=6, max_digits=9, verbose_name='latitude')), ('longitude', models.DecimalField( decimal_places=6, max_digits=9, verbose_name='latitude')), ], options={ 'ordering': ['event', 'latitude', 'longitude'], 'verbose_name': 'solar', 'verbose_name_plural': 'solars', }, ), migrations.AddField( model_name='periodictask', name='solar', field=models.ForeignKey( blank=True, help_text='Use a solar schedule', null=True, on_delete=django.db.models.deletion.CASCADE, to='django_celery_beat.SolarSchedule', verbose_name='solar'), ), ] django-celery-beat-1.1.1/django_celery_beat/migrations/0003_auto_20161209_0049.py000066400000000000000000000013521324227125300265500ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Generated by Django 1.9.11 on 2016-12-09 00:49 from __future__ import absolute_import, unicode_literals from django.db import migrations class Migration(migrations.Migration): dependencies = [ ('django_celery_beat', '0002_auto_20161118_0346'), ] operations = [ migrations.AlterModelOptions( name='solarschedule', options={ 'ordering': ('event', 'latitude', 'longitude'), 'verbose_name': 'solar event', 'verbose_name_plural': 'solar events'}, ), migrations.AlterUniqueTogether( name='solarschedule', unique_together=set([('event', 'latitude', 'longitude')]), ), ] django-celery-beat-1.1.1/django_celery_beat/migrations/0004_auto_20170221_0000.py000066400000000000000000000010271324227125300265250ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import absolute_import, unicode_literals from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('django_celery_beat', '0003_auto_20161209_0049'), ] operations = [ migrations.AlterField( model_name='solarschedule', name='longitude', field=models.DecimalField( verbose_name='longitude', max_digits=9, decimal_places=6), ), ] django-celery-beat-1.1.1/django_celery_beat/migrations/0005_add_solarschedule_events_choices.py000066400000000000000000000017321324227125300326720ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Generated by Django 1.9.1 on 2017-11-01 15:53 from __future__ import absolute_import, unicode_literals from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('django_celery_beat', '0004_auto_20170221_0000'), ] operations = [ migrations.AlterField( model_name='solarschedule', name='event', field=models.CharField(choices=[ ('dawn_astronomical', 'dawn_astronomical'), ('dawn_civil', 'dawn_civil'), ('dawn_nautical', 'dawn_nautical'), ('dusk_astronomical', 'dusk_astronomical'), ('dusk_civil', 'dusk_civil'), ('dusk_nautical', 'dusk_nautical'), ('solar_noon', 'solar_noon'), ('sunrise', 'sunrise'), ('sunset', 'sunset') ], max_length=24, verbose_name='event'), ), ] django-celery-beat-1.1.1/django_celery_beat/migrations/0006_auto_20180210_1226.py000066400000000000000000000020001324227125300265310ustar00rootroot00000000000000# Generated by Django 2.0.1 on 2018-02-10 12:26 from __future__ import absolute_import, unicode_literals from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('django_celery_beat', '0005_add_solarschedule_events_choices'), ] operations = [ migrations.AlterField( model_name='crontabschedule', name='day_of_month', field=models.CharField(default='*', max_length=124, verbose_name='day of month'), ), migrations.AlterField( model_name='crontabschedule', name='hour', field=models.CharField(default='*', max_length=96, verbose_name='hour'), ), migrations.AlterField( model_name='crontabschedule', name='minute', field=models.CharField(default='*', max_length=240, verbose_name='minute'), ), ] django-celery-beat-1.1.1/django_celery_beat/migrations/__init__.py000066400000000000000000000000001324227125300252220ustar00rootroot00000000000000django-celery-beat-1.1.1/django_celery_beat/models.py000066400000000000000000000260511324227125300226100ustar00rootroot00000000000000"""Database models.""" from __future__ import absolute_import, unicode_literals from datetime import timedelta from django.core.exceptions import MultipleObjectsReturned, ValidationError from django.db import models from django.db.models import signals from django.utils.translation import ugettext_lazy as _ from celery import schedules from celery.five import python_2_unicode_compatible from . import managers from .utils import now, make_aware DAYS = 'days' HOURS = 'hours' MINUTES = 'minutes' SECONDS = 'seconds' MICROSECONDS = 'microseconds' PERIOD_CHOICES = ( (DAYS, _('Days')), (HOURS, _('Hours')), (MINUTES, _('Minutes')), (SECONDS, _('Seconds')), (MICROSECONDS, _('Microseconds')), ) SOLAR_SCHEDULES = [(x, _(x)) for x in sorted(schedules.solar._all_events)] def cronexp(field): """Representation of cron expression.""" return field and str(field).replace(' ', '') or '*' @python_2_unicode_compatible class SolarSchedule(models.Model): """Schedule following astronomical patterns.""" event = models.CharField( _('event'), max_length=24, choices=SOLAR_SCHEDULES ) latitude = models.DecimalField( _('latitude'), max_digits=9, decimal_places=6 ) longitude = models.DecimalField( _('longitude'), max_digits=9, decimal_places=6 ) class Meta: """Table information.""" verbose_name = _('solar event') verbose_name_plural = _('solar events') ordering = ('event', 'latitude', 'longitude') unique_together = ('event', 'latitude', 'longitude') @property def schedule(self): return schedules.solar(self.event, self.latitude, self.longitude, nowfun=lambda: make_aware(now())) @classmethod def from_schedule(cls, schedule): spec = {'event': schedule.event, 'latitude': schedule.lat, 'longitude': schedule.lon} try: return cls.objects.get(**spec) except cls.DoesNotExist: return cls(**spec) except MultipleObjectsReturned: cls.objects.filter(**spec).delete() return cls(**spec) def __str__(self): return '{0} ({1}, {2})'.format( self.get_event_display(), self.latitude, self.longitude ) @python_2_unicode_compatible class IntervalSchedule(models.Model): """Schedule executing every n seconds.""" DAYS = DAYS HOURS = HOURS MINUTES = MINUTES SECONDS = SECONDS MICROSECONDS = MICROSECONDS PERIOD_CHOICES = PERIOD_CHOICES every = models.IntegerField(_('every'), null=False) period = models.CharField( _('period'), max_length=24, choices=PERIOD_CHOICES, ) class Meta: """Table information.""" verbose_name = _('interval') verbose_name_plural = _('intervals') ordering = ['period', 'every'] @property def schedule(self): return schedules.schedule( timedelta(**{self.period: self.every}), nowfun=lambda: make_aware(now()) ) @classmethod def from_schedule(cls, schedule, period=SECONDS): every = max(schedule.run_every.total_seconds(), 0) try: return cls.objects.get(every=every, period=period) except cls.DoesNotExist: return cls(every=every, period=period) except MultipleObjectsReturned: cls.objects.filter(every=every, period=period).delete() return cls(every=every, period=period) def __str__(self): if self.every == 1: return _('every {0.period_singular}').format(self) return _('every {0.every} {0.period}').format(self) @property def period_singular(self): return self.period[:-1] @python_2_unicode_compatible class CrontabSchedule(models.Model): """Crontab-like schedule.""" # # The worst case scenario for day of month is a list of all 31 day numbers # '[1, 2, ..., 31]' which has a length of 115. Likewise, minute can be # 0..59 and hour can be 0..23. Ensure we can accomodate these by allowing # 4 chars for each value (what we save on 0-9 accomodates the []). # We leave the other fields at their historical length. # minute = models.CharField(_('minute'), max_length=60 * 4, default='*') hour = models.CharField(_('hour'), max_length=24 * 4, default='*') day_of_week = models.CharField( _('day of week'), max_length=64, default='*', ) day_of_month = models.CharField( _('day of month'), max_length=31 * 4, default='*', ) month_of_year = models.CharField( _('month of year'), max_length=64, default='*', ) class Meta: """Table information.""" verbose_name = _('crontab') verbose_name_plural = _('crontabs') ordering = ['month_of_year', 'day_of_month', 'day_of_week', 'hour', 'minute'] def __str__(self): return '{0} {1} {2} {3} {4} (m/h/d/dM/MY)'.format( cronexp(self.minute), cronexp(self.hour), cronexp(self.day_of_week), cronexp(self.day_of_month), cronexp(self.month_of_year), ) @property def schedule(self): return schedules.crontab(minute=self.minute, hour=self.hour, day_of_week=self.day_of_week, day_of_month=self.day_of_month, month_of_year=self.month_of_year, nowfun=lambda: make_aware(now())) @classmethod def from_schedule(cls, schedule): spec = {'minute': schedule._orig_minute, 'hour': schedule._orig_hour, 'day_of_week': schedule._orig_day_of_week, 'day_of_month': schedule._orig_day_of_month, 'month_of_year': schedule._orig_month_of_year} try: return cls.objects.get(**spec) except cls.DoesNotExist: return cls(**spec) except MultipleObjectsReturned: cls.objects.filter(**spec).delete() return cls(**spec) class PeriodicTasks(models.Model): """Helper table for tracking updates to periodic tasks.""" ident = models.SmallIntegerField(default=1, primary_key=True, unique=True) last_update = models.DateTimeField(null=False) objects = managers.ExtendedManager() @classmethod def changed(cls, instance, **kwargs): if not instance.no_changes: cls.update_changed() @classmethod def update_changed(cls, **kwargs): cls.objects.update_or_create(ident=1, defaults={'last_update': now()}) @classmethod def last_change(cls): try: return cls.objects.get(ident=1).last_update except cls.DoesNotExist: pass @python_2_unicode_compatible class PeriodicTask(models.Model): """Model representing a periodic task.""" name = models.CharField( _('name'), max_length=200, unique=True, help_text=_('Useful description'), ) task = models.CharField(_('task name'), max_length=200) interval = models.ForeignKey( IntervalSchedule, on_delete=models.CASCADE, null=True, blank=True, verbose_name=_('interval'), ) crontab = models.ForeignKey( CrontabSchedule, on_delete=models.CASCADE, null=True, blank=True, verbose_name=_('crontab'), help_text=_('Use one of interval/crontab'), ) solar = models.ForeignKey( SolarSchedule, on_delete=models.CASCADE, null=True, blank=True, verbose_name=_('solar'), help_text=_('Use a solar schedule') ) args = models.TextField( _('Arguments'), blank=True, default='[]', help_text=_('JSON encoded positional arguments'), ) kwargs = models.TextField( _('Keyword arguments'), blank=True, default='{}', help_text=_('JSON encoded keyword arguments'), ) queue = models.CharField( _('queue'), max_length=200, blank=True, null=True, default=None, help_text=_('Queue defined in CELERY_TASK_QUEUES'), ) exchange = models.CharField( _('exchange'), max_length=200, blank=True, null=True, default=None, ) routing_key = models.CharField( _('routing key'), max_length=200, blank=True, null=True, default=None, ) expires = models.DateTimeField( _('expires'), blank=True, null=True, ) enabled = models.BooleanField( _('enabled'), default=True, ) last_run_at = models.DateTimeField( auto_now=False, auto_now_add=False, editable=False, blank=True, null=True, ) total_run_count = models.PositiveIntegerField( default=0, editable=False, ) date_changed = models.DateTimeField(auto_now=True) description = models.TextField(_('description'), blank=True) objects = managers.PeriodicTaskManager() no_changes = False class Meta: """Table information.""" verbose_name = _('periodic task') verbose_name_plural = _('periodic tasks') def validate_unique(self, *args, **kwargs): super(PeriodicTask, self).validate_unique(*args, **kwargs) if not self.interval and not self.crontab and not self.solar: raise ValidationError({ 'interval': [ 'One of interval, crontab, or solar must be set.' ] }) if self.interval and self.crontab and self.solar: raise ValidationError({ 'crontab': [ 'Only one of interval, crontab, or solar must be set' ] }) def save(self, *args, **kwargs): self.exchange = self.exchange or None self.routing_key = self.routing_key or None self.queue = self.queue or None if not self.enabled: self.last_run_at = None super(PeriodicTask, self).save(*args, **kwargs) def __str__(self): fmt = '{0.name}: {{no schedule}}' if self.interval: fmt = '{0.name}: {0.interval}' if self.crontab: fmt = '{0.name}: {0.crontab}' if self.solar: fmt = '{0.name}: {0.solar}' return fmt.format(self) @property def schedule(self): if self.interval: return self.interval.schedule if self.crontab: return self.crontab.schedule if self.solar: return self.solar.schedule signals.pre_delete.connect(PeriodicTasks.changed, sender=PeriodicTask) signals.pre_save.connect(PeriodicTasks.changed, sender=PeriodicTask) signals.pre_delete.connect( PeriodicTasks.update_changed, sender=IntervalSchedule) signals.post_save.connect( PeriodicTasks.update_changed, sender=IntervalSchedule) signals.post_delete.connect( PeriodicTasks.update_changed, sender=CrontabSchedule) signals.post_save.connect( PeriodicTasks.update_changed, sender=CrontabSchedule) signals.post_delete.connect( PeriodicTasks.update_changed, sender=SolarSchedule) signals.post_save.connect( PeriodicTasks.update_changed, sender=SolarSchedule) django-celery-beat-1.1.1/django_celery_beat/schedulers.py000066400000000000000000000234711324227125300234710ustar00rootroot00000000000000"""Beat Scheduler Implementation.""" from __future__ import absolute_import, unicode_literals import logging from multiprocessing.util import Finalize from celery import current_app from celery import schedules from celery.beat import Scheduler, ScheduleEntry from celery.five import values, items from celery.utils.encoding import safe_str, safe_repr from celery.utils.log import get_logger from kombu.utils.json import dumps, loads from django.db import transaction, close_old_connections from django.db.utils import DatabaseError from django.core.exceptions import ObjectDoesNotExist from .models import ( PeriodicTask, PeriodicTasks, CrontabSchedule, IntervalSchedule, SolarSchedule, ) from .utils import make_aware try: from celery.utils.time import is_naive except ImportError: # pragma: no cover from celery.utils.timeutils import is_naive # noqa # This scheduler must wake up more frequently than the # regular of 5 minutes because it needs to take external # changes to the schedule into account. DEFAULT_MAX_INTERVAL = 5 # seconds ADD_ENTRY_ERROR = """\ Cannot add entry %r to database schedule: %r. Contents: %r """ logger = get_logger(__name__) debug, info = logger.debug, logger.info class ModelEntry(ScheduleEntry): """Scheduler entry taken from database row.""" model_schedules = ( (schedules.crontab, CrontabSchedule, 'crontab'), (schedules.schedule, IntervalSchedule, 'interval'), (schedules.solar, SolarSchedule, 'solar'), ) save_fields = ['last_run_at', 'total_run_count', 'no_changes'] def __init__(self, model, app=None): """Initialize the model entry.""" self.app = app or current_app._get_current_object() self.name = model.name self.task = model.task try: self.schedule = model.schedule except model.DoesNotExist: logger.error( 'Disabling schedule %s that was removed from database', self.name, ) self._disable(model) try: self.args = loads(model.args or '[]') self.kwargs = loads(model.kwargs or '{}') except ValueError as exc: logger.exception( 'Removing schedule %s for argument deseralization error: %r', self.name, exc, ) self._disable(model) self.options = { 'queue': model.queue, 'exchange': model.exchange, 'routing_key': model.routing_key, 'expires': model.expires, } self.total_run_count = model.total_run_count self.model = model if not model.last_run_at: model.last_run_at = self._default_now() self.last_run_at = make_aware(model.last_run_at) def _disable(self, model): model.no_changes = True model.enabled = False model.save() def is_due(self): if not self.model.enabled: return False, 5.0 # 5 second delay for re-enable. return self.schedule.is_due(self.last_run_at) def _default_now(self): now = self.app.now() # The PyTZ datetime must be localised for the Django-Celery-Beat # scheduler to work. Keep in mind that timezone arithmatic # with a localized timezone may be inaccurate. return now.tzinfo.localize(now.replace(tzinfo=None)) def __next__(self): self.model.last_run_at = self.app.now() self.model.total_run_count += 1 self.model.no_changes = True return self.__class__(self.model) next = __next__ # for 2to3 def save(self): # Object may not be synchronized, so only # change the fields we care about. obj = type(self.model)._default_manager.get(pk=self.model.pk) for field in self.save_fields: setattr(obj, field, getattr(self.model, field)) obj.save() @classmethod def to_model_schedule(cls, schedule): for schedule_type, model_type, model_field in cls.model_schedules: schedule = schedules.maybe_schedule(schedule) if isinstance(schedule, schedule_type): model_schedule = model_type.from_schedule(schedule) model_schedule.save() return model_schedule, model_field raise ValueError( 'Cannot convert schedule type {0!r} to model'.format(schedule)) @classmethod def from_entry(cls, name, app=None, **entry): return cls(PeriodicTask._default_manager.update_or_create( name=name, defaults=cls._unpack_fields(**entry), ), app=app) @classmethod def _unpack_fields(cls, schedule, args=None, kwargs=None, relative=None, options=None, **entry): model_schedule, model_field = cls.to_model_schedule(schedule) entry.update( {model_field: model_schedule}, args=dumps(args or []), kwargs=dumps(kwargs or {}), **cls._unpack_options(**options or {}) ) return entry @classmethod def _unpack_options(cls, queue=None, exchange=None, routing_key=None, **kwargs): return { 'queue': queue, 'exchange': exchange, 'routing_key': routing_key, } def __repr__(self): return ''.format( safe_str(self.name), self.task, safe_repr(self.args), safe_repr(self.kwargs), self.schedule, ) class DatabaseScheduler(Scheduler): """Database-backed Beat Scheduler.""" Entry = ModelEntry Model = PeriodicTask Changes = PeriodicTasks _schedule = None _last_timestamp = None _initial_read = False def __init__(self, *args, **kwargs): """Initialize the database scheduler.""" self._dirty = set() Scheduler.__init__(self, *args, **kwargs) self._finalize = Finalize(self, self.sync, exitpriority=5) self.max_interval = ( kwargs.get('max_interval') or self.app.conf.beat_max_loop_interval or DEFAULT_MAX_INTERVAL) def setup_schedule(self): self.install_default_entries(self.schedule) self.update_from_dict(self.app.conf.beat_schedule) def all_as_schedule(self): debug('DatabaseScheduler: Fetching database schedule') s = {} for model in self.Model.objects.enabled(): try: s[model.name] = self.Entry(model, app=self.app) except ValueError: pass return s def schedule_changed(self): try: # If MySQL is running with transaction isolation level # REPEATABLE-READ (default), then we won't see changes done by # other transactions until the current transaction is # committed (Issue #41). try: transaction.commit() except transaction.TransactionManagementError: pass # not in transaction management. last, ts = self._last_timestamp, self.Changes.last_change() except DatabaseError as exc: logger.exception('Database gave error: %r', exc) return False try: if ts and ts > (last if last else ts): return True finally: self._last_timestamp = ts return False def reserve(self, entry): new_entry = next(entry) # Need to store entry by name, because the entry may change # in the mean time. self._dirty.add(new_entry.name) return new_entry def sync(self): info('Writing entries...') _tried = set() try: close_old_connections() with transaction.atomic(): while self._dirty: try: name = self._dirty.pop() _tried.add(name) self.schedule[name].save() except (KeyError, ObjectDoesNotExist): pass except DatabaseError as exc: # retry later self._dirty |= _tried logger.exception('Database error while sync: %r', exc) def update_from_dict(self, mapping): s = {} for name, entry_fields in items(mapping): try: entry = self.Entry.from_entry(name, app=self.app, **entry_fields) if entry.model.enabled: s[name] = entry except Exception as exc: logger.error(ADD_ENTRY_ERROR, name, exc, entry_fields) self.schedule.update(s) def install_default_entries(self, data): entries = {} if self.app.conf.result_expires: entries.setdefault( 'celery.backend_cleanup', { 'task': 'celery.backend_cleanup', 'schedule': schedules.crontab('0', '4', '*'), 'options': {'expires': 12 * 3600}, }, ) self.update_from_dict(entries) @property def schedule(self): update = False if not self._initial_read: debug('DatabaseScheduler: initial read') update = True self._initial_read = True elif self.schedule_changed(): info('DatabaseScheduler: Schedule changed.') update = True if update: self.sync() self._schedule = self.all_as_schedule() # the schedule changed, invalidate the heap in Scheduler.tick self._heap = None if logger.isEnabledFor(logging.DEBUG): debug('Current schedule:\n%s', '\n'.join( repr(entry) for entry in values(self._schedule)), ) return self._schedule django-celery-beat-1.1.1/django_celery_beat/templates/000077500000000000000000000000001324227125300227455ustar00rootroot00000000000000django-celery-beat-1.1.1/django_celery_beat/templates/admin/000077500000000000000000000000001324227125300240355ustar00rootroot00000000000000django-celery-beat-1.1.1/django_celery_beat/templates/admin/djcelery/000077500000000000000000000000001324227125300256365ustar00rootroot00000000000000django-celery-beat-1.1.1/django_celery_beat/templates/admin/djcelery/change_list.html000066400000000000000000000013241324227125300310040ustar00rootroot00000000000000{% extends "admin/change_list.html" %} {% load i18n %} {% block breadcrumbs %} {% if wrong_scheduler %}
  • Periodic tasks won't be dispatched unless you set the CELERYBEAT_SCHEDULER setting to djcelery.schedulers.DatabaseScheduler, or specify it using the -S option to celerybeat
{% endif %} {% endblock %} django-celery-beat-1.1.1/django_celery_beat/utils.py000066400000000000000000000025171324227125300224660ustar00rootroot00000000000000"""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 is_aware = timezone.is_aware # see Issue #222 now_localtime = getattr(timezone, 'template_localtime', timezone.localtime) def make_aware(value): """Force datatime to have timezone information.""" if getattr(settings, 'USE_TZ', False): # naive datetimes are assumed to be in UTC. if timezone.is_naive(value): value = timezone.make_aware(value, timezone.utc) # then convert to the Django configured timezone. default_tz = timezone.get_default_timezone() value = timezone.localtime(value, default_tz) return value def now(): """Return the current date and time.""" if getattr(settings, 'USE_TZ', False): return now_localtime(timezone.now()) else: return timezone.now() def is_database_scheduler(scheduler): """Return true if Celery is configured to use the db scheduler.""" if not scheduler: return False from kombu.utils import symbol_by_name from .schedulers import DatabaseScheduler return ( scheduler == 'django' or issubclass(symbol_by_name(scheduler), DatabaseScheduler) ) django-celery-beat-1.1.1/docs/000077500000000000000000000000001324227125300160775ustar00rootroot00000000000000django-celery-beat-1.1.1/docs/Makefile000066400000000000000000000202511324227125300175370ustar00rootroot00000000000000# 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-beat-1.1.1/docs/_static/000077500000000000000000000000001324227125300175255ustar00rootroot00000000000000django-celery-beat-1.1.1/docs/_static/.keep000066400000000000000000000000001324227125300204400ustar00rootroot00000000000000django-celery-beat-1.1.1/docs/_templates/000077500000000000000000000000001324227125300202345ustar00rootroot00000000000000django-celery-beat-1.1.1/docs/_templates/.keep000066400000000000000000000000001324227125300211470ustar00rootroot00000000000000django-celery-beat-1.1.1/docs/changelog.rst000066400000000000000000000000321324227125300205530ustar00rootroot00000000000000.. include:: ../Changelog django-celery-beat-1.1.1/docs/conf.py000066400000000000000000000014511324227125300173770ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import absolute_import, unicode_literals import os from sphinx_celery import conf globals().update(conf.build_config( 'django_celery_beat', __file__, project='django_celery_beat', # version_dev='2.0', # version_stable='1.4', canonical_url='http://django-celery-beat.readthedocs.io', webdomain='', github_project='celery/django-celery-beat', copyright='2016', django_settings='proj.settings', include_intersphinx={'python', 'sphinx', 'django', 'celery'}, path_additions=[os.path.join(os.pardir, 't')], html_logo='images/logo.png', html_favicon='images/favicon.ico', html_prepend_sidebars=[], apicheck_ignore_modules=[ 'django_celery_beat.apps', r'django_celery_beat.migrations.*', ], )) django-celery-beat-1.1.1/docs/copyright.rst000066400000000000000000000017011324227125300206400ustar00rootroot00000000000000Copyright ========= *django-celery-beat 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-beat *documentation* is offered under the Creative Commons *Attribution-ShareAlike 4.0 International* license the django-celery-beat *software* is offered under the `BSD License (3 Clause) `_ django-celery-beat-1.1.1/docs/glossary.rst000066400000000000000000000001431324227125300204720ustar00rootroot00000000000000.. _glossary: Glossary ======== .. glossary:: :sorted: term Description of term django-celery-beat-1.1.1/docs/images/000077500000000000000000000000001324227125300173445ustar00rootroot00000000000000django-celery-beat-1.1.1/docs/images/favicon.ico000066400000000000000000000064441324227125300214750ustar00rootroot00000000000000‰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-beat-1.1.1/docs/images/logo.png000066400000000000000000000632121324227125300210160ustar00rootroot00000000000000‰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-beat-1.1.1/docs/includes/000077500000000000000000000000001324227125300177055ustar00rootroot00000000000000django-celery-beat-1.1.1/docs/includes/installation.txt000066400000000000000000000016451324227125300231550ustar00rootroot00000000000000Installation ============ You can install django-celery-beat either via the Python Package Index (PyPI) or from source. To install using `pip`,:: $ pip install -U django-celery-beat Downloading and installing from source -------------------------------------- Download the latest version of django-celery-beat from http://pypi.python.org/pypi/django-celery-beat You can install it by doing the following,:: $ tar xvfz django-celery-beat-0.0.0.tar.gz $ cd django-celery-beat-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. Using the development version ----------------------------- With pip ~~~~~~~~ You can install the latest snapshot of django-celery-beat using the following pip command:: $ pip install https://github.com/celery/django-celery-beat/zipball/master#egg=django-celery-beat django-celery-beat-1.1.1/docs/includes/introduction.txt000066400000000000000000000133041324227125300231700ustar00rootroot00000000000000:Version: 1.1.1 :Web: http://django-celery-beat.readthedocs.io/ :Download: http://pypi.python.org/pypi/django-celery-beat :Source: http://github.com/celery/django-celery-beat :Keywords: django, celery, beat, periodic task, cron, scheduling About ===== This extension enables you to store the periodic task schedule in the database. The periodic tasks can be managed from the Django Admin interface, where you can create, edit and delete periodic tasks and how often they should run. Using the Extension =================== Usage and installation instructions for this extension are available from the `Celery documentation`_: http://docs.celeryproject.org/en/latest/userguide/periodic-tasks.html#using-custom-scheduler-classes .. _`Celery documentation`: http://docs.celeryproject.org/en/latest/userguide/periodic-tasks.html#using-custom-scheduler-classes Important Warning about Time Zones ================================== .. warning:: If you change the Django ``TIME_ZONE`` setting your periodic task schedule will still be based on the old timezone. To fix that you would have to reset the "last run time" for each periodic task:: >>> from django_celery_beat.models import PeriodicTask, PeriodicTasks >>> PeriodicTask.objects.all().update(last_run_at=None) >>> PeriodicTasks.changed() Note that this will reset the state as if the periodic tasks have never run before. Models ====== - ``django_celery_beat.models.PeriodicTask`` This model defines a single periodic task to be run. It must be associated with a schedule, which defines how often the task should run. - ``django_celery_beat.models.IntervalSchedule`` A schedule that runs at a specific interval (e.g. every 5 seconds). - ``django_celery_beat.models.CrontabSchedule`` A schedule with fields like entries in cron: ``minute hour day-of-week day_of_month month_of_year``. - ``django_celery_beat.models.PeriodicTasks`` This model is only used as an index to keep track of when the schedule has changed. Whenever you update a ``PeriodicTask`` a counter in this table is also incremented, which tells the ``celery beat`` service to reload the schedule from the database. If you update periodic tasks in bulk, you will need to update the counter manually:: >>> from django_celery_beat.models import PeriodicTasks >>> PeriodicTasks.changed() Example creating interval-based periodic task --------------------------------------------- To create a periodic task executing at an interval you must first create the interval object:: >>> from django_celery_beat.models import PeriodicTask, IntervalSchedule # executes every 10 seconds. >>> schedule, created = IntervalSchedule.objects.get_or_create( ... every=10, ... period=IntervalSchedule.SECONDS, ... ) That's all the fields you need: a period type and the frequency. You can choose between a specific set of periods: - ``IntervalSchedule.DAYS`` - ``IntervalSchedule.HOURS`` - ``IntervalSchedule.MINUTES`` - ``IntervalSchedule.SECONDS`` - ``IntervalSchedule.MICROSECONDS`` .. note:: If you have multiple periodic tasks executing every 10 seconds, then they should all point to the same schedule object. There's also a "choices tuple" available should you need to present this to the user:: >>> IntervalSchedule.PERIOD_CHOICES Now that we have defined the schedule object, we can create the periodic task entry:: >>> PeriodicTask.objects.create( ... interval=schedule, # we created this above. ... name='Importing contacts', # simply describes this periodic task. ... task='proj.tasks.import_contacts', # name of task. ... ) Note that this is a very basic example, you can also specify the arguments and keyword arguments used to execute the task, the ``queue`` to send it to[*], and set an expiry time. Here's an example specifying the arguments, note how JSON serialization is required:: >>> import json >>> from datetime import datetime, timedelta >>> PeriodicTask.objects.create( ... interval=schedule, # we created this above. ... name='Importing contacts', # simply describes this periodic task. ... task='proj.tasks.import_contacts', # name of task. ... args=json.dumps(['arg1', 'arg2']), ... kwargs=json.dumps({ ... 'be_careful': True, ... }), ... expires=datetime.utcnow() + timedelta(seconds=30) ... ) .. [*] you can also use low-level AMQP routing using the ``exchange`` and ``routing_key`` fields. Example creating crontab-based periodic task -------------------------------------------- A crontab schedule has the fields: ``minute``, ``hour``, ``day_of_week``, ``day_of_month`` and ``month_of_year`, so if you want the equivalent of a ``30 * * * *`` (execute every 30 minutes) crontab entry you specify:: >>> from django_celery_beat.models import CrontabSchedule, PeriodicTask >>> schedule, _ = CrontabSchedule.objects.get_or_create( ... minute='30', ... hour='*', ... day_of_week='*', ... day_of_month='*', ... month_of_year='*', ... ) Then to create a periodic task using this schedule, use the same approach as the interval-based periodic task earlier in this document, but instead of ``interval=schedule``, specify ``crontab=schedule``:: >>> PeriodicTask.objects.create( ... crontab=schedule, ... name='Importing contacts', ... task='proj.tasks.import_contacts', ... ) Temporarily disable a periodic task ----------------------------------- You can use the ``enabled`` flag to temporarily disable a periodic task:: >>> periodic_task.enabled = False >>> periodic_task.save() django-celery-beat-1.1.1/docs/index.rst000066400000000000000000000007721324227125300177460ustar00rootroot00000000000000======================================================================= django-celery-beat - Database-backed Periodic Tasks ======================================================================= .. 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-beat-1.1.1/docs/make.bat000066400000000000000000000160441324227125300175110ustar00rootroot00000000000000@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-beat-1.1.1/docs/reference/000077500000000000000000000000001324227125300200355ustar00rootroot00000000000000django-celery-beat-1.1.1/docs/reference/django-celery-beat.admin.rst000066400000000000000000000004351324227125300253140ustar00rootroot00000000000000===================================================== ``django_celery_beat.admin`` ===================================================== .. contents:: :local: .. currentmodule:: django_celery_beat.admin .. automodule:: django_celery_beat.admin :members: :undoc-members: django-celery-beat-1.1.1/docs/reference/django-celery-beat.managers.rst000066400000000000000000000004461324227125300260230ustar00rootroot00000000000000===================================================== ``django_celery_beat.managers`` ===================================================== .. contents:: :local: .. currentmodule:: django_celery_beat.managers .. automodule:: django_celery_beat.managers :members: :undoc-members: django-celery-beat-1.1.1/docs/reference/django-celery-beat.models.rst000066400000000000000000000004401324227125300255030ustar00rootroot00000000000000===================================================== ``django_celery_beat.models`` ===================================================== .. contents:: :local: .. currentmodule:: django_celery_beat.models .. automodule:: django_celery_beat.models :members: :undoc-members: django-celery-beat-1.1.1/docs/reference/django-celery-beat.rst000066400000000000000000000004131324227125300242210ustar00rootroot00000000000000===================================================== ``django_celery_beat`` ===================================================== .. contents:: :local: .. currentmodule:: django_celery_beat .. automodule:: django_celery_beat :members: :undoc-members: django-celery-beat-1.1.1/docs/reference/django-celery-beat.schedulers.rst000066400000000000000000000004541324227125300263660ustar00rootroot00000000000000===================================================== ``django_celery_beat.schedulers`` ===================================================== .. contents:: :local: .. currentmodule:: django_celery_beat.schedulers .. automodule:: django_celery_beat.schedulers :members: :undoc-members: django-celery-beat-1.1.1/docs/reference/django-celery-beat.utils.rst000066400000000000000000000004351324227125300253640ustar00rootroot00000000000000===================================================== ``django_celery_beat.utils`` ===================================================== .. contents:: :local: .. currentmodule:: django_celery_beat.utils .. automodule:: django_celery_beat.utils :members: :undoc-members: django-celery-beat-1.1.1/docs/reference/index.rst000066400000000000000000000004611324227125300216770ustar00rootroot00000000000000.. _apiref: =============== API Reference =============== :Release: |version| :Date: |today| .. toctree:: :maxdepth: 1 django-celery-beat django-celery-beat.models django-celery-beat.managers django-celery-beat.schedulers django-celery-beat.admin django-celery-beat.utils django-celery-beat-1.1.1/docs/templates/000077500000000000000000000000001324227125300200755ustar00rootroot00000000000000django-celery-beat-1.1.1/docs/templates/readme.txt000066400000000000000000000026021324227125300220730ustar00rootroot00000000000000===================================================================== Database-backed Periodic Tasks ===================================================================== |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-beat.svg?branch=master :alt: Build status :target: https://travis-ci.org/celery/django-celery-beat .. |coverage| image:: https://codecov.io/github/celery/django-celery-beat/coverage.svg?branch=master :target: https://codecov.io/github/celery/django-celery-beat?branch=master .. |license| image:: https://img.shields.io/pypi/l/django-celery-beat.svg :alt: BSD License :target: https://opensource.org/licenses/BSD-3-Clause .. |wheel| image:: https://img.shields.io/pypi/wheel/django-celery-beat.svg :alt: django-celery-beat can be installed via wheel :target: http://pypi.python.org/pypi/django-celery-beat/ .. |pyversion| image:: https://img.shields.io/pypi/pyversions/django-celery-beat.svg :alt: Supported Python versions. :target: http://pypi.python.org/pypi/django-celery-beat/ .. |pyimp| image:: https://img.shields.io/pypi/implementation/django-celery-beat.svg :alt: Support Python implementations. :target: http://pypi.python.org/pypi/django-celery-beat/ django-celery-beat-1.1.1/extra/000077500000000000000000000000001324227125300162725ustar00rootroot00000000000000django-celery-beat-1.1.1/extra/appveyor/000077500000000000000000000000001324227125300201375ustar00rootroot00000000000000django-celery-beat-1.1.1/extra/appveyor/install.ps1000066400000000000000000000053421324227125300222360ustar00rootroot00000000000000# 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-beat-1.1.1/extra/appveyor/run_with_compiler.cmd000066400000000000000000000034621324227125300243620ustar00rootroot00000000000000:: 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-beat-1.1.1/manage.py000077500000000000000000000004621324227125300167560ustar00rootroot00000000000000#!/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-beat-1.1.1/requirements/000077500000000000000000000000001324227125300176725ustar00rootroot00000000000000django-celery-beat-1.1.1/requirements/default.txt000066400000000000000000000000211324227125300220500ustar00rootroot00000000000000celery>=4.0,<5.0 django-celery-beat-1.1.1/requirements/docs.txt000066400000000000000000000001101324227125300213530ustar00rootroot00000000000000https://github.com/celery/sphinx_celery/archive/master.zip Django>=1.10 django-celery-beat-1.1.1/requirements/pkgutils.txt000066400000000000000000000001601324227125300222720ustar00rootroot00000000000000setuptools>=20.6.7 wheel>=0.29.0 flake8>=2.5.4 flakeplus>=1.1 tox>=2.3.1 sphinx2rst>=1.0 bumpversion pydocstyle django-celery-beat-1.1.1/requirements/test-ci.txt000066400000000000000000000000231324227125300217760ustar00rootroot00000000000000pytest-cov codecov django-celery-beat-1.1.1/requirements/test-django.txt000066400000000000000000000000071324227125300226470ustar00rootroot00000000000000django django-celery-beat-1.1.1/requirements/test-django110.txt000066400000000000000000000000231324227125300230670ustar00rootroot00000000000000django>=1.10,<1.11 django-celery-beat-1.1.1/requirements/test-django111.txt000066400000000000000000000000221324227125300230670ustar00rootroot00000000000000django>=1.11,<2.0 django-celery-beat-1.1.1/requirements/test-django18.txt000066400000000000000000000000211324227125300230140ustar00rootroot00000000000000django>=1.8,<1.9 django-celery-beat-1.1.1/requirements/test-django19.txt000066400000000000000000000000221324227125300230160ustar00rootroot00000000000000django>=1.9,<1.10 django-celery-beat-1.1.1/requirements/test-django20.txt000066400000000000000000000000211324227125300230050ustar00rootroot00000000000000django>=2.0,<2.1 django-celery-beat-1.1.1/requirements/test.txt000066400000000000000000000000571324227125300214140ustar00rootroot00000000000000case>=1.3.1 pytest>=3.0 pytest-django pytz>dev django-celery-beat-1.1.1/setup.cfg000066400000000000000000000005311324227125300167670ustar00rootroot00000000000000[tool:pytest] testpaths = t/unit/ python_classes = test_* DJANGO_SETTINGS_MODULE=t.proj.settings [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-beat-1.1.1/setup.py000066400000000000000000000104041324227125300166600ustar00rootroot00000000000000#!/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-beat' PACKAGE = 'django_celery_beat' E_UNSUPPORTED_PYTHON = '%s 1.0 requires %%s %%s or later!' % (NAME,) PYIMP = _pyimp() PY26_OR_LESS = sys.version_info < (2, 7) PY3 = sys.version_info[0] == 3 PY33_OR_LESS = PY3 and sys.version_info < (3, 4) PYPY_VERSION = getattr(sys, 'pypy_version_info', None) PYPY = PYPY_VERSION is not None PYPY24_ATLEAST = PYPY_VERSION and PYPY_VERSION >= (2, 4) if PY26_OR_LESS: raise Exception(E_UNSUPPORTED_PYTHON % (PYIMP, '2.7')) elif PY33_OR_LESS and not PYPY24_ATLEAST: raise Exception(E_UNSUPPORTED_PYTHON % (PYIMP, '3.4')) # -*- Classifiers -*- classes = """ Development Status :: 5 - Production/Stable License :: OSI Approved :: BSD License Programming Language :: Python Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy Framework :: Django Framework :: Django :: 1.8 Framework :: Django :: 1.9 Framework :: Django :: 1.10 Framework :: Django :: 1.11 Framework :: Django :: 2.0 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, PACKAGE, '__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 py.test')] 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, keywords='django celery beat periodic task database', author=meta['author'], author_email=meta['contact'], url=meta['homepage'], platforms=['any'], license='BSD', install_requires=reqs('default.txt'), tests_require=reqs('test.txt') + reqs('test-django.txt'), cmdclass={'test': pytest}, classifiers=classifiers, entry_points={ 'celery.beat_schedulers': [ 'django = django_celery_beat.schedulers:DatabaseScheduler', ], }, include_package_data=False, zip_safe=False, ) django-celery-beat-1.1.1/t/000077500000000000000000000000001324227125300154125ustar00rootroot00000000000000django-celery-beat-1.1.1/t/.coveragerc000066400000000000000000000003561324227125300175370ustar00rootroot00000000000000[run] branch = 1 cover_pylib = 0 include=*thorn* omit = */tests/*;testproj/*;testapp/*;*/migrations/* [report] omit = */python?.?/* */site-packages/* */pypy/* */tests/* */testproj/* */testapp/* */migrations/* django-celery-beat-1.1.1/t/__init__.py000066400000000000000000000000001324227125300175110ustar00rootroot00000000000000django-celery-beat-1.1.1/t/proj/000077500000000000000000000000001324227125300163645ustar00rootroot00000000000000django-celery-beat-1.1.1/t/proj/__init__.py000066400000000000000000000000561324227125300204760ustar00rootroot00000000000000from .celery import app as celery_app # noqa django-celery-beat-1.1.1/t/proj/celery.py000066400000000000000000000005641324227125300202260ustar00rootroot00000000000000from __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-beat-1.1.1/t/proj/settings.py000066400000000000000000000061251324227125300206020ustar00rootroot00000000000000""" 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 CELERY_DEFAULT_EXCHANGE = 'testcelery' CELERY_DEFAULT_ROUTING_KEY = 'testcelery' CELERY_DEFAULT_QUEUE = 'testcelery' CELERY_QUEUES = {'testcelery': {'binding_key': 'testcelery'}} CELERY_ACCEPT_CONTENT = ['pickle', 'json'] CELERY_TASK_SERIALIZER = 'pickle' CELERY_RESULT_SERIALIZER = 'pickle' # 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))) # 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_beat', ] MIDDLEWARE_CLASSES = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', ] 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' # Database # https://docs.djangoproject.com/en/1.9/ref/settings/#databases DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 'OPTIONS': { 'timeout': 1000, }, } } # 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 # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.9/howto/static-files/ STATIC_URL = '/static/' django-celery-beat-1.1.1/t/proj/urls.py000066400000000000000000000002651324227125300177260ustar00rootroot00000000000000from __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-beat-1.1.1/t/proj/wsgi.py000066400000000000000000000006721324227125300177140ustar00rootroot00000000000000""" 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", "proj.settings") application = get_wsgi_application() django-celery-beat-1.1.1/t/unit/000077500000000000000000000000001324227125300163715ustar00rootroot00000000000000django-celery-beat-1.1.1/t/unit/__init__.py000066400000000000000000000000001324227125300204700ustar00rootroot00000000000000django-celery-beat-1.1.1/t/unit/conftest.py000066400000000000000000000016271324227125300205760ustar00rootroot00000000000000from __future__ import absolute_import, unicode_literals import pytest from celery.contrib.testing.app import TestApp, Trap from celery.contrib.pytest import depends_on_current_app __all__ = ['app', 'depends_on_current_app'] @pytest.fixture(scope='session', autouse=True) def setup_default_app_trap(): from celery._state import set_default_app set_default_app(Trap()) @pytest.fixture() def app(celery_app): return celery_app @pytest.fixture(autouse=True) def test_cases_shortcuts(request, app, patching): if request.instance: @app.task def add(x, y): return x + y # IMPORTANT: We set an .app attribute for every test case class. request.instance.app = app request.instance.Celery = TestApp request.instance.add = add request.instance.patching = patching yield if request.instance: request.instance.app = None django-celery-beat-1.1.1/t/unit/test_schedulers.py000066400000000000000000000431251324227125300221500ustar00rootroot00000000000000from __future__ import absolute_import, unicode_literals import pytest from datetime import datetime, timedelta from itertools import count from django.contrib.admin.sites import AdminSite from django.contrib.messages.storage.fallback import FallbackStorage from django.test import RequestFactory from celery.five import monotonic, text_t from celery.schedules import schedule, crontab, solar from django_celery_beat import schedulers from django_celery_beat.admin import PeriodicTaskAdmin from django_celery_beat.models import ( PeriodicTask, PeriodicTasks, IntervalSchedule, CrontabSchedule, SolarSchedule ) from django_celery_beat.utils import make_aware _ids = count(0) @pytest.fixture(autouse=True) def no_multiprocessing_finalizers(patching): patching('multiprocessing.util.Finalize') patching('django_celery_beat.schedulers.Finalize') class EntryTrackSave(schedulers.ModelEntry): def __init__(self, *args, **kwargs): self.saved = 0 super(EntryTrackSave, self).__init__(*args, **kwargs) def save(self): self.saved += 1 super(EntryTrackSave, self).save() class EntrySaveRaises(schedulers.ModelEntry): def save(self): raise RuntimeError('this is expected') class TrackingScheduler(schedulers.DatabaseScheduler): Entry = EntryTrackSave def __init__(self, *args, **kwargs): self.flushed = 0 schedulers.DatabaseScheduler.__init__(self, *args, **kwargs) def sync(self): self.flushed += 1 schedulers.DatabaseScheduler.sync(self) @pytest.mark.django_db() class SchedulerCase: def create_model_interval(self, schedule, **kwargs): interval = IntervalSchedule.from_schedule(schedule) interval.save() return self.create_model(interval=interval, **kwargs) def create_model_crontab(self, schedule, **kwargs): crontab = CrontabSchedule.from_schedule(schedule) crontab.save() return self.create_model(crontab=crontab, **kwargs) def create_model_solar(self, schedule, **kwargs): solar = SolarSchedule.from_schedule(schedule) solar.save() return self.create_model(solar=solar, **kwargs) def create_conf_entry(self): name = 'thefoo{0}'.format(next(_ids)) return name, dict( task='djcelery.unittest.add{0}'.format(next(_ids)), schedule=timedelta(0, 600), args=(), relative=False, kwargs={}, options={'queue': 'extra_queue'} ) def create_model(self, Model=PeriodicTask, **kwargs): entry = dict( name='thefoo{0}'.format(next(_ids)), task='djcelery.unittest.add{0}'.format(next(_ids)), args='[2, 2]', kwargs='{"callback": "foo"}', queue='xaz', routing_key='cpu', exchange='foo', ) return Model(**dict(entry, **kwargs)) @pytest.mark.django_db() class test_ModelEntry(SchedulerCase): Entry = EntryTrackSave def test_entry(self): m = self.create_model_interval(schedule(timedelta(seconds=10))) e = self.Entry(m, app=self.app) assert e.args == [2, 2] assert e.kwargs == {'callback': 'foo'} assert e.schedule assert e.total_run_count == 0 assert isinstance(e.last_run_at, datetime) assert e.options['queue'] == 'xaz' assert e.options['exchange'] == 'foo' assert e.options['routing_key'] == 'cpu' right_now = self.app.now() m2 = self.create_model_interval( schedule(timedelta(seconds=10)), last_run_at=right_now, ) assert m2.last_run_at e2 = self.Entry(m2, app=self.app) assert e2.last_run_at is right_now e3 = e2.next() assert e3.last_run_at > e2.last_run_at assert e3.total_run_count == 1 @pytest.mark.django_db() class test_DatabaseSchedulerFromAppConf(SchedulerCase): Scheduler = TrackingScheduler @pytest.mark.django_db() @pytest.fixture(autouse=True) def setup_scheduler(self, app): self.app = app self.entry_name, entry = self.create_conf_entry() self.app.conf.beat_schedule = {self.entry_name: entry} self.m1 = PeriodicTask(name=self.entry_name) def test_constructor(self): s = self.Scheduler(app=self.app) assert isinstance(s._dirty, set) assert s._last_sync is None assert s.sync_every def test_periodic_task_model_enabled_schedule(self): s = self.Scheduler(app=self.app) sched = s.schedule assert len(sched) == 2 assert 'celery.backend_cleanup' in sched assert self.entry_name in sched for n, e in sched.items(): assert isinstance(e, s.Entry) def test_periodic_task_model_disabled_schedule(self): self.m1.enabled = False self.m1.save() s = self.Scheduler(app=self.app) sched = s.schedule assert sched assert len(sched) == 1 assert 'celery.backend_cleanup' in sched assert self.entry_name not in sched @pytest.mark.django_db() class test_DatabaseScheduler(SchedulerCase): Scheduler = TrackingScheduler @pytest.mark.django_db() @pytest.fixture(autouse=True) def setup_scheduler(self, app): self.app = app self.app.conf.beat_schedule = {} self.m1 = self.create_model_interval( schedule(timedelta(seconds=10))) self.m1.save() self.m1.refresh_from_db() self.m2 = self.create_model_interval( schedule(timedelta(minutes=20))) self.m2.save() self.m2.refresh_from_db() self.m3 = self.create_model_crontab( crontab(minute='2,4,5')) self.m3.save() self.m3.refresh_from_db() self.m4 = self.create_model_solar( solar('solar_noon', 48.06, 12.86)) self.m4.save() self.m4.refresh_from_db() # disabled, should not be in schedule m5 = self.create_model_interval( schedule(timedelta(seconds=1))) m5.enabled = False m5.save() self.s = self.Scheduler(app=self.app) def test_constructor(self): assert isinstance(self.s._dirty, set) assert self.s._last_sync is None assert self.s.sync_every def test_all_as_schedule(self): sched = self.s.schedule assert sched assert len(sched) == 5 assert 'celery.backend_cleanup' in sched for n, e in sched.items(): assert isinstance(e, self.s.Entry) def test_schedule_changed(self): self.m2.args = '[16, 16]' self.m2.save() e2 = self.s.schedule[self.m2.name] assert e2.args == [16, 16] self.m1.args = '[32, 32]' self.m1.save() e1 = self.s.schedule[self.m1.name] assert e1.args == [32, 32] e1 = self.s.schedule[self.m1.name] assert e1.args == [32, 32] self.m3.delete() with pytest.raises(KeyError): self.s.schedule.__getitem__(self.m3.name) def test_should_sync(self): assert self.s.should_sync() self.s._last_sync = monotonic() assert not self.s.should_sync() self.s._last_sync -= self.s.sync_every assert self.s.should_sync() def test_reserve(self): e1 = self.s.schedule[self.m1.name] self.s.schedule[self.m1.name] = self.s.reserve(e1) assert self.s.flushed == 1 e2 = self.s.schedule[self.m2.name] self.s.schedule[self.m2.name] = self.s.reserve(e2) assert self.s.flushed == 1 assert self.m2.name in self.s._dirty def test_sync_saves_last_run_at(self): e1 = self.s.schedule[self.m2.name] last_run = e1.last_run_at last_run2 = last_run - timedelta(days=1) e1.model.last_run_at = last_run2 self.s._dirty.add(self.m2.name) self.s.sync() e2 = self.s.schedule[self.m2.name] assert e2.last_run_at == last_run2 def test_sync_syncs_before_save(self): # Get the entry for m2 e1 = self.s.schedule[self.m2.name] # Increment the entry (but make sure it doesn't sync) self.s._last_sync = monotonic() e2 = self.s.schedule[e1.name] = self.s.reserve(e1) assert self.s.flushed == 1 # Fetch the raw object from db, change the args # and save the changes. m2 = PeriodicTask.objects.get(pk=self.m2.pk) m2.args = '[16, 16]' m2.save() # get_schedule should now see the schedule has changed. # and also sync the dirty objects. e3 = self.s.schedule[self.m2.name] assert self.s.flushed == 2 assert e3.last_run_at == e2.last_run_at assert e3.args == [16, 16] def test_periodic_task_disabled_and_enabled(self): # Get the entry for m2 e1 = self.s.schedule[self.m2.name] # Increment the entry (but make sure it doesn't sync) self.s._last_sync = monotonic() self.s.schedule[e1.name] = self.s.reserve(e1) assert self.s.flushed == 1 # Fetch the raw object from db, change the args # and save the changes. m2 = PeriodicTask.objects.get(pk=self.m2.pk) m2.enabled = False m2.save() # get_schedule should now see the schedule has changed. # and remove entry for m2 assert self.m2.name not in self.s.schedule assert self.s.flushed == 2 m2.enabled = True m2.save() # get_schedule should now see the schedule has changed. # and add entry for m2 assert self.m2.name in self.s.schedule assert self.s.flushed == 3 def test_periodic_task_disabled_while_reserved(self): # Get the entry for m2 e1 = self.s.schedule[self.m2.name] # Increment the entry (but make sure it doesn't sync) self.s._last_sync = monotonic() e2 = self.s.schedule[e1.name] = self.s.reserve(e1) assert self.s.flushed == 1 # Fetch the raw object from db, change the args # and save the changes. m2 = PeriodicTask.objects.get(pk=self.m2.pk) m2.enabled = False m2.save() # reserve is called because the task gets called from # tick after the database change is made self.s.reserve(e2) # get_schedule should now see the schedule has changed. # and remove entry for m2 assert self.m2.name not in self.s.schedule assert self.s.flushed == 2 def test_sync_not_dirty(self): self.s._dirty.clear() self.s.sync() def test_sync_object_gone(self): self.s._dirty.add('does-not-exist') self.s.sync() def test_sync_rollback_on_save_error(self): self.s.schedule[self.m1.name] = EntrySaveRaises(self.m1, app=self.app) self.s._dirty.add(self.m1.name) with pytest.raises(RuntimeError): self.s.sync() @pytest.mark.django_db() class test_models(SchedulerCase): def test_IntervalSchedule_unicode(self): assert (text_t(IntervalSchedule(every=1, period='seconds')) == 'every second') assert (text_t(IntervalSchedule(every=10, period='seconds')) == 'every 10 seconds') def test_CrontabSchedule_unicode(self): assert text_t(CrontabSchedule( minute=3, hour=3, day_of_week=None, )) == '3 3 * * * (m/h/d/dM/MY)' assert text_t(CrontabSchedule( minute=3, hour=3, day_of_week='tue', day_of_month='*/2', month_of_year='4,6', )) == '3 3 tue */2 4,6 (m/h/d/dM/MY)' def test_PeriodicTask_unicode_interval(self): p = self.create_model_interval(schedule(timedelta(seconds=10))) assert text_t(p) == '{0}: every 10.0 seconds'.format(p.name) def test_PeriodicTask_unicode_crontab(self): p = self.create_model_crontab(crontab( hour='4, 5', day_of_week='4, 5', )) assert text_t(p) == '{0}: * 4,5 4,5 * * (m/h/d/dM/MY)'.format(p.name) def test_PeriodicTask_unicode_solar(self): p = self.create_model_solar( solar('solar_noon', 48.06, 12.86), name='solar_event' ) assert text_t(p) == 'solar_event: {0} ({1}, {2})'.format( 'solar_noon', '48.06', '12.86' ) def test_PeriodicTask_schedule_property(self): p1 = self.create_model_interval(schedule(timedelta(seconds=10))) s1 = p1.schedule assert s1.run_every.total_seconds() == 10 p2 = self.create_model_crontab(crontab( hour='4, 5', minute='10,20,30', day_of_month='1-7', month_of_year='*/3', )) s2 = p2.schedule assert s2.hour == {4, 5} assert s2.minute == {10, 20, 30} assert s2.day_of_week == {0, 1, 2, 3, 4, 5, 6} assert s2.day_of_month == {1, 2, 3, 4, 5, 6, 7} assert s2.month_of_year == {1, 4, 7, 10} def test_PeriodicTask_unicode_no_schedule(self): p = self.create_model() assert text_t(p) == '{0}: {{no schedule}}'.format(p.name) def test_CrontabSchedule_schedule(self): s = CrontabSchedule( minute='3, 7', hour='3, 4', day_of_week='*', day_of_month='1, 16', month_of_year='1, 7', ) assert s.schedule.minute == {3, 7} assert s.schedule.hour == {3, 4} assert s.schedule.day_of_week == {0, 1, 2, 3, 4, 5, 6} assert s.schedule.day_of_month == {1, 16} assert s.schedule.month_of_year == {1, 7} def test_CrontabSchedule_long_schedule(self): s = CrontabSchedule( minute=str(list(range(60)))[1:-1], hour=str(list(range(24)))[1:-1], day_of_week=str(list(range(7)))[1:-1], day_of_month=str(list(range(1, 32)))[1:-1], month_of_year=str(list(range(1, 13)))[1:-1] ) assert s.schedule.minute == set(range(60)) assert s.schedule.hour == set(range(24)) assert s.schedule.day_of_week == set(range(7)) assert s.schedule.day_of_month == set(range(1, 32)) assert s.schedule.month_of_year == set(range(1, 13)) fields = [ 'minute', 'hour', 'day_of_week', 'day_of_month', 'month_of_year' ] for field in fields: str_length = len(str(getattr(s.schedule, field))) field_length = s._meta.get_field(field).max_length assert str_length <= field_length def test_SolarSchedule_schedule(self): s = SolarSchedule(event='solar_noon', latitude=48.06, longitude=12.86) dt = datetime(day=26, month=7, year=2050, hour=1, minute=0) dt_lastrun = make_aware(dt) assert s.schedule is not None isdue, nextcheck = s.schedule.is_due(dt_lastrun) assert isdue is False # False means task isn't due, but keep checking. assert (nextcheck > 0) and (isdue is False) or \ (nextcheck == s.max_interval) and (isdue is True) s2 = SolarSchedule(event='solar_noon', latitude=48.06, longitude=12.86) dt2 = datetime(day=26, month=7, year=2000, hour=1, minute=0) dt2_lastrun = make_aware(dt2) assert s2.schedule is not None isdue2, nextcheck2 = s2.schedule.is_due(dt2_lastrun) assert isdue2 is True # True means task is due and should run. assert (nextcheck2 > 0) and (isdue2 is True) or \ (nextcheck2 == s2.max_interval) and (isdue2 is False) @pytest.mark.django_db() class test_model_PeriodicTasks(SchedulerCase): def test_track_changes(self): assert PeriodicTasks.last_change() is None m1 = self.create_model_interval(schedule(timedelta(seconds=10))) m1.save() x = PeriodicTasks.last_change() assert x m1.args = '(23, 24)' m1.save() y = PeriodicTasks.last_change() assert y assert y > x @pytest.mark.django_db() class test_modeladmin_PeriodicTaskAdmin(SchedulerCase): @pytest.mark.django_db() @pytest.fixture(autouse=True) def setup_scheduler(self, app): self.app = app self.site = AdminSite() self.request_factory = RequestFactory() entry_name, entry = self.create_conf_entry() self.app.conf.beat_schedule = {entry_name: entry} self.m1 = PeriodicTask(name=entry_name) self.m1.task = 'celery.backend_cleanup' self.m1.save() entry_name, entry = self.create_conf_entry() self.app.conf.beat_schedule = {entry_name: entry} self.m2 = PeriodicTask(name=entry_name) self.m2.task = 'celery.backend_cleanup' self.m2.save() def patch_request(self, request): """patch request to allow for django messages storage""" setattr(request, 'session', 'session') messages = FallbackStorage(request) setattr(request, '_messages', messages) return request def test_run_task(self): ma = PeriodicTaskAdmin(PeriodicTask, self.site) self.request = self.patch_request(self.request_factory.get('/')) ma.run_tasks(self.request, PeriodicTask.objects.filter(id=self.m1.id)) assert len(self.request._messages._queued_messages) == 1 queued_message = self.request._messages._queued_messages[0].message assert queued_message == '1 task was successfully run' def test_run_tasks(self): ma = PeriodicTaskAdmin(PeriodicTask, self.site) self.request = self.patch_request(self.request_factory.get('/')) ma.run_tasks(self.request, PeriodicTask.objects.all()) assert len(self.request._messages._queued_messages) == 1 queued_message = self.request._messages._queued_messages[0].message assert queued_message == '2 tasks were successfully run' django-celery-beat-1.1.1/tox.ini000066400000000000000000000041101324227125300164560ustar00rootroot00000000000000[tox] envlist = py27-django{18,19,110,111} py34-django{18,19,110,111,20} py35-django{18,19,110,111,20} pypy-django{18,19,110,111} flake8 flakeplus apicheck linkcheck pydocstyle cov [travis:env] DJANGO = 1.8: django18 1.9: django19 1.10: django110 1.11: django111 2.0: django20 [testenv] deps= -r{toxinidir}/requirements/default.txt -r{toxinidir}/requirements/test.txt -r{toxinidir}/requirements/test-ci.txt cov: -r{toxinidir}/requirements/test-django.txt django20: -r{toxinidir}/requirements/test-django20.txt django111: -r{toxinidir}/requirements/test-django111.txt django110: -r{toxinidir}/requirements/test-django110.txt django19: -r{toxinidir}/requirements/test-django19.txt django18: -r{toxinidir}/requirements/test-django18.txt py{27,34,35,py},cov: ephem linkcheck,apicheck: -r{toxinidir}/requirements/docs.txt flake8,flakeplus,pydocstyle: -r{toxinidir}/requirements/pkgutils.txt sitepackages = False recreate = False commands = pip install -U https://github.com/celery/celery/zipball/master#egg=celery pip install -U https://github.com/celery/kombu/zipball/master#egg=kombu py.test -xv [testenv:apicheck] basepython = python3.5 commands = sphinx-build -W -b apicheck -d {envtmpdir}/doctrees docs docs/_build/apicheck [testenv:linkcheck] basepython = python3.5 commands = sphinx-build -W -b linkcheck -d {envtmpdir}/doctrees docs docs/_build/linkcheck [testenv:flake8] basepython = python2.7 commands = flake8 {toxinidir}/django_celery_beat {toxinidir}/t [testenv:flakeplus] basepython = python2.7 commands = flakeplus --2.7 {toxinidir}/django_celery_beat {toxinidir}/t [testenv:pydocstyle] basepython = python2.7 commands = pydocstyle {toxinidir}/django_celery_beat [testenv:cov] basepython = python3.5 usedevelop = true commands = pip install -U https://github.com/celery/celery/zipball/master#egg=celery pip install -U https://github.com/celery/kombu/zipball/master#egg=kombu py.test -x --cov=django_celery_beat --cov-report=xml --no-cov-on-fail