pax_global_header00006660000000000000000000000064140675200730014516gustar00rootroot0000000000000052 comment=a979d0a6a32e56e116a3fddaacd0d03ea79d0960 django-celery-beat-2.2.1/000077500000000000000000000000001406752007300151545ustar00rootroot00000000000000django-celery-beat-2.2.1/.bumpversion.cfg000066400000000000000000000005421406752007300202650ustar00rootroot00000000000000[bumpversion] current_version = 2.2.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-2.2.1/.cookiecutterrc000066400000000000000000000016571406752007300202130ustar00rootroot00000000000000# 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-2.2.1/.coveragerc000066400000000000000000000002511406752007300172730ustar00rootroot00000000000000[run] branch = 1 cover_pylib = 0 include = *django_celery_beat/* omit = django_celery_beat.tests.* [report] omit = */python?.?/* */site-packages/* */pypy/* django-celery-beat-2.2.1/.editorconfig000066400000000000000000000003151406752007300176300ustar00rootroot00000000000000# 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-2.2.1/.gitignore000066400000000000000000000005071406752007300171460ustar00rootroot00000000000000.DS_Store *.pyc *$py.class *.mo *~ .*.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/ .python-version venv django-celery-beat-2.2.1/.travis.yml000066400000000000000000000016401406752007300172660ustar00rootroot00000000000000language: python dist: bionic cache: false before_install: - sudo apt update - sudo apt install -y wget - wget http://packages.erlang-solutions.com/ubuntu/erlang_solutions.asc - sudo apt-key add erlang_solutions.asc - sudo apt install -y sqlite - sudo apt install -y erlang - sudo apt install -y erlang-nox - sudo apt install -y rabbitmq-server - sudo /etc/init.d/rabbitmq-server start python: - 3.6 - 3.7 - 3.8 env: - DJANGO=2.2 - DJANGO=3.0 - DJANGO=3.1 - DJANGO=3.2 os: - linux matrix: include: - { python: 3.6, env: TOXENV=upgradebeat130 } - { python: 3.6, env: TOXENV=upgradebeat140 } - { python: 3.8, env: TOXENV=flake8 } - { python: 3.8, env: TOXENV=pydocstyle } 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-2.2.1/AUTHORS000066400000000000000000000061661406752007300162350ustar00rootroot00000000000000========= 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-2.2.1/Changelog000066400000000000000000000133251406752007300167720ustar00rootroot00000000000000.. _changelog: ================ Change history ================ 2.2.1 ===== :release-date: 2021-07-02 11:15 a.m. UTC+6:00 :release-by: Asif Saif Uddin - Enable Django 3.2 CI and add default_auto_field - Fix locale in dir tree - Do not blindly delete duplicate schedules (#389) - used python:3.8-slim for lighter builds - Do not blindly delete duplicate schedules (#389) 2.2.0 ===== :release-date: 2021-01-19 2:30 p.m. UTC+6:00 :release-by: Asif Saif Uddin - Fixed compatibility with django-timezone-field>=4.1.0 - Fixed deprecation warnings: 'assertEquals' in tests. - Fixed SolarSchedule event choices i18n support. - Updated 'es' .po file metadata - Update 'fr' .po file metadata - New schema migrations for SolarSchedule events choices changes in models. 2.1.0 ===== :release-date: :release-by: Asif Saif Uddin - Fix string representation of CrontabSchedule, so it matches UNIX CRON expression format (#318) - If no schedule is selected in PeriodicTask form, raise a non-field error instead of an error bounded to the `interval` field (#327) - Fix some Spanish translations (#339) - Log "Writing entries..." message as DEBUG instead of INFO (#342) - Use CELERY_TIMEZONE setting as `CrontabSchedule.timezone` default instead of UTC (#346) - Fix bug in ClockedSchedule that made the schedule stuck after a clocked task was executed. The `enabled` field of ClockedSchedule has been dropped (#341) - Drop support for Python < 3.6 (#368) - Add support for Celery 5 and Django 3.1 (#368) 2.0.0 ===== :release-date: :release-by: - Added support for Django 3.0 - Dropped support for Django < 2.2 and Python < 3.5 1.6.0 ===== :release-date: 2020-02-01 4:30 p.m. UTC+6:00 :release-by: Asif Saif Uddin - Fixed invalid long_description (#255) - Exposed read-only field PeriodicTask.last_run_at in Django admin (#257) - Added docker config to ease development (#260, #261, #264, #288) - Added validation schedule validation on save (#271) - Added French translation (#286) - Fixed case where last_run_at = None and CELERY_TIMEZONE != TIME_ZONE (#294) 1.5.0 ===== :release-date: 2019-05-21 17:00 p.m. UTC+6:00 :release-by: Asif Saif Uddin - Fixed delay returned when a task has a start_time in the future. (#208) - PeriodicTaskAdmin: Declare some filtering, for usability (#215) - fix _default_now is_aware bug (#216) - Adds support for message headers for periodic tasks (#98) - make last_run_at tz aware before passing to celery (#233) .. _version-1.5.0: 1.4.0 ===== :release-date: 2018-12-09 1:30 p.m. UTC+2:00 :release-by: Omer Katz - Fix migrations dependencies. - Added the DJANGO_CELERY_BEAT_TZ_AWARE setting. .. _version-1.3.0: 1.3.0 ===== :release-date: 2018-11-12 17:30 p.m. UTC+2:00 :release-by: Omer Katz - Fix transaction handling while syncing the schedule. - Fix schedule type validation logic. - Scheduler no longer forgets the tasks after first schedule change. - Fix race condition for schedule_changed() resulting in erroneously closed connections. - Add support for task priorities when using RabbitMQ or Redis as broker. - Disabled tasks are now correctly deleted from the schedule. - Added name as search filter. .. _version-1.2.0: 1.2.0 ===== :release-date: 2018-10-08 16:00 p.m. UTC+3:00 :release-by: Omer Katz - Allow timezone-aware Cron schedules. - Retry later in case of InterfaceError in sync. - Show Periodic Task Description in panel admin. - Fix CrontabSchedule example. - Support Periodic Tasks with a start date and one-off tasks. - Fixes a problem with beat not reconnecting to MySQL (server restart, network problem, etc.) when checking if schedule has changed. - Add toggle admin action which allows to activate disabled tasks or deactivate enabled tasks. - Add fields validation for CrontabSchedule. - Drop support for Django<1.11. - Fix task heap invalidation bug which prevented scheduled tasks from running when syncing tasks from the database. - Raise a ValidationError when more than one type (solar, crontab or interval) of schedule is provided. .. _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-2.2.1/LICENSE000066400000000000000000000050741406752007300161670ustar00rootroot00000000000000Copyright (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-2.2.1/MANIFEST.in000066400000000000000000000006521406752007300167150ustar00rootroot00000000000000include 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 *.po *.mo recursive-include t *.py recursive-exclude * __pycache__ recursive-exclude * *.py[co] recursive-exclude * .*.sw[a-z] django-celery-beat-2.2.1/Makefile000066400000000000000000000072771406752007300166310ustar00rootroot00000000000000PROJ=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-2.2.1/README.rst000066400000000000000000000257231406752007300166540ustar00rootroot00000000000000===================================================================== Database-backed Periodic Tasks ===================================================================== |build-status| |coverage| |license| |wheel| |pyversion| |pyimp| :Version: 2.2.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`_. .. _`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: .. code-block:: Python >>> 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:: 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: .. code-block:: Python >>> from django_celery_beat.models import PeriodicTasks >>> PeriodicTasks.update_changed() Example creating interval-based periodic task --------------------------------------------- To create a periodic task executing at an interval you must first create the interval object: .. code-block:: Python >>> 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: .. code-block:: Python >>> IntervalSchedule.PERIOD_CHOICES Now that we have defined the schedule object, we can create the periodic task entry: .. code-block:: Python >>> 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: .. code-block:: Python >>> 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: .. code-block:: Python >>> 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='*', ... timezone=pytz.timezone('Canada/Pacific') ... ) The crontab schedule is linked to a specific timezone using the 'timezone' input parameter. 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``: .. code-block:: Python >>> 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: .. code-block:: Python >>> periodic_task.enabled = False >>> periodic_task.save() Example running periodic tasks ----------------------------------- The periodic tasks still need 'workers' to execute them. So make sure the default **Celery** package is installed. (If not installed, please follow the installation instructions here: https://github.com/celery/celery) Both the worker and beat services need to be running at the same time. 1. Start a Celery worker service (specify your Django project name):: $ celery -A [project-name] worker --loglevel=info 2. As a separate process, start the beat service (specify the Django scheduler):: $ celery -A [project-name] beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler **OR** you can use the -S (scheduler flag), for more options see ``celery beat --help``):: $ celery -A [project-name] beat -l info -S django Also, as an alternative, you can run the two steps above (worker and beat services) with only one command (recommended for **development environment only**):: $ celery -A [project-name] worker --beat --scheduler django --loglevel=info 3. Now you can add and manage your periodic tasks from the Django Admin interface. Installation ============ You can install django-celery-beat either via the Python Package Index (PyPI) or from source. To install using ``pip``: .. code-block:: bash $ 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 : .. code-block:: bash $ 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. After installation, add ``django_celery_beat`` to Django's settings module: .. code-block:: Python INSTALLED_APPS = [ ..., 'django_celery_beat', ] Run the ``django_celery_beat`` migrations using: .. code-block:: bash $ python manage.py migrate django_celery_beat Using the development version ----------------------------- With pip ~~~~~~~~ You can install the latest snapshot of django-celery-beat using the following pip command: .. code-block:: bash $ pip install https://github.com/celery/django-celery-beat/zipball/master#egg=django-celery-beat Developing django-celery-beat ----------------------------- To spin up a local development copy of django-celery-beat with Django admin at http://127.0.0.1:58000/admin/ run: .. code-block:: bash $ docker-compose up --build Log-in as user ``admin`` with password ``admin``. TZ Awareness: ------------- If you have a project that is time zone naive, you can set ``DJANGO_CELERY_BEAT_TZ_AWARE=False`` in your settings file. .. |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#foo :alt: BSD License :target: https://opensource.org/licenses/BSD-3-Clause .. |wheel| image:: https://img.shields.io/pypi/wheel/django-celery-beat.svg#foo :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#foo :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#foo :alt: Support Python implementations. :target: http://pypi.python.org/pypi/django-celery-beat/ django-celery-beat as part of the Tidelift Subscription ------------------------------------------------------- The maintainers of django-celery-beat and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. `Learn more`_. .. _Learn more: https://tidelift.com/subscription/pkg/pypi-django-celery-beat?utm_source=pypi-django-celery-beat&utm_medium=referral&utm_campaign=readme&utm_term=repo django-celery-beat-2.2.1/appveyor.yml000066400000000000000000000024511406752007300175460ustar00rootroot00000000000000environment: 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-2.2.1/django_celery_beat/000077500000000000000000000000001406752007300207545ustar00rootroot00000000000000django-celery-beat-2.2.1/django_celery_beat/__init__.py000066400000000000000000000017111406752007300230650ustar00rootroot00000000000000"""Database-backed Periodic Tasks.""" # :copyright: (c) 2016, Ask Solem. # All rights reserved. # :license: BSD (3 Clause), see LICENSE for more details. import re from collections import namedtuple import django __version__ = '2.2.1' __author__ = 'Asif Saif Uddin, Ask Solem' __contact__ = 'auvipy@gmail.com, 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__ = [] if django.VERSION < (3, 2): default_app_config = 'django_celery_beat.apps.BeatConfig' django-celery-beat-2.2.1/django_celery_beat/admin.py000066400000000000000000000177401406752007300224270ustar00rootroot00000000000000"""Periodic Task Admin interface.""" from django import forms from django.conf import settings from django.contrib import admin, messages from django.db.models import When, Value, Case from django.forms.widgets import Select from django.template.defaultfilters import pluralize from django.utils.translation import gettext_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, ClockedSchedule ) from .utils import is_database_scheduler 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().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 if data.get('expire_seconds') is not None and data.get('expires'): raise forms.ValidationError( _('Only one can be set, in expires and expire_seconds') ) 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 date_hierarchy = 'start_time' list_display = ('__str__', 'enabled', 'interval', 'start_time', 'last_run_at', 'one_off') list_filter = ['enabled', 'one_off', 'task', 'start_time', 'last_run_at'] actions = ('enable_tasks', 'disable_tasks', 'toggle_tasks', 'run_tasks') search_fields = ('name',) fieldsets = ( (None, { 'fields': ('name', 'regtask', 'task', 'enabled', 'description',), 'classes': ('extrapretty', 'wide'), }), ('Schedule', { 'fields': ('interval', 'crontab', 'solar', 'clocked', 'start_time', 'last_run_at', 'one_off'), 'classes': ('extrapretty', 'wide'), }), ('Arguments', { 'fields': ('args', 'kwargs'), 'classes': ('extrapretty', 'wide', 'collapse', 'in'), }), ('Execution Options', { 'fields': ('expires', 'expire_seconds', 'queue', 'exchange', 'routing_key', 'priority', 'headers'), 'classes': ('extrapretty', 'wide', 'collapse', 'in'), }), ) readonly_fields = ( 'last_run_at', ) 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().get_queryset(request) return qs.select_related('interval', 'crontab', 'solar', 'clocked') def _message_user_about_update(self, request, rows_updated, verb): """Send message about action to user. `verb` should shortly describe what have changed (e.g. 'enabled'). """ self.message_user( request, _('{0} task{1} {2} successfully {3}').format( rows_updated, pluralize(rows_updated), pluralize(rows_updated, _('was,were')), verb, ), ) def enable_tasks(self, request, queryset): rows_updated = queryset.update(enabled=True) PeriodicTasks.update_changed() self._message_user_about_update(request, rows_updated, 'enabled') 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_about_update(request, rows_updated, 'disabled') disable_tasks.short_description = _('Disable selected tasks') def _toggle_tasks_activity(self, queryset): return queryset.update(enabled=Case( When(enabled=True, then=Value(False)), default=Value(True), )) def toggle_tasks(self, request, queryset): rows_updated = self._toggle_tasks_activity(queryset) PeriodicTasks.update_changed() self._message_user_about_update(request, rows_updated, 'toggled') toggle_tasks.short_description = _('Toggle activity of 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), task.queue) for task in queryset] if any(t[0] is None for t in tasks): for i, t in enumerate(tasks): if t[0] is None: break # variable "i" will be set because list "tasks" is not empty not_found_task_name = queryset[i].task self.message_user( request, _('task "{0}" not found'.format(not_found_task_name)), level=messages.ERROR, ) return task_ids = [task.apply_async(args=args, kwargs=kwargs, queue=queue) if queue and len(queue) else task.apply_async(args=args, kwargs=kwargs) for task, args, kwargs, queue 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') class ClockedScheduleAdmin(admin.ModelAdmin): """Admin-interface for clocked schedules.""" fields = ( 'clocked_time', ) list_display = ( 'clocked_time', ) admin.site.register(IntervalSchedule) admin.site.register(CrontabSchedule) admin.site.register(SolarSchedule) admin.site.register(ClockedSchedule, ClockedScheduleAdmin) admin.site.register(PeriodicTask, PeriodicTaskAdmin) django-celery-beat-2.2.1/django_celery_beat/apps.py000066400000000000000000000006251406752007300222740ustar00rootroot00000000000000"""Django Application configuration.""" from django.apps import AppConfig from django.utils.translation import gettext_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') default_auto_field = 'django.db.models.AutoField' django-celery-beat-2.2.1/django_celery_beat/clockedschedule.py000066400000000000000000000023751406752007300244560ustar00rootroot00000000000000"""Clocked schedule Implementation.""" from celery import schedules from celery.utils.time import maybe_make_aware from .utils import NEVER_CHECK_TIMEOUT class clocked(schedules.BaseSchedule): """clocked schedule. Depends on PeriodicTask one_off=True """ def __init__(self, clocked_time, nowfun=None, app=None): """Initialize clocked.""" self.clocked_time = maybe_make_aware(clocked_time) super().__init__(nowfun=nowfun, app=app) def remaining_estimate(self, last_run_at): return self.clocked_time - self.now() def is_due(self, last_run_at): rem_delta = self.remaining_estimate(None) remaining_s = max(rem_delta.total_seconds(), 0) if remaining_s == 0: return schedules.schedstate(is_due=True, next=NEVER_CHECK_TIMEOUT) return schedules.schedstate(is_due=False, next=remaining_s) def __repr__(self): return ''.format(self.clocked_time) def __eq__(self, other): if isinstance(other, clocked): return self.clocked_time == other.clocked_time return False def __ne__(self, other): return not self.__eq__(other) def __reduce__(self): return self.__class__, (self.clocked_time, self.nowfun) django-celery-beat-2.2.1/django_celery_beat/locale/000077500000000000000000000000001406752007300222135ustar00rootroot00000000000000django-celery-beat-2.2.1/django_celery_beat/locale/es/000077500000000000000000000000001406752007300226225ustar00rootroot00000000000000django-celery-beat-2.2.1/django_celery_beat/locale/es/LC_MESSAGES/000077500000000000000000000000001406752007300244075ustar00rootroot00000000000000django-celery-beat-2.2.1/django_celery_beat/locale/es/LC_MESSAGES/django.po000066400000000000000000000343671406752007300262260ustar00rootroot00000000000000# Spanish translation strings for django-celery-beat. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # , 2020. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-04-04 01:30+0000\n" "PO-Revision-Date: 2021-04-03 22:36-0300\n" "Last-Translator: Luis Saavedra \n" "Language-Team: \n" "Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" "X-Generator: Poedit 2.3\n" #: django_celery_beat/admin.py:64 msgid "Task (registered)" msgstr "Tarea (registrada)" #: django_celery_beat/admin.py:68 msgid "Task (custom)" msgstr "Tarea (personalizada)" #: django_celery_beat/admin.py:85 msgid "Need name of task" msgstr "Nombre de tarea necesario" #: django_celery_beat/admin.py:91 django_celery_beat/models.py:586 msgid "Only one can be set, in expires and expire_seconds" msgstr "" "Sólo uno de los campos puede ser definido, en expiración y segundos de " "expiración" #: django_celery_beat/admin.py:101 #, python-format msgid "Unable to parse JSON: %s" msgstr "Incapaz de parsear el JSON: %s" #: django_celery_beat/admin.py:167 #, python-brace-format msgid "{0} task{1} {2} successfully {3}" msgstr "{0} tarea{1} {2} correctamente {3}" #: django_celery_beat/admin.py:170 django_celery_beat/admin.py:232 msgid "was,were" msgstr "fue,fueron" #: django_celery_beat/admin.py:179 msgid "Enable selected tasks" msgstr "Habilitar tareas seleccionadas" #: django_celery_beat/admin.py:185 msgid "Disable selected tasks" msgstr "Deshabilitar tareas seleccionadas" #: django_celery_beat/admin.py:197 msgid "Toggle activity of selected tasks" msgstr "Conmutar actividad de las tareas seleccionadas" #: django_celery_beat/admin.py:217 #, python-brace-format msgid "task \"{0}\" not found" msgstr "tarea \"{0}\" no encontrada" #: django_celery_beat/admin.py:229 #, python-brace-format msgid "{0} task{1} {2} successfully run" msgstr "{0} tarea{1} {2} correctamente ejecutadas" #: django_celery_beat/admin.py:235 msgid "Run selected tasks" msgstr "Ejecutar tareas seleccionadas" #: django_celery_beat/apps.py:13 msgid "Periodic Tasks" msgstr "Tareas Periódicas" #: django_celery_beat/models.py:26 msgid "Days" msgstr "Días" #: django_celery_beat/models.py:27 msgid "Hours" msgstr "Horas" #: django_celery_beat/models.py:28 msgid "Minutes" msgstr "Minutos" #: django_celery_beat/models.py:29 msgid "Seconds" msgstr "Segundos" #: django_celery_beat/models.py:30 msgid "Microseconds" msgstr "Microsegundos" #: django_celery_beat/models.py:34 msgid "Day" msgstr "Día" #: django_celery_beat/models.py:35 msgid "Hour" msgstr "Hora" #: django_celery_beat/models.py:36 msgid "Minute" msgstr "Minuto" #: django_celery_beat/models.py:37 msgid "Second" msgstr "Segundo" #: django_celery_beat/models.py:38 msgid "Microsecond" msgstr "Microsegundo" #: django_celery_beat/models.py:42 msgid "Astronomical dawn" msgstr "Amanecer astronómico" #: django_celery_beat/models.py:43 msgid "Civil dawn" msgstr "Amanecer civil" #: django_celery_beat/models.py:44 msgid "Nautical dawn" msgstr "Amanecer náutico" #: django_celery_beat/models.py:45 msgid "Astronomical dusk" msgstr "Anochecer astronómico" #: django_celery_beat/models.py:46 msgid "Civil dusk" msgstr "Anochecer civil" #: django_celery_beat/models.py:47 msgid "Nautical dusk" msgstr "Anochecer náutico" #: django_celery_beat/models.py:48 msgid "Solar noon" msgstr "Mediodía solar" #: django_celery_beat/models.py:49 msgid "Sunrise" msgstr "Amanecer" #: django_celery_beat/models.py:50 msgid "Sunset" msgstr "Puesta de sol" #: django_celery_beat/models.py:84 msgid "Solar Event" msgstr "Evento Solar" #: django_celery_beat/models.py:85 msgid "The type of solar event when the job should run" msgstr "El tipo de evento solar cuando el proceso debe ejecutarse" #: django_celery_beat/models.py:89 msgid "Latitude" msgstr "Latitud" #: django_celery_beat/models.py:90 msgid "Run the task when the event happens at this latitude" msgstr "Ejecutar la tarea cuando el evento ocurra a esta latitud" #: django_celery_beat/models.py:95 msgid "Longitude" msgstr "Longitud" #: django_celery_beat/models.py:96 msgid "Run the task when the event happens at this longitude" msgstr "Ejecutar la tarea cuando el evento ocurra a esta longitud" #: django_celery_beat/models.py:103 msgid "solar event" msgstr "evento solar" #: django_celery_beat/models.py:104 msgid "solar events" msgstr "eventos solares" #: django_celery_beat/models.py:153 msgid "Number of Periods" msgstr "Número de Períodos" #: django_celery_beat/models.py:154 msgid "Number of interval periods to wait before running the task again" msgstr "" "Número de períodos de intervalo a esperar antes de ejecutar esta tarea de " "nuevo" #: django_celery_beat/models.py:160 msgid "Interval Period" msgstr "Período de intervalo" #: django_celery_beat/models.py:161 msgid "The type of period between task runs (Example: days)" msgstr "El tipo de período entre ejecuciones de tarea (Ejemplo: días)" #: django_celery_beat/models.py:167 msgid "interval" msgstr "intervalo" #: django_celery_beat/models.py:168 msgid "intervals" msgstr "intervalos" #: django_celery_beat/models.py:195 msgid "every {}" msgstr "cada {}" #: django_celery_beat/models.py:200 msgid "every {} {}" msgstr "cada {} {}" #: django_celery_beat/models.py:211 msgid "Clock Time" msgstr "Hora y día" #: django_celery_beat/models.py:212 msgid "Run the task at clocked time" msgstr "Ejecuta la tarea en el momento indicado" #: django_celery_beat/models.py:218 django_celery_beat/models.py:219 msgid "clocked" msgstr "cronometrado" #: django_celery_beat/models.py:258 msgid "Minute(s)" msgstr "Minuto(s)" #: django_celery_beat/models.py:260 msgid "Cron Minutes to Run. Use \"*\" for \"all\". (Example: \"0,30\")" msgstr "" "Minutos Cron cuando ejecutar. Usa \"*\" para \"todos\". (Ejemplo: \"0,30\")" #: django_celery_beat/models.py:265 msgid "Hour(s)" msgstr "Hora(s)" #: django_celery_beat/models.py:267 msgid "Cron Hours to Run. Use \"*\" for \"all\". (Example: \"8,20\")" msgstr "" "Horas Cron cuando ejecutar. Usa \"*\" para \"todas\". (Ejemplo: \"8,20\")" #: django_celery_beat/models.py:272 msgid "Day(s) Of The Week" msgstr "Día(s) de la semana" #: django_celery_beat/models.py:274 msgid "Cron Days Of The Week to Run. Use \"*\" for \"all\". (Example: \"0,5\")" msgstr "" "Días de la semana Cron cuando ejecutar. Usa \"*\" para \"todos\". (Ejemplo: " "\"0,5\")" #: django_celery_beat/models.py:280 msgid "Day(s) Of The Month" msgstr "Día(s) del mes" #: django_celery_beat/models.py:282 msgid "" "Cron Days Of The Month to Run. Use \"*\" for \"all\". (Example: \"1,15\")" msgstr "" "Días del mes Cron cuando ejecutar. Usa \"*\" para \"todos\". (Ejemplo: " "\"1,15\")" #: django_celery_beat/models.py:288 msgid "Month(s) Of The Year" msgstr "Mes(es) del año" #: django_celery_beat/models.py:290 msgid "" "Cron Months Of The Year to Run. Use \"*\" for \"all\". (Example: \"0,6\")" msgstr "" "Meses del año Cron cuando ejecutar. Usa \"*\" para \"todos\". (Ejemplo: " "\"0,6\")" #: django_celery_beat/models.py:297 msgid "Cron Timezone" msgstr "Zona horaria Cron" #: django_celery_beat/models.py:299 msgid "Timezone to Run the Cron Schedule on. Default is UTC." msgstr "Zona horaria donde ejecutar la programación Cron. Por defecto UTC." #: django_celery_beat/models.py:305 msgid "crontab" msgstr "crontab" #: django_celery_beat/models.py:306 msgid "crontabs" msgstr "crontabs" #: django_celery_beat/models.py:390 msgid "Name" msgstr "Nombre" #: django_celery_beat/models.py:391 msgid "Short Description For This Task" msgstr "Descripción corta para esta tarea" #: django_celery_beat/models.py:396 msgid "" "The Name of the Celery Task that Should be Run. (Example: \"proj.tasks." "import_contacts\")" msgstr "" "Nombre de la tarea Celery que debe ser ejecutada. (Ejemplo: \"proj.tasks." "import_contacts\")" #: django_celery_beat/models.py:404 msgid "Interval Schedule" msgstr "Intervalo de programación" #: django_celery_beat/models.py:405 msgid "" "Interval Schedule to run the task on. Set only one schedule type, leave the " "others null." msgstr "" "Intervalo de programación donde ejecutar la tarea. Establece sólo un tipo de " "programación, deja el resto en blanco." #: django_celery_beat/models.py:410 msgid "Crontab Schedule" msgstr "Programación Crontab" #: django_celery_beat/models.py:411 msgid "" "Crontab Schedule to run the task on. Set only one schedule type, leave the " "others null." msgstr "" "Programación Crontab con la cual ejecutar la tarea. Establece sólo un tipo " "de programación, deja el resto en blanco." #: django_celery_beat/models.py:416 msgid "Solar Schedule" msgstr "Programación solar" #: django_celery_beat/models.py:417 msgid "" "Solar Schedule to run the task on. Set only one schedule type, leave the " "others null." msgstr "" "Programación solar con la cual ejecutar la tarea. Establece sólo un tipo de " "programación, deja el resto en blanco." #: django_celery_beat/models.py:422 msgid "Clocked Schedule" msgstr "Programación horaria" #: django_celery_beat/models.py:423 msgid "" "Clocked Schedule to run the task on. Set only one schedule type, leave the " "others null." msgstr "" "Programación horaria con la cual ejecutar la tarea. Establece sólo un tipo " "de programación, deja el resto en blanco." #: django_celery_beat/models.py:429 msgid "Positional Arguments" msgstr "Argumentos posicionales" #: django_celery_beat/models.py:431 msgid "JSON encoded positional arguments (Example: [\"arg1\", \"arg2\"])" msgstr "" "Argumentos posicionales codificados en formato JSON. (Ejemplo: [\"arg1\", " "\"arg2\"])" #: django_celery_beat/models.py:436 msgid "Keyword Arguments" msgstr "Agumentos opcionales" #: django_celery_beat/models.py:438 msgid "JSON encoded keyword arguments (Example: {\"argument\": \"value\"})" msgstr "" "Argumentos opcionales codificados en formato JSON. (Ejemplo: {\"argument\": " "\"value\"})" #: django_celery_beat/models.py:444 msgid "Queue Override" msgstr "Invalidación de cola" #: django_celery_beat/models.py:446 msgid "Queue defined in CELERY_TASK_QUEUES. Leave None for default queuing." msgstr "" "Cola definida en CELERY_TASK_QUEUES. Dejala nula para la cola por defecto." #: django_celery_beat/models.py:455 msgid "Exchange" msgstr "Intercambio" #: django_celery_beat/models.py:456 msgid "Override Exchange for low-level AMQP routing" msgstr "Invalida intercambio para enrutamiento de bajo nivel de AMQP" #: django_celery_beat/models.py:460 msgid "Routing Key" msgstr "Clave de enrutamiento" #: django_celery_beat/models.py:461 msgid "Override Routing Key for low-level AMQP routing" msgstr "" "Invalida la clave de enrutamiento para enrutamiento de bajo nivel de AMQP" #: django_celery_beat/models.py:465 msgid "AMQP Message Headers" msgstr "Cabeceras de mensaje de AMQP" #: django_celery_beat/models.py:466 msgid "JSON encoded message headers for the AMQP message." msgstr "Cacbeceras de mensaje de AMQP codificadas en formato JSON." #: django_celery_beat/models.py:472 msgid "Priority" msgstr "Prioridad" #: django_celery_beat/models.py:474 msgid "" "Priority Number between 0 and 255. Supported by: RabbitMQ, Redis (priority " "reversed, 0 is highest)." msgstr "" "Número de prioridad entre 0 and 255. Soportado por: RabbitMQ, Redis " "(prioridad invertida, 0 es la más alta)." #: django_celery_beat/models.py:479 msgid "Expires Datetime" msgstr "Fecha de caducidad" #: django_celery_beat/models.py:481 msgid "" "Datetime after which the schedule will no longer trigger the task to run" msgstr "" "Fecha después de la cual la programación no provocará que la tarea vuelva a " "ejecutarse" #: django_celery_beat/models.py:486 msgid "Expires timedelta with seconds" msgstr "Delta de tiempo de expiración en segundos" #: django_celery_beat/models.py:488 msgid "" "Timedelta with seconds which the schedule will no longer trigger the task to " "run" msgstr "" "Delta de Tiempo en segundos después de los cuales la programación no " "provocará que la tarea vuelva a ejecutarse" #: django_celery_beat/models.py:494 msgid "One-off Task" msgstr "Tarea de ejecución única" #: django_celery_beat/models.py:496 msgid "If True, the schedule will only run the task a single time" msgstr "Si es verdadera, la programación sólo lanzará la tarea una vez" #: django_celery_beat/models.py:500 msgid "Start Datetime" msgstr "Fecha de comienzo" #: django_celery_beat/models.py:502 msgid "Datetime when the schedule should begin triggering the task to run" msgstr "" "Fecha cuando la programación debe comenzar a provocar la ejecución de la " "tarea" #: django_celery_beat/models.py:507 msgid "Enabled" msgstr "Habilitada" #: django_celery_beat/models.py:508 msgid "Set to False to disable the schedule" msgstr "Establece a Falso para deshabilitar la programación" #: django_celery_beat/models.py:513 msgid "Last Run Datetime" msgstr "Fecha de última ejecución" #: django_celery_beat/models.py:515 msgid "" "Datetime that the schedule last triggered the task to run. Reset to None if " "enabled is set to False." msgstr "" "Fecha en la cual la programación ejecutó la tarea por última vez. " "Reinicializa a None si enabled está establecido como falso." #: django_celery_beat/models.py:520 msgid "Total Run Count" msgstr "Contador de ejecuciones totales" #: django_celery_beat/models.py:522 msgid "Running count of how many times the schedule has triggered the task" msgstr "Contador de cuentas veces ha sido ejecutada la tarea" #: django_celery_beat/models.py:527 msgid "Last Modified" msgstr "Última modificación" #: django_celery_beat/models.py:528 msgid "Datetime that this PeriodicTask was last modified" msgstr "Fecha en la cual esta tarea periódica fue modificada por última vez" #: django_celery_beat/models.py:532 msgid "Description" msgstr "Descripción" #: django_celery_beat/models.py:534 msgid "Detailed description about the details of this Periodic Task" msgstr "Descripción detallada sobre los detalles de esta tarea periódica" #: django_celery_beat/models.py:543 msgid "periodic task" msgstr "tarea periódica" #: django_celery_beat/models.py:544 msgid "periodic tasks" msgstr "tareas periódicas" #: django_celery_beat/templates/admin/djcelery/change_list.html:6 msgid "Home" msgstr "Inicio" django-celery-beat-2.2.1/django_celery_beat/locale/fr/000077500000000000000000000000001406752007300226225ustar00rootroot00000000000000django-celery-beat-2.2.1/django_celery_beat/locale/fr/LC_MESSAGES/000077500000000000000000000000001406752007300244075ustar00rootroot00000000000000django-celery-beat-2.2.1/django_celery_beat/locale/fr/LC_MESSAGES/django.po000066400000000000000000000345421406752007300262210ustar00rootroot00000000000000# French translation strings for django-celery-beat. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # , 2019. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2019-11-10 14:36+0000\n" "PO-Revision-Date: 2020-06-09 10:30\n" "Last-Translator: Álvaro Mondéjar \n" "Language-Team: n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #: django_celery_beat/admin.py:69 msgid "Task (registered)" msgstr "Tâche (enregistrée)" #: django_celery_beat/admin.py:73 msgid "Task (custom)" msgstr "Tâche (personalisée)" #: django_celery_beat/admin.py:90 msgid "Need name of task" msgstr "Besoin du nom de la tâche" #: django_celery_beat/admin.py:96 django_celery_beat/models.py:595 msgid "Only one can be set, in expires and expire_seconds" msgstr "Seulement un peu être définie, soit expires ou expire_seconds" #: django_celery_beat/admin.py:106 #, python-format msgid "Unable to parse JSON: %s" msgstr "Incapable d'analyser le JSON: %s" #: django_celery_beat/admin.py:172 #, python-brace-format msgid "{0} task{1} {2} successfully {3}" msgstr "{0} tâche{1} {2} avec succès {3}" #: django_celery_beat/admin.py:175 django_celery_beat/admin.py:237 msgid "was,were" msgstr "a été,ont été" #: django_celery_beat/admin.py:184 msgid "Enable selected tasks" msgstr "Active les tâches sélectionnées" #: django_celery_beat/admin.py:190 msgid "Disable selected tasks" msgstr "Désactive les tâches sélectionnées" #: django_celery_beat/admin.py:202 msgid "Toggle activity of selected tasks" msgstr "Bascule l'activité des tâches sélectionnées" #: django_celery_beat/admin.py:222 #, python-brace-format msgid "task \"{0}\" not found" msgstr "tâche \"{0}\" introuvable" #: django_celery_beat/admin.py:234 #, python-brace-format msgid "{0} task{1} {2} successfully run" msgstr "{0} tâche{1} {2} a fonctionnée avec succès" #: django_celery_beat/admin.py:240 msgid "Run selected tasks" msgstr "Démarre les tâches sélectionnées" #: django_celery_beat/apps.py:13 msgid "Periodic Tasks" msgstr "Tâches Périodique" #: django_celery_beat/models.py:26 msgid "Days" msgstr "Jours" #: django_celery_beat/models.py:27 msgid "Hours" msgstr "Heures" #: django_celery_beat/models.py:28 msgid "Minutes" msgstr "Minutes" #: django_celery_beat/models.py:29 msgid "Seconds" msgstr "Secondes" #: django_celery_beat/models.py:30 msgid "Microseconds" msgstr "Microsecondes" #: django_celery_beat/models.py:34 msgid "Day" msgstr "Jour" #: django_celery_beat/models.py:35 msgid "Hour" msgstr "Heure" #: django_celery_beat/models.py:36 msgid "Minute" msgstr "Minute" #: django_celery_beat/models.py:37 msgid "Second" msgstr "Seconde" #: django_celery_beat/models.py:38 msgid "Microsecond" msgstr "Microseconde" #: django_celery_beat/models.py:42 msgid "Astronomical dawn" msgstr "Aube astronomique" #: django_celery_beat/models.py:43 msgid "Civil dawn" msgstr "Aube civile" #: django_celery_beat/models.py:44 msgid "Nautical dawn" msgstr "Aube nautique" #: django_celery_beat/models.py:45 msgid "Astronomical dusk" msgstr "Crépuscule astronomique" #: django_celery_beat/models.py:46 msgid "Civil dusk" msgstr "Crépuscule civil" #: django_celery_beat/models.py:47 msgid "Nautical dusk" msgstr "Crépuscule nautique" #: django_celery_beat/models.py:48 msgid "Solar noon" msgstr "Midi solaire" #: django_celery_beat/models.py:49 msgid "Sunrise" msgstr "Lever du soleil" #: django_celery_beat/models.py:50 msgid "Sunset" msgstr "Coucher du soleil" #: django_celery_beat/models.py:82 msgid "Solar Event" msgstr "Évènement Solaire" #: django_celery_beat/models.py:83 msgid "The type of solar event when the job should run" msgstr "Le type d'évènement solaire pour lequel la tâche devrait démarrer" #: django_celery_beat/models.py:87 msgid "Latitude" msgstr "Latitude" #: django_celery_beat/models.py:88 msgid "Run the task when the event happens at this latitude" msgstr "Démarre cette tâche lorsque l'évènement se produit à cette latitude" #: django_celery_beat/models.py:93 msgid "Longitude" msgstr "Longitude" #: django_celery_beat/models.py:94 msgid "Run the task when the event happens at this longitude" msgstr "" "Démarre cette tâche lorsque cette évènement se produit à cette longitude" #: django_celery_beat/models.py:101 msgid "solar event" msgstr "évènement solaire" #: django_celery_beat/models.py:102 msgid "solar events" msgstr "évènements solaire" #: django_celery_beat/models.py:151 msgid "Number of Periods" msgstr "Nombre de Périodes" #: django_celery_beat/models.py:152 msgid "Number of interval periods to wait before running the task again" msgstr "" "Nombre d'intervale de périodes à attendre avant de démarrer la tâche à " "nouveau" #: django_celery_beat/models.py:158 msgid "Interval Period" msgstr "Période d'Intervale" #: django_celery_beat/models.py:159 msgid "The type of period between task runs (Example: days)" msgstr "Le type de période entre chaque démarrage de tâche (Exemple: jours)" #: django_celery_beat/models.py:165 msgid "interval" msgstr "intervale" #: django_celery_beat/models.py:166 msgid "intervals" msgstr "intervales" #: django_celery_beat/models.py:194 msgid "every {}" msgstr "chaque {}" #: django_celery_beat/models.py:199 msgid "every {} {}" msgstr "chaque {} {}" #: django_celery_beat/models.py:210 msgid "Clock Time" msgstr "Horaire" #: django_celery_beat/models.py:211 msgid "Run the task at clocked time" msgstr "Démarre la tâche à l'horaire définie" #: django_celery_beat/models.py:216 django_celery_beat/models.py:516 msgid "Enabled" msgstr "Activée" #: django_celery_beat/models.py:217 django_celery_beat/models.py:517 msgid "Set to False to disable the schedule" msgstr "Mettre à Faux pour désactiver la planification" #: django_celery_beat/models.py:223 django_celery_beat/models.py:224 msgid "clocked" msgstr "horaire" #: django_celery_beat/models.py:266 msgid "Minute(s)" msgstr "Minute⋅s" #: django_celery_beat/models.py:268 msgid "Cron Minutes to Run. Use \"*\" for \"all\". (Example: \"0,30\")" msgstr "" "Minutes Cron pour Démarrer. Utilisez \"*\" pour \"toutes\". (Exemple: " "\"0,30\")" #: django_celery_beat/models.py:273 msgid "Hour(s)" msgstr "Heure⋅s" #: django_celery_beat/models.py:275 msgid "Cron Hours to Run. Use \"*\" for \"all\". (Example: \"8,20\")" msgstr "" "Heures Cron pour Démarrer. Utilisez \"*\" pour \"toutes\". (Exemple: " "\"8,20\")" #: django_celery_beat/models.py:280 msgid "Day(s) Of The Week" msgstr "Jour⋅s De La Semaine" #: django_celery_beat/models.py:282 msgid "Cron Days Of The Week to Run. Use \"*\" for \"all\". (Example: \"0,5\")" msgstr "" "Jours De La Semaine Cron pour Démarrer. Utilisez \"*\" pour \"tous\". " "(Exemple: \"0,5\")" #: django_celery_beat/models.py:288 msgid "Day(s) Of The Month" msgstr "Jour⋅s Du Mois" #: django_celery_beat/models.py:290 msgid "" "Cron Days Of The Month to Run. Use \"*\" for \"all\". (Example: \"1,15\")" msgstr "" "Jours Du Mois Cron pour Démarrer. Utilisez \"*\" pour \"tous\". (Exemple: " "\"1,15\")" #: django_celery_beat/models.py:296 msgid "Month(s) Of The Year" msgstr "Mois De L'Année" #: django_celery_beat/models.py:298 msgid "" "Cron Months Of The Year to Run. Use \"*\" for \"all\". (Example: \"0,6\")" msgstr "" "Mois De L'Année Cron pour Démarrer. Utilisez \"*\" pour \"tous\". (Exemple:" " ,6\")" #: django_celery_beat/models.py:305 msgid "Cron Timezone" msgstr "Fuseau Horaire Cron" #: django_celery_beat/models.py:307 msgid "Timezone to Run the Cron Schedule on. Default is UTC." msgstr "" "Fuseau Horaire pour lequel démarrer la planification Cron. UTC par défaut." #: django_celery_beat/models.py:313 msgid "crontab" msgstr "crontab" #: django_celery_beat/models.py:314 msgid "crontabs" msgstr "crontabs" #: django_celery_beat/models.py:399 msgid "Name" msgstr "Nom" #: django_celery_beat/models.py:400 msgid "Short Description For This Task" msgstr "Description Courte Pour Cette Tâche" #: django_celery_beat/models.py:405 msgid "" "The Name of the Celery Task that Should be Run. (Example: \"proj.tasks." "import_contacts\")" msgstr "" "Le Nom de la Tâche Celery qui devrait être démarrée. (Exemple: \"proj.tasks." "import_contacts\")" #: django_celery_beat/models.py:413 msgid "Interval Schedule" msgstr "Planification intervalée" #: django_celery_beat/models.py:414 msgid "" "Interval Schedule to run the task on. Set only one schedule type, leave the " "others null." msgstr "" "Planification intervalée pour démarrer cette tâche. Ne mettez qu'un seul " "type de planification, laissez les autres vides" #: django_celery_beat/models.py:419 msgid "Crontab Schedule" msgstr "Planification Crontab" #: django_celery_beat/models.py:420 msgid "" "Crontab Schedule to run the task on. Set only one schedule type, leave the " "others null." msgstr "" "Planification Crontab pour démarrer cette tâche. Ne mettez qu'un seul type " "de planification, laissez les autres vides" #: django_celery_beat/models.py:425 msgid "Solar Schedule" msgstr "Planification Solaire" #: django_celery_beat/models.py:426 msgid "" "Solar Schedule to run the task on. Set only one schedule type, leave the " "others null." msgstr "" "Planification Solaire pour démarrer cette tâche. Ne mettez qu'un seul type " "de planification, laissez les autres vides" #: django_celery_beat/models.py:431 msgid "Clocked Schedule" msgstr "Planification Horaire" #: django_celery_beat/models.py:432 msgid "" "Clocked Schedule to run the task on. Set only one schedule type, leave the " "others null." msgstr "" "Planification Horaire pour démarrer cette tâche. Ne mettez qu'un seul type " "de planification, laissez les autres vides" #: django_celery_beat/models.py:438 msgid "Positional Arguments" msgstr "Arguments Positionnels" #: django_celery_beat/models.py:440 msgid "JSON encoded positional arguments (Example: [\"arg1\", \"arg2\"])" msgstr "Arguments positionnels encodés en JSON (Exemple: [\"arg1\", \"arg2\"])" #: django_celery_beat/models.py:445 msgid "Keyword Arguments" msgstr "Arguments Nommés" #: django_celery_beat/models.py:447 msgid "JSON encoded keyword arguments (Example: {\"argument\": \"value\"})" msgstr "Arguments nommés encodés en JSON (Exemple: {\"argument\": \"valeur\"})" #: django_celery_beat/models.py:453 msgid "Queue Override" msgstr "Surcharge de file d'attente" #: django_celery_beat/models.py:455 msgid "Queue defined in CELERY_TASK_QUEUES. Leave None for default queuing." msgstr "" "File d'attente définie dans CELERY_TASK_QEUEUS. Laissez Vide pour la mise en " "file d'attente par défaut." #: django_celery_beat/models.py:464 msgid "Exchange" msgstr "Échange" #: django_celery_beat/models.py:465 msgid "Override Exchange for low-level AMQP routing" msgstr "Surcharge d'échange pour un routage AMQP bas-niveau" #: django_celery_beat/models.py:469 msgid "Routing Key" msgstr "Clé de routage" #: django_celery_beat/models.py:470 msgid "Override Routing Key for low-level AMQP routing" msgstr "Surcharge de clé de route pour un routage AMQP bas-niveau" #: django_celery_beat/models.py:474 msgid "AMQP Message Headers" msgstr "Message d'en-têtes AMQP" #: django_celery_beat/models.py:475 msgid "JSON encoded message headers for the AMQP message." msgstr "Message d'en-têtes encodés en JSON pour le message AMQP" #: django_celery_beat/models.py:481 msgid "Priority" msgstr "Priorité" #: django_celery_beat/models.py:483 msgid "" "Priority Number between 0 and 255. Supported by: RabbitMQ, Redis (priority " "reversed, 0 is highest)." msgstr "" "Valeur de Priorité entre 0 et 255. Supporté par: RabbitMQ, Redis (priorité " "inversé, 0 est plus élevé)." #: django_celery_beat/models.py:488 msgid "Expires Datetime" msgstr "Date et heure d'expiration" #: django_celery_beat/models.py:490 msgid "" "Datetime after which the schedule will no longer trigger the task to run" msgstr "" "Date et heure après laquelle la planification ne déclenchera plus la tâche à " "démarrer" #: django_celery_beat/models.py:495 msgid "Expires timedelta with seconds" msgstr "Différence de temps en secondes d'expiration" #: django_celery_beat/models.py:497 msgid "" "Timedelta with seconds which the schedule will no longer trigger the task to " "run" msgstr "" "Différence de temps en secondes à laquelle la planification ne déclenchera " "plus la tâche à démarrer" #: django_celery_beat/models.py:503 msgid "One-off Task" msgstr "Tâche Ponctuelle" #: django_celery_beat/models.py:505 msgid "If True, the schedule will only run the task a single time" msgstr "Si Vrai, la planification ne démarrera la tâche qu'une seule fois" #: django_celery_beat/models.py:509 msgid "Start Datetime" msgstr "Date et heure de démarrage" #: django_celery_beat/models.py:511 msgid "Datetime when the schedule should begin triggering the task to run" msgstr "" "Date et heure à laquelle la planification devrait commencer à déclencher la " "tâche à démarrer" #: django_celery_beat/models.py:522 msgid "Last Run Datetime" msgstr "Date et heure du dernier démarrage" #: django_celery_beat/models.py:524 msgid "" "Datetime that the schedule last triggered the task to run. Reset to None if " "enabled is set to False." msgstr "" "Date et heure à laquelle la planification à dernièrement déclenchée la tâche " "à démarrer. Est remis à Vide si activé est mis à Faux" #: django_celery_beat/models.py:529 msgid "Total Run Count" msgstr "Nombre Total de Démarrage" #: django_celery_beat/models.py:531 msgid "Running count of how many times the schedule has triggered the task" msgstr "Compte combien de fois la planification a déclenchée la tâche" #: django_celery_beat/models.py:536 msgid "Last Modified" msgstr "Dernière modification" #: django_celery_beat/models.py:537 msgid "Datetime that this PeriodicTask was last modified" msgstr "Date et heure de la dernière modification de cette Tâche Périodique" #: django_celery_beat/models.py:541 msgid "Description" msgstr "Description" #: django_celery_beat/models.py:543 msgid "Detailed description about the details of this Periodic Task" msgstr "Description détaillée à propos des détails de cette Tâche Périodique" #: django_celery_beat/models.py:552 msgid "periodic task" msgstr "tâche périodiuqe" #: django_celery_beat/models.py:553 msgid "periodic tasks" msgstr "tâches périodique" django-celery-beat-2.2.1/django_celery_beat/locale/ru/000077500000000000000000000000001406752007300226415ustar00rootroot00000000000000django-celery-beat-2.2.1/django_celery_beat/locale/ru/LC_MESSAGES/000077500000000000000000000000001406752007300244265ustar00rootroot00000000000000django-celery-beat-2.2.1/django_celery_beat/locale/ru/LC_MESSAGES/django.po000066400000000000000000000350561406752007300262410ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: 1.5.0\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2019-06-14 17:06+1000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Daniil Kharkov \n" "Language-Team: LANGUAGE \n" "Language: ru\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" "%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n" "%100>=11 && n%100<=14)? 2 : 3);\n" #: django_celery_beat/admin.py:71 msgid "Task (registered)" msgstr "Задача (зарегистрированные)" #: django_celery_beat/admin.py:75 msgid "Task (custom)" msgstr "Задача (пользовательская)" #: django_celery_beat/admin.py:92 msgid "Need name of task" msgstr "Укажите название задачи" #: django_celery_beat/admin.py:103 #, python-format msgid "Unable to parse JSON: %s" msgstr "Невозможно проанализировать JSON: %s" #: django_celery_beat/admin.py:165 #, python-brace-format msgid "{0} task{1} {2} successfully {3}" msgstr "{0} задача {1} {2} успешно {3}" #: django_celery_beat/admin.py:168 django_celery_beat/admin.py:230 msgid "was,were" msgstr "был, были" #: django_celery_beat/admin.py:177 msgid "Enable selected tasks" msgstr "Включить выбранные задачи" #: django_celery_beat/admin.py:183 msgid "Disable selected tasks" msgstr "Выключить выбранные задачи" #: django_celery_beat/admin.py:195 msgid "Toggle activity of selected tasks" msgstr "Переключить активность выбранных задач" #: django_celery_beat/admin.py:215 #, python-brace-format msgid "task \"{0}\" not found" msgstr "задача \"{0}\" не найдена" #: django_celery_beat/admin.py:227 #, python-brace-format msgid "{0} task{1} {2} successfully run" msgstr "{0} задача{1} {2} успешно выполнена" #: django_celery_beat/admin.py:233 msgid "Run selected tasks" msgstr "Запустить выбранные задачи" #: django_celery_beat/apps.py:15 msgid "Periodic Tasks" msgstr "Периодические Задачи" #: django_celery_beat/models.py:29 msgid "Days" msgstr "Дни" #: django_celery_beat/models.py:30 msgid "Hours" msgstr "Часы" #: django_celery_beat/models.py:31 msgid "Minutes" msgstr "Минуты" #: django_celery_beat/models.py:32 msgid "Seconds" msgstr "Секунды" #: django_celery_beat/models.py:33 msgid "Microseconds" msgstr "Микросекунды" #: django_celery_beat/models.py:38 msgid "Day" msgstr "день" #: django_celery_beat/models.py:39 msgid "Hour" msgstr "время" #: django_celery_beat/models.py:40 msgid "Minute" msgstr "минут" #: django_celery_beat/models.py:41 msgid "Second" msgstr "Секунды" #: django_celery_beat/models.py:42 msgid "Microsecond" msgstr "Микросекунды" #: django_celery_beat/models.py:54 msgid "Solar Event" msgstr "Астрономическое" #: django_celery_beat/models.py:55 msgid "The type of solar event when the job should run" msgstr "Тип астрономического события для запуска задачи" #: django_celery_beat/models.py:59 msgid "Latitude" msgstr "Широта" #: django_celery_beat/models.py:60 msgid "Run the task when the event happens at this latitude" msgstr "Запуск задачи, когда событие происходит на данной широте" #: django_celery_beat/models.py:65 msgid "Longitude" msgstr "Долгота" #: django_celery_beat/models.py:66 msgid "Run the task when the event happens at this longitude" msgstr "Запуск задачи, когда событие происходит на данной долготе" #: django_celery_beat/models.py:73 msgid "solar event" msgstr "астрономическое событие" #: django_celery_beat/models.py:74 msgid "solar events" msgstr "астрономические события" #: django_celery_beat/models.py:124 msgid "Number of Periods" msgstr "Число периодов" #: django_celery_beat/models.py:125 msgid "Number of interval periods to wait before running the task again" msgstr "Количество периодов интервала перед новым запуском задачи" #: django_celery_beat/models.py:131 msgid "Interval Period" msgstr "Интервальный период" #: django_celery_beat/models.py:132 msgid "The type of period between task runs (Example: days)" msgstr "Тип периода между запусками задачи (Например: дни)" #: django_celery_beat/models.py:138 msgid "interval" msgstr "интервал" #: django_celery_beat/models.py:139 msgid "intervals" msgstr "интервалы" #: django_celery_beat/models.py:162 #, python-brace-format msgid "every {}" msgstr "каждые {}" #: django_celery_beat/models.py:163 #, python-brace-format msgid "every {} {}" msgstr "каждые {} {}" #: django_celery_beat/models.py:175 msgid "Clock Time" msgstr "Время" #: django_celery_beat/models.py:176 msgid "Run the task at clocked time" msgstr "Запуск задачи в указанное время" #: django_celery_beat/models.py:181 django_celery_beat/models.py:475 msgid "Enabled" msgstr "Активна" #: django_celery_beat/models.py:182 django_celery_beat/models.py:476 msgid "Set to False to disable the schedule" msgstr "Выключите для отключения расписания" #: django_celery_beat/models.py:188 django_celery_beat/models.py:189 msgid "clocked" msgstr "время" #: django_celery_beat/models.py:232 msgid "Minute(s)" msgstr "Минуты" #: django_celery_beat/models.py:234 msgid "Cron Minutes to Run. Use \"*\" for \"all\". (Example: \"0,30\")" msgstr "Cron минуты. Используйте \"*\" для \"каждую\". (Например: \"0,30\")" #: django_celery_beat/models.py:239 msgid "Hour(s)" msgstr "Часы" #: django_celery_beat/models.py:241 msgid "Cron Hours to Run. Use \"*\" for \"all\". (Example: \"8,20\")" msgstr "Cron часы. Используйте \"*\" для \"каждый\". (Например: \"8,20\")" #: django_celery_beat/models.py:246 msgid "Day(s) Of The Week" msgstr "Дни недели" #: django_celery_beat/models.py:248 msgid "Cron Days Of The Week to Run. Use \"*\" for \"all\". (Example: \"0,5\")" msgstr "Cron дни недели. Используйте \"*\" для \"каждый\". (Например: \"0,5\")" #: django_celery_beat/models.py:254 msgid "Day(s) Of The Month" msgstr "Дни" #: django_celery_beat/models.py:256 msgid "" "Cron Days Of The Month to Run. Use \"*\" for \"all\". (Example: \"1,15\")" msgstr "" "Cron дни. Используйте \"*\" для \"каждый\". (Например: \"1,15\")" #: django_celery_beat/models.py:262 msgid "Month(s) Of The Year" msgstr "Месяцы" #: django_celery_beat/models.py:264 msgid "" "Cron Months Of The Year to Run. Use \"*\" for \"all\". (Example: \"0,6\")" msgstr "" "Cron месяцы. Используйте \"*\" для \"каждый\". (Например: \"0,6\")" #: django_celery_beat/models.py:271 msgid "Cron Timezone" msgstr "Временная зона для Cron" #: django_celery_beat/models.py:273 msgid "Timezone to Run the Cron Schedule on. Default is UTC." msgstr "Временная зона для Cron расписания. UTC по умолчанию." #: django_celery_beat/models.py:279 msgid "crontab" msgstr "crontab" #: django_celery_beat/models.py:280 msgid "crontabs" msgstr "crontab" #: django_celery_beat/models.py:366 msgid "Name" msgstr "Название" #: django_celery_beat/models.py:367 msgid "Short Description For This Task" msgstr "Краткое описание для этой задачи" #: django_celery_beat/models.py:372 msgid "" "The Name of the Celery Task that Should be Run. (Example: \"proj.tasks." "import_contacts\")" msgstr "" "Имя запускаемой Celery задачи. (Например: \"proj.tasks." "import_contacts\")" #: django_celery_beat/models.py:380 msgid "Interval Schedule" msgstr "Интервал" #: django_celery_beat/models.py:381 msgid "" "Interval Schedule to run the task on. Set only one schedule type, leave the " "others null." msgstr "Интервальное расписание для запуска задачи. Выберите только один тип " "расписания, остальные оставьте пустыми." #: django_celery_beat/models.py:386 msgid "Crontab Schedule" msgstr "Crontab" #: django_celery_beat/models.py:387 msgid "" "Crontab Schedule to run the task on. Set only one schedule type, leave the " "others null." msgstr "Crontab расписание для запуска задачи. Выберите только один тип " "расписания, остальные оставьте пустыми." #: django_celery_beat/models.py:392 msgid "Solar Schedule" msgstr "Астрономическое" #: django_celery_beat/models.py:393 msgid "" "Solar Schedule to run the task on. Set only one schedule type, leave the " "others null." msgstr "Астрономическое расписание для запуска задачи. Выберите только один " "тип расписания, остальные оставьте пустыми." #: django_celery_beat/models.py:398 msgid "Clocked Schedule" msgstr "Хронометрическое" #: django_celery_beat/models.py:399 msgid "" "Clocked Schedule to run the task on. Set only one schedule type, leave the " "others null." msgstr "Хронометрическое расписание для запуска задачи. Выберите только один " "тип расписания, остальные оставьте пустыми." #: django_celery_beat/models.py:405 msgid "Positional Arguments" msgstr "Позиционные аргументы" #: django_celery_beat/models.py:407 msgid "JSON encoded positional arguments (Example: [\"arg1\", \"arg2\"])" msgstr "Закодированные в JSON позиционные аргументы (Например: [\"arg1\", \"arg2\"])" #: django_celery_beat/models.py:412 msgid "Keyword Arguments" msgstr "Именованные аргументы" #: django_celery_beat/models.py:414 msgid "JSON encoded keyword arguments (Example: {\"argument\": \"value\"})" msgstr "Закодированные в JSON именованные аргументы (Например: {\"argument\": \"value\"})" #: django_celery_beat/models.py:420 msgid "Queue Override" msgstr "Переопределение очереди" #: django_celery_beat/models.py:422 msgid "Queue defined in CELERY_TASK_QUEUES. Leave None for default queuing." msgstr "Очередь задана в CELERY_TASK_QUEUES. Оставьте None для стандартного распределения." #: django_celery_beat/models.py:431 msgid "Exchange" msgstr "Exchange" #: django_celery_beat/models.py:432 msgid "Override Exchange for low-level AMQP routing" msgstr "Override Exchange for low-level AMQP routing" #: django_celery_beat/models.py:436 msgid "Routing Key" msgstr "Ключ маршрутизации" #: django_celery_beat/models.py:437 msgid "Override Routing Key for low-level AMQP routing" msgstr "Override Routing Key for low-level AMQP routing" #: django_celery_beat/models.py:441 msgid "AMQP Message Headers" msgstr "Заголовки сообщения AMQP" #: django_celery_beat/models.py:442 msgid "JSON encoded message headers for the AMQP message." msgstr "Закодированные в JSON заголовки для AMQP сообщения." #: django_celery_beat/models.py:448 msgid "Priority" msgstr "Приоритет" #: django_celery_beat/models.py:450 msgid "" "Priority Number between 0 and 255. Supported by: RabbitMQ, Redis (priority " "reversed, 0 is highest)." msgstr "" "Число между 0 и 255. Поддерживается в: RabbitMQ, Redis (приоритет " "по убыванию, 0 наивысший)." #: django_celery_beat/models.py:455 msgid "Expires Datetime" msgstr "Истекает" #: django_celery_beat/models.py:457 msgid "" "Datetime after which the schedule will no longer trigger the task to run" msgstr "Время, после которого расписание больше не будет запускать задачу" #: django_celery_beat/models.py:462 msgid "One-off Task" msgstr "Одноразовая задача" #: django_celery_beat/models.py:464 msgid "If True, the schedule will only run the task a single time" msgstr "Если включено, то задача будет запущена только один раз" #: django_celery_beat/models.py:468 msgid "Start Datetime" msgstr "Время начала" #: django_celery_beat/models.py:470 msgid "Datetime when the schedule should begin triggering the task to run" msgstr "Время начала вызовов задачи расписанием" #: django_celery_beat/models.py:481 msgid "Last Run Datetime" msgstr "Последний запуск" #: django_celery_beat/models.py:483 msgid "" "Datetime that the schedule last triggered the task to run. Reset to None if " "enabled is set to False." msgstr "" "Время последнего вызова задачи. None если задача выключена." #: django_celery_beat/models.py:488 msgid "Total Run Count" msgstr "Запусков всего" #: django_celery_beat/models.py:490 msgid "Running count of how many times the schedule has triggered the task" msgstr "Количество запусков задачи этим расписанием" #: django_celery_beat/models.py:495 msgid "Last Modified" msgstr "Последнее изменение" #: django_celery_beat/models.py:496 msgid "Datetime that this PeriodicTask was last modified" msgstr "Время последнего изменения этой задачи" #: django_celery_beat/models.py:500 msgid "Description" msgstr "Описание" #: django_celery_beat/models.py:502 msgid "Detailed description about the details of this Periodic Task" msgstr "Подробное описание того, что делает эта задача" #: django_celery_beat/models.py:511 msgid "periodic task" msgstr "периодическая задача" #: django_celery_beat/models.py:512 msgid "periodic tasks" msgstr "периодические задачи" django-celery-beat-2.2.1/django_celery_beat/locale/zh_hans/000077500000000000000000000000001406752007300236455ustar00rootroot00000000000000django-celery-beat-2.2.1/django_celery_beat/locale/zh_hans/LC_MESSAGES/000077500000000000000000000000001406752007300254325ustar00rootroot00000000000000django-celery-beat-2.2.1/django_celery_beat/locale/zh_hans/LC_MESSAGES/django.po000066400000000000000000000322351406752007300272410ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-02-19 00:36+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Rainshaw \n" "Language-Team: x_zhuo \n" "Language: zh-hans \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" #: .\django_celery_beat\admin.py:64 msgid "Task (registered)" msgstr "任务 (已注册的)" #: .\django_celery_beat\admin.py:68 msgid "Task (custom)" msgstr "任务 (自定义)" #: .\django_celery_beat\admin.py:85 msgid "Need name of task" msgstr "任务需要一个名称" #: .\django_celery_beat\admin.py:91 .\django_celery_beat\models.py:589 msgid "Only one can be set, in expires and expire_seconds" msgstr "" #: .\django_celery_beat\admin.py:101 #, python-format msgid "Unable to parse JSON: %s" msgstr "无法解析 JSON: %s" #: .\django_celery_beat\admin.py:167 #, python-brace-format msgid "{0} task{1} {2} successfully {3}" msgstr "{0} 任务{1} {2} 成功 {3}" #: .\django_celery_beat\admin.py:170 .\django_celery_beat\admin.py:232 msgid "was,were" msgstr "将" #: .\django_celery_beat\admin.py:179 msgid "Enable selected tasks" msgstr "启用选中的任务" #: .\django_celery_beat\admin.py:185 msgid "Disable selected tasks" msgstr "禁用选中的任务" #: .\django_celery_beat\admin.py:197 msgid "Toggle activity of selected tasks" msgstr "切换选中的任务" #: .\django_celery_beat\admin.py:217 #, python-brace-format msgid "task \"{0}\" not found" msgstr "" #: .\django_celery_beat\admin.py:229 #, python-brace-format msgid "{0} task{1} {2} successfully run" msgstr "{0} 任务{1} {2} 启动成功" #: .\django_celery_beat\admin.py:235 msgid "Run selected tasks" msgstr "运行选中的任务" #: .\django_celery_beat\apps.py:13 msgid "Periodic Tasks" msgstr "周期任务" #: .\django_celery_beat\models.py:26 msgid "Days" msgstr "天" #: .\django_celery_beat\models.py:27 msgid "Hours" msgstr "小时" #: .\django_celery_beat\models.py:28 msgid "Minutes" msgstr "分钟" #: .\django_celery_beat\models.py:29 msgid "Seconds" msgstr "秒" #: .\django_celery_beat\models.py:30 msgid "Microseconds" msgstr "毫秒" #: .\django_celery_beat\models.py:34 msgid "Day" msgstr "天" #: .\django_celery_beat\models.py:35 msgid "Hour" msgstr "小时" #: .\django_celery_beat\models.py:36 msgid "Minute" msgstr "分钟" #: .\django_celery_beat\models.py:37 msgid "Second" msgstr "秒" #: .\django_celery_beat\models.py:38 msgid "Microsecond" msgstr "毫秒" #: .\django_celery_beat\models.py:42 msgid "Astronomical dawn" msgstr "天文黎明" #: .\django_celery_beat\models.py:43 msgid "Civil dawn" msgstr "民事黎明" #: .\django_celery_beat\models.py:44 msgid "Nautical dawn" msgstr "航海黎明" #: .\django_celery_beat\models.py:45 msgid "Astronomical dusk" msgstr "天文黄昏" #: .\django_celery_beat\models.py:46 msgid "Civil dusk" msgstr "民事黄昏" #: .\django_celery_beat\models.py:47 msgid "Nautical dusk" msgstr "航海黄昏" #: .\django_celery_beat\models.py:48 msgid "Solar noon" msgstr "正午" #: .\django_celery_beat\models.py:49 msgid "Sunrise" msgstr "日出" #: .\django_celery_beat\models.py:50 msgid "Sunset" msgstr "日落" #: .\django_celery_beat\models.py:84 msgid "Solar Event" msgstr "日程事件" #: .\django_celery_beat\models.py:85 msgid "The type of solar event when the job should run" msgstr "当任务应该执行时的日程事件类型" #: .\django_celery_beat\models.py:89 msgid "Latitude" msgstr "纬度" #: .\django_celery_beat\models.py:90 msgid "Run the task when the event happens at this latitude" msgstr "当在此纬度发生事件时执行任务" #: .\django_celery_beat\models.py:95 msgid "Longitude" msgstr "经度" #: .\django_celery_beat\models.py:96 msgid "Run the task when the event happens at this longitude" msgstr "当在此经度发生事件时执行任务" #: .\django_celery_beat\models.py:103 msgid "solar event" msgstr "日程事件" #: .\django_celery_beat\models.py:104 msgid "solar events" msgstr "日程事件" #: .\django_celery_beat\models.py:153 msgid "Number of Periods" msgstr "周期数" #: .\django_celery_beat\models.py:154 msgid "Number of interval periods to wait before running the task again" msgstr "再次执行任务之前要等待的间隔周期数" #: .\django_celery_beat\models.py:160 msgid "Interval Period" msgstr "间隔周期" #: .\django_celery_beat\models.py:161 msgid "The type of period between task runs (Example: days)" msgstr "任务每次执行之间的时间间隔类型(例如:天)" #: .\django_celery_beat\models.py:167 msgid "interval" msgstr "间隔" #: .\django_celery_beat\models.py:168 msgid "intervals" msgstr "间隔" #: .\django_celery_beat\models.py:196 msgid "every {}" msgstr "每 {}" #: .\django_celery_beat\models.py:201 msgid "every {} {}" msgstr "每 {} {}" #: .\django_celery_beat\models.py:212 msgid "Clock Time" msgstr "定时时间" #: .\django_celery_beat\models.py:213 msgid "Run the task at clocked time" msgstr "在定时时间执行任务" #: .\django_celery_beat\models.py:219 .\django_celery_beat\models.py:220 msgid "clocked" msgstr "定时" #: .\django_celery_beat\models.py:260 msgid "Minute(s)" msgstr "分钟" #: .\django_celery_beat\models.py:262 msgid "Cron Minutes to Run. Use \"*\" for \"all\". (Example: \"0,30\")" msgstr "计划执行的分钟。 将\"*\"用作\"all\"。(例如:\"0,30\")" #: .\django_celery_beat\models.py:267 msgid "Hour(s)" msgstr "小时" #: .\django_celery_beat\models.py:269 msgid "Cron Hours to Run. Use \"*\" for \"all\". (Example: \"8,20\")" msgstr "计划执行的小时。 将\"*\"用作\"all\"。(例如:\"8,20\")" #: .\django_celery_beat\models.py:274 msgid "Day(s) Of The Week" msgstr "一个星期的第几天" #: .\django_celery_beat\models.py:276 msgid "Cron Days Of The Week to Run. Use \"*\" for \"all\". (Example: \"0,5\")" msgstr "计划执行的每周的第几天。将\"*\"用作\"all\"。(例如:\"0,5\")" #: .\django_celery_beat\models.py:282 msgid "Day(s) Of The Month" msgstr "一个月的第几天" #: .\django_celery_beat\models.py:284 msgid "" "Cron Days Of The Month to Run. Use \"*\" for \"all\". (Example: \"1,15\")" msgstr "计划执行的每个月的第几天。将\"*\"用作\"all\"。(例如:\"0,5\")" #: .\django_celery_beat\models.py:290 msgid "Month(s) Of The Year" msgstr "一年的第几个月" #: .\django_celery_beat\models.py:292 msgid "" "Cron Months Of The Year to Run. Use \"*\" for \"all\". (Example: \"0,6\")" msgstr "计划执行的每一年的第几个月。将\"*\"用作\"all\"。(例如:\"0,5\")" #: .\django_celery_beat\models.py:299 msgid "Cron Timezone" msgstr "计划任务的时区" #: .\django_celery_beat\models.py:301 msgid "Timezone to Run the Cron Schedule on. Default is UTC." msgstr "执行计划任务表的时区。 默认为UTC。" #: .\django_celery_beat\models.py:307 msgid "crontab" msgstr "计划任务" #: .\django_celery_beat\models.py:308 msgid "crontabs" msgstr "计划任务" #: .\django_celery_beat\models.py:393 msgid "Name" msgstr "任务名" #: .\django_celery_beat\models.py:394 msgid "Short Description For This Task" msgstr "该任务的简短说明" #: .\django_celery_beat\models.py:399 msgid "" "The Name of the Celery Task that Should be Run. (Example: \"proj.tasks." "import_contacts\")" msgstr "被执行的任务的名称。(例如:\"proj.tasks.import_contacts\")" #: .\django_celery_beat\models.py:407 msgid "Interval Schedule" msgstr "间隔时间表" #: .\django_celery_beat\models.py:408 msgid "" "Interval Schedule to run the task on. Set only one schedule type, leave the " "others null." msgstr "执行任务的间隔时间表。 仅设置一种时间表类型,将其他保留为空。" #: .\django_celery_beat\models.py:413 msgid "Crontab Schedule" msgstr "计划时间表" #: .\django_celery_beat\models.py:414 msgid "" "Crontab Schedule to run the task on. Set only one schedule type, leave the " "others null." msgstr "执行任务的计划时间表。 仅设置一种时间表类型,将其他保留为空。" #: .\django_celery_beat\models.py:419 msgid "Solar Schedule" msgstr "日程时间表" #: .\django_celery_beat\models.py:420 msgid "" "Solar Schedule to run the task on. Set only one schedule type, leave the " "others null." msgstr "执行任务的日程时间表。 仅设置一种时间表类型,将其他保留为空。" #: .\django_celery_beat\models.py:425 msgid "Clocked Schedule" msgstr "定时时间表" #: .\django_celery_beat\models.py:426 msgid "" "Clocked Schedule to run the task on. Set only one schedule type, leave the " "others null." msgstr "执行任务的定时时间表。 仅设置一种时间表类型,将其他保留为空。" #: .\django_celery_beat\models.py:432 msgid "Positional Arguments" msgstr "位置参数" #: .\django_celery_beat\models.py:434 msgid "JSON encoded positional arguments (Example: [\"arg1\", \"arg2\"])" msgstr "JSON编码的位置参数(例如: [\"arg1\", \"arg2\"])" #: .\django_celery_beat\models.py:439 msgid "Keyword Arguments" msgstr "关键字参数" #: .\django_celery_beat\models.py:441 msgid "JSON encoded keyword arguments (Example: {\"argument\": \"value\"})" msgstr "JSON编码的关键字参数(例如: {\"argument\": \"value\"})" #: .\django_celery_beat\models.py:447 msgid "Queue Override" msgstr "队列覆盖" #: .\django_celery_beat\models.py:449 msgid "Queue defined in CELERY_TASK_QUEUES. Leave None for default queuing." msgstr "在 CELERY_TASK_QUEUES 定义的队列。保留空以进行默认排队。" #: .\django_celery_beat\models.py:458 msgid "Exchange" msgstr "交换机" #: .\django_celery_beat\models.py:459 msgid "Override Exchange for low-level AMQP routing" msgstr "覆盖交换机以进行低层级AMQP路由" #: .\django_celery_beat\models.py:463 msgid "Routing Key" msgstr "路由键" #: .\django_celery_beat\models.py:464 msgid "Override Routing Key for low-level AMQP routing" msgstr "覆盖路由键以进行低层级AMQP路由" #: .\django_celery_beat\models.py:468 msgid "AMQP Message Headers" msgstr "AMQP消息头" #: .\django_celery_beat\models.py:469 msgid "JSON encoded message headers for the AMQP message." msgstr "AMQP消息的JSON编码消息头。" #: .\django_celery_beat\models.py:475 msgid "Priority" msgstr "优先级" #: .\django_celery_beat\models.py:477 msgid "" "Priority Number between 0 and 255. Supported by: RabbitMQ, Redis (priority " "reversed, 0 is highest)." msgstr "优先级数字,介于0和255之间。支持者:RabbitMQ,Redis(优先级颠倒,0是最高)。" #: .\django_celery_beat\models.py:482 msgid "Expires Datetime" msgstr "过期时刻" #: .\django_celery_beat\models.py:484 msgid "" "Datetime after which the schedule will no longer trigger the task to run" msgstr "过期时刻,计划表将在此时刻后不再触发任务执行" #: .\django_celery_beat\models.py:489 msgid "Expires timedelta with seconds" msgstr "过期时间间隔,以秒为单位" #: .\django_celery_beat\models.py:491 msgid "" "Timedelta with seconds which the schedule will no longer trigger the task to " "run" msgstr "再过该秒后,不再触发任务执行" #: .\django_celery_beat\models.py:497 msgid "One-off Task" msgstr "一次任务" #: .\django_celery_beat\models.py:499 msgid "If True, the schedule will only run the task a single time" msgstr "如果为True,则计划将仅运行任务一次" #: .\django_celery_beat\models.py:503 msgid "Start Datetime" msgstr "开始时间" #: .\django_celery_beat\models.py:505 msgid "Datetime when the schedule should begin triggering the task to run" msgstr "时间表开始触发任务执行的时刻" #: .\django_celery_beat\models.py:510 msgid "Enabled" msgstr "已启用" #: .\django_celery_beat\models.py:511 msgid "Set to False to disable the schedule" msgstr "设置为False可禁用时间表" #: .\django_celery_beat\models.py:516 msgid "Last Run Datetime" msgstr "上次运行时刻" #: .\django_celery_beat\models.py:518 msgid "" "Datetime that the schedule last triggered the task to run. Reset to None if " "enabled is set to False." msgstr "最后一次触发任务执行的时刻。 如果enabled设置为False,则重置为None。" #: .\django_celery_beat\models.py:523 msgid "Total Run Count" msgstr "总运行次数" #: .\django_celery_beat\models.py:525 msgid "Running count of how many times the schedule has triggered the task" msgstr "任务执行多少次的运行计数" #: .\django_celery_beat\models.py:530 msgid "Last Modified" msgstr "最后修改" #: .\django_celery_beat\models.py:531 msgid "Datetime that this PeriodicTask was last modified" msgstr "该周期性任务的最后修改时刻" #: .\django_celery_beat\models.py:535 msgid "Description" msgstr "描述" #: .\django_celery_beat\models.py:537 msgid "Detailed description about the details of this Periodic Task" msgstr "有关此周期性任务的详细信息" #: .\django_celery_beat\models.py:546 msgid "periodic task" msgstr "周期性任务" #: .\django_celery_beat\models.py:547 msgid "periodic tasks" msgstr "周期性任务" django-celery-beat-2.2.1/django_celery_beat/managers.py000066400000000000000000000015561406752007300231320ustar00rootroot00000000000000"""Model managers.""" 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 fields.items()] 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-2.2.1/django_celery_beat/migrations/000077500000000000000000000000001406752007300231305ustar00rootroot00000000000000django-celery-beat-2.2.1/django_celery_beat/migrations/0001_initial.py000066400000000000000000000126671406752007300256070ustar00rootroot00000000000000# Generated by Django 1.9.5 on 2016-08-04 02:13 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-2.2.1/django_celery_beat/migrations/0002_auto_20161118_0346.py000066400000000000000000000037201406752007300265540ustar00rootroot00000000000000# Generated by Django 1.10.3 on 2016-11-18 03:46 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-2.2.1/django_celery_beat/migrations/0003_auto_20161209_0049.py000066400000000000000000000012301406752007300265500ustar00rootroot00000000000000# Generated by Django 1.9.11 on 2016-12-09 00:49 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-2.2.1/django_celery_beat/migrations/0004_auto_20170221_0000.py000066400000000000000000000007051406752007300265340ustar00rootroot00000000000000from 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-2.2.1/django_celery_beat/migrations/0005_add_solarschedule_events_choices.py000066400000000000000000000016101406752007300326720ustar00rootroot00000000000000# Generated by Django 1.9.1 on 2017-11-01 15:53 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-2.2.1/django_celery_beat/migrations/0006_auto_20180210_1226.py000066400000000000000000000017061406752007300265520ustar00rootroot00000000000000# Generated by Django 2.0.1 on 2018-02-10 12:26 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-2.2.1/django_celery_beat/migrations/0006_auto_20180322_0932.py000066400000000000000000000031041406752007300265530ustar00rootroot00000000000000# Generated by Django 1.11.7 on 2018-03-22 16:32 from django.db import migrations, models import timezone_field.fields class Migration(migrations.Migration): dependencies = [ ('django_celery_beat', '0005_add_solarschedule_events_choices'), # ('django_celery_beat', '0006_auto_20180210_1226'), ] operations = [ migrations.AlterModelOptions( name='crontabschedule', options={ 'ordering': [ 'month_of_year', 'day_of_month', 'day_of_week', 'hour', 'minute', 'timezone' ], 'verbose_name': 'crontab', 'verbose_name_plural': 'crontabs' }, ), migrations.AddField( model_name='crontabschedule', name='timezone', field=timezone_field.fields.TimeZoneField(default='UTC'), ), 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-2.2.1/django_celery_beat/migrations/0006_periodictask_priority.py000066400000000000000000000017631406752007300306000ustar00rootroot00000000000000# Generated by Django 2.0.6 on 2018-10-22 05:20 import django.core.validators from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ # depends on higher numbers due to a squashed migration # that was later removed due to migration issues it caused ('django_celery_beat', '0005_add_solarschedule_events_choices'), ('django_celery_beat', '0006_auto_20180210_1226'), ('django_celery_beat', '0006_auto_20180322_0932'), ('django_celery_beat', '0007_auto_20180521_0826'), ('django_celery_beat', '0008_auto_20180914_1922'), ] operations = [ migrations.AddField( model_name='periodictask', name='priority', field=models.PositiveIntegerField( blank=True, default=None, null=True, validators=[django.core.validators.MaxValueValidator(255)], verbose_name='priority'), ), ] django-celery-beat-2.2.1/django_celery_beat/migrations/0007_auto_20180521_0826.py000066400000000000000000000013561406752007300265660ustar00rootroot00000000000000# Generated by Django 1.10.7 on 2018-05-21 08:26 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('django_celery_beat', '0006_auto_20180322_0932'), ] operations = [ migrations.AddField( model_name='periodictask', name='one_off', field=models.BooleanField(default=False, verbose_name='one-off task'), ), migrations.AddField( model_name='periodictask', name='start_time', field=models.DateTimeField(blank=True, null=True, verbose_name='start_time'), ), ] django-celery-beat-2.2.1/django_celery_beat/migrations/0008_auto_20180914_1922.py000066400000000000000000000034641406752007300265750ustar00rootroot00000000000000# Generated by Django 2.0.3 on 2018-09-14 19:22 from django.db import migrations, models from django_celery_beat import validators class Migration(migrations.Migration): dependencies = [ ('django_celery_beat', '0007_auto_20180521_0826'), ] operations = [ migrations.AlterField( model_name='crontabschedule', name='day_of_month', field=models.CharField( default='*', max_length=124, validators=[validators.day_of_month_validator], verbose_name='day of month' ), ), migrations.AlterField( model_name='crontabschedule', name='day_of_week', field=models.CharField( default='*', max_length=64, validators=[validators.day_of_week_validator], verbose_name='day of week' ), ), migrations.AlterField( model_name='crontabschedule', name='hour', field=models.CharField( default='*', max_length=96, validators=[validators.hour_validator], verbose_name='hour' ), ), migrations.AlterField( model_name='crontabschedule', name='minute', field=models.CharField( default='*', max_length=240, validators=[validators.minute_validator], verbose_name='minute' ), ), migrations.AlterField( model_name='crontabschedule', name='month_of_year', field=models.CharField( default='*', max_length=64, validators=[validators.month_of_year_validator], verbose_name='month of year' ), ), ] django-celery-beat-2.2.1/django_celery_beat/migrations/0009_periodictask_headers.py000066400000000000000000000010671406752007300303320ustar00rootroot00000000000000# Generated by Django 2.1.5 on 2019-02-09 19:33 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('django_celery_beat', '0006_periodictask_priority'), ] operations = [ migrations.AddField( model_name='periodictask', name='headers', field=models.TextField( blank=True, default='{}', help_text='JSON encoded message headers', verbose_name='Message headers' ), ), ] django-celery-beat-2.2.1/django_celery_beat/migrations/0010_auto_20190429_0326.py000066400000000000000000000244671406752007300265730ustar00rootroot00000000000000# Generated by Django 1.11.20 on 2019-04-29 03:26 # this file is auto-generated so don't do flake8 on it # flake8: noqa import django.core.validators from django.db import migrations, models import django.db.models.deletion import django_celery_beat.validators import timezone_field.fields class Migration(migrations.Migration): dependencies = [ ('django_celery_beat', '0009_periodictask_headers'), ] operations = [ migrations.AlterField( model_name='crontabschedule', name='day_of_month', field=models.CharField(default='*', help_text='Cron Days Of The Month to Run. Use "*" for "all". (Example: "1,15")', max_length=124, validators=[django_celery_beat.validators.day_of_month_validator], verbose_name='Day(s) Of The Month'), ), migrations.AlterField( model_name='crontabschedule', name='day_of_week', field=models.CharField(default='*', help_text='Cron Days Of The Week to Run. Use "*" for "all". (Example: "0,5")', max_length=64, validators=[django_celery_beat.validators.day_of_week_validator], verbose_name='Day(s) Of The Week'), ), migrations.AlterField( model_name='crontabschedule', name='hour', field=models.CharField(default='*', help_text='Cron Hours to Run. Use "*" for "all". (Example: "8,20")', max_length=96, validators=[django_celery_beat.validators.hour_validator], verbose_name='Hour(s)'), ), migrations.AlterField( model_name='crontabschedule', name='minute', field=models.CharField(default='*', help_text='Cron Minutes to Run. Use "*" for "all". (Example: "0,30")', max_length=240, validators=[django_celery_beat.validators.minute_validator], verbose_name='Minute(s)'), ), migrations.AlterField( model_name='crontabschedule', name='month_of_year', field=models.CharField(default='*', help_text='Cron Months Of The Year to Run. Use "*" for "all". (Example: "0,6")', max_length=64, validators=[django_celery_beat.validators.month_of_year_validator], verbose_name='Month(s) Of The Year'), ), migrations.AlterField( model_name='crontabschedule', name='timezone', field=timezone_field.fields.TimeZoneField(default='UTC', help_text='Timezone to Run the Cron Schedule on. Default is UTC.', verbose_name='Cron Timezone'), ), migrations.AlterField( model_name='intervalschedule', name='every', field=models.IntegerField(help_text='Number of interval periods to wait before running the task again', validators=[django.core.validators.MinValueValidator(1)], verbose_name='Number of Periods'), ), migrations.AlterField( model_name='intervalschedule', name='period', field=models.CharField(choices=[('days', 'Days'), ('hours', 'Hours'), ('minutes', 'Minutes'), ('seconds', 'Seconds'), ('microseconds', 'Microseconds')], help_text='The type of period between task runs (Example: days)', max_length=24, verbose_name='Interval Period'), ), migrations.AlterField( model_name='periodictask', name='args', field=models.TextField(blank=True, default='[]', help_text='JSON encoded positional arguments (Example: ["arg1", "arg2"])', verbose_name='Positional Arguments'), ), migrations.AlterField( model_name='periodictask', name='crontab', field=models.ForeignKey(blank=True, help_text='Crontab Schedule to run the task on. Set only one schedule type, leave the others null.', null=True, on_delete=django.db.models.deletion.CASCADE, to='django_celery_beat.CrontabSchedule', verbose_name='Crontab Schedule'), ), migrations.AlterField( model_name='periodictask', name='date_changed', field=models.DateTimeField(auto_now=True, help_text='Datetime that this PeriodicTask was last modified', verbose_name='Last Modified'), ), migrations.AlterField( model_name='periodictask', name='description', field=models.TextField(blank=True, help_text='Detailed description about the details of this Periodic Task', verbose_name='Description'), ), migrations.AlterField( model_name='periodictask', name='enabled', field=models.BooleanField(default=True, help_text='Set to False to disable the schedule', verbose_name='Enabled'), ), migrations.AlterField( model_name='periodictask', name='exchange', field=models.CharField(blank=True, default=None, help_text='Override Exchange for low-level AMQP routing', max_length=200, null=True, verbose_name='Exchange'), ), migrations.AlterField( model_name='periodictask', name='expires', field=models.DateTimeField(blank=True, help_text='Datetime after which the schedule will no longer trigger the task to run', null=True, verbose_name='Expires Datetime'), ), migrations.AlterField( model_name='periodictask', name='headers', field=models.TextField(blank=True, default='{}', help_text='JSON encoded message headers for the AMQP message.', verbose_name='AMQP Message Headers'), ), migrations.AlterField( model_name='periodictask', name='interval', field=models.ForeignKey(blank=True, help_text='Interval Schedule to run the task on. Set only one schedule type, leave the others null.', null=True, on_delete=django.db.models.deletion.CASCADE, to='django_celery_beat.IntervalSchedule', verbose_name='Interval Schedule'), ), migrations.AlterField( model_name='periodictask', name='kwargs', field=models.TextField(blank=True, default='{}', help_text='JSON encoded keyword arguments (Example: {"argument": "value"})', verbose_name='Keyword Arguments'), ), migrations.AlterField( model_name='periodictask', name='last_run_at', field=models.DateTimeField(blank=True, editable=False, help_text='Datetime that the schedule last triggered the task to run. Reset to None if enabled is set to False.', null=True, verbose_name='Last Run Datetime'), ), migrations.AlterField( model_name='periodictask', name='name', field=models.CharField(help_text='Short Description For This Task', max_length=200, unique=True, verbose_name='Name'), ), migrations.AlterField( model_name='periodictask', name='one_off', field=models.BooleanField(default=False, help_text='If True, the schedule will only run the task a single time', verbose_name='One-off Task'), ), migrations.AlterField( model_name='periodictask', name='priority', field=models.PositiveIntegerField(blank=True, default=None, help_text='Priority Number between 0 and 255. Supported by: RabbitMQ, Redis (priority reversed, 0 is highest).', null=True, validators=[django.core.validators.MaxValueValidator(255)], verbose_name='Priority'), ), migrations.AlterField( model_name='periodictask', name='queue', field=models.CharField(blank=True, default=None, help_text='Queue defined in CELERY_TASK_QUEUES. Leave None for default queuing.', max_length=200, null=True, verbose_name='Queue Override'), ), migrations.AlterField( model_name='periodictask', name='routing_key', field=models.CharField(blank=True, default=None, help_text='Override Routing Key for low-level AMQP routing', max_length=200, null=True, verbose_name='Routing Key'), ), migrations.AlterField( model_name='periodictask', name='solar', field=models.ForeignKey(blank=True, help_text='Solar Schedule to run the task on. Set only one schedule type, leave the others null.', null=True, on_delete=django.db.models.deletion.CASCADE, to='django_celery_beat.SolarSchedule', verbose_name='Solar Schedule'), ), migrations.AlterField( model_name='periodictask', name='start_time', field=models.DateTimeField(blank=True, help_text='Datetime when the schedule should begin triggering the task to run', null=True, verbose_name='Start Datetime'), ), migrations.AlterField( model_name='periodictask', name='task', field=models.CharField(help_text='The Name of the Celery Task that Should be Run. (Example: "proj.tasks.import_contacts")', max_length=200, verbose_name='Task Name'), ), migrations.AlterField( model_name='periodictask', name='total_run_count', field=models.PositiveIntegerField(default=0, editable=False, help_text='Running count of how many times the schedule has triggered the task', verbose_name='Total Run Count'), ), 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')], help_text='The type of solar event when the job should run', max_length=24, verbose_name='Solar Event'), ), migrations.AlterField( model_name='solarschedule', name='latitude', field=models.DecimalField(decimal_places=6, help_text='Run the task when the event happens at this latitude', max_digits=9, validators=[django.core.validators.MinValueValidator(-90), django.core.validators.MaxValueValidator(90)], verbose_name='Latitude'), ), migrations.AlterField( model_name='solarschedule', name='longitude', field=models.DecimalField(decimal_places=6, help_text='Run the task when the event happens at this longitude', max_digits=9, validators=[django.core.validators.MinValueValidator(-180), django.core.validators.MaxValueValidator(180)], verbose_name='Longitude'), ), ] django-celery-beat-2.2.1/django_celery_beat/migrations/0011_auto_20190508_0153.py000066400000000000000000000025221406752007300265540ustar00rootroot00000000000000# Generated by Django 2.2 on 2019-05-08 01:53 # flake8: noqa from django.db import migrations, models import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ ('django_celery_beat', '0010_auto_20190429_0326'), ] operations = [ migrations.CreateModel( name='ClockedSchedule', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('clocked_time', models.DateTimeField(help_text='Run the task at clocked time', verbose_name='Clock Time')), ('enabled', models.BooleanField(default=True, editable=False, help_text='Set to False to disable the schedule', verbose_name='Enabled')), ], options={ 'verbose_name': 'clocked', 'verbose_name_plural': 'clocked', 'ordering': ['clocked_time'], }, ), migrations.AddField( model_name='periodictask', name='clocked', field=models.ForeignKey(blank=True, help_text='Clocked Schedule to run the task on. Set only one schedule type, leave the others null.', null=True, on_delete=django.db.models.deletion.CASCADE, to='django_celery_beat.ClockedSchedule', verbose_name='Clocked Schedule'), ), ] django-celery-beat-2.2.1/django_celery_beat/migrations/0012_periodictask_expire_seconds.py000066400000000000000000000011071406752007300317160ustar00rootroot00000000000000# Generated by Django 2.2.4 on 2019-08-30 00:46 # flake8: noqa from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('django_celery_beat', '0011_auto_20190508_0153'), ] operations = [ migrations.AddField( model_name='periodictask', name='expire_seconds', field=models.PositiveIntegerField(blank=True, help_text='Timedelta with seconds which the schedule will no longer trigger the task to run', null=True, verbose_name='Expires timedelta with seconds'), ), ] django-celery-beat-2.2.1/django_celery_beat/migrations/0013_auto_20200609_0727.py000066400000000000000000000012161406752007300265560ustar00rootroot00000000000000# Generated by Django 3.0.6 on 2020-06-09 07:27 # flake8: noqa from django.db import migrations import django_celery_beat.models import timezone_field.fields class Migration(migrations.Migration): dependencies = [ ('django_celery_beat', '0012_periodictask_expire_seconds'), ] operations = [ migrations.AlterField( model_name='crontabschedule', name='timezone', field=timezone_field.fields.TimeZoneField(default=django_celery_beat.models.crontab_schedule_celery_timezone, help_text='Timezone to Run the Cron Schedule on. Default is UTC.', verbose_name='Cron Timezone'), ), ] django-celery-beat-2.2.1/django_celery_beat/migrations/0014_remove_clockedschedule_enabled.py000066400000000000000000000005531406752007300323210ustar00rootroot00000000000000# Generated by Django 2.2.4 on 2019-08-30 00:46 # flake8: noqa from django.db import migrations class Migration(migrations.Migration): dependencies = [ ('django_celery_beat', '0013_auto_20200609_0727'), ] operations = [ migrations.RemoveField( model_name='clockedschedule', name='enabled', ), ] django-celery-beat-2.2.1/django_celery_beat/migrations/0015_edit_solarschedule_events_choices.py000066400000000000000000000014701406752007300330740ustar00rootroot00000000000000# Generated by Django 3.0.6 on 2020-12-13 15:00 # flake8: noqa from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('django_celery_beat', '0014_remove_clockedschedule_enabled'), ] operations = [ migrations.AlterField( model_name='solarschedule', name='event', field=models.CharField(choices=[('dawn_astronomical', 'Astronomical dawn'), ('dawn_civil', 'Civil dawn'), ('dawn_nautical', 'Nautical dawn'), ('dusk_astronomical', 'Astronomical dusk'), ('dusk_civil', 'Civil dusk'), ('dusk_nautical', 'Nautical dusk'), ('solar_noon', 'Solar noon'), ('sunrise', 'Sunrise'), ('sunset', 'Sunset')], help_text='The type of solar event when the job should run', max_length=24, verbose_name='Solar Event'), ), ] django-celery-beat-2.2.1/django_celery_beat/migrations/__init__.py000066400000000000000000000000001406752007300252270ustar00rootroot00000000000000django-celery-beat-2.2.1/django_celery_beat/models.py000066400000000000000000000515301406752007300226150ustar00rootroot00000000000000"""Database models.""" from datetime import timedelta import timezone_field from celery import schedules, current_app from django.conf import settings from django.core.exceptions import MultipleObjectsReturned, ValidationError from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from django.db.models import signals from django.utils.translation import gettext_lazy as _ from . import managers, validators from .tzcrontab import TzAwareCrontab from .utils import make_aware, now from .clockedschedule import clocked DAYS = 'days' HOURS = 'hours' MINUTES = 'minutes' SECONDS = 'seconds' MICROSECONDS = 'microseconds' PERIOD_CHOICES = ( (DAYS, _('Days')), (HOURS, _('Hours')), (MINUTES, _('Minutes')), (SECONDS, _('Seconds')), (MICROSECONDS, _('Microseconds')), ) SINGULAR_PERIODS = ( (DAYS, _('Day')), (HOURS, _('Hour')), (MINUTES, _('Minute')), (SECONDS, _('Second')), (MICROSECONDS, _('Microsecond')), ) SOLAR_SCHEDULES = [ ("dawn_astronomical", _("Astronomical dawn")), ("dawn_civil", _("Civil dawn")), ("dawn_nautical", _("Nautical dawn")), ("dusk_astronomical", _("Astronomical dusk")), ("dusk_civil", _("Civil dusk")), ("dusk_nautical", _("Nautical dusk")), ("solar_noon", _("Solar noon")), ("sunrise", _("Sunrise")), ("sunset", _("Sunset")), ] def cronexp(field): """Representation of cron expression.""" return field and str(field).replace(' ', '') or '*' def crontab_schedule_celery_timezone(): """Return timezone string from Django settings `CELERY_TIMEZONE` variable. If is not defined or is not a valid timezone, return `"UTC"` instead. """ try: CELERY_TIMEZONE = getattr( settings, '%s_TIMEZONE' % current_app.namespace) except AttributeError: return 'UTC' return CELERY_TIMEZONE if CELERY_TIMEZONE in [ choice[0].zone for choice in timezone_field. TimeZoneField.default_choices ] else 'UTC' class SolarSchedule(models.Model): """Schedule following astronomical patterns. Example: to run every sunrise in New York City: event='sunrise', latitude=40.7128, longitude=74.0060 """ event = models.CharField( max_length=24, choices=SOLAR_SCHEDULES, verbose_name=_('Solar Event'), help_text=_('The type of solar event when the job should run'), ) latitude = models.DecimalField( max_digits=9, decimal_places=6, verbose_name=_('Latitude'), help_text=_('Run the task when the event happens at this latitude'), validators=[MinValueValidator(-90), MaxValueValidator(90)], ) longitude = models.DecimalField( max_digits=9, decimal_places=6, verbose_name=_('Longitude'), help_text=_('Run the task when the event happens at this longitude'), validators=[MinValueValidator(-180), MaxValueValidator(180)], ) 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} # we do not check for MultipleObjectsReturned exception here because # the unique_together constraint safely prevents from duplicates try: return cls.objects.get(**spec) except cls.DoesNotExist: return cls(**spec) def __str__(self): return '{0} ({1}, {2})'.format( self.get_event_display(), self.latitude, self.longitude ) class IntervalSchedule(models.Model): """Schedule executing on a regular interval. Example: execute every 2 days every=2, period=DAYS """ DAYS = DAYS HOURS = HOURS MINUTES = MINUTES SECONDS = SECONDS MICROSECONDS = MICROSECONDS PERIOD_CHOICES = PERIOD_CHOICES every = models.IntegerField( null=False, verbose_name=_('Number of Periods'), help_text=_('Number of interval periods to wait before ' 'running the task again'), validators=[MinValueValidator(1)], ) period = models.CharField( max_length=24, choices=PERIOD_CHOICES, verbose_name=_('Interval Period'), help_text=_('The type of period between task runs (Example: days)'), ) 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: return cls.objects.filter(every=every, period=period).first() def __str__(self): readable_period = None if self.every == 1: for period, _readable_period in SINGULAR_PERIODS: if period == self.period: readable_period = _readable_period.lower() break return _('every {}').format(readable_period) for period, _readable_period in PERIOD_CHOICES: if period == self.period: readable_period = _readable_period.lower() break return _('every {} {}').format(self.every, readable_period) @property def period_singular(self): return self.period[:-1] class ClockedSchedule(models.Model): """clocked schedule.""" clocked_time = models.DateTimeField( verbose_name=_('Clock Time'), help_text=_('Run the task at clocked time'), ) class Meta: """Table information.""" verbose_name = _('clocked') verbose_name_plural = _('clocked') ordering = ['clocked_time'] def __str__(self): return '{}'.format(self.clocked_time) @property def schedule(self): c = clocked(clocked_time=self.clocked_time) return c @classmethod def from_schedule(cls, schedule): spec = {'clocked_time': schedule.clocked_time} try: return cls.objects.get(**spec) except cls.DoesNotExist: return cls(**spec) except MultipleObjectsReturned: return cls.objects.filter(**spec).first() class CrontabSchedule(models.Model): """Timezone Aware Crontab-like schedule. Example: Run every hour at 0 minutes for days of month 10-15 minute="0", hour="*", day_of_week="*", day_of_month="10-15", month_of_year="*" """ # # 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( max_length=60 * 4, default='*', verbose_name=_('Minute(s)'), help_text=_( 'Cron Minutes to Run. Use "*" for "all". (Example: "0,30")'), validators=[validators.minute_validator], ) hour = models.CharField( max_length=24 * 4, default='*', verbose_name=_('Hour(s)'), help_text=_( 'Cron Hours to Run. Use "*" for "all". (Example: "8,20")'), validators=[validators.hour_validator], ) day_of_week = models.CharField( max_length=64, default='*', verbose_name=_('Day(s) Of The Week'), help_text=_( 'Cron Days Of The Week to Run. Use "*" for "all". ' '(Example: "0,5")'), validators=[validators.day_of_week_validator], ) day_of_month = models.CharField( max_length=31 * 4, default='*', verbose_name=_('Day(s) Of The Month'), help_text=_( 'Cron Days Of The Month to Run. Use "*" for "all". ' '(Example: "1,15")'), validators=[validators.day_of_month_validator], ) month_of_year = models.CharField( max_length=64, default='*', verbose_name=_('Month(s) Of The Year'), help_text=_( 'Cron Months Of The Year to Run. Use "*" for "all". ' '(Example: "0,6")'), validators=[validators.month_of_year_validator], ) timezone = timezone_field.TimeZoneField( default=crontab_schedule_celery_timezone, verbose_name=_('Cron Timezone'), help_text=_( 'Timezone to Run the Cron Schedule on. Default is UTC.'), ) class Meta: """Table information.""" verbose_name = _('crontab') verbose_name_plural = _('crontabs') ordering = ['month_of_year', 'day_of_month', 'day_of_week', 'hour', 'minute', 'timezone'] def __str__(self): return '{0} {1} {2} {3} {4} (m/h/dM/MY/d) {5}'.format( cronexp(self.minute), cronexp(self.hour), cronexp(self.day_of_month), cronexp(self.month_of_year), cronexp(self.day_of_week), str(self.timezone) ) @property def schedule(self): crontab = 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, ) if getattr(settings, 'DJANGO_CELERY_BEAT_TZ_AWARE', True): crontab = TzAwareCrontab( 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, tz=self.timezone ) return crontab @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, 'timezone': schedule.tz } try: return cls.objects.get(**spec) except cls.DoesNotExist: return cls(**spec) except MultipleObjectsReturned: return cls.objects.filter(**spec).first() class PeriodicTasks(models.Model): """Helper table for tracking updates to periodic tasks. This stores a single row with ident=1. last_update is updated via django signals whenever anything is changed in the PeriodicTask model. Basically this acts like a DB data audit trigger. Doing this so we also track deletions, and not just insert/update. """ 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 class PeriodicTask(models.Model): """Model representing a periodic task.""" name = models.CharField( max_length=200, unique=True, verbose_name=_('Name'), help_text=_('Short Description For This Task'), ) task = models.CharField( max_length=200, verbose_name='Task Name', help_text=_('The Name of the Celery Task that Should be Run. ' '(Example: "proj.tasks.import_contacts")'), ) # You can only set ONE of the following schedule FK's # TODO: Redo this as a GenericForeignKey interval = models.ForeignKey( IntervalSchedule, on_delete=models.CASCADE, null=True, blank=True, verbose_name=_('Interval Schedule'), help_text=_('Interval Schedule to run the task on. ' 'Set only one schedule type, leave the others null.'), ) crontab = models.ForeignKey( CrontabSchedule, on_delete=models.CASCADE, null=True, blank=True, verbose_name=_('Crontab Schedule'), help_text=_('Crontab Schedule to run the task on. ' 'Set only one schedule type, leave the others null.'), ) solar = models.ForeignKey( SolarSchedule, on_delete=models.CASCADE, null=True, blank=True, verbose_name=_('Solar Schedule'), help_text=_('Solar Schedule to run the task on. ' 'Set only one schedule type, leave the others null.'), ) clocked = models.ForeignKey( ClockedSchedule, on_delete=models.CASCADE, null=True, blank=True, verbose_name=_('Clocked Schedule'), help_text=_('Clocked Schedule to run the task on. ' 'Set only one schedule type, leave the others null.'), ) # TODO: use django's JsonField args = models.TextField( blank=True, default='[]', verbose_name=_('Positional Arguments'), help_text=_( 'JSON encoded positional arguments ' '(Example: ["arg1", "arg2"])'), ) kwargs = models.TextField( blank=True, default='{}', verbose_name=_('Keyword Arguments'), help_text=_( 'JSON encoded keyword arguments ' '(Example: {"argument": "value"})'), ) queue = models.CharField( max_length=200, blank=True, null=True, default=None, verbose_name=_('Queue Override'), help_text=_( 'Queue defined in CELERY_TASK_QUEUES. ' 'Leave None for default queuing.'), ) # you can use low-level AMQP routing options here, # but you almost certaily want to leave these as None # http://docs.celeryproject.org/en/latest/userguide/routing.html#exchanges-queues-and-routing-keys exchange = models.CharField( max_length=200, blank=True, null=True, default=None, verbose_name=_('Exchange'), help_text=_('Override Exchange for low-level AMQP routing'), ) routing_key = models.CharField( max_length=200, blank=True, null=True, default=None, verbose_name=_('Routing Key'), help_text=_('Override Routing Key for low-level AMQP routing'), ) headers = models.TextField( blank=True, default='{}', verbose_name=_('AMQP Message Headers'), help_text=_('JSON encoded message headers for the AMQP message.'), ) priority = models.PositiveIntegerField( default=None, validators=[MaxValueValidator(255)], blank=True, null=True, verbose_name=_('Priority'), help_text=_( 'Priority Number between 0 and 255. ' 'Supported by: RabbitMQ, Redis (priority reversed, 0 is highest).') ) expires = models.DateTimeField( blank=True, null=True, verbose_name=_('Expires Datetime'), help_text=_( 'Datetime after which the schedule will no longer ' 'trigger the task to run'), ) expire_seconds = models.PositiveIntegerField( blank=True, null=True, verbose_name=_('Expires timedelta with seconds'), help_text=_( 'Timedelta with seconds which the schedule will no longer ' 'trigger the task to run'), ) one_off = models.BooleanField( default=False, verbose_name=_('One-off Task'), help_text=_( 'If True, the schedule will only run the task a single time'), ) start_time = models.DateTimeField( blank=True, null=True, verbose_name=_('Start Datetime'), help_text=_( 'Datetime when the schedule should begin ' 'triggering the task to run'), ) enabled = models.BooleanField( default=True, verbose_name=_('Enabled'), help_text=_('Set to False to disable the schedule'), ) last_run_at = models.DateTimeField( auto_now=False, auto_now_add=False, editable=False, blank=True, null=True, verbose_name=_('Last Run Datetime'), help_text=_( 'Datetime that the schedule last triggered the task to run. ' 'Reset to None if enabled is set to False.'), ) total_run_count = models.PositiveIntegerField( default=0, editable=False, verbose_name=_('Total Run Count'), help_text=_( 'Running count of how many times the schedule ' 'has triggered the task'), ) date_changed = models.DateTimeField( auto_now=True, verbose_name=_('Last Modified'), help_text=_('Datetime that this PeriodicTask was last modified'), ) description = models.TextField( blank=True, verbose_name=_('Description'), help_text=_( 'Detailed description about the details of this Periodic Task'), ) 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().validate_unique(*args, **kwargs) schedule_types = ['interval', 'crontab', 'solar', 'clocked'] selected_schedule_types = [s for s in schedule_types if getattr(self, s)] if len(selected_schedule_types) == 0: raise ValidationError( 'One of clocked, interval, crontab, or solar ' 'must be set.' ) err_msg = 'Only one of clocked, interval, crontab, '\ 'or solar must be set' if len(selected_schedule_types) > 1: error_info = {} for selected_schedule_type in selected_schedule_types: error_info[selected_schedule_type] = [err_msg] raise ValidationError(error_info) # clocked must be one off task if self.clocked and not self.one_off: err_msg = 'clocked must be one off, one_off must set True' raise ValidationError(err_msg) 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 self.headers = self.headers or None if not self.enabled: self.last_run_at = None self._clean_expires() self.validate_unique() super().save(*args, **kwargs) def _clean_expires(self): if self.expire_seconds is not None and self.expires: raise ValidationError( _('Only one can be set, in expires and expire_seconds') ) @property def expires_(self): return self.expires or self.expire_seconds 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}' if self.clocked: fmt = '{0.name}: {0.clocked}' 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 if self.clocked: return self.clocked.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) signals.post_delete.connect( PeriodicTasks.update_changed, sender=ClockedSchedule) signals.post_save.connect( PeriodicTasks.update_changed, sender=ClockedSchedule) django-celery-beat-2.2.1/django_celery_beat/schedulers.py000066400000000000000000000312771406752007300235010ustar00rootroot00000000000000"""Beat Scheduler Implementation.""" import datetime import logging import math from multiprocessing.util import Finalize from celery import current_app from celery import schedules from celery.beat import Scheduler, ScheduleEntry from celery.utils.log import get_logger from celery.utils.time import maybe_make_aware from kombu.utils.encoding import safe_str, safe_repr from kombu.utils.json import dumps, loads from django.conf import settings from django.db import transaction, close_old_connections from django.db.utils import DatabaseError, InterfaceError from django.core.exceptions import ObjectDoesNotExist from .models import ( PeriodicTask, PeriodicTasks, CrontabSchedule, IntervalSchedule, SolarSchedule, ClockedSchedule ) from .clockedschedule import clocked from .utils import NEVER_CHECK_TIMEOUT # 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, warning = logger.debug, logger.info, logger.warning class ModelEntry(ScheduleEntry): """Scheduler entry taken from database row.""" model_schedules = ( (schedules.crontab, CrontabSchedule, 'crontab'), (schedules.schedule, IntervalSchedule, 'interval'), (schedules.solar, SolarSchedule, 'solar'), (clocked, ClockedSchedule, 'clocked') ) 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 = {} for option in ['queue', 'exchange', 'routing_key', 'priority']: value = getattr(model, option) if value is None: continue self.options[option] = value if getattr(model, 'expires_', None): self.options['expires'] = getattr(model, 'expires_') self.options['headers'] = loads(model.headers or '{}') 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 = 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: # 5 second delay for re-enable. return schedules.schedstate(False, 5.0) # START DATE: only run after the `start_time`, if one exists. if self.model.start_time is not None: now = self._default_now() if getattr(settings, 'DJANGO_CELERY_BEAT_TZ_AWARE', True): now = maybe_make_aware(self._default_now()) if now < self.model.start_time: # The datetime is before the start date - don't run. # send a delay to retry on start_time delay = math.ceil( (self.model.start_time - now).total_seconds() ) return schedules.schedstate(False, delay) # ONE OFF TASK: Disable one off tasks after they've ran once if self.model.one_off and self.model.enabled \ and self.model.total_run_count > 0: self.model.enabled = False self.model.total_run_count = 0 # Reset self.model.no_changes = False # Mark the model entry as changed self.model.save() # Don't recheck return schedules.schedstate(False, NEVER_CHECK_TIMEOUT) # CAUTION: make_aware assumes settings.TIME_ZONE for naive datetimes, # while maybe_make_aware assumes utc for naive datetimes tz = self.app.timezone last_run_at_in_tz = maybe_make_aware(self.last_run_at).astimezone(tz) return self.schedule.is_due(last_run_at_in_tz) def _default_now(self): # 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. if getattr(settings, 'DJANGO_CELERY_BEAT_TZ_AWARE', True): now = self.app.now() now = now.tzinfo.localize(now.replace(tzinfo=None)) else: # this ends up getting passed to maybe_make_aware, which expects # all naive datetime objects to be in utc time. now = datetime.datetime.utcnow() return now def __next__(self): self.model.last_run_at = self._default_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, priority=None, headers=None, expire_seconds=None, **kwargs): return { 'queue': queue, 'exchange': exchange, 'routing_key': routing_key, 'priority': priority, 'headers': dumps(headers or {}), 'expire_seconds': expire_seconds, } 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 = True _heap_invalidated = 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: close_old_connections() # 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 except InterfaceError: warning( 'DatabaseScheduler: InterfaceError in schedule_changed(), ' 'waiting to retry in next call...' ) 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): if logger.isEnabledFor(logging.DEBUG): debug('Writing entries...') _tried = set() _failed = set() try: close_old_connections() while self._dirty: name = self._dirty.pop() try: self.schedule[name].save() _tried.add(name) except (KeyError, ObjectDoesNotExist): _failed.add(name) except DatabaseError as exc: logger.exception('Database error while sync: %r', exc) except InterfaceError: warning( 'DatabaseScheduler: InterfaceError in sync(), ' 'waiting to retry in next call...' ) finally: # retry later, only for the failed ones self._dirty |= _failed def update_from_dict(self, mapping): s = {} for name, entry_fields in mapping.items(): try: entry = self.Entry.from_entry(name, app=self.app, **entry_fields) if entry.model.enabled: s[name] = entry except Exception as exc: logger.exception(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': {'expire_seconds': 12 * 3600}, }, ) self.update_from_dict(entries) def schedules_equal(self, *args, **kwargs): if self._heap_invalidated: self._heap_invalidated = False return False return super().schedules_equal(*args, **kwargs) @property def schedule(self): initial = update = False if self._initial_read: debug('DatabaseScheduler: initial read') initial = update = True self._initial_read = False 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 if not initial: self._heap = [] self._heap_invalidated = True if logger.isEnabledFor(logging.DEBUG): debug('Current schedule:\n%s', '\n'.join( repr(entry) for entry in self._schedule.values()), ) return self._schedule django-celery-beat-2.2.1/django_celery_beat/templates/000077500000000000000000000000001406752007300227525ustar00rootroot00000000000000django-celery-beat-2.2.1/django_celery_beat/templates/admin/000077500000000000000000000000001406752007300240425ustar00rootroot00000000000000django-celery-beat-2.2.1/django_celery_beat/templates/admin/djcelery/000077500000000000000000000000001406752007300256435ustar00rootroot00000000000000django-celery-beat-2.2.1/django_celery_beat/templates/admin/djcelery/change_list.html000066400000000000000000000013241406752007300310110ustar00rootroot00000000000000{% 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-2.2.1/django_celery_beat/tzcrontab.py000066400000000000000000000050101406752007300233300ustar00rootroot00000000000000"""Timezone aware Cron schedule Implementation.""" from celery import schedules from collections import namedtuple from datetime import datetime import pytz schedstate = namedtuple('schedstate', ('is_due', 'next')) class TzAwareCrontab(schedules.crontab): """Timezone Aware Crontab.""" def __init__( self, minute='*', hour='*', day_of_week='*', day_of_month='*', month_of_year='*', tz=pytz.utc, app=None ): """Overwrite Crontab constructor to include a timezone argument.""" self.tz = tz nowfun = self.nowfunc super().__init__( minute=minute, hour=hour, day_of_week=day_of_week, day_of_month=day_of_month, month_of_year=month_of_year, nowfun=nowfun, app=app ) def nowfunc(self): return self.tz.normalize( pytz.utc.localize(datetime.utcnow()) ) def is_due(self, last_run_at): """Calculate when the next run will take place. Return tuple of (is_due, next_time_to_check). The last_run_at argument needs to be timezone aware. """ # convert last_run_at to the schedule timezone last_run_at = last_run_at.astimezone(self.tz) rem_delta = self.remaining_estimate(last_run_at) rem = max(rem_delta.total_seconds(), 0) due = rem == 0 if due: rem_delta = self.remaining_estimate(self.now()) rem = max(rem_delta.total_seconds(), 0) return schedstate(due, rem) # Needed to support pickling def __repr__(self): return """ """.format(self) def __reduce__(self): return (self.__class__, (self._orig_minute, self._orig_hour, self._orig_day_of_week, self._orig_day_of_month, self._orig_month_of_year, self.tz), None) def __eq__(self, other): if isinstance(other, schedules.crontab): return (other.month_of_year == self.month_of_year and other.day_of_month == self.day_of_month and other.day_of_week == self.day_of_week and other.hour == self.hour and other.minute == self.minute and other.tz == self.tz) return NotImplemented django-celery-beat-2.2.1/django_celery_beat/utils.py000066400000000000000000000030511406752007300224650ustar00rootroot00000000000000"""Utilities.""" # -- XXX This module must not use translation as that causes # -- a recursive loader import! from django.conf import settings from django.utils import timezone is_aware = timezone.is_aware # celery schedstate return None will make it not work NEVER_CHECK_TIMEOUT = 100000000 # 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) else: # naive datetimes are assumed to be in local timezone. if timezone.is_naive(value): value = timezone.make_aware(value, timezone.get_default_timezone()) 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-2.2.1/django_celery_beat/validators.py000066400000000000000000000055441406752007300235060ustar00rootroot00000000000000"""Validators.""" import crontab from django.core.exceptions import ValidationError class _CronSlices(crontab.CronSlices): """Cron slices with customized validation.""" def __init__(self, *args): super(crontab.CronSlices, self).__init__( [_CronSlice(info) for info in crontab.S_INFO] ) self.special = None self.setall(*args) self.is_valid = self.is_self_valid @classmethod def validate(cls, *args): try: cls(*args) except Exception as e: raise ValueError(e) class _CronSlice(crontab.CronSlice): """Cron slice with custom range parser.""" def get_range(self, *vrange): ret = _CronRange(self, *vrange) if ret.dangling is not None: return [ret.dangling, ret] return [ret] class _CronRange(crontab.CronRange): """Cron range parser class.""" # rewrite whole method to raise error on bad range def parse(self, value): if value.count('/') == 1: value, seq = value.split('/') try: self.seq = self.slice.parse_value(seq) except crontab.SundayError: self.seq = 1 value = "0-0" if self.seq < 1 or self.seq > self.slice.max: raise ValueError("Sequence can not be divided by zero or max") if value.count('-') == 1: vfrom, vto = value.split('-') self.vfrom = self.slice.parse_value(vfrom, sunday=0) try: self.vto = self.slice.parse_value(vto) except crontab.SundayError: if self.vfrom == 1: self.vfrom = 0 else: self.dangling = 0 self.vto = self.slice.parse_value(vto, sunday=6) if self.vto < self.vfrom: raise ValueError("Bad range '{0.vfrom}-{0.vto}'".format(self)) elif value == '*': self.all() else: raise ValueError('Unknown cron range value "%s"' % value) def crontab_validator(value): """Validate crontab.""" try: _CronSlices.validate(value) except ValueError as e: raise ValidationError(e) def minute_validator(value): """Validate minutes crontab value.""" _validate_crontab(value, 0) def hour_validator(value): """Validate hours crontab value.""" _validate_crontab(value, 1) def day_of_month_validator(value): """Validate day of month crontab value.""" _validate_crontab(value, 2) def month_of_year_validator(value): """Validate month crontab value.""" _validate_crontab(value, 3) def day_of_week_validator(value): """Validate day of week crontab value.""" _validate_crontab(value, 4) def _validate_crontab(value, index): tab = ['*'] * 5 tab[index] = value tab = ' '.join(tab) crontab_validator(tab) django-celery-beat-2.2.1/docker-compose.yml000066400000000000000000000024571406752007300206210ustar00rootroot00000000000000# Copyright (C) 2019 Sebastian Pipping # Licensed under the BSD License (3 clause, also known as the new BSD license) version: '3' services: base: build: context: . dockerfile: docker/base/Dockerfile command: ["sleep", "inf"] django: depends_on: - base - postgres - rabbit build: context: . dockerfile: docker/django/Dockerfile ports: - "127.0.0.1:58000:8000" entrypoint: ["/app/docker/django/entrypoint.sh"] command: ["python3", "manage.py", "runserver", "0.0.0.0:8000"] tty: true volumes: - './django_celery_beat/:/app/django_celery_beat/' celery-beat: depends_on: - base - postgres - rabbit build: context: . dockerfile: docker/celery-beat/Dockerfile entrypoint: ["/app/docker/celery-beat/entrypoint.sh"] environment: CELERY_BROKER_URL: 'amqp://guest:guest@rabbit:5672' command: ["python3", '-m', "celery", "-A", "mysite", "beat", "-l", "info", "--scheduler", "django_celery_beat.schedulers:DatabaseScheduler"] tty: true volumes: - './django_celery_beat/:/app/django_celery_beat/' rabbit: image: rabbitmq ports: - "5672:5672" postgres: image: postgres environment: POSTGRES_PASSWORD: s3cr3t django-celery-beat-2.2.1/docker/000077500000000000000000000000001406752007300164235ustar00rootroot00000000000000django-celery-beat-2.2.1/docker/base/000077500000000000000000000000001406752007300173355ustar00rootroot00000000000000django-celery-beat-2.2.1/docker/base/Dockerfile000066400000000000000000000021161406752007300213270ustar00rootroot00000000000000# Copyright (C) 2019 Sebastian Pipping # Licensed under the BSD License (3 clause, also known as the new BSD license) FROM python:3.8-slim ENV PATH=${PATH}:/root/.local/bin RUN apt-get update && apt-get install --yes --no-install-recommends \ wait-for-it RUN pip3 install --user \ django-createsuperuserwithpassword \ psycopg2-binary COPY setup.cfg setup.py /app/ COPY django_celery_beat/ /app/django_celery_beat/ COPY requirements/ /app/requirements/ WORKDIR /app RUN python3 setup.py develop --user WORKDIR / RUN django-admin startproject mysite WORKDIR /mysite/ RUN echo 'DATABASES = {"default": {"ENGINE": "django.db.backends.postgresql", "NAME": "postgres", "USER": "postgres","PASSWORD": "s3cr3t", "HOST": "postgres", "PORT": 5432}}' >> mysite/settings.py RUN echo 'ALLOWED_HOSTS = ["*"]' >> mysite/settings.py RUN echo 'INSTALLED_APPS += ("django_celery_beat", )' >> mysite/settings.py RUN echo 'INSTALLED_APPS += ("django_createsuperuserwithpassword", )' >> mysite/settings.py COPY docker/base/celery.py mysite/celery.py django-celery-beat-2.2.1/docker/base/celery.py000066400000000000000000000004201406752007300211660ustar00rootroot00000000000000# Copyright (C) 2019 Sebastian Pipping # Licensed under the BSD License (3 clause, also known as the new BSD license) import os from celery import Celery os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') app = Celery('mysite') django-celery-beat-2.2.1/docker/celery-beat/000077500000000000000000000000001406752007300206175ustar00rootroot00000000000000django-celery-beat-2.2.1/docker/celery-beat/Dockerfile000066400000000000000000000004361406752007300226140ustar00rootroot00000000000000# Copyright (C) 2019 Sebastian Pipping # Licensed under the BSD License (3 clause, also known as the new BSD license) ARG COMPOSE_PROJECT_NAME=django-celery-beat FROM ${COMPOSE_PROJECT_NAME}_base COPY docker/celery-beat/entrypoint.sh /app/docker/celery-beat/ django-celery-beat-2.2.1/docker/celery-beat/entrypoint.sh000077500000000000000000000003651406752007300233750ustar00rootroot00000000000000#! /usr/bin/env bash # Copyright (C) 2019 Sebastian Pipping # Licensed under the BSD License (3 clause, also known as the new BSD license) set -e PS4='# ' set -x wait-for-it django:8000 # due to migrations exec "$@" django-celery-beat-2.2.1/docker/django/000077500000000000000000000000001406752007300176655ustar00rootroot00000000000000django-celery-beat-2.2.1/docker/django/Dockerfile000066400000000000000000000004241406752007300216570ustar00rootroot00000000000000# Copyright (C) 2019 Sebastian Pipping # Licensed under the BSD License (3 clause, also known as the new BSD license) ARG COMPOSE_PROJECT_NAME=django-celery-beat FROM ${COMPOSE_PROJECT_NAME}_base COPY docker/django/entrypoint.sh /app/docker/django/ django-celery-beat-2.2.1/docker/django/entrypoint.sh000077500000000000000000000006331406752007300224410ustar00rootroot00000000000000#! /usr/bin/env bash # Copyright (C) 2019 Sebastian Pipping # Licensed under the BSD License (3 clause, also known as the new BSD license) set -e PS4='# ' set -x wait-for-it postgres:5432 python3 manage.py migrate python3 manage.py createsuperuserwithpassword \ --username admin \ --password admin \ --email admin@example.org \ --preserve exec "$@" django-celery-beat-2.2.1/docs/000077500000000000000000000000001406752007300161045ustar00rootroot00000000000000django-celery-beat-2.2.1/docs/Makefile000066400000000000000000000203171406752007300175470ustar00rootroot00000000000000# 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)/django_celery_beat/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/django_celery_beat/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-2.2.1/docs/_static/000077500000000000000000000000001406752007300175325ustar00rootroot00000000000000django-celery-beat-2.2.1/docs/_static/.keep000066400000000000000000000000001406752007300204450ustar00rootroot00000000000000django-celery-beat-2.2.1/docs/_templates/000077500000000000000000000000001406752007300202415ustar00rootroot00000000000000django-celery-beat-2.2.1/docs/_templates/.keep000066400000000000000000000000001406752007300211540ustar00rootroot00000000000000django-celery-beat-2.2.1/docs/changelog.rst000066400000000000000000000000321406752007300205600ustar00rootroot00000000000000.. include:: ../Changelog django-celery-beat-2.2.1/docs/conf.py000066400000000000000000000013571406752007300174110ustar00rootroot00000000000000# -*- coding: utf-8 -*- 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-2.2.1/docs/copyright.rst000066400000000000000000000017011406752007300206450ustar00rootroot00000000000000Copyright ========= *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-2.2.1/docs/glossary.rst000066400000000000000000000001431406752007300204770ustar00rootroot00000000000000.. _glossary: Glossary ======== .. glossary:: :sorted: term Description of term django-celery-beat-2.2.1/docs/images/000077500000000000000000000000001406752007300173515ustar00rootroot00000000000000django-celery-beat-2.2.1/docs/images/favicon.ico000066400000000000000000000064441406752007300215020ustar00rootroot00000000000000PNG  IHDR szziCCPICC Profile8TOA-b""6l1)4 FA$z$l 7`z1d!ԍ:55wO:zi c l,2qd#>("ⶬ^a;5'F" iZ+*Zqޤ 'QX\qQL8q-jʃ_ym1) ɝv KDHjrG ` *u{븕޲@˪g3\g0laS,+@5?eϚ漯ն~4<ع_}R 1 L'YfIDATX WKl\WcNm&TIPJ RPB,ڪ v,R%V,*+VTBxg- UNb9~df)@dʮb]S,$C 6?I>(IJ>2wa,c0hO!$1U,vCƪ)@@݅GtX">Ӑ`uXoXM?Gp,yp`M4 ԂzZu.7c&XI@g$ɈsKj.Jg mx֥<(>Jf[,T@5-jbSob\zI`ɝY̽"UR1!+^ZfE쬼$WmQߋBR1iu,ee[8<F]F޸c&k\}&dTDTgzK jb2a'd~Ȓ4d6˘>|YvԇG{,' 'G:!|İXfL ӶYQWu P<4U)K2z"l0[ WSc$FP z!oF^@RA̵h24StXhUlpKF~;A,Ą1,P#Kt? ѧa-[aXQ(&tbha#'x`7?ͮ@YeȍzEIMefv7֮_uc9Qs^ooK3gq7 DT71#`!e|u&T"*fVN?33!9<; Rln 8MtZu^XzD4I/%ڎQXiK.nة{zyr=6٪l8RgFXO:r;;R yhY\8ť.`HemudܘPɯK0#9X<G mie8C@wpKxYdCJw7d 즛X25BjRJN(ub<ơ+7[6yz;ǣU>.lڲC죨aP?#N0YL^֘a4SJw'㙖(#M*qY젢;?ྩ<>!PnL$NzXӳFk}sOχ﷿`|#[O>fc DH5Nuu#YԮ=٨^_Azulņ]}ٯך;n%W(>? x W ?rHIENDB`django-celery-beat-2.2.1/docs/images/logo.png000066400000000000000000000632121406752007300210230ustar00rootroot00000000000000PNG  IHDR>aiCCPICC ProfilexTkPe:g >hndStCkWZ6!Hm\$~ًo:w> كo{ a"L"4M'S9'^qZ/USO^C+hMJ&G@Ӳylto߫c՚  5"Yi\t։15LsX g8ocግ#f45@ B:K@8i ΁'&.)@ry[:Vͦ#wQ?HBd(B acĪL"JitTy8;(Gx_^[%׎ŷQ麲uan7m QH^eOQu6Su 2%vX ^*l O—ޭˀq,>S%LdB1CZ$M9P 'w\/].r#E|!3>_oa۾d1Zӑz'=~V+cjJtO%mN |-bWO+ o ^ IH.;S]i_s9*p.7U^s.3u |^,<;c=ma>Vt.[՟Ϫ x# ¡_2 pHYs   IDATx nY}sΦ,H#hA aYl(*q$ؘpSq1 %V!$!Ѯhf_w?3~W,&U.v9]y|c/eΕ /Ly#ya|]t>OmQv_%[vgn?Tǟ 4Q8.=y\9 \,._|'2x=- _k^7uONNc_yPcnC/C8y6#w`ĪUT ).B:>>ֶv/^' rnb[\~?,TzȻbpEq7?mwG̞611ٶ Jbᳳڏ;\\w)*m X]2.a 5 by۔W/Y罞\4y0Bt#C?ǂRߑno)L@N]n~W7>_{,Xw^_.]:ڹ~7ͬӫG7ϭj[k0,ǥV4i0D )N)o)&bĢK2jOJP IE2@'{vP'ċ' ߷O [x[Ql1Thz@c=_U Jp#=ip[a[a|00z83b/!UT V^s,,k ֓/ԹПjYx`5)D1CDm7?ꡅJ⑿7Xl|{v?6y+lC,O ݯK Z&U?oxYӟۚ;dN RQ9XP\V2I-AP.zHچm NOjIdDg@K^7h_ *VDg~uۇ*Fe $Me}c3.# $+ޜ{o;^;D]:-4i]_;}%{i0^vRQ2EU~&UѾ;^8dSLy0p]#jlorA{ʷPWIuo$5៏8=ڴGAi.4Bu.OL$1G:iT\xu8sp7Pmjͥ_{uܽ:w|sM1*в^Imè *S~ y 3T0x(ۤQ0q,!wȫ]X4W֓V 1Pv!k ` 4?\#WPy[?. ;hD'iF"? 'omF3 3(#&۩'6oE ]ZsG^ 钥_fy{;nkva]X]) 1z0'Q5WW_ca5^PE3 Y2I_ɂIKUJ裱9*:YpQp2˫vWΔiJ+6:*=؞55z)0dкpHVށvWVt{=7Qjom'Y)P[۷~-}u #y?WxAO G@ڤIf"2 0Qe( 7Z^I\=KcT%[08,Bm{s‰jt Ϯ1Qź7C+"ȹ)l*ƠUNфe/F@ &'~N4}ĭׇډ׏Z "C9'r'sNJBtI6ʋ!\7Q&T):Ӻ& 1 0'oi**$r,1Yp7QI 35eEYFWΠ N^W3> e^5Q!1Ox'/$ jo=R<QSk7*v\s (·HKo v"RKi(@8n(HL87SWޤP*yGHYU:QBnCdmmQQ ʢ0:L~UyPEQfE$AQTYšr:j_WQ`}t:0p3b#"!$4jND.VF! 'I!q/z*`gOGipeou>FOOSON9WP9ަR F)ɳ\l yjz."Ɯ)h`f#PØ.R%o)Nhi^8 r86XApd .M @T1@\1 g1B} Qko4%cn~jMO= XYf[X+Kt^pIa5Gnu>F(&hxh2GaB*nJ0N]akq7CH1kWFAsNr0R)(p*fL(&a Ӷ1Efgε/|?z=}=<Xگv[opk{-=v[a3@FЌcc)c+v)/#VzΛjHdLJ,*?q*TGF F<Ŋ%.dpyy cxd)V!|}֬IM}6m`j 1$(ɨa~3±oﵥ=׷wq:qk1cbZқ&έoOLo uÑo~-寸kKn9Ԏ\̓k|Rh+m}G( *1JI>"rh7 /Q6z8]4[(MW0y(b[P.1S":Cⵋi*7fm1qG (ꐃOdo><|{Ͽ@?]ZᕋE4u1CY`j#й٦ƦQZύŶݦo&~YQOOvkvniwW bC,//6jxLƘ3Zo5,oK$$֜0ւPlx6ø%!`ѝ lsUS𢯑%FD O$P(u . x'8i< ]3*Dr1xWYA[>GN>uګpMclMчs3 b|=MM*8?37Q ܑ2>qҠEi`喚U,_oO;o>R{ -͡x-xfoG(<^:˹1ĥ[͞Ymgu 'P,i(&gkN'K4ekkЋa9vK^~D8۾5W剳…%;Tp% hANRLbsK~+۝"r/K@FbJ BI̕ȖL݅{T N1a$Ub,j8f@(l06mG;,2ܯG.Mea螝i 5,L)_X^o 5m\g,o@f1 ?{<vƐ7F2Qef(+(ivKk ] ϛo`[:{2YTicP|dʔ %Bnj=<d/Qik<:qHeh<Й%I*e%$pG18ȴOFQcqOm7ͽ6M7<=;֦0yz~pOWAGGj (~Fۿ5BejXuJۻw&us0VOvp~^[÷>ok(tcr ̜HD)]Ro{!46>/ﺮ;_׾]>G\=C eYY+]`)SHΥnԾʤG$!H˔́]"G$3aJRB/1cHNvj|:O5t?wMY{!L ͣNfP( Pz 00:^Deh3R2rqឱu h2O TL܄%i _A'_ l;SzEvKŒbq~w?vjX5\ :\L;xp5Ta LG񮽋afTTfp/]`ηq轀 0TJ4 d#9Idz蟲c_嶷Fp/sd б>\f*sqGjNN1Ĺέb6$ k}aj9Zc$8'#| z}߾9Hn#Qsis18|h_{mCv뵍+Rgp 2h߻o&_ 0;;!X@bMz{~T[;v۝׶'O}4ӽuV QQ]c.:m5);˺7Ct\*<]pnnys(aN'`++́n61˪5ȐuQb%6 !e ll;{bۭ7m20MsH]5b//ߋB#i`oO=ߞfD;־_n{٭BF'Oj'OjN'x=ǏN=ܞ9gDڛ_1N!LCfX^8QPFDP.+2X\D]:,$YMUm+W0 [vQJ`&0lر3Dia1dx{hS>PBߺ!ί&k L;&X[kj{SЍsO*pV4=.tiާkbVnϟ񔵅q6l 3W}M{뿶}˷~C?f|{YsejCm^G޸wniվ V'o~?'?nO Ay"icĹdÖ{Pp 3}gPۿpC{^|{?P7~vK&h~诵=Kc𩱩xyA_ Mؤή#mP[ E9|4sQ!#o07kTZ(@cī³VAHT*ATR#l IDATr.p=硆)n)ۼ1\pLJ{=|u{׾=s3=_xmmrTtRm+mgoe{\ @/gթإ)a iCJT[|Hgt V}2˔#B)3+i)1nάlSs#_CNղ$, 'UM'mC,8XY]Cϴ[Y2|8Gu||=wͶF{sJ Yw9Uyo 9r:\ɾo?19b5D o1[#G'>Llomox&Mo~wcǟ3sc rPqgl!Ł$Q%eS pJlIu3%i%> #pd(m(=b(O vC⭓qaPvxiwi>^/zkip=qAfs THIhH:I# @URXskx=r0$:P8.Ȕge86 ͺC.(vU(ߛ9y*@pUgLsgBAX pm?b/q"W<)d=St_;&Rq$|Xs=Spk] -dڦzCBƩAt4D(0#;{ UEf0 ‹CLM. +R!Oڔ2q1<:MvYZZ͍ܱ 3K7dw:"thdPY;.eq'aNǮ0) 'BC K1S`#m\;Jũ}WlS Lm+E1udvw&)o#)Nb2q48t)dAQfxs~l)L >,,.9'lKLB,Y w\~vpQqHʵ+]J# *;a6t1(ҹ;z3JB*c}Б,\J1m`(Stt#+d8Y|9;yؚPgn9 }r.G!+wRdsxląSpLw!2pQe'h ǔ6Jq3 G̃%-4.Њl u|#|QεЌAJ1*gQ[+R;bڨ4έ>V׭(F3ӳ-`А0k=(f< h'1t߾}w~ag:˫; 0 0j*v|su'Vb٨4WQ 1&X[7-gh.cՓ,*4oʍ /2$#xz>\_ZYv nteV k\QGè~|P7y_ɠri6f]yT]qn!,Mi7 JksY@A990ƾ5 H\Y fr7\OOg,`*u:sw{X T8,An*9& r8?!\<E2^f|@-kr%ixu d)s Ù 爇V7D+R] 1I/" ՚\PǹΧg"k^k(CK`B3_Vn+QR!Pn%K*h i.謲9\ı7>#%n6pΜ+`[k'=VyS!QF"9C2Obnu8ݝ+RW! [2\B)Dj4 9WҨg0zw^4H[rguQFZFA$'_b`Q삋h?Ȭc4-#~PeQgh'bz._`{ɽӷGNlY@y싁{X+ og=WXH=Ti8%+u#\2$)O 5U8xsS 1fՊ957yfxW..5P4YEpKdT EG]̌MN:S nCCsn\ek#}o[#k 3HȢIFah_yJ_4 hayK^<֝ђaxG*7YYӹ^/X]*eDxv*kcEʫ1:H(,`6sP2d# k2hr[?DkCIt"LFWXf \`[}VRM= Z]8RE EK4HBq/pV34,j`|p6pr RL35œXhR!_VHc5e|1uAG\W(b9_k* Jz@$M!*ti-(8 c0c9/%Xę8ZS @_ GQ .4m%x@:wSiX!; qx*6]]{cig@#gq>S5*C/4w}N U2'[=8+;ɐtۑ3w!=lJNaDNi+ PTQr02Ɉ .#/G4rI`]̷#*)hpdE9J7Dýa[]MdDy1TZw>]]du(Q27{Hz/ V|oM`1h+Da*"?yK#Z^' 9 jwG6g/:4:F]9 ,\9ri!F`NgrG@6VAr (y25huM6 \]5KC h}@GU)\"LzM|< Gc?ܢWKdOd ;0TXy՟,4#BuKR7aW6OjXz\TxJ^+lb8L# - ~#7D6SWaxyIW s[!!BaY*'^c5_,fs iX8U_ʠ]/֝c A5Kp<-*Q˸Dx([~T~)鍥οP6c&ݨ6xuDl/M=OEMC'KM1ͯeiYpH Τ¶gzµ]yʩV%R}rz+OGpKe$H n@u:<x *W{!H/?\IHntQ k]aM#p`(Q+=ԆJT`È( #0,@K]ؾBvGnsЧ(R0- z ' S:XǴ I8eevپIS8]Q"Oo%\X[ #b22Ls` x{lQ_6daR&`ӿɐzqXUPUi;8yU!+%0TYb1 B?@JP-{K)NASa]:-zS6bԩY6l+ ay;$dӦpH GV.z5 3O"jp({'wj")QNs}+@3d"A&h\ %Xy ֺ/(0ݍ!3<)Pp*Vt 0 ݓpD +]$AeS>o22/ Ag9*0? penEP.]F\zMo_rQR(.qR_WF++@H04xHM*o2v$ -> ̚텓Oέ+`Igdb1Hÿ ́i#(h?B)m@l𬁾ϹBʂB2 vyœK,3 BYu~dmfFRu l']rR鑿0"A/hg!կF $YM(-AI_/wU:>]!Z"|_Ԫ(Q-Yg :u !^~_ 1=t%Pap:чlC),~--4{ 9F N@ΰ ~IQH5#RK^N5N*zt_fs-5IVMYhXB,u^ Ma67)T/u;:ċ3c2  %!DlWio.f|F~"n*l^E]FA6W"G.}Q̗d}ֲ4N][J# @o#LedN|PAH`2f GZHHC& ̟lVqsqƟ/29"{4W֌CL gZ^TJ # "-1$#4ɖ/^]jx>QqNE}k44OAeN<ܾ'?h{ "_z'U~jI ĝ#vdJ(:2+S!Z\C)]UH0pgHVRW{Tnժug%);aC%6`%~8n߁vsxaC[p̰'P{> o=|}o_-2PtiE;tt޾+9 R^fpNT|ut \wkU8Kg)=־gg?L6ÏSNQv?śCna69 IT)vCf8~yq+99KQiM*^`a "z?}ɗ^_b2"߰6/KVgސ {d?su9~=g*B o n|վ݇<ܮe;oLM͵1~ySLJu b#>6Z \*ړ?sZZ5, .Zl0~W~ud#oDu_nGYZ嗖wK;k l; xəW8Hjh6OxtvC,{VPocӽ3 AsL@0 u1>JW4'Q 6K?3`x;3K,+GJo7; yp3guA3'1~Ez6u慬E8JF6ůj{fc?vێ΂!3@-/ EI"櫢AuFEIwxH?$O#C_dL hfqQ"f@(1(`rkhh6&_%045ܻHߟ]E:,Rv(KL fN_oC_n0#V;5ZQEc*e3'۱gηg=yCZ`qmF)+Ǯ=iZwT=n67V;䒧*vMjSc#I9NPFMJ黀3hGyX:Gq*p%sS+q2Ṗ>oܮvíW<@}o]`Пs\>-^m-7%^wӾp'iLX{O]pζ<۞vy^?A[3s" $ӾUJNoyf]<^yr LjcֈL/B2=Me(&zkX Z=Q}|W0"4Ó+d&|wu V!H4A}ht+/%{&1ƥȍS5okwM_2C#CI9 㥯iX|^ nt~2n߇)DzCOr-[gl'$1m| FpToqmA,co^`Ԏ:ef<"(E `y@ՉR^a!!8QXNnN[_ov9|>]?a? Q||x++Ƨ p6ɚ/(}|XX# û@O3ݔw9b%Ҳ F iҳ0AʿpT ?0>πrcTt@mC Hq@3M] o>Ң"wu=G<5>v_̇n?p:}E M<3p& "Hi״?~q/1B [ *P,J/G2v@>hv!5}9d.>$:0ByH?ow* %~Rs_w8t?o? S#5ĥZ,&<FS+< Ҡ:bdeRѯ CB8heV(!2hHsK.: a۾/\MʌU_K.1|]ww#03)L:8&:,b:=)B7}=um񪩶z=g z#nh2Q# *ۼi5М*~q*C38wX\84T?n\)|ϻ!|>!5!Jvj V>*d}zen?v~j#,\]˷O}h59-`;.Rd 0qNzY @CxWOl9֎=E<%+ᄃ}߻CGN=}i\LsuT Dz~rd nVu>TyꦾzK+~4 PpԾB0P7CD7:BMD/Y>a| IȔ׻!SAX"Fp'Omqg펯xuY$=vvy zn"2b7k Ac5&o$uB4!}6x;_پnlv-WŃ(]?Ξh_xYdvϹR x5jP%+eL(2<嘞bDGKipGjjHQWJW0{]#v@L5>UDvUF -5M·>8bPa:'NϬ~x-_l~?N]}W6λTϐ-) e, k癧϶13|Hw;h[np;įc8k+%g1ny_<> _zT gMUK:rPΦ܇q$N:ԡ܍6/T=Qjc̠pНD.!罈\|t l[??1C#@u˺ }uv=ϵps?G=g>a4rKsI>Ǵϙ%Loo\sU;r͡vKrci_[?pblJZVۣGmg~b_WsctcaųD?=BL KN#/ dȜuG`@9Rm" C^J ˕N.ʬLR)@\To(|]41ZI[q\S>^׶7fFx.#cϝ?sr):d>~w`1LuTЏn0Nᅓ'N ,?d RG"^ ]Z4 en0v$ , ZtOAEwWFa G}`WJ A/Rnx=G=BxivapQ+XOEJ< nl b˞M (sOE ^51' nck~18xܬY#ߐ׭^r'hhKR: p'}1UhT*lu5ձ3_)g+xz@eiBNeT ixRH#Ԩ~]B eLjC].`#F01aEy:r[sHQ8RF<:ybYk^(8Qp[s+cn[(^30&x-n5J ^E:e\ hϠ3K9]{XFH 6HZ !x1V8ƺxѭUk Z+XseXjBGC8ɨҭ˵uT|§2r0b'ϐ,z0{?ܭZ&Vx4t~(nxvShUy O Z5 <(ɓ)C{]9 O 2 V26hqّY9H݅L}(Ӷ0JWĶ%jW,#rW(Be ^#BTP/2E\p"2R`0(p GS%Ql@MzM9|hgϝ! R5i=*;y.ZӰQRe()7Н}Wf_/gi Odzn-nSg7mg7X/WT^8ѥw3,l0f|+m$N"/" se\`>Xf*X WzL'e6u0Gq~2 zuڶf2[eMZ*9N%>tK?)TjU~* @WiqxQmO^S,ۗ<5pgϻbnZ~v>hn r}_jq̪ҙ\^7w$ئ?8_\a"B;a 8!Քixj >Sk6~#fٜ;+<Tݠpg&*)(Y. "1ԡM"|ؚ[Xn?O?kz1eҕ @͝99=5/ߺg(ۍ >W-*o\3HGX,+!GUFͷc#:F-N/XB0|O8"(Ca,|οvVશ|Oia7GS0%$|M_$(Oy wȻ0<څWgكGDZr=SHf8ҭ4IDATL}v=.;n[,KH*u׷' S{X+CSۨer$0\%IEі;x+М\MMJ^{C{*p;HSc(8sse~9pͨJ2ڡ%j /SbT&繎,.;ʷ~UkU6szyf艧pG>|qsEH%5mómصly䁯Z7yY\A~1:\\7+XsuA b$6A97?ݡ~u.<7jHGt<W[q庲z F^pj y{cmʹ/ٸoeg>>peK@*Wcw&gvͭG~Zn٬/2MNM+%\*仏,nW 'Y/ZHү){I=:.xt |x4ISY'UC!:~m^TbY@vVW{&VX78nɹ|} E ˧?Mc_5mvcM.X=j˫֗B%@=3`ʩ7mbn K."c ^.8 !J?\ ċ|$?_Lʿx1/_# =!˔PM(%IENDB`django-celery-beat-2.2.1/docs/includes/000077500000000000000000000000001406752007300177125ustar00rootroot00000000000000django-celery-beat-2.2.1/docs/includes/installation.txt000066400000000000000000000016451406752007300231620ustar00rootroot00000000000000Installation ============ 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-2.2.1/docs/includes/introduction.txt000066400000000000000000000160171406752007300232010ustar00rootroot00000000000000:Version: 2.2.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 at 30 minutes past the hour every hour) 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() Example running periodic tasks ----------------------------------- The periodic tasks still need 'workers' to execute them. So make sure the default **Celery** package is installed. (If not installed, please follow the installation instructions here: https://github.com/celery/celery) Both the worker and beat services need to be running at the same time. 1. Start a Celery worker service (specify your Django project name):: $ celery -A [project-name] worker --loglevel=info 2. As a separate process, start the beat service (specify the Django scheduler):: $ celery -A [project-name] beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler **OR** you can use the -S (scheduler flag), for more options see ``celery beat --help``):: $ celery -A [project-name] beat -l info -S django **OR** you can set the scheduler through Django's settings:: CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler' Also, as an alternative, you can run the two steps above (worker and beat services) with only one command (recommended for **development environment only**):: $ celery -A [project-name] worker --beat --scheduler django --loglevel=info 3. Now you can add and manage your periodic tasks from the Django Admin interface. django-celery-beat-2.2.1/docs/index.rst000066400000000000000000000007721406752007300177530ustar00rootroot00000000000000======================================================================= 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-2.2.1/docs/make.bat000066400000000000000000000161121406752007300175120ustar00rootroot00000000000000@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%/django_celery_beat/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/django_celery_beat/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-2.2.1/docs/reference/000077500000000000000000000000001406752007300200425ustar00rootroot00000000000000django-celery-beat-2.2.1/docs/reference/django-celery-beat.admin.rst000066400000000000000000000004351406752007300253210ustar00rootroot00000000000000===================================================== ``django_celery_beat.admin`` ===================================================== .. contents:: :local: .. currentmodule:: django_celery_beat.admin .. automodule:: django_celery_beat.admin :members: :undoc-members: django-celery-beat-2.2.1/docs/reference/django-celery-beat.managers.rst000066400000000000000000000004461406752007300260300ustar00rootroot00000000000000===================================================== ``django_celery_beat.managers`` ===================================================== .. contents:: :local: .. currentmodule:: django_celery_beat.managers .. automodule:: django_celery_beat.managers :members: :undoc-members: django-celery-beat-2.2.1/docs/reference/django-celery-beat.models.rst000066400000000000000000000004401406752007300255100ustar00rootroot00000000000000===================================================== ``django_celery_beat.models`` ===================================================== .. contents:: :local: .. currentmodule:: django_celery_beat.models .. automodule:: django_celery_beat.models :members: :undoc-members: django-celery-beat-2.2.1/docs/reference/django-celery-beat.rst000066400000000000000000000004131406752007300242260ustar00rootroot00000000000000===================================================== ``django_celery_beat`` ===================================================== .. contents:: :local: .. currentmodule:: django_celery_beat .. automodule:: django_celery_beat :members: :undoc-members: django-celery-beat-2.2.1/docs/reference/django-celery-beat.schedulers.rst000066400000000000000000000004541406752007300263730ustar00rootroot00000000000000===================================================== ``django_celery_beat.schedulers`` ===================================================== .. contents:: :local: .. currentmodule:: django_celery_beat.schedulers .. automodule:: django_celery_beat.schedulers :members: :undoc-members: django-celery-beat-2.2.1/docs/reference/django-celery-beat.tzcrontab.rst000066400000000000000000000004511406752007300262350ustar00rootroot00000000000000===================================================== ``django_celery_beat.tzcrontab`` ===================================================== .. contents:: :local: .. currentmodule:: django_celery_beat.tzcrontab .. automodule:: django_celery_beat.tzcrontab :members: :undoc-members: django-celery-beat-2.2.1/docs/reference/django-celery-beat.utils.rst000066400000000000000000000004351406752007300253710ustar00rootroot00000000000000===================================================== ``django_celery_beat.utils`` ===================================================== .. contents:: :local: .. currentmodule:: django_celery_beat.utils .. automodule:: django_celery_beat.utils :members: :undoc-members: django-celery-beat-2.2.1/docs/reference/django-celery-beat.validators.rst000066400000000000000000000004541406752007300264020ustar00rootroot00000000000000===================================================== ``django_celery_beat.validators`` ===================================================== .. contents:: :local: .. currentmodule:: django_celery_beat.validators .. automodule:: django_celery_beat.validators :members: :undoc-members: django-celery-beat-2.2.1/docs/reference/index.rst000066400000000000000000000005641406752007300217100ustar00rootroot00000000000000.. _apiref: =============== API Reference =============== :Release: |version| :Date: |today| .. toctree:: :maxdepth: 1 django-celery-beat django-celery-beat.models django-celery-beat.tzcrontab django-celery-beat.managers django-celery-beat.schedulers django-celery-beat.admin django-celery-beat.utils django-celery-beat.validators django-celery-beat-2.2.1/docs/templates/000077500000000000000000000000001406752007300201025ustar00rootroot00000000000000django-celery-beat-2.2.1/docs/templates/readme.txt000066400000000000000000000026021406752007300221000ustar00rootroot00000000000000===================================================================== 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-2.2.1/extra/000077500000000000000000000000001406752007300162775ustar00rootroot00000000000000django-celery-beat-2.2.1/extra/appveyor/000077500000000000000000000000001406752007300201445ustar00rootroot00000000000000django-celery-beat-2.2.1/extra/appveyor/install.ps1000066400000000000000000000053421406752007300222430ustar00rootroot00000000000000# 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-2.2.1/extra/appveyor/run_with_compiler.cmd000066400000000000000000000034621406752007300243670ustar00rootroot00000000000000:: 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-2.2.1/issue_template.md000066400000000000000000000006151406752007300205230ustar00rootroot00000000000000### Summary: Include a *brief* descrioption of the problem here, and fill out the version info below. * Celery Version: * Celery-Beat Version: ### Exact steps to reproduce the issue: 1. 2. 3. ### Detailed information Please include more detailed information here, such as relevant information about your system setup, things you did to try and debug the problem, log messages, etc. django-celery-beat-2.2.1/manage.py000077500000000000000000000003711406752007300167620ustar00rootroot00000000000000#!/usr/bin/env python 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-2.2.1/requirements/000077500000000000000000000000001406752007300176775ustar00rootroot00000000000000django-celery-beat-2.2.1/requirements/default.txt000066400000000000000000000001111406752007300220550ustar00rootroot00000000000000celery>=5.0,<6.0 django-timezone-field>=4.1.0,<5.0 python-crontab>=2.3.4 django-celery-beat-2.2.1/requirements/docs.txt000066400000000000000000000003001406752007300213610ustar00rootroot00000000000000Django>=2.2,<4.0 https://github.com/celery/sphinx_celery/archive/master.zip https://github.com/celery/kombu/zipball/master#egg=kombu https://github.com/celery/celery/zipball/master#egg=celery django-celery-beat-2.2.1/requirements/pkgutils.txt000066400000000000000000000001571406752007300223050ustar00rootroot00000000000000setuptools>=40.8.0 wheel>=0.33.1 flake8>=3.8.3 flakeplus>=1.1 tox>=2.3.1 sphinx2rst>=1.0 bumpversion pydocstyledjango-celery-beat-2.2.1/requirements/runtime.txt000066400000000000000000000000211406752007300221140ustar00rootroot00000000000000Django>=2.2,<4.0 django-celery-beat-2.2.1/requirements/test-ci.txt000066400000000000000000000000231406752007300220030ustar00rootroot00000000000000pytest-cov codecov django-celery-beat-2.2.1/requirements/test-django.txt000066400000000000000000000000211406752007300226500ustar00rootroot00000000000000Django>=2.2,<4.0 django-celery-beat-2.2.1/requirements/test-django22.txt000066400000000000000000000000231406752007300230160ustar00rootroot00000000000000django>=2.2.2,<2.3 django-celery-beat-2.2.1/requirements/test-django30.txt000066400000000000000000000000211406752007300230130ustar00rootroot00000000000000django>=3.0,<3.1 django-celery-beat-2.2.1/requirements/test-django31.txt000066400000000000000000000000211406752007300230140ustar00rootroot00000000000000django>=3.1,<3.2 django-celery-beat-2.2.1/requirements/test-django32.txt000066400000000000000000000000211406752007300230150ustar00rootroot00000000000000django>=3.2,<3.3 django-celery-beat-2.2.1/requirements/test.txt000066400000000000000000000001171406752007300214160ustar00rootroot00000000000000case>=1.3.1 pytest-django>=2.2,<4.0 pytz>dev pytest<4.0.0 pytest-timeout ephem django-celery-beat-2.2.1/setup.cfg000066400000000000000000000005451406752007300170010ustar00rootroot00000000000000[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, W503, W504 [pep257] ignore = D102,D104,D203,D105,D213 match-dir = [^migrations] [wheel] universal = 1 django-celery-beat-2.2.1/setup.py000066400000000000000000000102311406752007300166630ustar00rootroot00000000000000#!/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() PY36_OR_LESS = sys.version_info < (3, 6) PYPY_VERSION = getattr(sys, 'pypy_version_info', None) PYPY24_ATLEAST = PYPY_VERSION and PYPY_VERSION >= (2, 4) if PY36_OR_LESS and not PYPY24_ATLEAST: raise Exception(E_UNSUPPORTED_PYTHON % (PYIMP, '3.6')) # -*- Classifiers -*- classes = """ Development Status :: 5 - Production/Stable License :: OSI Approved :: BSD License Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy Framework :: Django Framework :: Django :: 2.2 Framework :: Django :: 3.0 Framework :: Django :: 3.1 Operating System :: OS Independent Topic :: Communications Topic :: System :: Distributed Computing Topic :: Software Development :: Libraries :: Python Modules """ classifiers = [s.strip() for s in classes.split('\n') if s] # -*- Distribution Meta -*- re_meta = re.compile(r'__(\w+?)__\s*=\s*(.*)') re_doc = re.compile(r'^"""(.+?)"""') def add_default(m): attr_name, attr_value = m.groups() return ((attr_name, attr_value.strip("\"'")),) def add_doc(m): return (('doc', m.groups()[0]),) pats = {re_meta: add_default, re_doc: add_doc} here = os.path.abspath(os.path.dirname(__file__)) with open(os.path.join(here, 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() long_description_content_type = 'text/x-rst' else: long_description = 'See http://pypi.python.org/pypi/%s' % (NAME,) long_description_content_type = 'text/markdown' # -*- %%% -*- 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, long_description_content_type=long_description_content_type, 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') + reqs('runtime.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=True, zip_safe=False, ) django-celery-beat-2.2.1/t/000077500000000000000000000000001406752007300154175ustar00rootroot00000000000000django-celery-beat-2.2.1/t/.coveragerc000066400000000000000000000003561406752007300175440ustar00rootroot00000000000000[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-2.2.1/t/__init__.py000066400000000000000000000000001406752007300175160ustar00rootroot00000000000000django-celery-beat-2.2.1/t/proj/000077500000000000000000000000001406752007300163715ustar00rootroot00000000000000django-celery-beat-2.2.1/t/proj/__init__.py000066400000000000000000000000561406752007300205030ustar00rootroot00000000000000from .celery import app as celery_app # noqa django-celery-beat-2.2.1/t/proj/celery.py000066400000000000000000000004721406752007300202310ustar00rootroot00000000000000import 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-2.2.1/t/proj/settings.py000066400000000000000000000061621406752007300206100ustar00rootroot00000000000000""" 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/ """ 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 = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', ] ROOT_URLCONF = 't.proj.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] WSGI_APPLICATION = 't.proj.wsgi.application' # 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_TZ_AWARE = True django-celery-beat-2.2.1/t/proj/urls.py000066400000000000000000000001731406752007300177310ustar00rootroot00000000000000from django.conf.urls import url from django.contrib import admin urlpatterns = [ url(r'^admin/', admin.site.urls), ] django-celery-beat-2.2.1/t/proj/wsgi.py000066400000000000000000000006001406752007300177100ustar00rootroot00000000000000""" 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/ """ 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-2.2.1/t/unit/000077500000000000000000000000001406752007300163765ustar00rootroot00000000000000django-celery-beat-2.2.1/t/unit/__init__.py000066400000000000000000000000001406752007300204750ustar00rootroot00000000000000django-celery-beat-2.2.1/t/unit/conftest.py000066400000000000000000000025251406752007300206010ustar00rootroot00000000000000import pytest # we have to import the pytest plugin fixtures here, # in case user did not do the `python setup.py develop` yet, # that installs the pytest plugin into the setuptools registry. from celery.contrib.pytest import (celery_app, celery_enable_logging, celery_parameters, depends_on_current_app, celery_config, use_celery_app_trap) from celery.contrib.testing.app import TestApp, Trap # Tricks flake8 into silencing redefining fixtures warnings. __all__ = ( 'celery_app', 'celery_enable_logging', 'depends_on_current_app', 'celery_parameters', 'celery_config', 'use_celery_app_trap' ) @pytest.fixture(scope='session', autouse=True) def setup_default_app_trap(): from celery._state import set_default_app set_default_app(Trap()) @pytest.fixture() def app(celery_app): return celery_app @pytest.fixture(autouse=True) def test_cases_shortcuts(request, app, patching): if request.instance: @app.task def add(x, y): return x + y # IMPORTANT: We set an .app attribute for every test case class. request.instance.app = app request.instance.Celery = TestApp request.instance.add = add request.instance.patching = patching yield if request.instance: request.instance.app = None django-celery-beat-2.2.1/t/unit/test_admin.py000066400000000000000000000115461406752007300211060ustar00rootroot00000000000000import pytest from django.core.exceptions import ValidationError from django.test import TestCase from itertools import combinations from django_celery_beat.admin import PeriodicTaskAdmin from django_celery_beat.models import \ DAYS, \ PeriodicTask, \ CrontabSchedule, \ IntervalSchedule, \ SolarSchedule, \ ClockedSchedule @pytest.mark.django_db() class ActionsTests(TestCase): @classmethod def setUpTestData(cls): super().setUpTestData() cls.interval_schedule = IntervalSchedule.objects.create(every=10, period=DAYS) def test_toggle_action(self): PeriodicTask.objects.create(name='name1', task='task1', enabled=False, interval=self.interval_schedule) PeriodicTask.objects.create(name='name2', task='task2', enabled=True, interval=self.interval_schedule) PeriodicTask.objects.create(name='name3', task='task3', enabled=False, interval=self.interval_schedule) qs = PeriodicTask.objects.all() PeriodicTaskAdmin(PeriodicTask, None)._toggle_tasks_activity(qs) e1 = PeriodicTask.objects.get(name='name1', task='task1').enabled e2 = PeriodicTask.objects.get(name='name2', task='task2').enabled e3 = PeriodicTask.objects.get(name='name3', task='task3').enabled self.assertTrue(e1) self.assertFalse(e2) self.assertTrue(e3) def test_toggle_action_all_enabled(self): PeriodicTask.objects.create(name='name1', task='task1', enabled=True, interval=self.interval_schedule) PeriodicTask.objects.create(name='name2', task='task2', enabled=True, interval=self.interval_schedule) PeriodicTask.objects.create(name='name3', task='task3', enabled=True, interval=self.interval_schedule) qs = PeriodicTask.objects.all() PeriodicTaskAdmin(PeriodicTask, None)._toggle_tasks_activity(qs) e1 = PeriodicTask.objects.get(name='name1', task='task1').enabled e2 = PeriodicTask.objects.get(name='name2', task='task2').enabled e3 = PeriodicTask.objects.get(name='name3', task='task3').enabled self.assertFalse(e1) self.assertFalse(e2) self.assertFalse(e3) def test_toggle_action_all_disabled(self): PeriodicTask.objects.create(name='name1', task='task1', enabled=False, interval=self.interval_schedule) PeriodicTask.objects.create(name='name2', task='task2', enabled=False, interval=self.interval_schedule) PeriodicTask.objects.create(name='name3', task='task3', enabled=False, interval=self.interval_schedule) qs = PeriodicTask.objects.all() PeriodicTaskAdmin(PeriodicTask, None)._toggle_tasks_activity(qs) e1 = PeriodicTask.objects.get(name='name1', task='task1').enabled e2 = PeriodicTask.objects.get(name='name2', task='task2').enabled e3 = PeriodicTask.objects.get(name='name3', task='task3').enabled self.assertTrue(e1) self.assertTrue(e2) self.assertTrue(e3) @pytest.mark.django_db() class ValidateUniqueTests(TestCase): def test_validate_unique_raises_if_schedule_not_set(self): with self.assertRaises(ValidationError) as cm: PeriodicTask(name='task0').validate_unique() self.assertEqual( cm.exception.args[0], 'One of clocked, interval, crontab, or solar must be set.', ) def test_validate_unique_raises_for_multiple_schedules(self): schedules = [ ('crontab', CrontabSchedule()), ('interval', IntervalSchedule()), ('solar', SolarSchedule()), ('clocked', ClockedSchedule()) ] expected_error_msg = ( 'Only one of clocked, interval, crontab, or solar ' 'must be set' ) for i, options in enumerate(combinations(schedules, 2)): name = 'task{}'.format(i) options_dict = dict(options) with self.assertRaises(ValidationError) as cm: PeriodicTask(name=name, **options_dict).validate_unique() errors = cm.exception.args[0] self.assertEqual(errors.keys(), options_dict.keys()) for error_msg in errors.values(): self.assertEqual(error_msg, [expected_error_msg]) def test_validate_unique_not_raises(self): PeriodicTask(crontab=CrontabSchedule()).validate_unique() PeriodicTask(interval=IntervalSchedule()).validate_unique() PeriodicTask(solar=SolarSchedule()).validate_unique() PeriodicTask(clocked=ClockedSchedule(), one_off=True).validate_unique() django-celery-beat-2.2.1/t/unit/test_crontabs.py000066400000000000000000000332011406752007300216210ustar00rootroot00000000000000from unittest import TestCase from django.core.exceptions import ValidationError from django_celery_beat import validators class MinuteTests(TestCase): def test_good(self): try: validators.minute_validator('*') validators.minute_validator('0') validators.minute_validator('1') validators.minute_validator('54') validators.minute_validator('59') validators.minute_validator('1,2,59') validators.minute_validator('43,2') validators.minute_validator('5,20,25,43') validators.minute_validator('1-4') validators.minute_validator('1-29') validators.minute_validator('45-59') validators.minute_validator('*/4') validators.minute_validator('*/43') validators.minute_validator('1-2/43') except ValidationError as e: self.fail(e) def test_space(self): with self.assertRaises(ValidationError): validators.minute_validator('1, 2') def test_big_number(self): with self.assertRaises(ValidationError): validators.minute_validator('60') with self.assertRaises(ValidationError): validators.minute_validator('420') with self.assertRaises(ValidationError): validators.minute_validator('100500') def test_text(self): with self.assertRaises(ValidationError): validators.minute_validator('fsd') with self.assertRaises(ValidationError): validators.minute_validator('.') with self.assertRaises(ValidationError): validators.minute_validator('432a') def test_out_range(self): with self.assertRaises(ValidationError): validators.minute_validator('0-432') with self.assertRaises(ValidationError): validators.minute_validator('342-432') with self.assertRaises(ValidationError): validators.minute_validator('4-60') def test_bad_range(self): with self.assertRaises(ValidationError): validators.minute_validator('10-4') def test_bad_slice(self): with self.assertRaises(ValidationError): validators.minute_validator('*/100') with self.assertRaises(ValidationError): validators.minute_validator('10/30') with self.assertRaises(ValidationError): validators.minute_validator('10-20/100') class HourTests(TestCase): def test_good(self): try: validators.hour_validator('*') validators.hour_validator('0') validators.hour_validator('1') validators.hour_validator('22') validators.hour_validator('23') validators.hour_validator('1,2,23') validators.hour_validator('23,2') validators.hour_validator('5,20,21,22') validators.hour_validator('1-4') validators.hour_validator('1-23') validators.hour_validator('*/4') validators.hour_validator('*/22') validators.hour_validator('1-2/5') except ValidationError as e: self.fail(e) def test_space(self): with self.assertRaises(ValidationError): validators.hour_validator('1, 2') def test_big_number(self): with self.assertRaises(ValidationError): validators.hour_validator('24') with self.assertRaises(ValidationError): validators.hour_validator('420') with self.assertRaises(ValidationError): validators.hour_validator('100500') def test_text(self): with self.assertRaises(ValidationError): validators.hour_validator('fsd') with self.assertRaises(ValidationError): validators.hour_validator('.') with self.assertRaises(ValidationError): validators.hour_validator('432a') def test_out_range(self): with self.assertRaises(ValidationError): validators.hour_validator('0-24') with self.assertRaises(ValidationError): validators.hour_validator('342-432') with self.assertRaises(ValidationError): validators.hour_validator('4-25') def test_bad_range(self): with self.assertRaises(ValidationError): validators.hour_validator('10-4') def test_bad_slice(self): with self.assertRaises(ValidationError): validators.hour_validator('*/100') with self.assertRaises(ValidationError): validators.hour_validator('10/30') with self.assertRaises(ValidationError): validators.hour_validator('10-20/100') class DayOfMonthTests(TestCase): def test_good(self): try: validators.day_of_month_validator('*') validators.day_of_month_validator('1') validators.day_of_month_validator('29') validators.day_of_month_validator('31') validators.day_of_month_validator('1,2,31') validators.day_of_month_validator('30,2') validators.day_of_month_validator('5,20,25,31') validators.day_of_month_validator('1-4') validators.day_of_month_validator('1-30') validators.day_of_month_validator('*/4') validators.day_of_month_validator('*/22') validators.day_of_month_validator('1-2/5') except ValidationError as e: self.fail(e) def test_space(self): with self.assertRaises(ValidationError): validators.day_of_month_validator('1, 2') def test_zero(self): with self.assertRaises(ValidationError): validators.day_of_month_validator('0') def test_big_number(self): with self.assertRaises(ValidationError): validators.day_of_month_validator('32') with self.assertRaises(ValidationError): validators.day_of_month_validator('420') with self.assertRaises(ValidationError): validators.day_of_month_validator('100500') def test_text(self): with self.assertRaises(ValidationError): validators.day_of_month_validator('fsd') with self.assertRaises(ValidationError): validators.day_of_month_validator('.') with self.assertRaises(ValidationError): validators.day_of_month_validator('432a') def test_out_range(self): with self.assertRaises(ValidationError): validators.day_of_month_validator('0-32') with self.assertRaises(ValidationError): validators.day_of_month_validator('342-432') with self.assertRaises(ValidationError): validators.day_of_month_validator('4-33') def test_bad_range(self): with self.assertRaises(ValidationError): validators.day_of_month_validator('10-4') def test_bad_slice(self): with self.assertRaises(ValidationError): validators.day_of_month_validator('*/100') with self.assertRaises(ValidationError): validators.day_of_month_validator('10/30') with self.assertRaises(ValidationError): validators.day_of_month_validator('10-20/100') class MonthTests(TestCase): def test_good(self): try: validators.month_of_year_validator('*') validators.month_of_year_validator('1') validators.month_of_year_validator('10') validators.month_of_year_validator('12') validators.month_of_year_validator('1,2,12') validators.month_of_year_validator('12,2') validators.month_of_year_validator('5,10,11,12') validators.month_of_year_validator('1-4') validators.month_of_year_validator('1-12') validators.month_of_year_validator('*/4') validators.month_of_year_validator('*/12') validators.month_of_year_validator('1-2/12') except ValidationError as e: self.fail(e) def test_good_month_name(self): try: validators.month_of_year_validator('jan') validators.month_of_year_validator('feb') validators.month_of_year_validator('mar') validators.month_of_year_validator('apr') validators.month_of_year_validator('may') validators.month_of_year_validator('jun') validators.month_of_year_validator('jul') validators.month_of_year_validator('aug') validators.month_of_year_validator('sep') validators.month_of_year_validator('oct') validators.month_of_year_validator('nov') validators.month_of_year_validator('dec') except ValidationError as e: self.fail(e) def test_good_month_name_case(self): try: validators.month_of_year_validator('jan') validators.month_of_year_validator('JAN') validators.month_of_year_validator('JaN') except ValidationError as e: self.fail(e) def test_space(self): with self.assertRaises(ValidationError): validators.month_of_year_validator('1, 2') def test_zero(self): with self.assertRaises(ValidationError): validators.month_of_year_validator('0') def test_big_number(self): with self.assertRaises(ValidationError): validators.month_of_year_validator('13') with self.assertRaises(ValidationError): validators.month_of_year_validator('420') with self.assertRaises(ValidationError): validators.month_of_year_validator('100500') def test_text(self): with self.assertRaises(ValidationError): validators.month_of_year_validator('fsd') with self.assertRaises(ValidationError): validators.month_of_year_validator('.') with self.assertRaises(ValidationError): validators.month_of_year_validator('432a') def test_out_range(self): with self.assertRaises(ValidationError): validators.month_of_year_validator('0-13') with self.assertRaises(ValidationError): validators.month_of_year_validator('342-432') with self.assertRaises(ValidationError): validators.month_of_year_validator('4-14') def test_bad_range(self): with self.assertRaises(ValidationError): validators.month_of_year_validator('10-4') def test_bad_slice(self): with self.assertRaises(ValidationError): validators.month_of_year_validator('*/13') with self.assertRaises(ValidationError): validators.month_of_year_validator('10/30') with self.assertRaises(ValidationError): validators.month_of_year_validator('10-20/100') class DayOfWeekTests(TestCase): def test_good(self): try: validators.day_of_week_validator('*') validators.day_of_week_validator('1') validators.day_of_week_validator('6') validators.day_of_week_validator('7') validators.day_of_week_validator('1,2,6') validators.day_of_week_validator('6,2') validators.day_of_week_validator('5,6,4,6') validators.day_of_week_validator('1-4') validators.day_of_week_validator('1-7') validators.day_of_week_validator('*/4') validators.day_of_week_validator('*/6') validators.day_of_week_validator('2-7/5') except ValidationError as e: self.fail(e) def test_good_month_name(self): try: validators.day_of_week_validator('sun') validators.day_of_week_validator('mon') validators.day_of_week_validator('tue') validators.day_of_week_validator('wed') validators.day_of_week_validator('thu') validators.day_of_week_validator('fri') validators.day_of_week_validator('sat') except ValidationError as e: self.fail(e) def test_good_month_name_case(self): try: validators.day_of_week_validator('mon') validators.day_of_week_validator('MoN') validators.day_of_week_validator('MON') except ValidationError as e: self.fail(e) def test_space(self): with self.assertRaises(ValidationError): validators.day_of_week_validator('1, 2') def test_big_number(self): with self.assertRaises(ValidationError): validators.day_of_week_validator('8') with self.assertRaises(ValidationError): validators.day_of_week_validator('420') with self.assertRaises(ValidationError): validators.day_of_week_validator('100500') def test_text(self): with self.assertRaises(ValidationError): validators.day_of_week_validator('fsd') with self.assertRaises(ValidationError): validators.day_of_week_validator('.') with self.assertRaises(ValidationError): validators.day_of_week_validator('432a') def test_out_range(self): with self.assertRaises(ValidationError): validators.day_of_week_validator('0-32') with self.assertRaises(ValidationError): validators.day_of_week_validator('342-432') with self.assertRaises(ValidationError): validators.day_of_week_validator('4-33') def test_bad_range(self): with self.assertRaises(ValidationError): validators.day_of_week_validator('10-4') def test_bad_slice(self): with self.assertRaises(ValidationError): validators.day_of_week_validator('*/8') with self.assertRaises(ValidationError): validators.day_of_week_validator('10/30') with self.assertRaises(ValidationError): validators.day_of_week_validator('10-20/100') django-celery-beat-2.2.1/t/unit/test_models.py000066400000000000000000000130521406752007300212730ustar00rootroot00000000000000import os from celery import schedules from django.test import TestCase, override_settings from django.apps import apps from django.db.migrations.state import ProjectState from django.db.migrations.autodetector import MigrationAutodetector from django.db.migrations.loader import MigrationLoader from django.db.migrations.questioner import NonInteractiveMigrationQuestioner from django.utils import timezone import timezone_field from django_celery_beat import migrations as beat_migrations from django_celery_beat.models import ( crontab_schedule_celery_timezone, SolarSchedule, CrontabSchedule, ClockedSchedule, IntervalSchedule, ) class MigrationTests(TestCase): def test_no_future_duplicate_migration_numbers(self): """Verify no duplicate migration numbers. Migration files with the same number can cause issues with backward migrations, so avoid them. """ path = os.path.dirname(beat_migrations.__file__) files = [f[:4] for f in os.listdir(path) if f.endswith('.py')] expected_duplicates = [ (3, '0006'), ] duplicates_extra = sum(count - 1 for count, _ in expected_duplicates) duplicates_numbers = [number for _, number in expected_duplicates] self.assertEqual( len(files), len(set(files)) + duplicates_extra, msg=('Detected migration files with the same migration number' ' (besides {})'.format(' and '.join(duplicates_numbers)))) def test_models_match_migrations(self): """Make sure that no model changes exist. This logic is taken from django's makemigrations.py file. Here just detect if model changes exist that require a migration, and if so we fail. """ app_labels = ['django_celery_beat'] loader = MigrationLoader(None, ignore_no_migrations=True) questioner = NonInteractiveMigrationQuestioner( specified_apps=app_labels, dry_run=False) autodetector = MigrationAutodetector( loader.project_state(), ProjectState.from_apps(apps), questioner, ) changes = autodetector.changes( graph=loader.graph, trim_to_apps=app_labels, convert_apps=app_labels, migration_name='fake_name', ) self.assertTrue( not changes, msg='Model changes exist that do not have a migration') class TestDuplicatesMixin: def _test_duplicate_schedules(self, cls, kwargs, schedule=None): sched1 = cls.objects.create(**kwargs) cls.objects.create(**kwargs) self.assertEqual(cls.objects.filter(**kwargs).count(), 2) # try to create a duplicate schedule from a celery schedule if schedule is None: schedule = sched1.schedule sched3 = cls.from_schedule(schedule) # the schedule should be the first of the 2 previous duplicates self.assertEqual(sched3, sched1) # and the duplicates should not be deleted ! self.assertEqual(cls.objects.filter(**kwargs).count(), 2) class CrontabScheduleTestCase(TestCase, TestDuplicatesMixin): FIRST_VALID_TIMEZONE = timezone_field.\ TimeZoneField.default_choices[0][0].zone def test_default_timezone_without_settings_config(self): assert crontab_schedule_celery_timezone() == "UTC" @override_settings(CELERY_TIMEZONE=FIRST_VALID_TIMEZONE) def test_default_timezone_with_settings_config(self): assert crontab_schedule_celery_timezone() == self.FIRST_VALID_TIMEZONE def test_duplicate_schedules(self): # See: https://github.com/celery/django-celery-beat/issues/322 kwargs = { "minute": "*", "hour": "4", "day_of_week": "*", "day_of_month": "*", "month_of_year": "*", "day_of_week": "*", } schedule = schedules.crontab(hour="4") self._test_duplicate_schedules(CrontabSchedule, kwargs, schedule) class SolarScheduleTestCase(TestCase): EVENT_CHOICES = SolarSchedule._meta.get_field("event").choices def test_celery_solar_schedules_sorted(self): assert all( self.EVENT_CHOICES[i] <= self.EVENT_CHOICES[i + 1] for i in range(len(self.EVENT_CHOICES) - 1) ), "SolarSchedule event choices are unsorted" def test_celery_solar_schedules_included_as_event_choices(self): """Make sure that all Celery solar schedules are included in SolarSchedule `event` field choices, keeping synchronized Celery solar events with `django-celery-beat` supported solar events. This test is necessary because Celery solar schedules are hardcoded at models so that Django can discover their translations. """ event_choices_values = [value for value, tr in self.EVENT_CHOICES] for solar_event in schedules.solar._all_events: assert solar_event in event_choices_values for event_choice in event_choices_values: assert event_choice in schedules.solar._all_events class IntervalScheduleTestCase(TestCase, TestDuplicatesMixin): def test_duplicate_schedules(self): kwargs = {'every': 1, 'period': IntervalSchedule.SECONDS} schedule = schedules.schedule(run_every=1.0) self._test_duplicate_schedules(IntervalSchedule, kwargs, schedule) class ClockedScheduleTestCase(TestCase, TestDuplicatesMixin): def test_duplicate_schedules(self): kwargs = {'clocked_time': timezone.now()} self._test_duplicate_schedules(ClockedSchedule, kwargs) django-celery-beat-2.2.1/t/unit/test_schedulers.py000066400000000000000000000676731406752007300221730ustar00rootroot00000000000000import math import os import time import pytest from datetime import datetime, timedelta from itertools import count from time import monotonic from django.contrib.admin.sites import AdminSite from django.contrib.messages.storage.fallback import FallbackStorage from django.test import RequestFactory, override_settings from django.utils import timezone from celery.schedules import schedule, crontab, solar from django_celery_beat import schedulers from django_celery_beat.clockedschedule import clocked from django_celery_beat.admin import PeriodicTaskAdmin from django_celery_beat.models import ( PeriodicTask, PeriodicTasks, IntervalSchedule, CrontabSchedule, SolarSchedule, ClockedSchedule, DAYS ) from django_celery_beat.utils import make_aware, NEVER_CHECK_TIMEOUT _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().__init__(*args, **kwargs) def save(self): self.saved += 1 super().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_model_clocked(self, schedule, **kwargs): clocked = ClockedSchedule.from_schedule(schedule) clocked.save() return self.create_model(clocked=clocked, one_off=True, **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', priority=1, headers='{"_schema_name": "foobar"}', exchange='foo', ) return Model(**dict(entry, **kwargs)) def create_interval_schedule(self): return IntervalSchedule.objects.create(every=10, period=DAYS) @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' assert e.options['priority'] == 1 assert e.options['headers'] == {'_schema_name': 'foobar'} 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 @override_settings( USE_TZ=False, DJANGO_CELERY_BEAT_TZ_AWARE=False ) @pytest.mark.usefixtures('depends_on_current_app') @timezone.override('Europe/Berlin') @pytest.mark.celery(timezone='Europe/Berlin') def test_entry_is_due__no_use_tz(self): old_tz = os.environ.get("TZ") os.environ["TZ"] = "Europe/Berlin" if hasattr(time, "tzset"): time.tzset() assert self.app.timezone.zone == 'Europe/Berlin' # simulate last_run_at from DB - not TZ aware but localtime right_now = datetime.utcnow() m = self.create_model_crontab( crontab(minute='*/10'), last_run_at=right_now, ) e = self.Entry(m, app=self.app) assert e.is_due().is_due is False assert e.is_due().next <= 600 # 10 minutes; see above if old_tz is not None: os.environ["TZ"] = old_tz else: del os.environ["TZ"] if hasattr(time, "tzset"): time.tzset() @override_settings( USE_TZ=False, DJANGO_CELERY_BEAT_TZ_AWARE=False ) @pytest.mark.usefixtures('depends_on_current_app') @timezone.override('Europe/Berlin') @pytest.mark.celery(timezone='Europe/Berlin') def test_entry_and_model_last_run_at_with_utc_no_use_tz(self, monkeypatch): old_tz = os.environ.get("TZ") os.environ["TZ"] = "Europe/Berlin" if hasattr(time, "tzset"): time.tzset() assert self.app.timezone.zone == 'Europe/Berlin' # simulate last_run_at from DB - not TZ aware but localtime right_now = datetime.utcnow() # make sure to use fixed date time monkeypatch.setattr(self.Entry, '_default_now', lambda o: right_now) m = self.create_model_crontab( crontab(minute='*/10') ) m.save() e = self.Entry(m, app=self.app) e.save() m.refresh_from_db() assert m.last_run_at == e.last_run_at if old_tz is not None: os.environ["TZ"] = old_tz else: del os.environ["TZ"] if hasattr(time, "tzset"): time.tzset() @override_settings( USE_TZ=False, DJANGO_CELERY_BEAT_TZ_AWARE=False, TIME_ZONE="Europe/Berlin", CELERY_TIMEZONE="America/New_York" ) @pytest.mark.usefixtures('depends_on_current_app') @timezone.override('Europe/Berlin') @pytest.mark.celery(timezone='America/New_York') def test_entry_is_due__celery_timezone_doesnt_match_time_zone(self): old_tz = os.environ.get("TZ") os.environ["TZ"] = "Europe/Berlin" if hasattr(time, "tzset"): time.tzset() assert self.app.timezone.zone == 'America/New_York' # simulate last_run_at all none, doing the same thing that # _default_now() would do right_now = datetime.utcnow() m = self.create_model_crontab( crontab(minute='*/10'), last_run_at=right_now, ) e = self.Entry(m, app=self.app) assert e.is_due().is_due is False assert e.is_due().next <= 600 # 10 minutes; see above if old_tz is not None: os.environ["TZ"] = old_tz else: del os.environ["TZ"] if hasattr(time, "tzset"): time.tzset() def test_task_with_start_time(self): interval = 10 right_now = self.app.now() one_interval_ago = right_now - timedelta(seconds=interval) m = self.create_model_interval(schedule(timedelta(seconds=interval)), start_time=right_now, last_run_at=one_interval_ago) e = self.Entry(m, app=self.app) isdue, delay = e.is_due() assert isdue assert delay == interval tomorrow = right_now + timedelta(days=1) m2 = self.create_model_interval(schedule(timedelta(seconds=interval)), start_time=tomorrow, last_run_at=one_interval_ago) e2 = self.Entry(m2, app=self.app) isdue, delay = e2.is_due() assert not isdue assert delay == math.ceil((tomorrow - right_now).total_seconds()) def test_one_off_task(self): interval = 10 right_now = self.app.now() one_interval_ago = right_now - timedelta(seconds=interval) m = self.create_model_interval(schedule(timedelta(seconds=interval)), one_off=True, last_run_at=one_interval_ago, total_run_count=0) e = self.Entry(m, app=self.app) isdue, delay = e.is_due() assert isdue assert delay == interval m2 = self.create_model_interval(schedule(timedelta(seconds=interval)), one_off=True, last_run_at=one_interval_ago, total_run_count=1) e2 = self.Entry(m2, app=self.app) isdue, delay = e2.is_due() assert not isdue assert delay == NEVER_CHECK_TIMEOUT @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, interval=self.create_interval_schedule()) 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) if n == 'celery.backend_cleanup': assert e.options['expires'] == 12 * 3600 assert e.model.expires is None assert e.model.expire_seconds == 12 * 3600 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() dt_aware = make_aware(datetime(day=26, month=7, year=3000, hour=1, minute=0)) # future time self.m6 = self.create_model_clocked( clocked(dt_aware) ) self.m6.save() self.m6.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) == 6 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() def test_update_scheduler_heap_invalidation(self, monkeypatch): # mock "schedule_changed" to always trigger update for # all calls to schedule, as a change may occur at any moment monkeypatch.setattr(self.s, 'schedule_changed', lambda: True) self.s.tick() def test_heap_size_is_constant(self, monkeypatch): # heap size is constant unless the schedule changes monkeypatch.setattr(self.s, 'schedule_changed', lambda: True) expected_heap_size = len(self.s.schedule.values()) self.s.tick() assert len(self.s._heap) == expected_heap_size self.s.tick() assert len(self.s._heap) == expected_heap_size def test_scheduler_schedules_equality_on_change(self, monkeypatch): monkeypatch.setattr(self.s, 'schedule_changed', lambda: False) assert self.s.schedules_equal(self.s.schedule, self.s.schedule) monkeypatch.setattr(self.s, 'schedule_changed', lambda: True) assert not self.s.schedules_equal(self.s.schedule, self.s.schedule) def test_heap_always_return_the_first_item(self): interval = 10 s1 = schedule(timedelta(seconds=interval)) m1 = self.create_model_interval(s1, enabled=False) m1.last_run_at = self.app.now() - timedelta(seconds=interval + 2) m1.save() m1.refresh_from_db() s2 = schedule(timedelta(seconds=interval)) m2 = self.create_model_interval(s2, enabled=True) m2.last_run_at = self.app.now() - timedelta(seconds=interval + 1) m2.save() m2.refresh_from_db() e1 = EntryTrackSave(m1, self.app) # because the disabled task e1 runs first, e2 will never be executed e2 = EntryTrackSave(m2, self.app) s = self.Scheduler(app=self.app) s.schedule.clear() s.schedule[e1.name] = e1 s.schedule[e2.name] = e2 tried = set() for _ in range(len(s.schedule) * 8): tick_interval = s.tick() if tick_interval and tick_interval > 0.0: tried.add(s._heap[0].entry.name) time.sleep(tick_interval) if s.should_sync(): s.sync() assert len(tried) == 1 and tried == set([e1.name]) @pytest.mark.django_db() class test_models(SchedulerCase): def test_IntervalSchedule_unicode(self): assert (str(IntervalSchedule(every=1, period='seconds')) == 'every second') assert (str(IntervalSchedule(every=10, period='seconds')) == 'every 10 seconds') def test_CrontabSchedule_unicode(self): assert str(CrontabSchedule( minute=3, hour=3, day_of_week=None, )) == '3 3 * * * (m/h/dM/MY/d) UTC' assert str(CrontabSchedule( minute=3, hour=3, day_of_week='tue', day_of_month='*/2', month_of_year='4,6', )) == '3 3 */2 4,6 tue (m/h/dM/MY/d) UTC' def test_PeriodicTask_unicode_interval(self): p = self.create_model_interval(schedule(timedelta(seconds=10))) assert str(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 str(p) == """{0}: * 4,5 * * 4,5 (m/h/dM/MY/d) UTC""".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 str(p) == 'solar_event: {0} ({1}, {2})'.format( 'Solar noon', '48.06', '12.86' ) def test_PeriodicTask_unicode_clocked(self): time = make_aware(datetime.now()) p = self.create_model_clocked( clocked(time), name='clocked_event' ) assert str(p) == '{0}: {1}'.format( 'clocked_event', str(time) ) 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 str(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) def test_ClockedSchedule_schedule(self): due_datetime = make_aware(datetime(day=26, month=7, year=3000, hour=1, minute=0)) # future time s = ClockedSchedule(clocked_time=due_datetime) dt = datetime(day=25, 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) due_datetime = make_aware(datetime.now()) s = ClockedSchedule(clocked_time=due_datetime) dt2_lastrun = make_aware(datetime.now()) assert s.schedule is not None isdue2, nextcheck2 = s.schedule.is_due(dt2_lastrun) assert isdue2 is True # True means task is due and should run. assert (nextcheck2 == NEVER_CHECK_TIMEOUT) and (isdue2 is True) @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() interval_schedule = self.create_interval_schedule() entry_name, entry = self.create_conf_entry() self.app.conf.beat_schedule = {entry_name: entry} self.m1 = PeriodicTask(name=entry_name, interval=interval_schedule) 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, interval=interval_schedule) 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 # don't hang if broker is down # https://github.com/celery/celery/issues/4627 @pytest.mark.timeout(5) 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' # don't hang if broker is down # https://github.com/celery/celery/issues/4627 @pytest.mark.timeout(5) 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-2.2.1/tox.ini000066400000000000000000000073161406752007300164760ustar00rootroot00000000000000[tox] envlist = py36-django{22,30,31,32} py37-django{22,30,31,32} py38-django{22,30,31,32} pypy3-django{22,30,31,32} flake8 flakeplus apicheck linkcheck pydocstyle cov [travis:env] DJANGO = 2.2: django22 3.0: django30 3.1: django31 3.2: django32 [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 django22: -r{toxinidir}/requirements/test-django22.txt django30: -r{toxinidir}/requirements/test-django30.txt django31: -r{toxinidir}/requirements/test-django31.txt django32: -r{toxinidir}/requirements/test-django32.txt linkcheck,apicheck: -r{toxinidir}/requirements/docs.txt flake8,flakeplus,pydocstyle: -r{toxinidir}/requirements/pkgutils.txt sitepackages = False recreate = False commands = pip list py.test -xv [testenv:upgradebeat130] basepython = python3.6 setenv = GIT_TAG = v1.3.0 whitelist_externals = * commands = # save current hash so we can come back to it bash -c "git rev-parse HEAD > commit.hash" # first install our starting version git fetch --tags git checkout {env:GIT_TAG} # must use older versions for starting with older celery-beat pip install "django>=1.11.17,<2.0" pip install "django-timezone-field<4.0" pip install "celery<5.0.0" pip list # run the migration for the older version python manage.py migrate django_celery_beat # now return to previous hash and ensure all migrations continue to work bash -c "cat commit.hash | git checkout -" pip install "django>=3.0,<4.0" pip install "django-timezone-field>=4.0,<5.0" # now make sure migrations still work backward and forward python manage.py migrate django_celery_beat python manage.py migrate django_celery_beat 0001 python manage.py migrate django_celery_beat [testenv:upgradebeat140] basepython = python3.6 whitelist_externals = * setenv = GIT_TAG = v1.4.0 commands = # save current hash so we can come back to it bash -c "git rev-parse HEAD > commit.hash" # first install our starting version git fetch --tags git checkout {env:GIT_TAG} # must use older versions for starting with older celery-beat pip install "django>=1.11.17,<2.0" pip install "django-timezone-field<4.0" pip install "celery<5.0.0" pip list # run the migration for the older version python manage.py migrate django_celery_beat # now return to previous hash and ensure all migrations continue to work bash -c "cat commit.hash | git checkout -" pip install "django>=3.0,<4.0" pip install "django-timezone-field>=4.0,<5.0" # now make sure migrations still work backward and forward python manage.py migrate django_celery_beat python manage.py migrate django_celery_beat 0001 python manage.py migrate django_celery_beat [testenv:apicheck] basepython = python3.6 commands = sphinx-build -W -b apicheck -d {envtmpdir}/doctrees docs docs/_build/apicheck [testenv:linkcheck] basepython = python3.6 commands = sphinx-build -W -b linkcheck -d {envtmpdir}/doctrees docs docs/_build/linkcheck [testenv:flake8] basepython = python3.8 commands = python -m flake8 {toxinidir}/django_celery_beat {toxinidir}/t [testenv:pydocstyle] basepython = python3.8 commands = pydocstyle {toxinidir}/django_celery_beat [testenv:cov] basepython = python3.8 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 pip install Django==3.0 py.test -x --cov=django_celery_beat --cov-report=xml --no-cov-on-fail