├── .bumpversion.cfg ├── .cookiecutterrc ├── .coveragerc ├── .editorconfig ├── .env.sample ├── .github ├── dependabot.yml └── workflows │ ├── build.yml │ ├── codeql-analysis.yml │ └── test.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── AUTHORS ├── Changelog ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── django_celery_beat ├── __init__.py ├── admin.py ├── apps.py ├── clockedschedule.py ├── locale │ ├── de │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── es │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── fa │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── fr │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── ko │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── ru │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ └── zh_Hans │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20161118_0346.py │ ├── 0003_auto_20161209_0049.py │ ├── 0004_auto_20170221_0000.py │ ├── 0005_add_solarschedule_events_choices.py │ ├── 0006_auto_20180210_1226.py │ ├── 0006_auto_20180322_0932.py │ ├── 0006_periodictask_priority.py │ ├── 0007_auto_20180521_0826.py │ ├── 0008_auto_20180914_1922.py │ ├── 0009_periodictask_headers.py │ ├── 0010_auto_20190429_0326.py │ ├── 0011_auto_20190508_0153.py │ ├── 0012_periodictask_expire_seconds.py │ ├── 0013_auto_20200609_0727.py │ ├── 0014_remove_clockedschedule_enabled.py │ ├── 0015_edit_solarschedule_events_choices.py │ ├── 0016_alter_crontabschedule_timezone.py │ ├── 0017_alter_crontabschedule_month_of_year.py │ ├── 0018_improve_crontab_helptext.py │ ├── 0019_alter_periodictasks_options.py │ └── __init__.py ├── models.py ├── querysets.py ├── schedulers.py ├── signals.py ├── templates │ └── admin │ │ └── djcelery │ │ ├── change_list.html │ │ └── change_periodictask_form.html ├── tzcrontab.py ├── utils.py └── validators.py ├── docker-compose.yml ├── docker ├── base │ ├── Dockerfile │ └── celery.py ├── celery-beat │ ├── Dockerfile │ └── entrypoint.sh └── django │ ├── Dockerfile │ └── entrypoint.sh ├── docs ├── Makefile ├── _static │ └── .keep ├── _templates │ └── .keep ├── changelog.rst ├── conf.py ├── copyright.rst ├── glossary.rst ├── images │ ├── favicon.ico │ └── logo.png ├── includes │ ├── installation.txt │ └── introduction.txt ├── index.rst ├── make.bat ├── reference │ ├── django-celery-beat.admin.rst │ ├── django-celery-beat.clockedschedule.rst │ ├── django-celery-beat.models.rst │ ├── django-celery-beat.querysets.rst │ ├── django-celery-beat.rst │ ├── django-celery-beat.schedulers.rst │ ├── django-celery-beat.signals.rst │ ├── django-celery-beat.tzcrontab.rst │ ├── django-celery-beat.utils.rst │ ├── django-celery-beat.validators.rst │ └── index.rst └── templates │ └── readme.txt ├── issue_template.md ├── manage.py ├── pyproject.toml ├── requirements ├── default.txt ├── docs.txt ├── pkgutils.txt ├── runtime.txt ├── test-ci.txt ├── test-django.txt └── test.txt ├── setup.cfg ├── setup.py ├── t ├── .coveragerc ├── __init__.py ├── proj │ ├── __init__.py │ ├── celery.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py └── unit │ ├── __init__.py │ ├── conftest.py │ ├── test_admin.py │ ├── test_crontabs.py │ ├── test_models.py │ ├── test_schedulers.py │ └── test_utils.py └── tox.ini /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 2.8.1 3 | commit = True 4 | tag = True 5 | parse = (?P\d+)\.(?P\d+)\.(?P\d+)(?P[a-z]+)? 6 | serialize = 7 | {major}.{minor}.{patch}{releaselevel} 8 | {major}.{minor}.{patch} 9 | 10 | [bumpversion:file:django_celery_beat/__init__.py] 11 | 12 | [bumpversion:file:docs/includes/introduction.txt] 13 | 14 | [bumpversion:file:README.rst] 15 | -------------------------------------------------------------------------------- /.cookiecutterrc: -------------------------------------------------------------------------------- 1 | # This file exists so you can easily regenerate your project. 2 | # 3 | # `cookiepatcher` is a convenient shim around `cookiecutter` 4 | # for regenerating projects (it will generate a .cookiecutterrc 5 | # automatically for any template). To use it: 6 | # 7 | # pip install cookiepatcher 8 | # cookiepatcher gh:ionelmc/cookiecutter-pylibrary project-path 9 | # 10 | # See: 11 | # https://pypi.python.org/pypi/cookiecutter 12 | # 13 | # Alternatively, you can run: 14 | # 15 | # cookiecutter --overwrite-if-exists --config-file=project-path/.cookiecutterrc gh:ionelmc/cookiecutter-pylibrary 16 | 17 | default_context: 18 | 19 | email: 'ask@celeryproject.org' 20 | full_name: 'Ask Solem' 21 | github_username: 'celery' 22 | project_name: 'django-celery-beat' 23 | project_short_description: 'Database-backed Periodic Tasks' 24 | project_slug: 'django-celery-beat' 25 | version: '1.0.0' 26 | year: '2016' 27 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = 1 3 | cover_pylib = 0 4 | include = *django_celery_beat/* 5 | omit = django_celery_beat.tests.* 6 | 7 | [report] 8 | omit = 9 | */python?.?/* 10 | */site-packages/* 11 | */pypy/* 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [Makefile] 14 | indent_style = tab 15 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | # CELERY 2 | # ----------------------------------------------------------------------------- 3 | # CELERY_BROKER_URL=amqp://guest:guest@rabbitmq:5672/ 4 | 5 | # DJANGO 6 | # ----------------------------------------------------------------------------- 7 | DJANGO_PORT=58000 8 | DJANGO_HOST=127.0.0.1 9 | 10 | # RABBITMQ 11 | # ----------------------------------------------------------------------------- 12 | # RABBITMQ_HOST= 13 | # RABBITMQ_PASSWORD= 14 | RABBITMQ_PORT=5672 15 | # RABBITMQ_USER= 16 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "pip" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build & Publish to PyPI 2 | 3 | on: 4 | workflow_dispatch: 5 | # Ensure the build works on main 6 | push: 7 | branches: [main] 8 | tags: ['*'] 9 | # Ensure the build works on each pull request 10 | pull_request: 11 | 12 | jobs: 13 | build: 14 | name: Build distribution 15 | runs-on: blacksmith-4vcpu-ubuntu-2204 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: Set up Python 21 | uses: useblacksmith/setup-python@v6 22 | with: 23 | python-version: "3.x" 24 | 25 | - name: Install pypa/build 26 | run: python3 -m pip install build --user 27 | 28 | - name: Build a binary wheel and a source tarball 29 | run: python3 -m build 30 | 31 | - name: Store the distribution packages 32 | uses: actions/upload-artifact@v4 33 | with: 34 | name: python-package-distributions 35 | path: dist/ 36 | 37 | publish-to-pypi: 38 | name: Publish to PyPI 39 | runs-on: blacksmith-4vcpu-ubuntu-2204 40 | 41 | needs: [build] 42 | 43 | if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes 44 | 45 | environment: 46 | name: pypi 47 | url: https://pypi.org/p/django-celery-beat 48 | 49 | permissions: 50 | id-token: write # IMPORTANT: mandatory for trusted publishing 51 | 52 | steps: 53 | - name: Download all the dists 54 | uses: actions/download-artifact@v4 55 | with: 56 | name: python-package-distributions 57 | path: dist/ 58 | 59 | - name: Publish distribution 📦 to PyPI 60 | uses: pypa/gh-action-pypi-publish@release/v1 61 | 62 | github-release: 63 | name: Upload them to GitHub Release 64 | runs-on: blacksmith-4vcpu-ubuntu-2204 65 | 66 | needs: [publish-to-pypi] 67 | 68 | permissions: 69 | contents: write # IMPORTANT: mandatory for making GitHub Releases 70 | 71 | steps: 72 | - name: Download all the dists 73 | uses: actions/download-artifact@v4 74 | with: 75 | name: python-package-distributions 76 | path: dist/ 77 | 78 | - name: Create GitHub Release 79 | env: 80 | GITHUB_TOKEN: ${{ github.token }} 81 | run: >- 82 | gh release create 83 | '${{ github.ref_name }}' 84 | --repo '${{ github.repository }}' 85 | --notes "" 86 | 87 | - name: Upload artifact to GitHub Release 88 | env: 89 | GITHUB_TOKEN: ${{ github.token }} 90 | run: >- 91 | gh release upload 92 | '${{ github.ref_name }}' dist/** 93 | --repo '${{ github.repository }}' 94 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | 21 | jobs: 22 | analyze: 23 | name: Analyze 24 | runs-on: blacksmith-4vcpu-ubuntu-2204 25 | permissions: 26 | actions: read 27 | contents: read 28 | security-events: write 29 | 30 | strategy: 31 | fail-fast: false 32 | matrix: 33 | language: [ 'python' ] 34 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 35 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 36 | 37 | steps: 38 | - name: Checkout repository 39 | uses: actions/checkout@v4 40 | 41 | # Initializes the CodeQL tools for scanning. 42 | - name: Initialize CodeQL 43 | uses: github/codeql-action/init@v3 44 | with: 45 | languages: ${{ matrix.language }} 46 | # If you wish to specify custom queries, you can do so here or in a config file. 47 | # By default, queries listed here will override any specified in a config file. 48 | # Prefix the list here with "+" to use these queries and those in the config file. 49 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 50 | 51 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 52 | # If this step fails, then you should remove it and run the build manually (see below) 53 | - name: Autobuild 54 | uses: github/codeql-action/autobuild@v3 55 | 56 | # ℹ️ Command-line programs to run using the OS shell. 57 | # 📚 https://git.io/JvXDl 58 | 59 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 60 | # and modify them (or add more) to build your code if your project 61 | # uses a compiled language 62 | 63 | #- run: | 64 | # make bootstrap 65 | # make release 66 | 67 | - name: Perform CodeQL Analysis 68 | uses: github/codeql-action/analyze@v3 69 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: blacksmith-4vcpu-ubuntu-2204 12 | strategy: 13 | fail-fast: false 14 | matrix: # https://docs.djangoproject.com/en/stable/faq/install/#what-python-version-can-i-use-with-django 15 | django-version: ["3.2", "4.2", "5.1", "5.2"] 16 | python-version: ['3.8','3.9', '3.10', '3.11', '3.12', '3.13', 'pypy-3.10'] 17 | exclude: 18 | - django-version: "3.2" 19 | python-version: "3.11" 20 | - django-version: "3.2" 21 | python-version: "3.12" 22 | - django-version: "3.2" 23 | python-version: "3.13" 24 | - django-version: "4.2" 25 | python-version: "3.13" 26 | - django-version: "5.1" 27 | python-version: "3.9" 28 | - django-version: "5.2" 29 | python-version: "3.9" 30 | - django-version: "5.1" 31 | python-version: "3.8" 32 | - django-version: "5.2" 33 | python-version: "3.8" 34 | 35 | services: 36 | rabbitmq: 37 | image: rabbitmq 38 | ports: 39 | - "5672:5672" 40 | 41 | steps: 42 | - uses: actions/checkout@v4 43 | - name: Set up Python ${{ matrix.python-version }} 44 | uses: useblacksmith/setup-python@v6 45 | with: 46 | python-version: ${{ matrix.python-version }} 47 | - name: Install dependencies 48 | run: | 49 | python -m pip install --upgrade pip 50 | python -m pip install tox tox-gh-actions 51 | - name: Test with tox 52 | run: | 53 | if [ "${{ matrix.python-version }}" != "pypy-3.10" ]; then 54 | tox -- --cov=django_celery_beat --cov-report=xml --no-cov-on-fail --cov-report term 55 | else 56 | tox 57 | fi 58 | env: 59 | DJANGO: ${{ matrix.django-version }} 60 | - name: Upload coverage reports to Codecov 61 | if: ${{ matrix.python-version != 'pypy-3.10' }} 62 | uses: codecov/codecov-action@v5 63 | with: 64 | fail_ci_if_error: true # optional (default = false) 65 | token: ${{ secrets.CODECOV_TOKEN }} 66 | verbose: true # optional (default = false) 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | *$py.class 4 | *~ 5 | .*.sw[pon] 6 | dist/ 7 | *.egg-info 8 | *.egg 9 | *.egg/ 10 | build/ 11 | .build/ 12 | _build/ 13 | pip-log.txt 14 | .directory 15 | erl_crash.dump 16 | *.db 17 | Documentation/ 18 | .tox/ 19 | .ropeproject/ 20 | .project 21 | .pydevproject 22 | .idea/ 23 | .vscode/ 24 | .coverage 25 | celery/tests/cover/ 26 | .ve* 27 | cover/ 28 | .vagrant/ 29 | *.sqlite3 30 | .cache/ 31 | htmlcov/ 32 | coverage.xml 33 | .eggs/ 34 | .python-version 35 | venv 36 | .env 37 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | exclude: "migrations" 3 | 4 | repos: 5 | - repo: https://github.com/asottile/pyupgrade 6 | rev: v3.20.0 7 | hooks: 8 | - id: pyupgrade 9 | args: ["--py37-plus"] 10 | 11 | - repo: https://github.com/PyCQA/flake8 12 | rev: 7.2.0 13 | hooks: 14 | - id: flake8 15 | 16 | - repo: https://github.com/asottile/yesqa 17 | rev: v1.5.0 18 | hooks: 19 | - id: yesqa 20 | 21 | - repo: https://github.com/pre-commit/pre-commit-hooks 22 | rev: v5.0.0 23 | hooks: 24 | - id: check-merge-conflict 25 | - id: check-toml 26 | - id: check-yaml 27 | - id: mixed-line-ending 28 | 29 | - repo: https://github.com/pycqa/isort 30 | rev: 6.0.1 31 | hooks: 32 | - id: isort 33 | 34 | - repo: https://github.com/adamchainz/django-upgrade 35 | rev: 1.25.0 36 | hooks: 37 | - id: django-upgrade 38 | args: [--target-version, "3.2"] 39 | 40 | - repo: https://github.com/astral-sh/ruff-pre-commit 41 | rev: v0.11.12 42 | hooks: # Format before linting 43 | # - id: ruff-format 44 | - id: ruff 45 | 46 | - repo: https://github.com/tox-dev/pyproject-fmt 47 | rev: v2.6.0 48 | hooks: 49 | - id: pyproject-fmt 50 | 51 | - repo: https://github.com/abravalheri/validate-pyproject 52 | rev: v0.24.1 53 | hooks: 54 | - id: validate-pyproject 55 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | build: 3 | os: ubuntu-20.04 4 | tools: 5 | python: "3.8" 6 | sphinx: 7 | configuration: docs/conf.py 8 | python: 9 | install: 10 | - requirements: requirements/docs.txt 11 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | ========= 2 | AUTHORS 3 | ========= 4 | :order: sorted 5 | 6 | Aaron Ross 7 | Adam Endicott 8 | Alex Stapleton 9 | Alexandr Artemyev 10 | Alvaro Vega 11 | Andrew Frankel 12 | Andrew Watts 13 | Andrii Kostenko 14 | Anton Novosyolov 15 | Ask Solem 16 | Asif Saif Uddin 17 | Augusto Becciu 18 | Ben Firshman 19 | Brad Jasper 20 | Brett Gibson 21 | Brian Rosner 22 | Charlie DeTar 23 | Christopher Grebs 24 | Dan LaMotte 25 | Darjus Loktevic 26 | David Fischer 27 | David Ziegler 28 | Diego Andres Sanabria Martin 29 | Dmitriy Krasilnikov 30 | Donald Stufft 31 | Eldon Stegall 32 | Eugene Nagornyi 33 | Felix Berger 35 | Glenn Washburn 36 | Gnrhxni 37 | Greg Taylor 38 | Grégoire Cachet 39 | Hari 40 | Idan Zalzberg 41 | Ionel Maries Cristian 42 | Jaeyoung Heo 43 | Jannis Leidel 44 | Jason Baker 45 | Jay States 46 | Jeff Balogh 47 | Jeff Fischer 48 | Jeffrey Hu 49 | Jens Alm 50 | Jerzy Kozera 51 | Jesper Noehr 52 | Jimmy Bradshaw 53 | Joey Wilhelm 54 | John Andrews 55 | John Watson 56 | Jonas Haag 57 | Jonatan Heyman 58 | Josh Drake 59 | José Moreira 60 | Jude Nagurney 61 | Justin Quick 62 | Keith Perkins 63 | Kirill Panshin 64 | Mark Hellewell 65 | Mark Heppner 66 | Mark Lavin 67 | Mark Stover 68 | Maxim Bodyansky 69 | Michael Elsdoerfer 70 | Michael van Tellingen 71 | Mikhail Korobov 72 | Olivier Tabone 73 | Patrick Altman 74 | Piotr Bulinski 75 | Piotr Sikora 76 | Reza Lotun 77 | Rockallite Wulf 78 | Roger Barnes 79 | Roman Imankulov 80 | Rune Halvorsen 81 | Sam Cooke 82 | Scott Rubin 83 | Sean Creeley 84 | Serj Zavadsky 85 | Simon Charette 86 | Spencer Ellinor 87 | Theo Spears 88 | Timo Sugliani 89 | Vincent Driessen 90 | Vitaly Babiy 91 | Vladislav Poluhin 92 | Weipin Xia 93 | Wes Turner 94 | Wes Winham 95 | Williams Mendez 96 | WoLpH 97 | dongweiming 98 | -------------------------------------------------------------------------------- /Changelog: -------------------------------------------------------------------------------- 1 | .. _changelog: 2 | 3 | ================ 4 | Change history 5 | ================ 6 | 7 | Next 8 | ==== 9 | 10 | .. _version-2.8.1: 11 | 12 | 2.8.1 13 | ===== 14 | :release-date: 2025-05-13 15 | :release-by: Asif Saif Uddin (@auvipy) 16 | 17 | - Fixed regression by big code refactoring. 18 | 19 | 20 | .. _version-2.8.0: 21 | 22 | 2.8.0 23 | ===== 24 | :release-date: 2025-04-16 25 | :release-by: Asif Saif Uddin (@auvipy) 26 | 27 | - Add official support for Django 5.2. 28 | - Issue 796: remove days of the week from human readable description when the whole week is specified. 29 | - fix 'exipres', 'expire_seconds' not working normal as expected. 30 | - fix long period task will never be triggered (#717). 31 | - Fix for missing periodic task name in results (#812). 32 | - refactor(db-scheduler): optimize all_as_schedule query (#835). 33 | - feat(admin): add task field to search_fields. 34 | - Fix the time calculation problem caused by start_time (#844). 35 | - Added Python 3.13 support. 36 | 37 | 38 | .. _version-2.7.0: 39 | 40 | 2.7.0 41 | ===== 42 | :release-date: 2024-08-21 43 | :release-by: Christian Clauss (@cclauss) 44 | 45 | Added 46 | ~~~~~ 47 | 48 | - Add official support for Django 5.1 (:github_pr:`761`) 49 | - Relax Django upper version to allow version 5.1 (:github_pr:`756`) 50 | - Add ``PeriodicTaskInline`` to the schedule admin to showing tasks using the schedule (:github_pr:`743`) 51 | 52 | Fixed 53 | ~~~~~ 54 | 55 | - Fix display of long name day of week in admin (:github_pr:`745`) 56 | - Fix a few French translations (:github_pr:`760`) 57 | - Fix documentation about timezone changes (:github_pr:`769`) 58 | - Remove usages of deprecated setuptools API ``setuptools.command.test`` (:github_pr:`771`) 59 | - Clean up running of ``setup.py`` as it's deprecated (:github_pr:`737`) 60 | 61 | Project infrastructure 62 | ~~~~~~~~~~~~~~~~~~~~~~ 63 | 64 | - Automate PyPI release from GitHub actions using trusted publisher (:github_pr:`749`) 65 | - Simplify logic for minimum Python requirement (:github_pr:`765`) 66 | - Add Ruff to pre-commit config (:github_pr:`778`) 67 | 68 | .. _version-2.6.0: 69 | 70 | 2.6.0 71 | ===== 72 | :release-date: 2024-03-03 73 | :release-by: Christian Clauss (@cclauss) 74 | 75 | - Avoid crash when can not get human readable description (#648). 76 | - Update codeql-analysis.yml (#653). 77 | - Fix CI: Change assert self.app.timezone.zone to assert self.app.timezone.key (#664). 78 | - Drop Django 4.0 from CI to avoid security issues (#662). 79 | - Fix Issue #388: Celery Beat scheduled tasks may be executed repeatedly (#660). 80 | - Update README.rst (#670). 81 | - Update runtime.txt to include Django 5.0 (#681). 82 | - Replace case.patching fixture with mockeypatch + MagicMock (#692). 83 | - Update README.rst - Crontab effect description (#689). 84 | - Update supported Python & Django version in setup.py (#672). 85 | - Add Python 3.12 to test matrix and add classifier (#690). 86 | - Django v5.0: django.utils.timezone.utc alias --> datetime.timezone.utc (#703). 87 | - Upgrade GitHub Actions and PyPy 3.10 and Django 5.0 (#699). 88 | - Testing Django v5.0 on pypy-3.10-v7.3.14 passes (#705). 89 | - Prepare for release v2.6.0 to support Py3.12 and Dj5.0 (#712). 90 | - GitHub Actions: Do not hardcode an out-of-date version of PyPy (#715). 91 | - Use the same order in the admin as in the cron schedule expression (#716). 92 | - Upgrade pip and GitHub Actions dependencies with dependabot (#721). 93 | - Bump github/codeql-action from 2 to 3 (#722). 94 | - Bump actions/checkout from 3 to 4 (#723). 95 | - Update pytest requirement from <8.0,>=6.2.5 to >=6.2.5,<9.0 (#724). 96 | - Remove requirements/test-djangoXY.txt (#728). 97 | - Remove code for unsupported django.VERSION < (3, 2) (#729). 98 | - Added sphinxcontrib-django to extensions (#736). 99 | 100 | 101 | .. _version-2.5.0: 102 | 103 | 2.5.0 104 | ===== 105 | :release-date: 2023-03-14 4:00 p.m. UTC+6:00 106 | :release-by: Asif Saif Uddin 107 | 108 | - Prefetch_related on PeriodicTaskQuerySet.enabled (#608). 109 | - Clarify month range (#615). 110 | - Declare support for Django 4.2 & Python 3.11. 111 | - Adding human readable descriptions of crontab schedules (#622). 112 | - Start time heap block fix (#636). 113 | 114 | 115 | .. _version-2.4.0: 116 | 117 | 2.4.0 118 | ===== 119 | :release-date: 2022-10-19 7:15 p.m. UTC+6:00 120 | :release-by: Asif Saif Uddin 121 | 122 | - Fixed error path for zh-Hans translate (#548). 123 | - Django>=3.2,<4.2 (#567). 124 | - fix: downgrade importlib-metadata<5.0 until celery 5.3.0 release. 125 | - Fixed signals can not connect to OneToOneField (#572) (#573). 126 | - Remove superseded ExtendedQuerySet as it's functionality is built in. 127 | - Wrapped fieldset labels of PeriodicTaskAdmin around gettext_lazy. 128 | - fix: update PeriodicTask from entry (#344). 129 | 130 | 131 | .. _version-2.3.0: 132 | 133 | 2.3.0 134 | ===== 135 | :release-date: 136 | :release-by: 137 | 138 | - Admin "disable_tasks" action also updates PeriodicTask's last_run_at field 139 | - feat: add periodic_task_name in favor of celery/django-celery-results 140 | - Fix ClockedSchedule and PeriodicTasks showing UTC time when Time Zone 141 | - Change last_run_at=None when using disable tasks admin action (#501) 142 | - fix the conflict with celery configuration (#525) 143 | - A unit Test to make sure ClockedSchedule and PeriodicTasks are shown 144 | - Django 4.0 and Python 3.10 support (#528) 145 | 146 | 147 | .. _version-2.2.1: 148 | 149 | 2.2.1 150 | ===== 151 | :release-date: 2021-07-02 11:15 a.m. UTC+6:00 152 | :release-by: Asif Saif Uddin 153 | 154 | - Require celery>=5.0,<6.0 155 | - Enable Django 3.2 CI and add default_auto_field 156 | - Fix locale in dir tree 157 | - Do not blindly delete duplicate schedules (#389) 158 | - Use `python:3.8-slim` for lighter builds 159 | 160 | .. _version-2.2.0: 161 | 162 | 2.2.0 163 | ===== 164 | :release-date: 2021-01-19 2:30 p.m. UTC+6:00 165 | :release-by: Asif Saif Uddin 166 | 167 | - Fixed compatibility with django-timezone-field>=4.1.0 168 | - Fixed deprecation warnings: 'assertEquals' in tests. 169 | - Fixed SolarSchedule event choices i18n support. 170 | - Updated 'es' .po file metadata 171 | - Update 'fr' .po file metadata 172 | - New schema migrations for SolarSchedule events choices changes in models. 173 | 174 | .. _version-2.1.0: 175 | 176 | 2.1.0 177 | ===== 178 | :release-date: 2020-10-20 179 | :release-by: Asif Saif Uddin 180 | 181 | - Fix string representation of CrontabSchedule, so it matches UNIX CRON expression format (#318) 182 | - If no schedule is selected in PeriodicTask form, raise a non-field error instead of an error bounded to the `interval` field (#327) 183 | - Fix some Spanish translations (#339) 184 | - Log "Writing entries..." message as DEBUG instead of INFO (#342) 185 | - Use CELERY_TIMEZONE setting as `CrontabSchedule.timezone` default instead of UTC (#346) 186 | - Fix bug in ClockedSchedule that made the schedule stuck after a clocked task was executed. The `enabled` field of ClockedSchedule has been dropped (#341) 187 | - Drop support for Python < 3.6 (#368) 188 | - Add support for Celery 5 and Django 3.1 (#368) 189 | 190 | .. _version-2.0.0: 191 | 192 | 2.0.0 193 | ===== 194 | :release-date: 2020-03-18 195 | :release-by: Asif Saif Uddin 196 | 197 | - Added support for Django 3.0 198 | - Dropped support for Django < 2.2 and Python < 3.5 199 | 200 | .. _version-1.6.0: 201 | 202 | 1.6.0 203 | ===== 204 | :release-date: 2020-02-01 4:30 p.m. UTC+6:00 205 | :release-by: Asif Saif Uddin 206 | 207 | - Fixed invalid long_description (#255) 208 | - Exposed read-only field PeriodicTask.last_run_at in Django admin (#257) 209 | - Added docker config to ease development (#260, #261, #264, #288) 210 | - Added validation schedule validation on save (#271) 211 | - Added French translation (#286) 212 | - Fixed case where last_run_at = None and CELERY_TIMEZONE != TIME_ZONE (#294) 213 | 214 | .. _version-1.5.0: 215 | 216 | 1.5.0 217 | ===== 218 | :release-date: 2019-05-21 17:00 p.m. UTC+6:00 219 | :release-by: Asif Saif Uddin 220 | 221 | - Fixed delay returned when a task has a start_time in the future. (#208) 222 | - PeriodicTaskAdmin: Declare some filtering, for usability (#215) 223 | - fix _default_now is_aware bug (#216) 224 | - Adds support for message headers for periodic tasks (#98) 225 | - make last_run_at tz aware before passing to celery (#233) 226 | 227 | .. _version-1.4.0: 228 | 229 | 1.4.0 230 | ===== 231 | :release-date: 2018-12-09 1:30 p.m. UTC+2:00 232 | :release-by: Omer Katz 233 | 234 | - Fix migrations dependencies. 235 | - Added the DJANGO_CELERY_BEAT_TZ_AWARE setting. 236 | 237 | .. _version-1.3.0: 238 | 239 | 1.3.0 240 | ===== 241 | :release-date: 2018-11-12 17:30 p.m. UTC+2:00 242 | :release-by: Omer Katz 243 | 244 | - Fix transaction handling while syncing the schedule. 245 | - Fix schedule type validation logic. 246 | - Scheduler no longer forgets the tasks after first schedule change. 247 | - Fix race condition for schedule_changed() resulting in erroneously closed connections. 248 | - Add support for task priorities when using RabbitMQ or Redis as broker. 249 | - Disabled tasks are now correctly deleted from the schedule. 250 | - Added name as search filter. 251 | 252 | .. _version-1.2.0: 253 | 254 | 1.2.0 255 | ===== 256 | :release-date: 2018-10-08 16:00 p.m. UTC+3:00 257 | :release-by: Omer Katz 258 | 259 | - Allow timezone-aware Cron schedules. 260 | - Retry later in case of InterfaceError in sync. 261 | - Show Periodic Task Description in panel admin. 262 | - Fix CrontabSchedule example. 263 | - Support Periodic Tasks with a start date and one-off tasks. 264 | - Fixes a problem with beat not reconnecting to MySQL (server restart, network problem, etc.) when checking if schedule has changed. 265 | - Add toggle admin action which allows to activate disabled tasks or deactivate enabled tasks. 266 | - Add fields validation for CrontabSchedule. 267 | - Drop support for Django<1.11. 268 | - Fix task heap invalidation bug which prevented scheduled tasks from running when syncing tasks from the database. 269 | - Raise a ValidationError when more than one type (solar, crontab or interval) of schedule is provided. 270 | 271 | .. _version-1.1.1: 272 | 273 | 1.1.1 274 | ===== 275 | :release-date: 2018-2-18 2:30 p.m. UTC+3:00 276 | :release-by: Omer Katz 277 | 278 | - Fix interval schedules by providing nowfun. 279 | - Removing code that forced last_run_at to be timezone naive for no reason, made timezone aware. Fixes crontab schedules after celery/celery#4173. 280 | - Entry.last_run_at is no-longer timezone naive. 281 | - Use a localized PyTZ timezone object for now() otherwise conversions fail scheduling breaks resulting in constant running of tasks or possibly not running ever. 282 | - Fix endless migrations creation for solar schedules events. 283 | - Prevent MySQL has gone away errors. 284 | - Added support for Django 2.0. 285 | - Adjust CrontabSchedule's minutes, hour & day_of_month fields max length 286 | 287 | .. _version-1.1.0: 288 | 289 | 1.1.0 290 | ===== 291 | :release-date: 2017-10-31 2:30 p.m. UTC+3:00 292 | :release-by: Omer Katz 293 | 294 | - Adds default_app_config (Issue celery/celery#3567) 295 | - Adds "run now" admin action for tasks. 296 | - Adds admin actions to toggle tasks. 297 | - Add solar schedules (Issue #8) 298 | - Notify beat of changes when Interval/Crontab models change. (Issue celery/celery#3683) 299 | - Fix PeriodicTask.enable sync issues 300 | - Notify beat of changes when Solar model changes. 301 | - Resolve CSS class conflict with django-adminlte2 package. 302 | - We now support Django 1.11 303 | - Deletes are now performed cascadingly. 304 | - Return schedule for solar periodic tasks so that Celery Beat does not crash when one is scheduled. 305 | - Adding nowfun to solar and crontab schedulers so that the Django timezone is used. 306 | 307 | .. _version-1.0.1: 308 | 309 | 1.0.1 310 | ===== 311 | :release-date: 2016-11-07 02:28 p.m. PST 312 | :release-by: Ask Solem 313 | 314 | - Now depends on Celery 4.0.0. 315 | - Migration modules were not included in the distribution. 316 | - Adds documentation: http://django-celery-beat.readthedocs.io/ 317 | 318 | .. _version-1.0.0: 319 | 320 | 1.0.0 321 | ===== 322 | :release-date: 2016-09-08 03:19 p.m. PDT 323 | :release-by: Ask Solem 324 | 325 | - Initial release 326 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2016 Ask Solem. All Rights Reserved. 2 | Copyright (c) 2012-2014 GoPivotal, Inc. All Rights Reserved. 3 | Copyright (c) 2009-2012 Ask Solem. All Rights Reserved. 4 | 5 | django-celery-beat is licensed under The BSD License (3 Clause, also known as 6 | the new BSD license). The license is an OSI approved Open Source 7 | license and is GPL-compatible(1). 8 | 9 | The license text can also be found here: 10 | http://www.opensource.org/licenses/BSD-3-Clause 11 | 12 | License 13 | ======= 14 | 15 | Redistribution and use in source and binary forms, with or without 16 | modification, are permitted provided that the following conditions are met: 17 | * Redistributions of source code must retain the above copyright 18 | notice, this list of conditions and the following disclaimer. 19 | * Redistributions in binary form must reproduce the above copyright 20 | notice, this list of conditions and the following disclaimer in the 21 | documentation and/or other materials provided with the distribution. 22 | * Neither the name of Ask Solem nor the 23 | names of its contributors may be used to endorse or promote products 24 | derived from this software without specific prior written permission. 25 | 26 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 27 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 28 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 29 | PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Ask Solem OR CONTRIBUTORS 30 | BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 31 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 32 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 33 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 34 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 35 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 36 | POSSIBILITY OF SUCH DAMAGE. 37 | 38 | Documentation License 39 | ===================== 40 | 41 | The documentation portion of django-celery-beat (the rendered contents of the 42 | "docs" directory of a software distribution or checkout) is supplied 43 | under the "Creative Commons Attribution-ShareAlike 4.0 44 | International" (CC BY-SA 4.0) License as described by 45 | http://creativecommons.org/licenses/by-sa/4.0/ 46 | 47 | Footnotes 48 | ========= 49 | (1) A GPL-compatible license makes it possible to 50 | combine django-celery-beat with other software that is released 51 | under the GPL, it does not mean that we're distributing 52 | django-celery-beat under the GPL license. The BSD license, unlike the GPL, 53 | let you distribute a modified version without making your 54 | changes open source. 55 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include Changelog 2 | include LICENSE 3 | include README.rst 4 | include MANIFEST.in 5 | include setup.cfg 6 | include setup.py 7 | include manage.py 8 | recursive-include docs * 9 | recursive-include extra/* 10 | recursive-include examples * 11 | recursive-include requirements *.txt *.rst 12 | recursive-include django_celery_beat *.py *.html *.po *.mo 13 | recursive-include t *.py 14 | 15 | recursive-exclude * __pycache__ 16 | recursive-exclude * *.py[co] 17 | recursive-exclude * .*.sw[a-z] 18 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJ=django_celery_beat 2 | PGPIDENT="Celery Security Team" 3 | PYTHON=python 4 | PYTEST=pytest 5 | GIT=git 6 | TOX=tox 7 | ICONV=iconv 8 | FLAKE8=flake8 9 | PYDOCSTYLE=pydocstyle 10 | SPHINX2RST=sphinx2rst 11 | 12 | SPHINX_DIR=docs/ 13 | SPHINX_BUILDDIR="${SPHINX_DIR}/_build" 14 | README=README.rst 15 | README_SRC="docs/templates/readme.txt" 16 | CONTRIBUTING=CONTRIBUTING.rst 17 | CONTRIBUTING_SRC="docs/contributing.rst" 18 | SPHINX_HTMLDIR="${SPHINX_BUILDDIR}/html" 19 | DOCUMENTATION=Documentation 20 | 21 | TESTDIR=t 22 | 23 | all: help 24 | 25 | help: 26 | @echo "docs - Build documentation." 27 | @echo "test-all - Run tests for all supported python versions." 28 | @echo "distcheck ---------- - Check distribution for problems." 29 | @echo " test - Run unittests using current python." 30 | @echo " lint ------------ - Check codebase for problems." 31 | @echo " apicheck - Check API reference coverage." 32 | @echo " configcheck - Check configuration reference coverage." 33 | @echo " readmecheck - Check README.rst encoding." 34 | @echo " contribcheck - Check CONTRIBUTING.rst encoding" 35 | @echo " flakes -------- - Check code for syntax and style errors." 36 | @echo " flakecheck - Run flake8 on the source code." 37 | @echo " pep257check - Run flakeplus on the source code." 38 | @echo "readme - Regenerate README.rst file." 39 | @echo "contrib - Regenerate CONTRIBUTING.rst file" 40 | @echo "clean-dist --------- - Clean all distribution build artifacts." 41 | @echo " clean-git-force - Remove all uncomitted files." 42 | @echo " clean ------------ - Non-destructive clean" 43 | @echo " clean-pyc - Remove .pyc/__pycache__ files" 44 | @echo " clean-docs - Remove documentation build artifacts." 45 | @echo " clean-build - Remove build artifacts." 46 | @echo "bump - Bump patch version number." 47 | @echo "bump-minor - Bump minor version number." 48 | @echo "bump-major - Bump major version number." 49 | @echo "release - Make PyPI release." 50 | 51 | clean: clean-docs clean-pyc clean-build 52 | 53 | clean-dist: clean clean-git-force 54 | 55 | bump: 56 | bumpversion patch 57 | 58 | bump-minor: 59 | bumpversion minor 60 | 61 | bump-major: 62 | bumpversion major 63 | 64 | release: 65 | python -m pip install --upgrade build twine 66 | python -m build 67 | twine upload --sign --identity="$(PGPIDENT) dist/*" 68 | 69 | Documentation: 70 | (cd "$(SPHINX_DIR)"; $(MAKE) html) 71 | mv "$(SPHINX_HTMLDIR)" $(DOCUMENTATION) 72 | 73 | docs: Documentation 74 | 75 | clean-docs: 76 | -rm -rf "$(SPHINX_BUILDDIR)" 77 | 78 | lint: flakecheck apicheck configcheck readmecheck 79 | 80 | apicheck: 81 | (cd "$(SPHINX_DIR)"; $(MAKE) apicheck) 82 | 83 | configcheck: 84 | true 85 | 86 | flakecheck: 87 | $(FLAKE8) "$(PROJ)" "$(TESTDIR)" 88 | 89 | flakediag: 90 | -$(MAKE) flakecheck 91 | 92 | pep257check: 93 | $(PYDOCSTYLE) "$(PROJ)" 94 | 95 | flakes: flakediag pep257check 96 | 97 | clean-readme: 98 | -rm -f $(README) 99 | 100 | readmecheck: 101 | $(ICONV) -f ascii -t ascii $(README) >/dev/null 102 | 103 | $(README): 104 | $(SPHINX2RST) "$(README_SRC)" --ascii > $@ 105 | 106 | readme: clean-readme $(README) readmecheck 107 | 108 | clean-contrib: 109 | -rm -f "$(CONTRIBUTING)" 110 | 111 | $(CONTRIBUTING): 112 | $(SPHINX2RST) "$(CONTRIBUTING_SRC)" > $@ 113 | 114 | contrib: clean-contrib $(CONTRIBUTING) 115 | 116 | clean-pyc: 117 | -find . -type f -a \( -name "*.pyc" -o -name "*$$py.class" \) | xargs rm 118 | -find . -type d -name "__pycache__" | xargs rm -r 119 | 120 | removepyc: clean-pyc 121 | 122 | clean-build: 123 | rm -rf build/ dist/ .eggs/ *.egg-info/ .tox/ .coverage cover/ 124 | 125 | clean-git: 126 | $(GIT) clean -xdn 127 | 128 | clean-git-force: 129 | $(GIT) clean -xdf 130 | 131 | test-all: clean-pyc 132 | $(TOX) 133 | 134 | test: 135 | $(PYTHON) -m $(PYTEST) 136 | 137 | cov: 138 | (cd $(TESTDIR); $(PYTEST) -x --cov="$(PROJ)" --cov-report=html) 139 | 140 | build: 141 | $(PYTHON) -m build 142 | 143 | distcheck: lint test clean 144 | 145 | dist: readme contrib clean-dist build 146 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ===================================================================== 2 | Database-backed Periodic Tasks 3 | ===================================================================== 4 | 5 | |build-status| |coverage| |license| |wheel| |pyversion| |pyimp| 6 | 7 | :Version: 2.8.1 8 | :Web: http://django-celery-beat.readthedocs.io/ 9 | :Download: http://pypi.python.org/pypi/django-celery-beat 10 | :Source: http://github.com/celery/django-celery-beat 11 | :DeepWiki: |deepwiki| 12 | :Keywords: django, celery, beat, periodic task, cron, scheduling 13 | 14 | About 15 | ===== 16 | 17 | This extension enables you to store the periodic task schedule in the 18 | database. 19 | 20 | The periodic tasks can be managed from the Django Admin interface, where you 21 | can create, edit and delete periodic tasks and how often they should run. 22 | 23 | Using the Extension 24 | =================== 25 | 26 | Usage and installation instructions for this extension are available 27 | from the `Celery documentation`_. 28 | 29 | .. _`Celery documentation`: 30 | http://docs.celeryproject.org/en/latest/userguide/periodic-tasks.html#using-custom-scheduler-classes 31 | 32 | Important Warning about Time Zones 33 | ================================== 34 | 35 | .. warning:: 36 | If you change the Django ``TIME_ZONE`` setting your periodic task schedule 37 | will still be based on the old timezone. 38 | 39 | To fix that you would have to reset the "last run time" for each periodic task: 40 | 41 | .. code-block:: Python 42 | 43 | >>> from django_celery_beat.models import PeriodicTask, PeriodicTasks 44 | >>> PeriodicTask.objects.all().update(last_run_at=None) 45 | >>> PeriodicTasks.update_changed() 46 | 47 | 48 | 49 | .. note:: 50 | This will reset the state as if the periodic tasks have never run before. 51 | 52 | 53 | Models 54 | ====== 55 | 56 | - ``django_celery_beat.models.PeriodicTask`` 57 | 58 | This model defines a single periodic task to be run. 59 | 60 | It must be associated with a schedule, which defines how often the task should 61 | run. 62 | 63 | - ``django_celery_beat.models.IntervalSchedule`` 64 | 65 | A schedule that runs at a specific interval (e.g. every 5 seconds). 66 | 67 | - ``django_celery_beat.models.CrontabSchedule`` 68 | 69 | A schedule with fields like entries in cron: 70 | ``minute hour day-of-week day_of_month month_of_year``. 71 | 72 | - ``django_celery_beat.models.PeriodicTasks`` 73 | 74 | This model is only used as an index to keep track of when the schedule has 75 | changed. 76 | 77 | Whenever you update a ``PeriodicTask`` a counter in this table is also 78 | incremented, which tells the ``celery beat`` service to reload the schedule 79 | from the database. 80 | 81 | If you update periodic tasks in bulk, you will need to update the counter 82 | manually: 83 | 84 | .. code-block:: Python 85 | 86 | >>> from django_celery_beat.models import PeriodicTasks 87 | >>> PeriodicTasks.update_changed() 88 | 89 | Example creating interval-based periodic task 90 | --------------------------------------------- 91 | 92 | To create a periodic task executing at an interval you must first 93 | create the interval object: 94 | 95 | .. code-block:: Python 96 | 97 | >>> from django_celery_beat.models import PeriodicTask, IntervalSchedule 98 | 99 | # executes every 10 seconds. 100 | >>> schedule, created = IntervalSchedule.objects.get_or_create( 101 | ... every=10, 102 | ... period=IntervalSchedule.SECONDS, 103 | ... ) 104 | 105 | That's all the fields you need: a period type and the frequency. 106 | 107 | You can choose between a specific set of periods: 108 | 109 | 110 | - ``IntervalSchedule.DAYS`` 111 | - ``IntervalSchedule.HOURS`` 112 | - ``IntervalSchedule.MINUTES`` 113 | - ``IntervalSchedule.SECONDS`` 114 | - ``IntervalSchedule.MICROSECONDS`` 115 | 116 | .. note:: 117 | If you have multiple periodic tasks executing every 10 seconds, 118 | then they should all point to the same schedule object. 119 | 120 | There's also a "choices tuple" available should you need to present this 121 | to the user: 122 | 123 | 124 | .. code-block:: Python 125 | 126 | >>> IntervalSchedule.PERIOD_CHOICES 127 | 128 | 129 | Now that we have defined the schedule object, we can create the periodic task 130 | entry: 131 | 132 | .. code-block:: Python 133 | 134 | >>> PeriodicTask.objects.create( 135 | ... interval=schedule, # we created this above. 136 | ... name='Importing contacts', # simply describes this periodic task. 137 | ... task='proj.tasks.import_contacts', # name of task. 138 | ... ) 139 | 140 | 141 | Note that this is a very basic example, you can also specify the arguments 142 | and keyword arguments used to execute the task, the ``queue`` to send it 143 | to[*], and set an expiry time. 144 | 145 | Here's an example specifying the arguments, note how JSON serialization is 146 | required: 147 | 148 | .. code-block:: Python 149 | 150 | >>> import json 151 | >>> from datetime import datetime, timedelta 152 | 153 | >>> PeriodicTask.objects.create( 154 | ... interval=schedule, # we created this above. 155 | ... name='Importing contacts', # simply describes this periodic task. 156 | ... task='proj.tasks.import_contacts', # name of task. 157 | ... args=json.dumps(['arg1', 'arg2']), 158 | ... kwargs=json.dumps({ 159 | ... 'be_careful': True, 160 | ... }), 161 | ... expires=datetime.utcnow() + timedelta(seconds=30) 162 | ... ) 163 | 164 | 165 | .. [*] you can also use low-level AMQP routing using the ``exchange`` and 166 | ``routing_key`` fields. 167 | 168 | Example creating crontab-based periodic task 169 | -------------------------------------------- 170 | 171 | A crontab schedule has the fields: ``minute``, ``hour``, ``day_of_week``, 172 | ``day_of_month`` and ``month_of_year``, so if you want the equivalent 173 | of a ``30 * * * *`` (execute 30 minutes past every hour) crontab entry you specify: 174 | 175 | .. code-block:: Python 176 | 177 | >>> from django_celery_beat.models import CrontabSchedule, PeriodicTask 178 | >>> schedule, _ = CrontabSchedule.objects.get_or_create( 179 | ... minute='30', 180 | ... hour='*', 181 | ... day_of_week='*', 182 | ... day_of_month='*', 183 | ... month_of_year='*', 184 | ... timezone=zoneinfo.ZoneInfo('Canada/Pacific') 185 | ... ) 186 | 187 | The crontab schedule is linked to a specific timezone using the 'timezone' input parameter. 188 | 189 | Then to create a periodic task using this schedule, use the same approach as 190 | the interval-based periodic task earlier in this document, but instead 191 | of ``interval=schedule``, specify ``crontab=schedule``: 192 | 193 | .. code-block:: Python 194 | 195 | >>> PeriodicTask.objects.create( 196 | ... crontab=schedule, 197 | ... name='Importing contacts', 198 | ... task='proj.tasks.import_contacts', 199 | ... ) 200 | 201 | Temporarily disable a periodic task 202 | ----------------------------------- 203 | 204 | You can use the ``enabled`` flag to temporarily disable a periodic task: 205 | 206 | .. code-block:: Python 207 | 208 | >>> periodic_task.enabled = False 209 | >>> periodic_task.save() 210 | 211 | 212 | Example running periodic tasks 213 | ----------------------------------- 214 | 215 | The periodic tasks still need 'workers' to execute them. 216 | So make sure the default **Celery** package is installed. 217 | (If not installed, please follow the installation instructions 218 | here: https://github.com/celery/celery) 219 | 220 | Both the worker and beat services need to be running at the same time. 221 | 222 | 1. Start a Celery worker service (specify your Django project name):: 223 | 224 | $ celery -A [project-name] worker --loglevel=info 225 | 226 | 227 | 2. As a separate process, start the beat service (specify the Django scheduler):: 228 | 229 | $ celery -A [project-name] beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler 230 | 231 | **OR** you can use the -S (scheduler flag), for more options see ``celery beat --help``):: 232 | 233 | $ celery -A [project-name] beat -l info -S django 234 | 235 | Also, as an alternative, you can run the two steps above (worker and beat services) 236 | with only one command (recommended for **development environment only**):: 237 | 238 | $ celery -A [project-name] worker --beat --scheduler django --loglevel=info 239 | 240 | 241 | 3. Now you can add and manage your periodic tasks from the Django Admin interface. 242 | 243 | 244 | 245 | 246 | Installation 247 | ============ 248 | 249 | You can install django-celery-beat either via the Python Package Index (PyPI) 250 | or from source. 251 | 252 | To install using ``pip``: 253 | 254 | .. code-block:: bash 255 | 256 | $ pip install --upgrade django-celery-beat 257 | 258 | Downloading and installing from source 259 | -------------------------------------- 260 | 261 | Download the latest version of django-celery-beat from 262 | http://pypi.python.org/pypi/django-celery-beat 263 | 264 | You can install it by doing the following : 265 | 266 | .. code-block:: bash 267 | 268 | $ python3 -m venv .venv 269 | $ source .venv/bin/activate 270 | $ pip install --upgrade build pip 271 | $ tar xvfz django-celery-beat-0.0.0.tar.gz 272 | $ cd django-celery-beat-0.0.0 273 | $ python -m build 274 | $ pip install --upgrade . 275 | 276 | After installation, add ``django_celery_beat`` to Django's settings module: 277 | 278 | 279 | .. code-block:: Python 280 | 281 | INSTALLED_APPS = [ 282 | ..., 283 | 'django_celery_beat', 284 | ] 285 | 286 | 287 | Run the ``django_celery_beat`` migrations using: 288 | 289 | .. code-block:: bash 290 | 291 | $ python manage.py migrate django_celery_beat 292 | 293 | 294 | Using the development version 295 | ----------------------------- 296 | 297 | With pip 298 | ~~~~~~~~ 299 | 300 | You can install the latest main version of django-celery-beat using the following 301 | pip command: 302 | 303 | .. code-block:: bash 304 | 305 | $ pip install git+https://github.com/celery/django-celery-beat#egg=django-celery-beat 306 | 307 | 308 | Developing django-celery-beat 309 | ----------------------------- 310 | 311 | To spin up a local development copy of django-celery-beat with Django admin at http://127.0.0.1:58000/admin/ run: 312 | 313 | .. code-block:: bash 314 | 315 | $ docker-compose up --build 316 | 317 | Log-in as user ``admin`` with password ``admin``. 318 | 319 | 320 | TZ Awareness: 321 | ------------- 322 | 323 | If you have a project that is time zone naive, you can set ``DJANGO_CELERY_BEAT_TZ_AWARE=False`` in your settings file. 324 | 325 | 326 | .. |build-status| image:: https://github.com/celery/django-celery-beat/actions/workflows/test.yml/badge.svg 327 | :alt: Build status 328 | :target: https://github.com/celery/django-celery-beat/actions/workflows/test.yml 329 | 330 | .. |coverage| image:: https://codecov.io/github/celery/django-celery-beat/coverage.svg?branch=main 331 | :target: https://codecov.io/github/celery/django-celery-beat?branch=main 332 | 333 | .. |license| image:: https://img.shields.io/pypi/l/django-celery-beat.svg#foo 334 | :alt: BSD License 335 | :target: https://opensource.org/licenses/BSD-3-Clause 336 | 337 | .. |wheel| image:: https://img.shields.io/pypi/wheel/django-celery-beat.svg#foo 338 | :alt: django-celery-beat can be installed via wheel 339 | :target: http://pypi.python.org/pypi/django-celery-beat/ 340 | 341 | .. |pyversion| image:: https://img.shields.io/pypi/pyversions/django-celery-beat.svg#foo 342 | :alt: Supported Python versions. 343 | :target: http://pypi.python.org/pypi/django-celery-beat/ 344 | 345 | .. |pyimp| image:: https://img.shields.io/pypi/implementation/django-celery-beat.svg#foo 346 | :alt: Support Python implementations. 347 | :target: http://pypi.python.org/pypi/django-celery-beat/ 348 | 349 | .. |deepwiki| image:: https://devin.ai/assets/deepwiki-badge.png 350 | :alt: Ask http://DeepWiki.com 351 | :target: https://deepwiki.com/celery/django-celery-beat 352 | :width: 125px 353 | 354 | django-celery-beat as part of the Tidelift Subscription 355 | ------------------------------------------------------- 356 | 357 | 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`_. 358 | 359 | .. _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 360 | -------------------------------------------------------------------------------- /django_celery_beat/__init__.py: -------------------------------------------------------------------------------- 1 | """Database-backed Periodic Tasks.""" 2 | # :copyright: (c) 2016, Ask Solem. 3 | # All rights reserved. 4 | # :license: BSD (3 Clause), see LICENSE for more details. 5 | import re 6 | from collections import namedtuple 7 | 8 | __version__ = '2.8.1' 9 | __author__ = 'Asif Saif Uddin, Ask Solem' 10 | __contact__ = 'auvipy@gmail.com, ask@celeryproject.org' 11 | __homepage__ = 'https://github.com/celery/django-celery-beat' 12 | __docformat__ = 'restructuredtext' 13 | 14 | # -eof meta- 15 | 16 | version_info_t = namedtuple('version_info_t', ( 17 | 'major', 'minor', 'micro', 'releaselevel', 'serial', 18 | )) 19 | 20 | # bumpversion can only search for {current_version} 21 | # so we have to parse the version here. 22 | _temp = re.match( 23 | r'(\d+)\.(\d+).(\d+)(.+)?', __version__).groups() 24 | VERSION = version_info = version_info_t( 25 | int(_temp[0]), int(_temp[1]), int(_temp[2]), _temp[3] or '', '') 26 | del _temp 27 | del re 28 | 29 | __all__ = [] 30 | -------------------------------------------------------------------------------- /django_celery_beat/admin.py: -------------------------------------------------------------------------------- 1 | """Periodic Task Admin interface.""" 2 | from celery import current_app 3 | from celery.utils import cached_property 4 | from django import forms 5 | from django.conf import settings 6 | from django.contrib import admin, messages 7 | from django.db.models import Case, Value, When 8 | from django.forms.widgets import Select 9 | from django.template.defaultfilters import pluralize 10 | from django.utils.translation import gettext_lazy as _ 11 | from django.utils.translation import ngettext_lazy 12 | from kombu.utils.json import loads 13 | 14 | from .models import (ClockedSchedule, CrontabSchedule, IntervalSchedule, 15 | PeriodicTask, PeriodicTasks, SolarSchedule) 16 | from .utils import is_database_scheduler 17 | 18 | 19 | class TaskSelectWidget(Select): 20 | """Widget that lets you choose between task names.""" 21 | 22 | celery_app = current_app 23 | _choices = None 24 | 25 | def tasks_as_choices(self): 26 | _ = self._modules 27 | tasks = sorted(name for name in self.celery_app.tasks 28 | if not name.startswith('celery.')) 29 | return (('', ''), ) + tuple(zip(tasks, tasks)) 30 | 31 | @property 32 | def choices(self): 33 | if self._choices is None: 34 | self._choices = self.tasks_as_choices() 35 | return self._choices 36 | 37 | @choices.setter 38 | def choices(self, _): 39 | # ChoiceField.__init__ sets ``self.choices = choices`` 40 | # which would override ours. 41 | pass 42 | 43 | @cached_property 44 | def _modules(self): 45 | self.celery_app.loader.import_default_modules() 46 | 47 | 48 | class TaskChoiceField(forms.ChoiceField): 49 | """Field that lets you choose between task names.""" 50 | 51 | widget = TaskSelectWidget 52 | 53 | def valid_value(self, value): 54 | return True 55 | 56 | 57 | class PeriodicTaskForm(forms.ModelForm): 58 | """Form that lets you create and modify periodic tasks.""" 59 | 60 | regtask = TaskChoiceField( 61 | label=_('Task (registered)'), 62 | required=False, 63 | ) 64 | task = forms.CharField( 65 | label=_('Task (custom)'), 66 | required=False, 67 | max_length=200, 68 | ) 69 | 70 | class Meta: 71 | """Form metadata.""" 72 | 73 | model = PeriodicTask 74 | exclude = () 75 | 76 | def clean(self): 77 | data = super().clean() 78 | regtask = data.get('regtask') 79 | if regtask: 80 | data['task'] = regtask 81 | if not data['task']: 82 | exc = forms.ValidationError(_('Need name of task')) 83 | self._errors['task'] = self.error_class(exc.messages) 84 | raise exc 85 | 86 | if data.get('expire_seconds') is not None and data.get('expires'): 87 | raise forms.ValidationError( 88 | _('Only one can be set, in expires and expire_seconds') 89 | ) 90 | return data 91 | 92 | def _clean_json(self, field): 93 | value = self.cleaned_data[field] 94 | try: 95 | loads(value) 96 | except ValueError as exc: 97 | raise forms.ValidationError( 98 | _('Unable to parse JSON: %s') % exc, 99 | ) 100 | return value 101 | 102 | def clean_args(self): 103 | return self._clean_json('args') 104 | 105 | def clean_kwargs(self): 106 | return self._clean_json('kwargs') 107 | 108 | 109 | @admin.register(PeriodicTask) 110 | class PeriodicTaskAdmin(admin.ModelAdmin): 111 | """Admin-interface for periodic tasks.""" 112 | 113 | form = PeriodicTaskForm 114 | model = PeriodicTask 115 | celery_app = current_app 116 | date_hierarchy = 'start_time' 117 | list_display = ('name', 'enabled', 'scheduler', 'interval', 'start_time', 118 | 'last_run_at', 'one_off') 119 | list_filter = ['enabled', 'one_off', 'task', 'start_time', 'last_run_at'] 120 | actions = ('enable_tasks', 'disable_tasks', 'toggle_tasks', 'run_tasks') 121 | search_fields = ('name', 'task',) 122 | fieldsets = ( 123 | (None, { 124 | 'fields': ('name', 'regtask', 'task', 'enabled', 'description',), 125 | 'classes': ('extrapretty', 'wide'), 126 | }), 127 | (_('Schedule'), { 128 | 'fields': ('interval', 'crontab', 'crontab_translation', 'solar', 129 | 'clocked', 'start_time', 'last_run_at', 'one_off'), 130 | 'classes': ('extrapretty', 'wide'), 131 | }), 132 | (_('Arguments'), { 133 | 'fields': ('args', 'kwargs'), 134 | 'classes': ('extrapretty', 'wide', 'collapse', 'in'), 135 | }), 136 | (_('Execution Options'), { 137 | 'fields': ('expires', 'expire_seconds', 'queue', 'exchange', 138 | 'routing_key', 'priority', 'headers'), 139 | 'classes': ('extrapretty', 'wide', 'collapse', 'in'), 140 | }), 141 | ) 142 | readonly_fields = ( 143 | 'last_run_at', 'crontab_translation', 144 | ) 145 | 146 | def crontab_translation(self, obj): 147 | return obj.crontab.human_readable 148 | 149 | change_form_template = 'admin/djcelery/change_periodictask_form.html' 150 | 151 | def changeform_view(self, request, object_id=None, form_url='', 152 | extra_context=None): 153 | extra_context = extra_context or {} 154 | crontabs = CrontabSchedule.objects.all() 155 | crontab_dict = {} 156 | for crontab in crontabs: 157 | crontab_dict[crontab.id] = crontab.human_readable 158 | extra_context['readable_crontabs'] = crontab_dict 159 | return super().changeform_view(request, object_id, 160 | extra_context=extra_context) 161 | 162 | def changelist_view(self, request, extra_context=None): 163 | extra_context = extra_context or {} 164 | scheduler = getattr(settings, 'CELERY_BEAT_SCHEDULER', None) 165 | extra_context['wrong_scheduler'] = not is_database_scheduler(scheduler) 166 | return super().changelist_view( 167 | request, extra_context) 168 | 169 | def get_queryset(self, request): 170 | qs = super().get_queryset(request) 171 | return qs.select_related('interval', 'crontab', 'solar', 'clocked') 172 | 173 | @admin.action( 174 | description=_('Enable selected tasks') 175 | ) 176 | def enable_tasks(self, request, queryset): 177 | rows_updated = queryset.update(enabled=True) 178 | PeriodicTasks.update_changed() 179 | self.message_user( 180 | request, 181 | ngettext_lazy( 182 | '{0} task was successfully enabled', 183 | '{0} tasks were successfully enabled', 184 | rows_updated 185 | ).format(rows_updated) 186 | ) 187 | 188 | @admin.action( 189 | description=_('Disable selected tasks') 190 | ) 191 | def disable_tasks(self, request, queryset): 192 | rows_updated = queryset.update(enabled=False, last_run_at=None) 193 | PeriodicTasks.update_changed() 194 | self.message_user( 195 | request, 196 | ngettext_lazy( 197 | '{0} task was successfully disabled', 198 | '{0} tasks were successfully disabled', 199 | rows_updated 200 | ).format(rows_updated) 201 | ) 202 | 203 | def _toggle_tasks_activity(self, queryset): 204 | return queryset.update(enabled=Case( 205 | When(enabled=True, then=Value(False)), 206 | default=Value(True), 207 | )) 208 | 209 | @admin.action( 210 | description=_('Toggle activity of selected tasks') 211 | ) 212 | def toggle_tasks(self, request, queryset): 213 | rows_updated = self._toggle_tasks_activity(queryset) 214 | PeriodicTasks.update_changed() 215 | self.message_user( 216 | request, 217 | ngettext_lazy( 218 | '{0} task was successfully toggled', 219 | '{0} tasks were successfully toggled', 220 | rows_updated 221 | ).format(rows_updated) 222 | ) 223 | 224 | @admin.action( 225 | description=_('Run selected tasks') 226 | ) 227 | def run_tasks(self, request, queryset): 228 | self.celery_app.loader.import_default_modules() 229 | tasks = [(self.celery_app.tasks.get(task.task), 230 | loads(task.args), 231 | loads(task.kwargs), 232 | task.queue, 233 | task.name) 234 | for task in queryset] 235 | 236 | if any(t[0] is None for t in tasks): 237 | for i, t in enumerate(tasks): 238 | if t[0] is None: 239 | break 240 | 241 | # variable "i" will be set because list "tasks" is not empty 242 | not_found_task_name = queryset[i].task 243 | 244 | self.message_user( 245 | request, 246 | _(f'task "{not_found_task_name}" not found'), 247 | level=messages.ERROR, 248 | ) 249 | return 250 | 251 | task_ids = [ 252 | task.apply_async( 253 | args=args, 254 | kwargs=kwargs, 255 | queue=queue, 256 | headers={'periodic_task_name': periodic_task_name} 257 | ) 258 | if queue and len(queue) 259 | else task.apply_async( 260 | args=args, 261 | kwargs=kwargs, 262 | headers={'periodic_task_name': periodic_task_name} 263 | ) 264 | for task, args, kwargs, queue, periodic_task_name in tasks 265 | ] 266 | tasks_run = len(task_ids) 267 | self.message_user( 268 | request, 269 | _('{0} task{1} {2} successfully run').format( 270 | tasks_run, 271 | pluralize(tasks_run), 272 | pluralize(tasks_run, _('was,were')), 273 | ), 274 | ) 275 | 276 | 277 | class PeriodicTaskInline(admin.TabularInline): 278 | model = PeriodicTask 279 | fields = ('name', 'task', 'args', 'kwargs') 280 | readonly_fields = fields 281 | can_delete = False 282 | extra = 0 283 | show_change_link = True 284 | verbose_name = "Periodic Tasks Using This Schedule" 285 | verbose_name_plural = verbose_name 286 | 287 | def has_add_permission(self, request, obj): 288 | return False 289 | 290 | 291 | class ScheduleAdmin(admin.ModelAdmin): 292 | inlines = [PeriodicTaskInline] 293 | 294 | 295 | @admin.register(ClockedSchedule) 296 | class ClockedScheduleAdmin(ScheduleAdmin): 297 | """Admin-interface for clocked schedules.""" 298 | 299 | fields = ( 300 | 'clocked_time', 301 | ) 302 | list_display = ( 303 | 'clocked_time', 304 | ) 305 | 306 | 307 | @admin.register(CrontabSchedule) 308 | class CrontabScheduleAdmin(ScheduleAdmin): 309 | """Admin class for CrontabSchedule.""" 310 | 311 | list_display = ('__str__', 'human_readable') 312 | fields = ('human_readable', 'minute', 'hour', 'day_of_month', 313 | 'month_of_year', 'day_of_week', 'timezone') 314 | readonly_fields = ('human_readable', ) 315 | 316 | 317 | @admin.register(SolarSchedule) 318 | class SolarScheduleAdmin(ScheduleAdmin): 319 | """Admin class for SolarSchedule.""" 320 | pass 321 | 322 | 323 | @admin.register(IntervalSchedule) 324 | class IntervalScheduleAdmin(ScheduleAdmin): 325 | """Admin class for IntervalSchedule.""" 326 | pass 327 | -------------------------------------------------------------------------------- /django_celery_beat/apps.py: -------------------------------------------------------------------------------- 1 | """Django Application configuration.""" 2 | from django.apps import AppConfig 3 | from django.utils.translation import gettext_lazy as _ 4 | 5 | __all__ = ['BeatConfig'] 6 | 7 | 8 | class BeatConfig(AppConfig): 9 | """Default configuration for django_celery_beat app.""" 10 | 11 | name = 'django_celery_beat' 12 | label = 'django_celery_beat' 13 | verbose_name = _('Periodic Tasks') 14 | default_auto_field = 'django.db.models.AutoField' 15 | 16 | def ready(self): 17 | from .signals import signals_connect 18 | signals_connect() 19 | -------------------------------------------------------------------------------- /django_celery_beat/clockedschedule.py: -------------------------------------------------------------------------------- 1 | """Clocked schedule Implementation.""" 2 | 3 | from celery import schedules 4 | from celery.utils.time import maybe_make_aware 5 | 6 | from .utils import NEVER_CHECK_TIMEOUT 7 | 8 | 9 | class clocked(schedules.BaseSchedule): 10 | """clocked schedule. 11 | 12 | Depends on PeriodicTask one_off=True 13 | """ 14 | 15 | def __init__(self, clocked_time, nowfun=None, app=None): 16 | """Initialize clocked.""" 17 | self.clocked_time = maybe_make_aware(clocked_time) 18 | super().__init__(nowfun=nowfun, app=app) 19 | 20 | def remaining_estimate(self, last_run_at): 21 | return self.clocked_time - self.now() 22 | 23 | def is_due(self, last_run_at): 24 | rem_delta = self.remaining_estimate(None) 25 | remaining_s = max(rem_delta.total_seconds(), 0) 26 | if remaining_s == 0: 27 | return schedules.schedstate(is_due=True, next=NEVER_CHECK_TIMEOUT) 28 | return schedules.schedstate(is_due=False, next=remaining_s) 29 | 30 | def __repr__(self): 31 | return f'' 32 | 33 | def __eq__(self, other): 34 | if isinstance(other, clocked): 35 | return self.clocked_time == other.clocked_time 36 | return False 37 | 38 | def __ne__(self, other): 39 | return not self.__eq__(other) 40 | 41 | def __reduce__(self): 42 | return self.__class__, (self.clocked_time, self.nowfun) 43 | -------------------------------------------------------------------------------- /django_celery_beat/locale/de/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celery/django-celery-beat/0ade27bc52f92499d8c9000bacfa90bedcba3129/django_celery_beat/locale/de/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /django_celery_beat/locale/es/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celery/django-celery-beat/0ade27bc52f92499d8c9000bacfa90bedcba3129/django_celery_beat/locale/es/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /django_celery_beat/locale/fa/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celery/django-celery-beat/0ade27bc52f92499d8c9000bacfa90bedcba3129/django_celery_beat/locale/fa/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /django_celery_beat/locale/fr/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celery/django-celery-beat/0ade27bc52f92499d8c9000bacfa90bedcba3129/django_celery_beat/locale/fr/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /django_celery_beat/locale/ko/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celery/django-celery-beat/0ade27bc52f92499d8c9000bacfa90bedcba3129/django_celery_beat/locale/ko/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /django_celery_beat/locale/ru/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celery/django-celery-beat/0ade27bc52f92499d8c9000bacfa90bedcba3129/django_celery_beat/locale/ru/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /django_celery_beat/locale/zh_Hans/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celery/django-celery-beat/0ade27bc52f92499d8c9000bacfa90bedcba3129/django_celery_beat/locale/zh_Hans/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /django_celery_beat/locale/zh_Hans/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: PACKAGE VERSION\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2024-01-25 16:58+0800\n" 11 | "PO-Revision-Date: 2022-10-14 23:48+0200\n" 12 | "Last-Translator: Rainshaw \n" 13 | "Language-Team: x_zhuo \n" 14 | "Language: zh-Hans \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=1; plural=0;\n" 19 | 20 | #: django_celery_beat/admin.py:60 21 | msgid "Task (registered)" 22 | msgstr "任务 (已注册的)" 23 | 24 | #: django_celery_beat/admin.py:64 25 | msgid "Task (custom)" 26 | msgstr "任务 (自定义)" 27 | 28 | #: django_celery_beat/admin.py:81 29 | msgid "Need name of task" 30 | msgstr "任务需要一个名称" 31 | 32 | #: django_celery_beat/admin.py:87 django_celery_beat/models.py:618 33 | msgid "Only one can be set, in expires and expire_seconds" 34 | msgstr "不可以同时设置 expires 和 expire_seconds 字段" 35 | 36 | #: django_celery_beat/admin.py:97 37 | #, python-format 38 | msgid "Unable to parse JSON: %s" 39 | msgstr "无法解析 JSON: %s" 40 | 41 | #: django_celery_beat/admin.py:125 42 | #, fuzzy 43 | #| msgid "Solar Schedule" 44 | msgid "Schedule" 45 | msgstr "日程时间表" 46 | 47 | #: django_celery_beat/admin.py:130 48 | #, fuzzy 49 | #| msgid "Keyword Arguments" 50 | msgid "Arguments" 51 | msgstr "关键字参数" 52 | 53 | #: django_celery_beat/admin.py:134 54 | msgid "Execution Options" 55 | msgstr "" 56 | 57 | #: django_celery_beat/admin.py:179 58 | #, python-brace-format 59 | msgid "{0} task{1} {2} successfully {3}" 60 | msgstr "{0} 任务{1} {2} 成功 {3}" 61 | 62 | #: django_celery_beat/admin.py:182 django_celery_beat/admin.py:249 63 | msgid "was,were" 64 | msgstr "将" 65 | 66 | #: django_celery_beat/admin.py:191 67 | msgid "Enable selected tasks" 68 | msgstr "启用选中的任务" 69 | 70 | #: django_celery_beat/admin.py:197 71 | msgid "Disable selected tasks" 72 | msgstr "禁用选中的任务" 73 | 74 | #: django_celery_beat/admin.py:209 75 | msgid "Toggle activity of selected tasks" 76 | msgstr "切换选中的任务" 77 | 78 | #: django_celery_beat/admin.py:230 79 | #, fuzzy, python-brace-format 80 | #| msgid "task \"{0}\" not found" 81 | msgid "task \"{not_found_task_name}\" not found" 82 | msgstr "未找到\"{0}\"任务" 83 | 84 | #: django_celery_beat/admin.py:246 85 | #, python-brace-format 86 | msgid "{0} task{1} {2} successfully run" 87 | msgstr "{0} 任务{1} {2} 启动成功" 88 | 89 | #: django_celery_beat/admin.py:252 90 | msgid "Run selected tasks" 91 | msgstr "运行选中的任务" 92 | 93 | #: django_celery_beat/apps.py:13 94 | msgid "Periodic Tasks" 95 | msgstr "周期任务" 96 | 97 | #: django_celery_beat/models.py:31 98 | msgid "Days" 99 | msgstr "天" 100 | 101 | #: django_celery_beat/models.py:32 102 | msgid "Hours" 103 | msgstr "小时" 104 | 105 | #: django_celery_beat/models.py:33 106 | msgid "Minutes" 107 | msgstr "分钟" 108 | 109 | #: django_celery_beat/models.py:34 110 | msgid "Seconds" 111 | msgstr "秒" 112 | 113 | #: django_celery_beat/models.py:35 114 | msgid "Microseconds" 115 | msgstr "毫秒" 116 | 117 | #: django_celery_beat/models.py:39 118 | msgid "Day" 119 | msgstr "天" 120 | 121 | #: django_celery_beat/models.py:40 122 | msgid "Hour" 123 | msgstr "小时" 124 | 125 | #: django_celery_beat/models.py:41 126 | msgid "Minute" 127 | msgstr "分钟" 128 | 129 | #: django_celery_beat/models.py:42 130 | msgid "Second" 131 | msgstr "秒" 132 | 133 | #: django_celery_beat/models.py:43 134 | msgid "Microsecond" 135 | msgstr "毫秒" 136 | 137 | #: django_celery_beat/models.py:47 138 | msgid "Astronomical dawn" 139 | msgstr "天文黎明" 140 | 141 | #: django_celery_beat/models.py:48 142 | msgid "Civil dawn" 143 | msgstr "民事黎明" 144 | 145 | #: django_celery_beat/models.py:49 146 | msgid "Nautical dawn" 147 | msgstr "航海黎明" 148 | 149 | #: django_celery_beat/models.py:50 150 | msgid "Astronomical dusk" 151 | msgstr "天文黄昏" 152 | 153 | #: django_celery_beat/models.py:51 154 | msgid "Civil dusk" 155 | msgstr "民事黄昏" 156 | 157 | #: django_celery_beat/models.py:52 158 | msgid "Nautical dusk" 159 | msgstr "航海黄昏" 160 | 161 | #: django_celery_beat/models.py:53 162 | msgid "Solar noon" 163 | msgstr "正午" 164 | 165 | #: django_celery_beat/models.py:54 166 | msgid "Sunrise" 167 | msgstr "日出" 168 | 169 | #: django_celery_beat/models.py:55 170 | msgid "Sunset" 171 | msgstr "日落" 172 | 173 | #: django_celery_beat/models.py:89 174 | msgid "Solar Event" 175 | msgstr "日程事件" 176 | 177 | #: django_celery_beat/models.py:90 178 | msgid "The type of solar event when the job should run" 179 | msgstr "当任务应该执行时的日程事件类型" 180 | 181 | #: django_celery_beat/models.py:94 182 | msgid "Latitude" 183 | msgstr "纬度" 184 | 185 | #: django_celery_beat/models.py:95 186 | msgid "Run the task when the event happens at this latitude" 187 | msgstr "当在此纬度发生事件时执行任务" 188 | 189 | #: django_celery_beat/models.py:100 190 | msgid "Longitude" 191 | msgstr "经度" 192 | 193 | #: django_celery_beat/models.py:101 194 | msgid "Run the task when the event happens at this longitude" 195 | msgstr "当在此经度发生事件时执行任务" 196 | 197 | #: django_celery_beat/models.py:108 198 | msgid "solar event" 199 | msgstr "日程事件" 200 | 201 | #: django_celery_beat/models.py:109 202 | msgid "solar events" 203 | msgstr "日程事件" 204 | 205 | #: django_celery_beat/models.py:159 206 | msgid "Number of Periods" 207 | msgstr "周期数" 208 | 209 | #: django_celery_beat/models.py:160 210 | msgid "Number of interval periods to wait before running the task again" 211 | msgstr "再次执行任务之前要等待的间隔周期数" 212 | 213 | #: django_celery_beat/models.py:166 214 | msgid "Interval Period" 215 | msgstr "间隔周期" 216 | 217 | #: django_celery_beat/models.py:167 218 | msgid "The type of period between task runs (Example: days)" 219 | msgstr "任务每次执行之间的时间间隔类型(例如:天)" 220 | 221 | #: django_celery_beat/models.py:173 222 | msgid "interval" 223 | msgstr "间隔" 224 | 225 | #: django_celery_beat/models.py:174 226 | msgid "intervals" 227 | msgstr "间隔" 228 | 229 | #: django_celery_beat/models.py:201 230 | msgid "every {}" 231 | msgstr "每 {}" 232 | 233 | #: django_celery_beat/models.py:206 234 | msgid "every {} {}" 235 | msgstr "每 {} {}" 236 | 237 | #: django_celery_beat/models.py:217 238 | msgid "Clock Time" 239 | msgstr "定时时间" 240 | 241 | #: django_celery_beat/models.py:218 242 | msgid "Run the task at clocked time" 243 | msgstr "在定时时间执行任务" 244 | 245 | #: django_celery_beat/models.py:224 .\models.py:225 246 | msgid "clocked" 247 | msgstr "定时" 248 | 249 | #: django_celery_beat/models.py:265 250 | msgid "Minute(s)" 251 | msgstr "分钟" 252 | 253 | #: django_celery_beat/models.py:267 254 | msgid "Cron Minutes to Run. Use \"*\" for \"all\". (Example: \"0,30\")" 255 | msgstr "计划执行的分钟。 将\"*\"用作\"all\"。(例如:\"0,30\")" 256 | 257 | #: django_celery_beat/models.py:272 258 | msgid "Hour(s)" 259 | msgstr "小时" 260 | 261 | #: django_celery_beat/models.py:274 262 | msgid "Cron Hours to Run. Use \"*\" for \"all\". (Example: \"8,20\")" 263 | msgstr "计划执行的小时。 将\"*\"用作\"all\"。(例如:\"8,20\")" 264 | 265 | #: django_celery_beat/models.py:279 266 | msgid "Day(s) Of The Month" 267 | msgstr "一个月的第几天" 268 | 269 | #: django_celery_beat/models.py:281 270 | msgid "" 271 | "Cron Days Of The Month to Run. Use \"*\" for \"all\". (Example: \"1,15\")" 272 | msgstr "计划执行的每个月的第几天。将\"*\"用作\"all\"。(例如:\"0,5\")" 273 | 274 | #: django_celery_beat/models.py:287 275 | msgid "Month(s) Of The Year" 276 | msgstr "一年的第几个月" 277 | 278 | #: django_celery_beat/models.py:289 279 | #, fuzzy 280 | #| msgid "" 281 | #| "Cron Months Of The Year to Run. Use \"*\" for \"all\". (Example: \"0,6\")" 282 | msgid "" 283 | "Cron Months (1-12) Of The Year to Run. Use \"*\" for \"all\". (Example: " 284 | "\"1,12\")" 285 | msgstr "计划执行的每一年的第几个月。将\"*\"用作\"all\"。(例如:\"0,5\")" 286 | 287 | #: django_celery_beat/models.py:295 288 | msgid "Day(s) Of The Week" 289 | msgstr "一个星期的第几天" 290 | 291 | #: django_celery_beat/models.py:297 292 | #, fuzzy 293 | #| msgid "" 294 | #| "Cron Days Of The Week to Run. Use \"*\" for \"all\". (Example: \"0,5\")" 295 | msgid "" 296 | "Cron Days Of The Week to Run. Use \"*\" for \"all\", Sunday is 0 or 7, " 297 | "Monday is 1. (Example: \"0,5\")" 298 | msgstr "计划执行的每周的第几天。将\"*\"用作\"all\"。(例如:\"0,5\")" 299 | 300 | #: django_celery_beat/models.py:305 301 | msgid "Cron Timezone" 302 | msgstr "计划任务的时区" 303 | 304 | #: django_celery_beat/models.py:307 305 | msgid "Timezone to Run the Cron Schedule on. Default is UTC." 306 | msgstr "执行计划任务表的时区。 默认为UTC。" 307 | 308 | #: django_celery_beat/models.py:313 309 | msgid "crontab" 310 | msgstr "计划任务" 311 | 312 | #: django_celery_beat/models.py:314 313 | msgid "crontabs" 314 | msgstr "计划任务" 315 | 316 | #: django_celery_beat/models.py:392 317 | msgid "periodic task track" 318 | msgstr "周期性任务追踪" 319 | 320 | #: django_celery_beat/models.py:393 321 | msgid "periodic task tracks" 322 | msgstr "周期性任务追踪" 323 | 324 | #: django_celery_beat/models.py:417 325 | msgid "Name" 326 | msgstr "任务名" 327 | 328 | #: django_celery_beat/models.py:418 329 | msgid "Short Description For This Task" 330 | msgstr "该任务的简短说明" 331 | 332 | #: django_celery_beat/models.py:423 333 | msgid "" 334 | "The Name of the Celery Task that Should be Run. (Example: \"proj.tasks." 335 | "import_contacts\")" 336 | msgstr "被执行的任务的名称。(例如:\"proj.tasks.import_contacts\")" 337 | 338 | #: django_celery_beat/models.py:431 339 | msgid "Interval Schedule" 340 | msgstr "间隔时间表" 341 | 342 | #: django_celery_beat/models.py:432 343 | msgid "" 344 | "Interval Schedule to run the task on. Set only one schedule type, leave the " 345 | "others null." 346 | msgstr "执行任务的间隔时间表。 仅设置一种时间表类型,将其他保留为空。" 347 | 348 | #: django_celery_beat/models.py:437 349 | msgid "Crontab Schedule" 350 | msgstr "计划时间表" 351 | 352 | #: django_celery_beat/models.py:438 353 | msgid "" 354 | "Crontab Schedule to run the task on. Set only one schedule type, leave the " 355 | "others null." 356 | msgstr "执行任务的计划时间表。 仅设置一种时间表类型,将其他保留为空。" 357 | 358 | #: django_celery_beat/models.py:443 359 | msgid "Solar Schedule" 360 | msgstr "日程时间表" 361 | 362 | #: django_celery_beat/models.py:444 363 | msgid "" 364 | "Solar Schedule to run the task on. Set only one schedule type, leave the " 365 | "others null." 366 | msgstr "执行任务的日程时间表。 仅设置一种时间表类型,将其他保留为空。" 367 | 368 | #: django_celery_beat/models.py:449 369 | msgid "Clocked Schedule" 370 | msgstr "定时时间表" 371 | 372 | #: django_celery_beat/models.py:450 373 | msgid "" 374 | "Clocked Schedule to run the task on. Set only one schedule type, leave the " 375 | "others null." 376 | msgstr "执行任务的定时时间表。 仅设置一种时间表类型,将其他保留为空。" 377 | 378 | #: django_celery_beat/models.py:456 379 | msgid "Positional Arguments" 380 | msgstr "位置参数" 381 | 382 | #: django_celery_beat/models.py:458 383 | msgid "JSON encoded positional arguments (Example: [\"arg1\", \"arg2\"])" 384 | msgstr "JSON编码的位置参数(例如: [\"arg1\", \"arg2\"])" 385 | 386 | #: django_celery_beat/models.py:463 387 | msgid "Keyword Arguments" 388 | msgstr "关键字参数" 389 | 390 | #: django_celery_beat/models.py:465 391 | msgid "JSON encoded keyword arguments (Example: {\"argument\": \"value\"})" 392 | msgstr "JSON编码的关键字参数(例如: {\"argument\": \"value\"})" 393 | 394 | #: django_celery_beat/models.py:471 395 | msgid "Queue Override" 396 | msgstr "队列覆盖" 397 | 398 | #: django_celery_beat/models.py:473 399 | msgid "Queue defined in CELERY_TASK_QUEUES. Leave None for default queuing." 400 | msgstr "在 CELERY_TASK_QUEUES 定义的队列。保留空以进行默认排队。" 401 | 402 | #: django_celery_beat/models.py:482 403 | msgid "Exchange" 404 | msgstr "交换机" 405 | 406 | #: django_celery_beat/models.py:483 407 | msgid "Override Exchange for low-level AMQP routing" 408 | msgstr "覆盖交换机以进行低层级AMQP路由" 409 | 410 | #: django_celery_beat/models.py:487 411 | msgid "Routing Key" 412 | msgstr "路由键" 413 | 414 | #: django_celery_beat/models.py:488 415 | msgid "Override Routing Key for low-level AMQP routing" 416 | msgstr "覆盖路由键以进行低层级AMQP路由" 417 | 418 | #: django_celery_beat/models.py:492 419 | msgid "AMQP Message Headers" 420 | msgstr "AMQP消息头" 421 | 422 | #: django_celery_beat/models.py:493 423 | msgid "JSON encoded message headers for the AMQP message." 424 | msgstr "AMQP消息的JSON编码消息头。" 425 | 426 | #: django_celery_beat/models.py:499 427 | msgid "Priority" 428 | msgstr "优先级" 429 | 430 | #: django_celery_beat/models.py:501 431 | msgid "" 432 | "Priority Number between 0 and 255. Supported by: RabbitMQ, Redis (priority " 433 | "reversed, 0 is highest)." 434 | msgstr "" 435 | "优先级数字,介于0和255之间。支持者:RabbitMQ,Redis(优先级颠倒,0是最高)。" 436 | 437 | #: django_celery_beat/models.py:506 438 | msgid "Expires Datetime" 439 | msgstr "过期时刻" 440 | 441 | #: django_celery_beat/models.py:508 442 | msgid "" 443 | "Datetime after which the schedule will no longer trigger the task to run" 444 | msgstr "过期时刻,计划表将在此时刻后不再触发任务执行" 445 | 446 | #: django_celery_beat/models.py:513 447 | msgid "Expires timedelta with seconds" 448 | msgstr "过期时间间隔,以秒为单位" 449 | 450 | #: django_celery_beat/models.py:515 451 | msgid "" 452 | "Timedelta with seconds which the schedule will no longer trigger the task to " 453 | "run" 454 | msgstr "再过该秒后,不再触发任务执行" 455 | 456 | #: django_celery_beat/models.py:521 457 | msgid "One-off Task" 458 | msgstr "一次任务" 459 | 460 | #: django_celery_beat/models.py:523 461 | msgid "If True, the schedule will only run the task a single time" 462 | msgstr "如果为True,则计划将仅运行任务一次" 463 | 464 | #: django_celery_beat/models.py:527 465 | msgid "Start Datetime" 466 | msgstr "开始时间" 467 | 468 | #: django_celery_beat/models.py:529 469 | msgid "Datetime when the schedule should begin triggering the task to run" 470 | msgstr "时间表开始触发任务执行的时刻" 471 | 472 | #: django_celery_beat/models.py:534 473 | msgid "Enabled" 474 | msgstr "已启用" 475 | 476 | #: django_celery_beat/models.py:535 477 | msgid "Set to False to disable the schedule" 478 | msgstr "设置为False可禁用时间表" 479 | 480 | #: django_celery_beat/models.py:540 481 | msgid "Last Run Datetime" 482 | msgstr "上次运行时刻" 483 | 484 | #: django_celery_beat/models.py:542 485 | msgid "" 486 | "Datetime that the schedule last triggered the task to run. Reset to None if " 487 | "enabled is set to False." 488 | msgstr "最后一次触发任务执行的时刻。 如果enabled设置为False,则重置为None。" 489 | 490 | #: django_celery_beat/models.py:547 491 | msgid "Total Run Count" 492 | msgstr "总运行次数" 493 | 494 | #: django_celery_beat/models.py:549 495 | msgid "Running count of how many times the schedule has triggered the task" 496 | msgstr "任务执行多少次的运行计数" 497 | 498 | #: django_celery_beat/models.py:554 499 | msgid "Last Modified" 500 | msgstr "最后修改" 501 | 502 | #: django_celery_beat/models.py:555 503 | msgid "Datetime that this PeriodicTask was last modified" 504 | msgstr "该周期性任务的最后修改时刻" 505 | 506 | #: django_celery_beat/models.py:559 507 | msgid "Description" 508 | msgstr "描述" 509 | 510 | #: django_celery_beat/models.py:561 511 | msgid "Detailed description about the details of this Periodic Task" 512 | msgstr "有关此周期性任务的详细信息" 513 | 514 | #: django_celery_beat/models.py:570 515 | msgid "periodic task" 516 | msgstr "周期性任务" 517 | 518 | #: django_celery_beat/models.py:571 519 | msgid "periodic tasks" 520 | msgstr "周期性任务" 521 | 522 | #: django_celery_beat/templates/admin/djcelery/change_list.html:6 523 | msgid "Home" 524 | msgstr "首页" 525 | -------------------------------------------------------------------------------- /django_celery_beat/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.9.5 on 2016-08-04 02:13 2 | from django.db import migrations, models 3 | import django.db.models.deletion 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='CrontabSchedule', 16 | fields=[ 17 | ('id', models.AutoField( 18 | auto_created=True, primary_key=True, 19 | serialize=False, verbose_name='ID')), 20 | ('minute', models.CharField( 21 | default='*', max_length=64, verbose_name='minute')), 22 | ('hour', models.CharField( 23 | default='*', max_length=64, verbose_name='hour')), 24 | ('day_of_week', models.CharField( 25 | default='*', max_length=64, verbose_name='day of week')), 26 | ('day_of_month', models.CharField( 27 | default='*', max_length=64, verbose_name='day of month')), 28 | ('month_of_year', models.CharField( 29 | default='*', max_length=64, verbose_name='month of year')), 30 | ], 31 | options={ 32 | 'ordering': [ 33 | 'month_of_year', 'day_of_month', 34 | 'day_of_week', 'hour', 'minute', 35 | ], 36 | 'verbose_name': 'crontab', 37 | 'verbose_name_plural': 'crontabs', 38 | }, 39 | ), 40 | migrations.CreateModel( 41 | name='IntervalSchedule', 42 | fields=[ 43 | ('id', models.AutoField( 44 | auto_created=True, primary_key=True, 45 | serialize=False, verbose_name='ID')), 46 | ('every', models.IntegerField(verbose_name='every')), 47 | ('period', models.CharField( 48 | choices=[ 49 | ('days', 'Days'), 50 | ('hours', 'Hours'), 51 | ('minutes', 'Minutes'), 52 | ('seconds', 'Seconds'), 53 | ('microseconds', 'Microseconds'), 54 | ], 55 | max_length=24, 56 | verbose_name='period')), 57 | ], 58 | options={ 59 | 'ordering': ['period', 'every'], 60 | 'verbose_name': 'interval', 61 | 'verbose_name_plural': 'intervals', 62 | }, 63 | ), 64 | migrations.CreateModel( 65 | name='PeriodicTask', 66 | fields=[ 67 | ('id', models.AutoField( 68 | auto_created=True, primary_key=True, 69 | serialize=False, verbose_name='ID')), 70 | ('name', models.CharField( 71 | help_text='Useful description', max_length=200, 72 | unique=True, verbose_name='name')), 73 | ('task', models.CharField( 74 | max_length=200, verbose_name='task name')), 75 | ('args', models.TextField( 76 | blank=True, default='[]', 77 | help_text='JSON encoded positional arguments', 78 | verbose_name='Arguments')), 79 | ('kwargs', models.TextField( 80 | blank=True, default='{}', 81 | help_text='JSON encoded keyword arguments', 82 | verbose_name='Keyword arguments')), 83 | ('queue', models.CharField( 84 | blank=True, default=None, 85 | help_text='Queue defined in CELERY_TASK_QUEUES', 86 | max_length=200, null=True, verbose_name='queue')), 87 | ('exchange', models.CharField( 88 | blank=True, default=None, max_length=200, 89 | null=True, verbose_name='exchange')), 90 | ('routing_key', models.CharField( 91 | blank=True, default=None, 92 | max_length=200, null=True, verbose_name='routing key')), 93 | ('expires', models.DateTimeField( 94 | blank=True, null=True, verbose_name='expires')), 95 | ('enabled', models.BooleanField( 96 | default=True, verbose_name='enabled')), 97 | ('last_run_at', models.DateTimeField( 98 | blank=True, editable=False, null=True)), 99 | ('total_run_count', models.PositiveIntegerField( 100 | default=0, editable=False)), 101 | ('date_changed', models.DateTimeField(auto_now=True)), 102 | ('description', models.TextField( 103 | blank=True, verbose_name='description')), 104 | ('crontab', models.ForeignKey( 105 | blank=True, help_text='Use one of interval/crontab', 106 | null=True, on_delete=django.db.models.deletion.CASCADE, 107 | to='django_celery_beat.CrontabSchedule', 108 | verbose_name='crontab')), 109 | ('interval', models.ForeignKey( 110 | blank=True, null=True, 111 | on_delete=django.db.models.deletion.CASCADE, 112 | to='django_celery_beat.IntervalSchedule', 113 | verbose_name='interval')), 114 | ], 115 | options={ 116 | 'verbose_name': 'periodic task', 117 | 'verbose_name_plural': 'periodic tasks', 118 | }, 119 | ), 120 | migrations.CreateModel( 121 | name='PeriodicTasks', 122 | fields=[ 123 | ('ident', models.SmallIntegerField( 124 | default=1, primary_key=True, 125 | serialize=False, unique=True)), 126 | ('last_update', models.DateTimeField()), 127 | ], 128 | ), 129 | ] 130 | -------------------------------------------------------------------------------- /django_celery_beat/migrations/0002_auto_20161118_0346.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10.3 on 2016-11-18 03:46 2 | from django.db import migrations, models 3 | import django.db.models.deletion 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('django_celery_beat', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='SolarSchedule', 15 | fields=[ 16 | ('id', models.AutoField( 17 | auto_created=True, primary_key=True, 18 | serialize=False, verbose_name='ID')), 19 | ('event', models.CharField( 20 | choices=[('dusk_nautical', 'dusk_nautical'), 21 | ('dawn_astronomical', 'dawn_astronomical'), 22 | ('dawn_nautical', 'dawn_nautical'), 23 | ('dawn_civil', 'dawn_civil'), 24 | ('sunset', 'sunset'), 25 | ('solar_noon', 'solar_noon'), 26 | ('dusk_astronomical', 'dusk_astronomical'), 27 | ('sunrise', 'sunrise'), 28 | ('dusk_civil', 'dusk_civil')], 29 | max_length=24, verbose_name='event')), 30 | ('latitude', models.DecimalField( 31 | decimal_places=6, max_digits=9, verbose_name='latitude')), 32 | ('longitude', models.DecimalField( 33 | decimal_places=6, max_digits=9, verbose_name='latitude')), 34 | ], 35 | options={ 36 | 'ordering': ['event', 'latitude', 'longitude'], 37 | 'verbose_name': 'solar', 38 | 'verbose_name_plural': 'solars', 39 | }, 40 | ), 41 | migrations.AddField( 42 | model_name='periodictask', 43 | name='solar', 44 | field=models.ForeignKey( 45 | blank=True, help_text='Use a solar schedule', 46 | null=True, on_delete=django.db.models.deletion.CASCADE, 47 | to='django_celery_beat.SolarSchedule', verbose_name='solar'), 48 | ), 49 | ] 50 | -------------------------------------------------------------------------------- /django_celery_beat/migrations/0003_auto_20161209_0049.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.9.11 on 2016-12-09 00:49 2 | from django.db import migrations 3 | 4 | 5 | class Migration(migrations.Migration): 6 | 7 | dependencies = [ 8 | ('django_celery_beat', '0002_auto_20161118_0346'), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterModelOptions( 13 | name='solarschedule', 14 | options={ 15 | 'ordering': ('event', 'latitude', 'longitude'), 16 | 'verbose_name': 'solar event', 17 | 'verbose_name_plural': 'solar events'}, 18 | ), 19 | migrations.AlterUniqueTogether( 20 | name='solarschedule', 21 | unique_together=set([('event', 'latitude', 'longitude')]), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /django_celery_beat/migrations/0004_auto_20170221_0000.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations, models 2 | 3 | 4 | class Migration(migrations.Migration): 5 | 6 | dependencies = [ 7 | ('django_celery_beat', '0003_auto_20161209_0049'), 8 | ] 9 | 10 | operations = [ 11 | migrations.AlterField( 12 | model_name='solarschedule', 13 | name='longitude', 14 | field=models.DecimalField( 15 | verbose_name='longitude', 16 | max_digits=9, 17 | decimal_places=6), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /django_celery_beat/migrations/0005_add_solarschedule_events_choices.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.9.1 on 2017-11-01 15:53 2 | from django.db import migrations, models 3 | 4 | 5 | class Migration(migrations.Migration): 6 | 7 | dependencies = [ 8 | ('django_celery_beat', '0004_auto_20170221_0000'), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name='solarschedule', 14 | name='event', 15 | field=models.CharField(choices=[ 16 | ('dawn_astronomical', 'dawn_astronomical'), 17 | ('dawn_civil', 'dawn_civil'), 18 | ('dawn_nautical', 'dawn_nautical'), 19 | ('dusk_astronomical', 'dusk_astronomical'), 20 | ('dusk_civil', 'dusk_civil'), 21 | ('dusk_nautical', 'dusk_nautical'), 22 | ('solar_noon', 'solar_noon'), 23 | ('sunrise', 'sunrise'), 24 | ('sunset', 'sunset') 25 | ], 26 | max_length=24, verbose_name='event'), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /django_celery_beat/migrations/0006_auto_20180210_1226.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.1 on 2018-02-10 12:26 2 | from django.db import migrations, models 3 | 4 | 5 | class Migration(migrations.Migration): 6 | 7 | dependencies = [ 8 | ('django_celery_beat', '0005_add_solarschedule_events_choices'), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name='crontabschedule', 14 | name='day_of_month', 15 | field=models.CharField(default='*', max_length=124, 16 | verbose_name='day of month'), 17 | ), 18 | migrations.AlterField( 19 | model_name='crontabschedule', 20 | name='hour', 21 | field=models.CharField(default='*', max_length=96, 22 | verbose_name='hour'), 23 | ), 24 | migrations.AlterField( 25 | model_name='crontabschedule', 26 | name='minute', 27 | field=models.CharField(default='*', max_length=240, 28 | verbose_name='minute'), 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /django_celery_beat/migrations/0006_auto_20180322_0932.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.11.7 on 2018-03-22 16:32 2 | from django.db import migrations, models 3 | import timezone_field.fields 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('django_celery_beat', '0005_add_solarschedule_events_choices'), 10 | # ('django_celery_beat', '0006_auto_20180210_1226'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterModelOptions( 15 | name='crontabschedule', 16 | options={ 17 | 'ordering': [ 18 | 'month_of_year', 'day_of_month', 19 | 'day_of_week', 'hour', 'minute', 'timezone' 20 | ], 21 | 'verbose_name': 'crontab', 22 | 'verbose_name_plural': 'crontabs' 23 | }, 24 | ), 25 | migrations.AddField( 26 | model_name='crontabschedule', 27 | name='timezone', 28 | field=timezone_field.fields.TimeZoneField(default='UTC'), 29 | ), 30 | migrations.AlterField( 31 | model_name='crontabschedule', 32 | name='day_of_month', 33 | field=models.CharField( 34 | default='*', max_length=124, verbose_name='day of month' 35 | ), 36 | ), 37 | migrations.AlterField( 38 | model_name='crontabschedule', 39 | name='hour', 40 | field=models.CharField( 41 | default='*', max_length=96, verbose_name='hour' 42 | ), 43 | ), 44 | migrations.AlterField( 45 | model_name='crontabschedule', 46 | name='minute', 47 | field=models.CharField( 48 | default='*', max_length=240, verbose_name='minute' 49 | ), 50 | ), 51 | ] 52 | -------------------------------------------------------------------------------- /django_celery_beat/migrations/0006_periodictask_priority.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.6 on 2018-10-22 05:20 2 | import django.core.validators 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | # depends on higher numbers due to a squashed migration 9 | # that was later removed due to migration issues it caused 10 | ('django_celery_beat', '0005_add_solarschedule_events_choices'), 11 | ('django_celery_beat', '0006_auto_20180210_1226'), 12 | ('django_celery_beat', '0006_auto_20180322_0932'), 13 | ('django_celery_beat', '0007_auto_20180521_0826'), 14 | ('django_celery_beat', '0008_auto_20180914_1922'), 15 | ] 16 | 17 | operations = [ 18 | migrations.AddField( 19 | model_name='periodictask', 20 | name='priority', 21 | field=models.PositiveIntegerField( 22 | blank=True, 23 | default=None, 24 | null=True, 25 | validators=[django.core.validators.MaxValueValidator(255)], 26 | verbose_name='priority'), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /django_celery_beat/migrations/0007_auto_20180521_0826.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10.7 on 2018-05-21 08:26 2 | from django.db import migrations, models 3 | 4 | 5 | class Migration(migrations.Migration): 6 | 7 | dependencies = [ 8 | ('django_celery_beat', '0006_auto_20180322_0932'), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name='periodictask', 14 | name='one_off', 15 | field=models.BooleanField(default=False, 16 | verbose_name='one-off task'), 17 | ), 18 | migrations.AddField( 19 | model_name='periodictask', 20 | name='start_time', 21 | field=models.DateTimeField(blank=True, 22 | null=True, 23 | verbose_name='start_time'), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /django_celery_beat/migrations/0008_auto_20180914_1922.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.3 on 2018-09-14 19:22 2 | from django.db import migrations, models 3 | from django_celery_beat import validators 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ('django_celery_beat', '0007_auto_20180521_0826'), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name='crontabschedule', 14 | name='day_of_month', 15 | field=models.CharField( 16 | default='*', max_length=124, 17 | validators=[validators.day_of_month_validator], 18 | verbose_name='day of month' 19 | ), 20 | ), 21 | migrations.AlterField( 22 | model_name='crontabschedule', 23 | name='day_of_week', 24 | field=models.CharField( 25 | default='*', max_length=64, 26 | validators=[validators.day_of_week_validator], 27 | verbose_name='day of week' 28 | ), 29 | ), 30 | migrations.AlterField( 31 | model_name='crontabschedule', 32 | name='hour', 33 | field=models.CharField( 34 | default='*', max_length=96, 35 | validators=[validators.hour_validator], 36 | verbose_name='hour' 37 | ), 38 | ), 39 | migrations.AlterField( 40 | model_name='crontabschedule', 41 | name='minute', 42 | field=models.CharField( 43 | default='*', max_length=240, 44 | validators=[validators.minute_validator], 45 | verbose_name='minute' 46 | ), 47 | ), 48 | migrations.AlterField( 49 | model_name='crontabschedule', 50 | name='month_of_year', 51 | field=models.CharField( 52 | default='*', max_length=64, 53 | validators=[validators.month_of_year_validator], 54 | verbose_name='month of year' 55 | ), 56 | ), 57 | ] 58 | -------------------------------------------------------------------------------- /django_celery_beat/migrations/0009_periodictask_headers.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.5 on 2019-02-09 19:33 2 | from django.db import migrations, models 3 | 4 | 5 | class Migration(migrations.Migration): 6 | 7 | dependencies = [ 8 | ('django_celery_beat', '0006_periodictask_priority'), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name='periodictask', 14 | name='headers', 15 | field=models.TextField( 16 | blank=True, 17 | default='{}', 18 | help_text='JSON encoded message headers', 19 | verbose_name='Message headers' 20 | ), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /django_celery_beat/migrations/0010_auto_20190429_0326.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.11.20 on 2019-04-29 03:26 2 | 3 | # this file is auto-generated so don't do flake8 on it 4 | # flake8: noqa 5 | import django.core.validators 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | import django_celery_beat.validators 9 | import timezone_field.fields 10 | 11 | 12 | class Migration(migrations.Migration): 13 | 14 | dependencies = [ 15 | ('django_celery_beat', '0009_periodictask_headers'), 16 | ] 17 | 18 | operations = [ 19 | migrations.AlterField( 20 | model_name='crontabschedule', 21 | name='day_of_month', 22 | 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'), 23 | ), 24 | migrations.AlterField( 25 | model_name='crontabschedule', 26 | name='day_of_week', 27 | 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'), 28 | ), 29 | migrations.AlterField( 30 | model_name='crontabschedule', 31 | name='hour', 32 | 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)'), 33 | ), 34 | migrations.AlterField( 35 | model_name='crontabschedule', 36 | name='minute', 37 | 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)'), 38 | ), 39 | migrations.AlterField( 40 | model_name='crontabschedule', 41 | name='month_of_year', 42 | 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'), 43 | ), 44 | migrations.AlterField( 45 | model_name='crontabschedule', 46 | name='timezone', 47 | field=timezone_field.fields.TimeZoneField(default='UTC', help_text='Timezone to Run the Cron Schedule on. Default is UTC.', verbose_name='Cron Timezone'), 48 | ), 49 | migrations.AlterField( 50 | model_name='intervalschedule', 51 | name='every', 52 | 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'), 53 | ), 54 | migrations.AlterField( 55 | model_name='intervalschedule', 56 | name='period', 57 | 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'), 58 | ), 59 | migrations.AlterField( 60 | model_name='periodictask', 61 | name='args', 62 | field=models.TextField(blank=True, default='[]', help_text='JSON encoded positional arguments (Example: ["arg1", "arg2"])', verbose_name='Positional Arguments'), 63 | ), 64 | migrations.AlterField( 65 | model_name='periodictask', 66 | name='crontab', 67 | 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'), 68 | ), 69 | migrations.AlterField( 70 | model_name='periodictask', 71 | name='date_changed', 72 | field=models.DateTimeField(auto_now=True, help_text='Datetime that this PeriodicTask was last modified', verbose_name='Last Modified'), 73 | ), 74 | migrations.AlterField( 75 | model_name='periodictask', 76 | name='description', 77 | field=models.TextField(blank=True, help_text='Detailed description about the details of this Periodic Task', verbose_name='Description'), 78 | ), 79 | migrations.AlterField( 80 | model_name='periodictask', 81 | name='enabled', 82 | field=models.BooleanField(default=True, help_text='Set to False to disable the schedule', verbose_name='Enabled'), 83 | ), 84 | migrations.AlterField( 85 | model_name='periodictask', 86 | name='exchange', 87 | field=models.CharField(blank=True, default=None, help_text='Override Exchange for low-level AMQP routing', max_length=200, null=True, verbose_name='Exchange'), 88 | ), 89 | migrations.AlterField( 90 | model_name='periodictask', 91 | name='expires', 92 | 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'), 93 | ), 94 | migrations.AlterField( 95 | model_name='periodictask', 96 | name='headers', 97 | field=models.TextField(blank=True, default='{}', help_text='JSON encoded message headers for the AMQP message.', verbose_name='AMQP Message Headers'), 98 | ), 99 | migrations.AlterField( 100 | model_name='periodictask', 101 | name='interval', 102 | 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'), 103 | ), 104 | migrations.AlterField( 105 | model_name='periodictask', 106 | name='kwargs', 107 | field=models.TextField(blank=True, default='{}', help_text='JSON encoded keyword arguments (Example: {"argument": "value"})', verbose_name='Keyword Arguments'), 108 | ), 109 | migrations.AlterField( 110 | model_name='periodictask', 111 | name='last_run_at', 112 | 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'), 113 | ), 114 | migrations.AlterField( 115 | model_name='periodictask', 116 | name='name', 117 | field=models.CharField(help_text='Short Description For This Task', max_length=200, unique=True, verbose_name='Name'), 118 | ), 119 | migrations.AlterField( 120 | model_name='periodictask', 121 | name='one_off', 122 | field=models.BooleanField(default=False, help_text='If True, the schedule will only run the task a single time', verbose_name='One-off Task'), 123 | ), 124 | migrations.AlterField( 125 | model_name='periodictask', 126 | name='priority', 127 | 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'), 128 | ), 129 | migrations.AlterField( 130 | model_name='periodictask', 131 | name='queue', 132 | 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'), 133 | ), 134 | migrations.AlterField( 135 | model_name='periodictask', 136 | name='routing_key', 137 | 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'), 138 | ), 139 | migrations.AlterField( 140 | model_name='periodictask', 141 | name='solar', 142 | 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'), 143 | ), 144 | migrations.AlterField( 145 | model_name='periodictask', 146 | name='start_time', 147 | field=models.DateTimeField(blank=True, help_text='Datetime when the schedule should begin triggering the task to run', null=True, verbose_name='Start Datetime'), 148 | ), 149 | migrations.AlterField( 150 | model_name='periodictask', 151 | name='task', 152 | 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'), 153 | ), 154 | migrations.AlterField( 155 | model_name='periodictask', 156 | name='total_run_count', 157 | 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'), 158 | ), 159 | migrations.AlterField( 160 | model_name='solarschedule', 161 | name='event', 162 | 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'), 163 | ), 164 | migrations.AlterField( 165 | model_name='solarschedule', 166 | name='latitude', 167 | 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'), 168 | ), 169 | migrations.AlterField( 170 | model_name='solarschedule', 171 | name='longitude', 172 | 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'), 173 | ), 174 | ] 175 | -------------------------------------------------------------------------------- /django_celery_beat/migrations/0011_auto_20190508_0153.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2019-05-08 01:53 2 | # flake8: noqa 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('django_celery_beat', '0010_auto_20190429_0326'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='ClockedSchedule', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('clocked_time', models.DateTimeField(help_text='Run the task at clocked time', verbose_name='Clock Time')), 19 | ('enabled', models.BooleanField(default=True, editable=False, help_text='Set to False to disable the schedule', verbose_name='Enabled')), 20 | ], 21 | options={ 22 | 'verbose_name': 'clocked', 23 | 'verbose_name_plural': 'clocked', 24 | 'ordering': ['clocked_time'], 25 | }, 26 | ), 27 | migrations.AddField( 28 | model_name='periodictask', 29 | name='clocked', 30 | 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'), 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /django_celery_beat/migrations/0012_periodictask_expire_seconds.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.4 on 2019-08-30 00:46 2 | # flake8: noqa 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('django_celery_beat', '0011_auto_20190508_0153'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='periodictask', 15 | name='expire_seconds', 16 | 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'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /django_celery_beat/migrations/0013_auto_20200609_0727.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.6 on 2020-06-09 07:27 2 | # flake8: noqa 3 | from django.db import migrations 4 | import django_celery_beat.models 5 | import timezone_field.fields 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('django_celery_beat', '0012_periodictask_expire_seconds'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='crontabschedule', 17 | name='timezone', 18 | 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'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /django_celery_beat/migrations/0014_remove_clockedschedule_enabled.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.4 on 2019-08-30 00:46 2 | # flake8: noqa 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('django_celery_beat', '0013_auto_20200609_0727'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='clockedschedule', 15 | name='enabled', 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /django_celery_beat/migrations/0015_edit_solarschedule_events_choices.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.6 on 2020-12-13 15:00 2 | # flake8: noqa 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('django_celery_beat', '0014_remove_clockedschedule_enabled'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='solarschedule', 15 | name='event', 16 | 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'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /django_celery_beat/migrations/0016_alter_crontabschedule_timezone.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.3 on 2022-03-21 20:20 2 | # flake8: noqa 3 | from django.db import migrations 4 | import django_celery_beat.models 5 | import timezone_field.fields 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('django_celery_beat', '0015_edit_solarschedule_events_choices'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='crontabschedule', 17 | name='timezone', 18 | field=timezone_field.fields.TimeZoneField( 19 | default= 20 | django_celery_beat.models.crontab_schedule_celery_timezone, 21 | help_text= 22 | 'Timezone to Run the Cron Schedule on. Default is UTC.', 23 | use_pytz=False, verbose_name='Cron Timezone'), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /django_celery_beat/migrations/0017_alter_crontabschedule_month_of_year.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.4 on 2022-12-17 09:21 2 | 3 | from django.db import migrations, models 4 | import django_celery_beat.validators 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('django_celery_beat', '0016_alter_crontabschedule_timezone'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='crontabschedule', 16 | name='month_of_year', 17 | field=models.CharField(default='*', help_text='Cron Months (1-12) Of The Year to Run. Use "*" for "all". (Example: "1,12")', max_length=64, validators=[django_celery_beat.validators.month_of_year_validator], verbose_name='Month(s) Of The Year'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /django_celery_beat/migrations/0018_improve_crontab_helptext.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.16 on 2022-12-23 22:30 2 | 3 | from django.db import migrations, models 4 | import django_celery_beat.validators 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('django_celery_beat', '0017_alter_crontabschedule_month_of_year'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='crontabschedule', 16 | name='day_of_week', 17 | field=models.CharField(default='*', help_text='Cron Days Of The Week to Run. Use "*" for "all", Sunday is 0 or 7, Monday is 1. (Example: "0,5")', max_length=64, validators=[django_celery_beat.validators.day_of_week_validator], verbose_name='Day(s) Of The Week'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /django_celery_beat/migrations/0019_alter_periodictasks_options.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.1 on 2024-07-04 07:32 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('django_celery_beat', '0018_improve_crontab_helptext'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='periodictasks', 15 | options={'verbose_name': 'periodic task track', 'verbose_name_plural': 'periodic task tracks'}, 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /django_celery_beat/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celery/django-celery-beat/0ade27bc52f92499d8c9000bacfa90bedcba3129/django_celery_beat/migrations/__init__.py -------------------------------------------------------------------------------- /django_celery_beat/querysets.py: -------------------------------------------------------------------------------- 1 | """Model querysets.""" 2 | from django.db import models 3 | 4 | 5 | class PeriodicTaskQuerySet(models.QuerySet): 6 | """QuerySet for PeriodicTask.""" 7 | 8 | def enabled(self): 9 | return self.filter(enabled=True).prefetch_related( 10 | "interval", "crontab", "solar", "clocked" 11 | ) 12 | -------------------------------------------------------------------------------- /django_celery_beat/signals.py: -------------------------------------------------------------------------------- 1 | """Django Application signals.""" 2 | 3 | 4 | def signals_connect(): 5 | """Connect to signals.""" 6 | from django.db.models import signals 7 | 8 | from .models import (ClockedSchedule, CrontabSchedule, IntervalSchedule, 9 | PeriodicTask, PeriodicTasks, SolarSchedule) 10 | 11 | signals.pre_save.connect( 12 | PeriodicTasks.changed, sender=PeriodicTask 13 | ) 14 | signals.pre_delete.connect( 15 | PeriodicTasks.changed, sender=PeriodicTask 16 | ) 17 | 18 | signals.post_save.connect( 19 | PeriodicTasks.update_changed, sender=IntervalSchedule 20 | ) 21 | signals.pre_delete.connect( 22 | PeriodicTasks.update_changed, sender=IntervalSchedule 23 | ) 24 | 25 | signals.post_save.connect( 26 | PeriodicTasks.update_changed, sender=CrontabSchedule 27 | ) 28 | signals.post_delete.connect( 29 | PeriodicTasks.update_changed, sender=CrontabSchedule 30 | ) 31 | 32 | signals.post_save.connect( 33 | PeriodicTasks.update_changed, sender=SolarSchedule 34 | ) 35 | signals.post_delete.connect( 36 | PeriodicTasks.update_changed, sender=SolarSchedule 37 | ) 38 | 39 | signals.post_save.connect( 40 | PeriodicTasks.update_changed, sender=ClockedSchedule 41 | ) 42 | signals.post_delete.connect( 43 | PeriodicTasks.update_changed, sender=ClockedSchedule 44 | ) 45 | -------------------------------------------------------------------------------- /django_celery_beat/templates/admin/djcelery/change_list.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_list.html" %} 2 | {% load i18n %} 3 | 4 | {% block breadcrumbs %} 5 | 10 | {% if wrong_scheduler %} 11 |
    12 |
  • 13 | Periodic tasks won't be dispatched unless you set the 14 | CELERY_BEAT_SCHEDULER setting to 15 | djcelery.schedulers.DatabaseScheduler, 16 | or specify it using the -S option to celerybeat 17 |
  • 18 |
19 | {% endif %} 20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /django_celery_beat/templates/admin/djcelery/change_periodictask_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/change_form.html' %} 2 | 3 | {% block admin_change_form_document_ready %} 4 | {{ block.super }} 5 | 6 | {{ readable_crontabs|json_script:"readable-crontabs" }} 7 | 22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /django_celery_beat/tzcrontab.py: -------------------------------------------------------------------------------- 1 | """Timezone aware Cron schedule Implementation.""" 2 | from collections import namedtuple 3 | from datetime import datetime, timezone 4 | 5 | from celery import schedules 6 | 7 | schedstate = namedtuple('schedstate', ('is_due', 'next')) 8 | 9 | 10 | class TzAwareCrontab(schedules.crontab): 11 | """Timezone Aware Crontab.""" 12 | 13 | def __init__( 14 | self, minute='*', hour='*', day_of_week='*', 15 | day_of_month='*', month_of_year='*', tz=timezone.utc, app=None 16 | ): 17 | """Overwrite Crontab constructor to include a timezone argument.""" 18 | self.tz = tz 19 | 20 | nowfun = self.nowfunc 21 | 22 | super().__init__( 23 | minute=minute, hour=hour, day_of_week=day_of_week, 24 | day_of_month=day_of_month, 25 | month_of_year=month_of_year, nowfun=nowfun, app=app 26 | ) 27 | 28 | def nowfunc(self): 29 | return datetime.now(self.tz) 30 | 31 | def is_due(self, last_run_at): 32 | """Calculate when the next run will take place. 33 | 34 | Return tuple of ``(is_due, next_time_to_check)``. 35 | The ``last_run_at`` argument needs to be timezone aware. 36 | 37 | """ 38 | # convert last_run_at to the schedule timezone 39 | last_run_at = last_run_at.astimezone(self.tz) 40 | 41 | return super().is_due(last_run_at) 42 | 43 | # Needed to support pickling 44 | def __repr__(self): 45 | return """ 48 | """.format(self) 49 | 50 | def __reduce__(self): 51 | return (self.__class__, (self._orig_minute, 52 | self._orig_hour, 53 | self._orig_day_of_week, 54 | self._orig_day_of_month, 55 | self._orig_month_of_year, 56 | self.tz), None) 57 | 58 | def __eq__(self, other): 59 | if isinstance(other, schedules.crontab): 60 | return (other.month_of_year == self.month_of_year 61 | and other.day_of_month == self.day_of_month 62 | and other.day_of_week == self.day_of_week 63 | and other.hour == self.hour 64 | and other.minute == self.minute 65 | and other.tz == self.tz) 66 | return NotImplemented 67 | -------------------------------------------------------------------------------- /django_celery_beat/utils.py: -------------------------------------------------------------------------------- 1 | """Utilities.""" 2 | import datetime 3 | # -- XXX This module must not use translation as that causes 4 | # -- a recursive loader import! 5 | from datetime import timezone as datetime_timezone 6 | 7 | try: 8 | from zoneinfo import ZoneInfo # Python 3.9+ 9 | except ImportError: 10 | from backports.zoneinfo import ZoneInfo # Python 3.8 11 | 12 | from django.conf import settings 13 | from django.utils import timezone 14 | 15 | is_aware = timezone.is_aware 16 | # celery schedstate return None will make it not work 17 | NEVER_CHECK_TIMEOUT = 100000000 18 | 19 | # see Issue #222 20 | now_localtime = getattr(timezone, 'template_localtime', timezone.localtime) 21 | 22 | 23 | def make_aware(value): 24 | """Force datatime to have timezone information.""" 25 | if getattr(settings, 'USE_TZ', False): 26 | # naive datetimes are assumed to be in UTC. 27 | if timezone.is_naive(value): 28 | value = timezone.make_aware(value, datetime_timezone.utc) 29 | # then convert to the Django configured timezone. 30 | default_tz = timezone.get_default_timezone() 31 | value = timezone.localtime(value, default_tz) 32 | elif timezone.is_naive(value): 33 | # naive datetimes are assumed to be in local timezone. 34 | value = timezone.make_aware(value, timezone.get_default_timezone()) 35 | return value 36 | 37 | 38 | def now(): 39 | """Return the current date and time.""" 40 | if getattr(settings, 'USE_TZ', False): 41 | return now_localtime(timezone.now()) 42 | else: 43 | return timezone.now() 44 | 45 | 46 | def aware_now(): 47 | if getattr(settings, 'USE_TZ', True): 48 | # When USE_TZ is True, return timezone.now() 49 | return timezone.now() 50 | else: 51 | # When USE_TZ is False, use the project's timezone 52 | project_tz = ZoneInfo(getattr(settings, 'TIME_ZONE', 'UTC')) 53 | return datetime.datetime.now(project_tz) 54 | 55 | 56 | def is_database_scheduler(scheduler): 57 | """Return true if Celery is configured to use the db scheduler.""" 58 | if not scheduler: 59 | return False 60 | from kombu.utils import symbol_by_name 61 | 62 | from .schedulers import DatabaseScheduler 63 | return ( 64 | scheduler == 'django' 65 | or issubclass(symbol_by_name(scheduler), DatabaseScheduler) 66 | ) 67 | -------------------------------------------------------------------------------- /django_celery_beat/validators.py: -------------------------------------------------------------------------------- 1 | """Validators.""" 2 | 3 | import crontab 4 | from django.core.exceptions import ValidationError 5 | 6 | 7 | class _CronSlices(crontab.CronSlices): 8 | """Cron slices with customized validation.""" 9 | 10 | def __init__(self, *args): 11 | super(crontab.CronSlices, self).__init__( 12 | [_CronSlice(info) for info in crontab.S_INFO] 13 | ) 14 | self.special = None 15 | self.setall(*args) 16 | self.is_valid = self.is_self_valid 17 | 18 | @classmethod 19 | def validate(cls, *args): 20 | try: 21 | cls(*args) 22 | except Exception as e: 23 | raise ValueError(e) 24 | 25 | 26 | class _CronSlice(crontab.CronSlice): 27 | """Cron slice with custom range parser.""" 28 | 29 | def get_range(self, *vrange): 30 | ret = _CronRange(self, *vrange) 31 | if ret.dangling is not None: 32 | return [ret.dangling, ret] 33 | return [ret] 34 | 35 | 36 | class _CronRange(crontab.CronRange): 37 | """Cron range parser class.""" 38 | 39 | # rewrite whole method to raise error on bad range 40 | def parse(self, value): 41 | if value.count('/') == 1: 42 | value, seq = value.split('/') 43 | try: 44 | self.seq = self.slice.parse_value(seq) 45 | except crontab.SundayError: 46 | self.seq = 1 47 | value = "0-0" 48 | if self.seq < 1 or self.seq > self.slice.max: 49 | raise ValueError("Sequence can not be divided by zero or max") 50 | if value.count('-') == 1: 51 | vfrom, vto = value.split('-') 52 | self.vfrom = self.slice.parse_value(vfrom, sunday=0) 53 | try: 54 | self.vto = self.slice.parse_value(vto) 55 | except crontab.SundayError: 56 | if self.vfrom == 1: 57 | self.vfrom = 0 58 | else: 59 | self.dangling = 0 60 | self.vto = self.slice.parse_value(vto, sunday=6) 61 | if self.vto < self.vfrom: 62 | raise ValueError("Bad range '{0.vfrom}-{0.vto}'".format(self)) 63 | elif value == '*': 64 | self.all() 65 | else: 66 | raise ValueError('Unknown cron range value "%s"' % value) 67 | 68 | 69 | def crontab_validator(value): 70 | """Validate crontab.""" 71 | try: 72 | _CronSlices.validate(value) 73 | except ValueError as e: 74 | raise ValidationError(e) 75 | 76 | 77 | def minute_validator(value): 78 | """Validate minutes crontab value.""" 79 | _validate_crontab(value, 0) 80 | 81 | 82 | def hour_validator(value): 83 | """Validate hours crontab value.""" 84 | _validate_crontab(value, 1) 85 | 86 | 87 | def day_of_month_validator(value): 88 | """Validate day of month crontab value.""" 89 | _validate_crontab(value, 2) 90 | 91 | 92 | def month_of_year_validator(value): 93 | """Validate month crontab value.""" 94 | _validate_crontab(value, 3) 95 | 96 | 97 | def day_of_week_validator(value): 98 | """Validate day of week crontab value.""" 99 | _validate_crontab(value, 4) 100 | 101 | 102 | def _validate_crontab(value, index): 103 | tab = ['*'] * 5 104 | tab[index] = value 105 | tab = ' '.join(tab) 106 | crontab_validator(tab) 107 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2020-2030 Asif Saif Uddin and contributors. 2 | # Copyright (C) 2019 Sebastian Pipping 3 | # Licensed under the BSD License (3 clause, also known as the new BSD license) 4 | 5 | version: '3' 6 | 7 | services: 8 | base: 9 | build: 10 | context: . 11 | dockerfile: docker/base/Dockerfile 12 | tags: 13 | - django-celery-beat_base:latest 14 | command: ["sleep", "inf"] 15 | 16 | django: 17 | depends_on: 18 | - base 19 | - postgres 20 | - rabbit 21 | build: 22 | context: . 23 | dockerfile: docker/django/Dockerfile 24 | ports: 25 | - "${DJANGO_HOST:-127.0.0.1}:${DJANGO_PORT:-58000}:8000" 26 | entrypoint: ["/app/docker/django/entrypoint.sh"] 27 | command: ["python3", "manage.py", "runserver", "0.0.0.0:${DJANGO_PORT:-8000}"] 28 | tty: true 29 | volumes: 30 | - './django_celery_beat/:/app/django_celery_beat/' 31 | 32 | celery-beat: 33 | depends_on: 34 | - base 35 | - postgres 36 | - rabbit 37 | build: 38 | context: . 39 | dockerfile: docker/celery-beat/Dockerfile 40 | entrypoint: ["/app/docker/celery-beat/entrypoint.sh"] 41 | environment: 42 | CELERY_BROKER_URL: 'amqp://${RABBITMQ_USER:-guest}:${RABBITMQ_PASSWORD:-guest}@${RABBITMQ_HOST:-rabbit}:${RABBITMQ_PORT:-5672}' 43 | command: ["python3", '-m', "celery", "-A", "mysite", "beat", "-l", "info", "--scheduler", "django_celery_beat.schedulers:DatabaseScheduler"] 44 | tty: true 45 | volumes: 46 | - './django_celery_beat/:/app/django_celery_beat/' 47 | 48 | rabbit: 49 | image: rabbitmq 50 | ports: 51 | - "${RABBITMQ_PORT:-5672}:5672" 52 | postgres: 53 | image: postgres 54 | environment: 55 | POSTGRES_PASSWORD: s3cr3t 56 | -------------------------------------------------------------------------------- /docker/base/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2020-2030 Asif Saif Uddin and contributors. 2 | # Copyright (C) 2019 Sebastian Pipping 3 | # Licensed under the BSD License (3 clause, also known as the new BSD license) 4 | 5 | FROM python:3.12-slim 6 | 7 | ENV PATH=${PATH}:/root/.local/bin 8 | 9 | RUN apt-get update && apt-get install --yes --no-install-recommends \ 10 | wait-for-it 11 | 12 | RUN pip3 install --user \ 13 | build \ 14 | django-createsuperuserwithpassword \ 15 | psycopg2-binary 16 | 17 | COPY setup.cfg setup.py /app/ 18 | COPY django_celery_beat/ /app/django_celery_beat/ 19 | COPY requirements/ /app/requirements/ 20 | 21 | 22 | WORKDIR /app 23 | 24 | RUN pip3 install --user --editable ".[develop]" 25 | 26 | WORKDIR / 27 | 28 | RUN django-admin startproject mysite 29 | 30 | 31 | WORKDIR /mysite/ 32 | 33 | RUN echo 'DATABASES = {"default": {"ENGINE": "django.db.backends.postgresql", "NAME": "postgres", "USER": "postgres","PASSWORD": "s3cr3t", "HOST": "postgres", "PORT": 5432}}' >> mysite/settings.py 34 | RUN echo 'ALLOWED_HOSTS = ["*"]' >> mysite/settings.py 35 | RUN echo 'INSTALLED_APPS += ("django_celery_beat", )' >> mysite/settings.py 36 | RUN echo 'INSTALLED_APPS += ("django_createsuperuserwithpassword", )' >> mysite/settings.py 37 | 38 | COPY docker/base/celery.py mysite/celery.py 39 | -------------------------------------------------------------------------------- /docker/base/celery.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2020-2030 Asif Saif Uddin and contributors. 2 | # Copyright (C) 2019 Sebastian Pipping 3 | # Licensed under the BSD License (3 clause, also known as the new BSD license) 4 | 5 | import os 6 | 7 | from celery import Celery 8 | 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') 10 | 11 | app = Celery('mysite') 12 | -------------------------------------------------------------------------------- /docker/celery-beat/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2020-2030 Asif Saif Uddin and contributors. 2 | # Copyright (C) 2019 Sebastian Pipping 3 | # Licensed under the BSD License (3 clause, also known as the new BSD license) 4 | 5 | ARG COMPOSE_PROJECT_NAME=django-celery-beat 6 | FROM ${COMPOSE_PROJECT_NAME}_base 7 | 8 | COPY docker/celery-beat/entrypoint.sh /app/docker/celery-beat/ 9 | -------------------------------------------------------------------------------- /docker/celery-beat/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | # Copyright (C) 2020-2030 Asif Saif Uddin and contributors. 3 | # Copyright (C) 2019 Sebastian Pipping 4 | # Licensed under the BSD License (3 clause, also known as the new BSD license) 5 | 6 | set -e 7 | 8 | PS4='# ' 9 | set -x 10 | 11 | wait-for-it django:8000 # due to migrations 12 | 13 | exec "$@" 14 | -------------------------------------------------------------------------------- /docker/django/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2020-2030 Asif Saif Uddin and contributors. 2 | # Copyright (C) 2019 Sebastian Pipping 3 | # Licensed under the BSD License (3 clause, also known as the new BSD license) 4 | 5 | ARG COMPOSE_PROJECT_NAME=django-celery-beat 6 | FROM ${COMPOSE_PROJECT_NAME}_base 7 | 8 | COPY docker/django/entrypoint.sh /app/docker/django/ 9 | -------------------------------------------------------------------------------- /docker/django/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | # Copyright (C) 2020-2030 Asif Saif Uddin and contributors. 3 | # Copyright (C) 2019 Sebastian Pipping 4 | # Licensed under the BSD License (3 clause, also known as the new BSD license) 5 | 6 | set -e 7 | 8 | PS4='# ' 9 | set -x 10 | 11 | wait-for-it postgres:5432 12 | 13 | python3 manage.py migrate 14 | 15 | python3 manage.py createsuperuserwithpassword \ 16 | --username admin \ 17 | --password admin \ 18 | --email admin@example.org \ 19 | --preserve 20 | 21 | exec "$@" 22 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don\'t have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help 23 | help: 24 | @echo "Please use \`make ' where is one of" 25 | @echo " html to make standalone HTML files" 26 | @echo " dirhtml to make HTML files named index.html in directories" 27 | @echo " singlehtml to make a single large HTML file" 28 | @echo " pickle to make pickle files" 29 | @echo " json to make JSON files" 30 | @echo " htmlhelp to make HTML files and a HTML help project" 31 | @echo " qthelp to make HTML files and a qthelp project" 32 | @echo " applehelp to make an Apple Help Book" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " epub3 to make an epub3" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | @echo " apicheck to verify that all modules are present in autodoc" 51 | @echo " configcheck to verify that all modules are present in autodoc" 52 | @echo " spelling to run a spell checker on the documentation" 53 | 54 | .PHONY: clean 55 | clean: 56 | rm -rf $(BUILDDIR)/* 57 | 58 | .PHONY: html 59 | html: 60 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 61 | @echo 62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 63 | 64 | .PHONY: dirhtml 65 | dirhtml: 66 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 67 | @echo 68 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 69 | 70 | .PHONY: singlehtml 71 | singlehtml: 72 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 73 | @echo 74 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 75 | 76 | .PHONY: pickle 77 | pickle: 78 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 79 | @echo 80 | @echo "Build finished; now you can process the pickle files." 81 | 82 | .PHONY: json 83 | json: 84 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 85 | @echo 86 | @echo "Build finished; now you can process the JSON files." 87 | 88 | .PHONY: htmlhelp 89 | htmlhelp: 90 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 91 | @echo 92 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 93 | ".hhp project file in $(BUILDDIR)/htmlhelp." 94 | 95 | .PHONY: qthelp 96 | qthelp: 97 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 98 | @echo 99 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 100 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 101 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PROJ.qhcp" 102 | @echo "To view the help file:" 103 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PROJ.qhc" 104 | 105 | .PHONY: applehelp 106 | applehelp: 107 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 108 | @echo 109 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 110 | @echo "N.B. You won't be able to view it unless you put it in" \ 111 | "~/Library/Documentation/Help or install it in your application" \ 112 | "bundle." 113 | 114 | .PHONY: devhelp 115 | devhelp: 116 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 117 | @echo 118 | @echo "Build finished." 119 | @echo "To view the help file:" 120 | @echo "# mkdir -p $$HOME/.local/share/devhelp/PROJ" 121 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PROJ" 122 | @echo "# devhelp" 123 | 124 | .PHONY: epub 125 | epub: 126 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 127 | @echo 128 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 129 | 130 | .PHONY: epub3 131 | epub3: 132 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 133 | @echo 134 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." 135 | 136 | .PHONY: latex 137 | latex: 138 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 139 | @echo 140 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 141 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 142 | "(use \`make latexpdf' here to do that automatically)." 143 | 144 | .PHONY: latexpdf 145 | latexpdf: 146 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 147 | @echo "Running LaTeX files through pdflatex..." 148 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 149 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 150 | 151 | .PHONY: latexpdfja 152 | latexpdfja: 153 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 154 | @echo "Running LaTeX files through platex and dvipdfmx..." 155 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 156 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 157 | 158 | .PHONY: text 159 | text: 160 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 161 | @echo 162 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 163 | 164 | .PHONY: man 165 | man: 166 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 167 | @echo 168 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 169 | 170 | .PHONY: texinfo 171 | texinfo: 172 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 173 | @echo 174 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 175 | @echo "Run \`make' in that directory to run these through makeinfo" \ 176 | "(use \`make info' here to do that automatically)." 177 | 178 | .PHONY: info 179 | info: 180 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 181 | @echo "Running Texinfo files through makeinfo..." 182 | make -C $(BUILDDIR)/texinfo info 183 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 184 | 185 | .PHONY: gettext 186 | gettext: 187 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/django_celery_beat/locale 188 | @echo 189 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/django_celery_beat/locale." 190 | 191 | .PHONY: changes 192 | changes: 193 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 194 | @echo 195 | @echo "The overview file is in $(BUILDDIR)/changes." 196 | 197 | .PHONY: linkcheck 198 | linkcheck: 199 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 200 | @echo 201 | @echo "Link check complete; look for any errors in the above output " \ 202 | "or in $(BUILDDIR)/linkcheck/output.txt." 203 | 204 | .PHONY: doctest 205 | doctest: 206 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 207 | @echo "Testing of doctests in the sources finished, look at the " \ 208 | "results in $(BUILDDIR)/doctest/output.txt." 209 | 210 | .PHONY: coverage 211 | coverage: 212 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 213 | @echo "Testing of coverage in the sources finished, look at the " \ 214 | "results in $(BUILDDIR)/coverage/python.txt." 215 | 216 | .PHONY: apicheck 217 | apicheck: 218 | $(SPHINXBUILD) -b apicheck $(ALLSPHINXOPTS) $(BUILDDIR)/apicheck 219 | 220 | .PHONY: configcheck 221 | configcheck: 222 | $(SPHINXBUILD) -b configcheck $(ALLSPHINXOPTS) $(BUILDDIR)/configcheck 223 | 224 | .PHONY: spelling 225 | spelling: 226 | SPELLCHECK=1 $(SPHINXBUILD) -b spelling $(ALLSPHINXOPTS) $(BUILDDIR)/spelling 227 | 228 | .PHONY: xml 229 | xml: 230 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 231 | @echo 232 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 233 | 234 | .PHONY: pseudoxml 235 | pseudoxml: 236 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 237 | @echo 238 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 239 | -------------------------------------------------------------------------------- /docs/_static/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celery/django-celery-beat/0ade27bc52f92499d8c9000bacfa90bedcba3129/docs/_static/.keep -------------------------------------------------------------------------------- /docs/_templates/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celery/django-celery-beat/0ade27bc52f92499d8c9000bacfa90bedcba3129/docs/_templates/.keep -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../Changelog 2 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from sphinx_celery import conf 4 | 5 | globals().update(conf.build_config( 6 | 'django_celery_beat', __file__, 7 | project='django_celery_beat', 8 | # version_dev='2.0', 9 | # version_stable='1.4', 10 | canonical_url='http://django-celery-beat.readthedocs.io', 11 | webdomain='', 12 | github_project='celery/django-celery-beat', 13 | copyright='2016', 14 | django_settings='proj.settings', 15 | include_intersphinx={'python', 'sphinx', 'django', 'celery'}, 16 | path_additions=[os.path.join(os.pardir, 't')], 17 | html_logo='images/logo.png', 18 | html_favicon='images/favicon.ico', 19 | html_prepend_sidebars=[], 20 | apicheck_ignore_modules=[ 21 | 'django_celery_beat.apps', 22 | r'django_celery_beat.migrations.*', 23 | ], 24 | extlinks={ 25 | 'github_project': ( 26 | 'https://github.com/%s', 27 | 'GitHub project %s', 28 | ), 29 | 'github_pr': ( 30 | 'https://github.com/celery/django-celery-beat/pull/%s', 31 | 'GitHub PR #%s', 32 | ), 33 | }, 34 | extra_intersphinx_mapping={ 35 | 'django-celery-results': ( 36 | 'https://django-celery-results.readthedocs.io/en/latest/', 37 | None 38 | ), 39 | }, 40 | extensions=['sphinxcontrib_django'] 41 | )) 42 | 43 | intersphinx_mapping = globals().get('intersphinx_mapping', {}) 44 | intersphinx_mapping['celery'] = ( 45 | 'https://celery.readthedocs.io/en/main/', None) 46 | globals().update({'intersphinx_mapping': intersphinx_mapping}) 47 | -------------------------------------------------------------------------------- /docs/copyright.rst: -------------------------------------------------------------------------------- 1 | Copyright 2 | ========= 3 | 4 | *django-celery-beat User Manual* 5 | 6 | by Ask Solem & Asif Saif Uddin 7 | 8 | .. |copy| unicode:: U+000A9 .. COPYRIGHT SIGN 9 | 10 | Copyright |copy| 2016, Ask Solem, 2017-2036, Asif Saif Uddin, 11 | 12 | All rights reserved. This material may be copied or distributed only 13 | subject to the terms and conditions set forth in the `Creative Commons 14 | Attribution-ShareAlike 4.0 International 15 | `_ license. 16 | 17 | You may share and adapt the material, even for commercial purposes, but 18 | you must give the original author credit. 19 | If you alter, transform, or build upon this 20 | work, you may distribute the resulting work only under the same license or 21 | a license compatible to this one. 22 | 23 | .. note:: 24 | 25 | While the django-celery-beat *documentation* is offered under the 26 | Creative Commons *Attribution-ShareAlike 4.0 International* license 27 | the django-celery-beat *software* is offered under the 28 | `BSD License (3 Clause) `_ 29 | -------------------------------------------------------------------------------- /docs/glossary.rst: -------------------------------------------------------------------------------- 1 | .. _glossary: 2 | 3 | Glossary 4 | ======== 5 | 6 | .. glossary:: 7 | :sorted: 8 | 9 | term 10 | Description of term 11 | -------------------------------------------------------------------------------- /docs/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celery/django-celery-beat/0ade27bc52f92499d8c9000bacfa90bedcba3129/docs/images/favicon.ico -------------------------------------------------------------------------------- /docs/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celery/django-celery-beat/0ade27bc52f92499d8c9000bacfa90bedcba3129/docs/images/logo.png -------------------------------------------------------------------------------- /docs/includes/installation.txt: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | You can install django-celery-beat either via the Python Package Index (PyPI) 5 | or from source. 6 | 7 | To install using `pip`,:: 8 | 9 | $ pip install --upgrade django-celery-beat 10 | 11 | Installing the current default branch 12 | ------------------------------------- 13 | 14 | $ python3 -m venv .venv 15 | $ source .venv/bin/activate 16 | $ pip install --upgrade pip 17 | $ pip install git+https://github.com/celery/django-celery-beat.git 18 | 19 | Downloading and installing from source 20 | -------------------------------------- 21 | 22 | Download the latest version of django-celery-beat from 23 | http://pypi.python.org/pypi/django-celery-beat 24 | 25 | You can install it by doing the following,:: 26 | 27 | $ python3 -m venv .venv 28 | $ source .venv/bin/activate 29 | $ pip install --upgrade build pip 30 | $ tar xvfz django-celery-beat-0.0.0.tar.gz 31 | $ cd django-celery-beat-0.0.0 32 | $ python -m build 33 | $ pip install . 34 | 35 | Using the development version 36 | ----------------------------- 37 | 38 | With pip 39 | ~~~~~~~~ 40 | 41 | You can install the latest snapshot of django-celery-beat using the following 42 | pip command:: 43 | 44 | $ pip install https://github.com/celery/django-celery-beat/zipball/master#egg=django-celery-beat 45 | -------------------------------------------------------------------------------- /docs/includes/introduction.txt: -------------------------------------------------------------------------------- 1 | :Version: 2.8.1 2 | :Web: http://django-celery-beat.readthedocs.io/ 3 | :Download: http://pypi.python.org/pypi/django-celery-beat 4 | :Source: http://github.com/celery/django-celery-beat 5 | :Keywords: django, celery, beat, periodic task, cron, scheduling 6 | 7 | About 8 | ===== 9 | 10 | This extension enables you to store the periodic task schedule in the 11 | database. 12 | 13 | The periodic tasks can be managed from the Django Admin interface, where you 14 | can create, edit and delete periodic tasks and how often they should run. 15 | 16 | Using the Extension 17 | =================== 18 | 19 | Usage and installation instructions for this extension are available 20 | from the :ref:`Celery documentation `. 21 | 22 | Important Warning about Time Zones 23 | ================================== 24 | 25 | .. warning:: 26 | 27 | If you change the Django :setting:`TIME_ZONE` setting your periodic task schedule 28 | will still be based on the old timezone. 29 | 30 | To fix that you would have to reset the "last run time" for each periodic 31 | task: 32 | 33 | >>> from django_celery_beat.models import PeriodicTask, PeriodicTasks 34 | >>> PeriodicTask.objects.update(last_run_at=None) 35 | >>> PeriodicTasks.update_changed() 36 | 37 | Note that this will reset the state as if the periodic tasks have never run 38 | before. 39 | 40 | Models 41 | ====== 42 | 43 | - :class:`django_celery_beat.models.PeriodicTask` 44 | 45 | This model defines a single periodic task to be run. 46 | 47 | It must be associated with a schedule, which defines how often the task should 48 | run. 49 | 50 | - :class:`django_celery_beat.models.IntervalSchedule` 51 | 52 | A schedule that runs at a specific interval (e.g. every 5 seconds). 53 | 54 | - :class:`django_celery_beat.models.CrontabSchedule` 55 | 56 | A schedule with fields like entries in cron: 57 | ``minute hour day-of-week day_of_month month_of_year``. 58 | 59 | - :class:`django_celery_beat.models.PeriodicTasks` 60 | 61 | This model is only used as an index to keep track of when the schedule has 62 | changed. 63 | 64 | Whenever you update a :class:`~django_celery_beat.models.PeriodicTask`, a counter in this table is also 65 | incremented, which tells the ``celery beat`` service to reload the schedule 66 | from the database. 67 | 68 | If you update periodic tasks in bulk, you will need to update the counter 69 | manually: 70 | 71 | >>> from django_celery_beat.models import PeriodicTasks 72 | >>> PeriodicTasks.changed() 73 | 74 | Example creating interval-based periodic task 75 | --------------------------------------------- 76 | 77 | To create a periodic task executing at an interval you must first 78 | create the interval object:: 79 | 80 | >>> from django_celery_beat.models import PeriodicTask, IntervalSchedule 81 | 82 | # executes every 10 seconds. 83 | >>> schedule, created = IntervalSchedule.objects.get_or_create( 84 | ... every=10, 85 | ... period=IntervalSchedule.SECONDS, 86 | ... ) 87 | 88 | That's all the fields you need: a period type and the frequency. 89 | 90 | You can choose between a specific set of periods: 91 | 92 | 93 | - :data:`IntervalSchedule.DAYS ` 94 | - :data:`IntervalSchedule.HOURS ` 95 | - :data:`IntervalSchedule.MINUTES ` 96 | - :data:`IntervalSchedule.SECONDS ` 97 | - :data:`IntervalSchedule.MICROSECONDS ` 98 | 99 | .. note:: 100 | 101 | If you have multiple periodic tasks executing every 10 seconds, 102 | then they should all point to the same schedule object. 103 | 104 | There's also a "choices tuple" available should you need to present this 105 | to the user: 106 | 107 | >>> IntervalSchedule.PERIOD_CHOICES 108 | 109 | 110 | Now that we have defined the schedule object, we can create the periodic task 111 | entry: 112 | 113 | >>> PeriodicTask.objects.create( 114 | ... interval=schedule, # we created this above. 115 | ... name='Importing contacts', # simply describes this periodic task. 116 | ... task='proj.tasks.import_contacts', # name of task. 117 | ... ) 118 | 119 | 120 | Note that this is a very basic example, you can also specify the arguments 121 | and keyword arguments used to execute the task, the ``queue`` to send it 122 | to [#f1]_, and set an expiry time. 123 | 124 | Here's an example specifying the arguments, note how JSON serialization is 125 | required: 126 | 127 | >>> import json 128 | >>> from datetime import datetime, timedelta 129 | 130 | >>> PeriodicTask.objects.create( 131 | ... interval=schedule, # we created this above. 132 | ... name='Importing contacts', # simply describes this periodic task. 133 | ... task='proj.tasks.import_contacts', # name of task. 134 | ... args=json.dumps(['arg1', 'arg2']), 135 | ... kwargs=json.dumps({ 136 | ... 'be_careful': True, 137 | ... }), 138 | ... expires=datetime.utcnow() + timedelta(seconds=30) 139 | ... ) 140 | 141 | 142 | .. [#f1] you can also use low-level AMQP routing using the ``exchange`` and 143 | ``routing_key`` fields. 144 | 145 | Example creating crontab-based periodic task 146 | -------------------------------------------- 147 | 148 | A crontab schedule has the fields: ``minute``, ``hour``, ``day_of_week``, 149 | ``day_of_month`` and ``month_of_year``, so if you want the equivalent 150 | of a ``30 * * * *`` (execute at 30 minutes past the hour every hour) crontab 151 | entry you specify: 152 | 153 | >>> from django_celery_beat.models import CrontabSchedule, PeriodicTask 154 | >>> schedule, _ = CrontabSchedule.objects.get_or_create( 155 | ... minute='30', 156 | ... hour='*', 157 | ... day_of_week='*', 158 | ... day_of_month='*', 159 | ... month_of_year='*', 160 | ... ) 161 | 162 | 163 | Then to create a periodic task using this schedule, use the same approach as 164 | the interval-based periodic task earlier in this document, but instead 165 | of ``interval=schedule``, specify ``crontab=schedule``: 166 | 167 | >>> PeriodicTask.objects.create( 168 | ... crontab=schedule, 169 | ... name='Importing contacts', 170 | ... task='proj.tasks.import_contacts', 171 | ... ) 172 | 173 | Temporarily disable a periodic task 174 | ----------------------------------- 175 | 176 | You can use the ``enabled`` flag to temporarily disable a periodic task: 177 | 178 | >>> periodic_task.enabled = False 179 | >>> periodic_task.save() 180 | 181 | 182 | Example running periodic tasks 183 | ------------------------------ 184 | 185 | The periodic tasks still need 'workers' to execute them. 186 | So make sure the default **Celery** package is installed. 187 | (If not installed, please follow the installation instructions 188 | here: :github_project:`celery/celery`) 189 | 190 | Both the worker and beat services need to be running at the same time. 191 | 192 | 1. Start a Celery worker service (specify your Django project name): 193 | 194 | .. code-block:: sh 195 | 196 | $ celery -A [project-name] worker --loglevel=info 197 | 198 | 199 | 2. As a separate process, start the beat service (specify the Django scheduler): 200 | 201 | .. code-block:: sh 202 | 203 | $ celery -A [project-name] beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler 204 | 205 | 206 | **OR** you can use the -S (scheduler flag), for more options see ``celery beat --help``): 207 | 208 | .. code-block:: sh 209 | 210 | $ celery -A [project-name] beat -l info -S django 211 | 212 | **OR** you can set the scheduler through Django's settings: 213 | 214 | .. code-block:: sh 215 | 216 | CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler' 217 | 218 | 219 | 220 | Also, as an alternative, you can run the two steps above (worker and beat services) 221 | with only one command (recommended for **development environment only**): 222 | 223 | .. code-block:: sh 224 | 225 | $ celery -A [project-name] worker --beat --scheduler django --loglevel=info 226 | 227 | 228 | 3. Now you can add and manage your periodic tasks from the Django Admin interface. 229 | 230 | 231 | 232 | Working with django-celery-results 233 | ----------------------------------- 234 | 235 | Now you can store :attr:`PeriodicTask.name ` 236 | to django-celery-results (``TaskResult.periodic_task_name``). 237 | 238 | Suppose we have two periodic tasks, their schedules are different, but the tasks are the same. 239 | 240 | +-----------+------------------+------+---------------+ 241 | | name | task | args | schedule | 242 | +===========+==================+======+===============+ 243 | | schedule1 | some.celery.task | (1,) | every hour | 244 | | schedule2 | some.celery.task | (2,) | every 2 hours | 245 | +-----------+------------------+------+---------------+ 246 | 247 | Now you can distinguish the source of the task from the results by the ``periodic_task_name`` field. 248 | 249 | +--------+------------------+--------------------+ 250 | | id | task_name | periodic_task_name | 251 | +========+==================+====================+ 252 | | uuid1 | some.celery.task | schedule1 | 253 | | uuid2 | some.celery.task | schedule1 | 254 | | uuid3 | some.celery.task | schedule2 | 255 | | uuid4 | some.celery.task | schedule2 | 256 | +--------+------------------+--------------------+ 257 | 258 | (more technical details here: :github_pr:`477`, :github_pr:`261`) 259 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ======================================================================= 2 | django-celery-beat - Database-backed Periodic Tasks 3 | ======================================================================= 4 | 5 | .. include:: includes/introduction.txt 6 | 7 | Contents 8 | ======== 9 | 10 | .. toctree:: 11 | :maxdepth: 1 12 | 13 | copyright 14 | 15 | .. toctree:: 16 | :maxdepth: 2 17 | 18 | reference/index 19 | 20 | .. toctree:: 21 | :maxdepth: 1 22 | 23 | changelog 24 | glossary 25 | 26 | Indices and tables 27 | ================== 28 | 29 | * :ref:`genindex` 30 | * :ref:`modindex` 31 | * :ref:`search` 32 | 33 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. epub3 to make an epub3 31 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 32 | echo. text to make text files 33 | echo. man to make manual pages 34 | echo. texinfo to make Texinfo files 35 | echo. gettext to make PO message catalogs 36 | echo. changes to make an overview over all changed/added/deprecated items 37 | echo. xml to make Docutils-native XML files 38 | echo. pseudoxml to make pseudoxml-XML files for display purposes 39 | echo. linkcheck to check all external links for integrity 40 | echo. doctest to run all doctests embedded in the documentation if enabled 41 | echo. coverage to run coverage check of the documentation if enabled 42 | goto end 43 | ) 44 | 45 | if "%1" == "clean" ( 46 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 47 | del /q /s %BUILDDIR%\* 48 | goto end 49 | ) 50 | 51 | 52 | REM Check if sphinx-build is available and fallback to Python version if any 53 | %SPHINXBUILD% 1>NUL 2>NUL 54 | if errorlevel 9009 goto sphinx_python 55 | goto sphinx_ok 56 | 57 | :sphinx_python 58 | 59 | set SPHINXBUILD=python -m sphinx.__init__ 60 | %SPHINXBUILD% 2> nul 61 | if errorlevel 9009 ( 62 | echo. 63 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 64 | echo.installed, then set the SPHINXBUILD environment variable to point 65 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 66 | echo.may add the Sphinx directory to PATH. 67 | echo. 68 | echo.If you don't have Sphinx installed, grab it from 69 | echo.http://sphinx-doc.org/ 70 | exit /b 1 71 | ) 72 | 73 | :sphinx_ok 74 | 75 | 76 | if "%1" == "html" ( 77 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 78 | if errorlevel 1 exit /b 1 79 | echo. 80 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 81 | goto end 82 | ) 83 | 84 | if "%1" == "dirhtml" ( 85 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 86 | if errorlevel 1 exit /b 1 87 | echo. 88 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 89 | goto end 90 | ) 91 | 92 | if "%1" == "singlehtml" ( 93 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 94 | if errorlevel 1 exit /b 1 95 | echo. 96 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 97 | goto end 98 | ) 99 | 100 | if "%1" == "pickle" ( 101 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 102 | if errorlevel 1 exit /b 1 103 | echo. 104 | echo.Build finished; now you can process the pickle files. 105 | goto end 106 | ) 107 | 108 | if "%1" == "json" ( 109 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 110 | if errorlevel 1 exit /b 1 111 | echo. 112 | echo.Build finished; now you can process the JSON files. 113 | goto end 114 | ) 115 | 116 | if "%1" == "htmlhelp" ( 117 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 118 | if errorlevel 1 exit /b 1 119 | echo. 120 | echo.Build finished; now you can run HTML Help Workshop with the ^ 121 | .hhp project file in %BUILDDIR%/htmlhelp. 122 | goto end 123 | ) 124 | 125 | if "%1" == "qthelp" ( 126 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 127 | if errorlevel 1 exit /b 1 128 | echo. 129 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 130 | .qhcp project file in %BUILDDIR%/qthelp, like this: 131 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\PROJ.qhcp 132 | echo.To view the help file: 133 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\PROJ.ghc 134 | goto end 135 | ) 136 | 137 | if "%1" == "devhelp" ( 138 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 139 | if errorlevel 1 exit /b 1 140 | echo. 141 | echo.Build finished. 142 | goto end 143 | ) 144 | 145 | if "%1" == "epub" ( 146 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 147 | if errorlevel 1 exit /b 1 148 | echo. 149 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 150 | goto end 151 | ) 152 | 153 | if "%1" == "epub3" ( 154 | %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 155 | if errorlevel 1 exit /b 1 156 | echo. 157 | echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. 158 | goto end 159 | ) 160 | 161 | if "%1" == "latex" ( 162 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 163 | if errorlevel 1 exit /b 1 164 | echo. 165 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 166 | goto end 167 | ) 168 | 169 | if "%1" == "latexpdf" ( 170 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 171 | cd %BUILDDIR%/latex 172 | make all-pdf 173 | cd %~dp0 174 | echo. 175 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 176 | goto end 177 | ) 178 | 179 | if "%1" == "latexpdfja" ( 180 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 181 | cd %BUILDDIR%/latex 182 | make all-pdf-ja 183 | cd %~dp0 184 | echo. 185 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 186 | goto end 187 | ) 188 | 189 | if "%1" == "text" ( 190 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 191 | if errorlevel 1 exit /b 1 192 | echo. 193 | echo.Build finished. The text files are in %BUILDDIR%/text. 194 | goto end 195 | ) 196 | 197 | if "%1" == "man" ( 198 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 199 | if errorlevel 1 exit /b 1 200 | echo. 201 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 202 | goto end 203 | ) 204 | 205 | if "%1" == "texinfo" ( 206 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 207 | if errorlevel 1 exit /b 1 208 | echo. 209 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 210 | goto end 211 | ) 212 | 213 | if "%1" == "gettext" ( 214 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/django_celery_beat/locale 215 | if errorlevel 1 exit /b 1 216 | echo. 217 | echo.Build finished. The message catalogs are in %BUILDDIR%/django_celery_beat/locale. 218 | goto end 219 | ) 220 | 221 | if "%1" == "changes" ( 222 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 223 | if errorlevel 1 exit /b 1 224 | echo. 225 | echo.The overview file is in %BUILDDIR%/changes. 226 | goto end 227 | ) 228 | 229 | if "%1" == "linkcheck" ( 230 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 231 | if errorlevel 1 exit /b 1 232 | echo. 233 | echo.Link check complete; look for any errors in the above output ^ 234 | or in %BUILDDIR%/linkcheck/output.txt. 235 | goto end 236 | ) 237 | 238 | if "%1" == "doctest" ( 239 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 240 | if errorlevel 1 exit /b 1 241 | echo. 242 | echo.Testing of doctests in the sources finished, look at the ^ 243 | results in %BUILDDIR%/doctest/output.txt. 244 | goto end 245 | ) 246 | 247 | if "%1" == "coverage" ( 248 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 249 | if errorlevel 1 exit /b 1 250 | echo. 251 | echo.Testing of coverage in the sources finished, look at the ^ 252 | results in %BUILDDIR%/coverage/python.txt. 253 | goto end 254 | ) 255 | 256 | if "%1" == "xml" ( 257 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 258 | if errorlevel 1 exit /b 1 259 | echo. 260 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 261 | goto end 262 | ) 263 | 264 | if "%1" == "pseudoxml" ( 265 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 266 | if errorlevel 1 exit /b 1 267 | echo. 268 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 269 | goto end 270 | ) 271 | 272 | :end 273 | -------------------------------------------------------------------------------- /docs/reference/django-celery-beat.admin.rst: -------------------------------------------------------------------------------- 1 | ===================================================== 2 | ``django_celery_beat.admin`` 3 | ===================================================== 4 | 5 | .. contents:: 6 | :local: 7 | .. currentmodule:: django_celery_beat.admin 8 | 9 | .. automodule:: django_celery_beat.admin 10 | :members: 11 | :undoc-members: 12 | -------------------------------------------------------------------------------- /docs/reference/django-celery-beat.clockedschedule.rst: -------------------------------------------------------------------------------- 1 | ===================================================== 2 | ``django_celery_beat.clockedschedule`` 3 | ===================================================== 4 | 5 | .. contents:: 6 | :local: 7 | .. currentmodule:: django_celery_beat.clockedschedule 8 | 9 | .. automodule:: django_celery_beat.clockedschedule 10 | :members: 11 | :undoc-members: 12 | -------------------------------------------------------------------------------- /docs/reference/django-celery-beat.models.rst: -------------------------------------------------------------------------------- 1 | ===================================================== 2 | ``django_celery_beat.models`` 3 | ===================================================== 4 | 5 | .. contents:: 6 | :local: 7 | .. currentmodule:: django_celery_beat.models 8 | 9 | .. automodule:: django_celery_beat.models 10 | :members: 11 | -------------------------------------------------------------------------------- /docs/reference/django-celery-beat.querysets.rst: -------------------------------------------------------------------------------- 1 | ===================================================== 2 | ``django_celery_beat.querysets`` 3 | ===================================================== 4 | 5 | .. contents:: 6 | :local: 7 | .. currentmodule:: django_celery_beat.querysets 8 | 9 | .. automodule:: django_celery_beat.querysets 10 | :members: 11 | :undoc-members: 12 | -------------------------------------------------------------------------------- /docs/reference/django-celery-beat.rst: -------------------------------------------------------------------------------- 1 | ===================================================== 2 | ``django_celery_beat`` 3 | ===================================================== 4 | 5 | .. contents:: 6 | :local: 7 | .. currentmodule:: django_celery_beat 8 | 9 | .. automodule:: django_celery_beat 10 | :members: 11 | :undoc-members: 12 | -------------------------------------------------------------------------------- /docs/reference/django-celery-beat.schedulers.rst: -------------------------------------------------------------------------------- 1 | ===================================================== 2 | ``django_celery_beat.schedulers`` 3 | ===================================================== 4 | 5 | .. contents:: 6 | :local: 7 | .. currentmodule:: django_celery_beat.schedulers 8 | 9 | .. automodule:: django_celery_beat.schedulers 10 | :members: 11 | :undoc-members: 12 | -------------------------------------------------------------------------------- /docs/reference/django-celery-beat.signals.rst: -------------------------------------------------------------------------------- 1 | ===================================================== 2 | ``django_celery_beat.signals`` 3 | ===================================================== 4 | 5 | .. contents:: 6 | :local: 7 | .. currentmodule:: django_celery_beat.signals 8 | 9 | .. automodule:: django_celery_beat.signals 10 | :members: 11 | :undoc-members: 12 | -------------------------------------------------------------------------------- /docs/reference/django-celery-beat.tzcrontab.rst: -------------------------------------------------------------------------------- 1 | ===================================================== 2 | ``django_celery_beat.tzcrontab`` 3 | ===================================================== 4 | 5 | .. contents:: 6 | :local: 7 | .. currentmodule:: django_celery_beat.tzcrontab 8 | 9 | .. automodule:: django_celery_beat.tzcrontab 10 | :members: 11 | :undoc-members: 12 | -------------------------------------------------------------------------------- /docs/reference/django-celery-beat.utils.rst: -------------------------------------------------------------------------------- 1 | ===================================================== 2 | ``django_celery_beat.utils`` 3 | ===================================================== 4 | 5 | .. contents:: 6 | :local: 7 | .. currentmodule:: django_celery_beat.utils 8 | 9 | .. automodule:: django_celery_beat.utils 10 | :members: 11 | :undoc-members: 12 | -------------------------------------------------------------------------------- /docs/reference/django-celery-beat.validators.rst: -------------------------------------------------------------------------------- 1 | ===================================================== 2 | ``django_celery_beat.validators`` 3 | ===================================================== 4 | 5 | .. contents:: 6 | :local: 7 | .. currentmodule:: django_celery_beat.validators 8 | 9 | .. automodule:: django_celery_beat.validators 10 | :members: 11 | :undoc-members: 12 | -------------------------------------------------------------------------------- /docs/reference/index.rst: -------------------------------------------------------------------------------- 1 | .. _apiref: 2 | 3 | =============== 4 | API Reference 5 | =============== 6 | 7 | :Release: |version| 8 | :Date: |today| 9 | 10 | .. toctree:: 11 | :maxdepth: 1 12 | 13 | django-celery-beat 14 | django-celery-beat.models 15 | django-celery-beat.tzcrontab 16 | django-celery-beat.querysets 17 | django-celery-beat.schedulers 18 | django-celery-beat.admin 19 | django-celery-beat.utils 20 | django-celery-beat.validators 21 | django-celery-beat.clockedschedule 22 | django-celery-beat.signals 23 | -------------------------------------------------------------------------------- /docs/templates/readme.txt: -------------------------------------------------------------------------------- 1 | ===================================================================== 2 | Database-backed Periodic Tasks 3 | ===================================================================== 4 | 5 | |build-status| |coverage| |license| |wheel| |pyversion| |pyimp| 6 | 7 | .. include:: ../includes/introduction.txt 8 | 9 | .. include:: ../includes/installation.txt 10 | 11 | .. |build-status| image:: https://secure.travis-ci.org/celery/django-celery-beat.svg?branch=master 12 | :alt: Build status 13 | :target: https://travis-ci.org/celery/django-celery-beat 14 | 15 | .. |coverage| image:: https://codecov.io/github/celery/django-celery-beat/coverage.svg?branch=master 16 | :target: https://codecov.io/github/celery/django-celery-beat?branch=master 17 | 18 | .. |license| image:: https://img.shields.io/pypi/l/django-celery-beat.svg 19 | :alt: BSD License 20 | :target: https://opensource.org/licenses/BSD-3-Clause 21 | 22 | .. |wheel| image:: https://img.shields.io/pypi/wheel/django-celery-beat.svg 23 | :alt: django-celery-beat can be installed via wheel 24 | :target: http://pypi.python.org/pypi/django-celery-beat/ 25 | 26 | .. |pyversion| image:: https://img.shields.io/pypi/pyversions/django-celery-beat.svg 27 | :alt: Supported Python versions. 28 | :target: http://pypi.python.org/pypi/django-celery-beat/ 29 | 30 | .. |pyimp| image:: https://img.shields.io/pypi/implementation/django-celery-beat.svg 31 | :alt: Support Python implementations. 32 | :target: http://pypi.python.org/pypi/django-celery-beat/ 33 | -------------------------------------------------------------------------------- /issue_template.md: -------------------------------------------------------------------------------- 1 | ### Summary: 2 | 3 | Include a *brief* description of the problem here, and fill out the version info below. 4 | 5 | * Celery Version: 6 | * Celery-Beat Version: 7 | 8 | ### Exact steps to reproduce the issue: 9 | 1. 10 | 2. 11 | 3. 12 | 13 | ### Detailed information 14 | 15 | 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. 16 | 17 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import sys 4 | 5 | if __name__ == '__main__': 6 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 't.proj.settings') 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.ruff] 2 | target-version = "py38" 3 | 4 | lint.select = [ 5 | "A", # flake8-builtins 6 | "AIR", # Airflow 7 | "ASYNC", # flake8-async 8 | "BLE", # flake8-blind-except 9 | "C4", # flake8-comprehensions 10 | "C90", # McCabe cyclomatic complexity 11 | "DJ", # flake8-django 12 | "E", # pycodestyle 13 | "EXE", # flake8-executable 14 | "F", # Pyflakes 15 | "FA", # flake8-future-annotations 16 | "FLY", # flynt 17 | "FURB", # refurb 18 | "G", # flake8-logging-format 19 | "ICN", # flake8-import-conventions 20 | "INP", # flake8-no-pep420 21 | "ISC", # flake8-implicit-str-concat 22 | "LOG", # flake8-logging 23 | "N", # pep8-naming 24 | "NPY", # NumPy-specific rules 25 | "PD", # pandas-vet 26 | "PERF", # Perflint 27 | "PGH", # pygrep-hooks 28 | "PIE", # flake8-pie 29 | "PL", # Pylint 30 | "PT", # flake8-pytest-style 31 | "RSE", # flake8-raise 32 | "S", # flake8-bandit 33 | "SIM", # flake8-simplify 34 | "SLOT", # flake8-slots 35 | "T10", # flake8-debugger 36 | "TCH", # flake8-type-checking 37 | "TID", # flake8-tidy-imports 38 | "UP", # pyupgrade 39 | "W", # pycodestyle 40 | "YTT", # flake8-2020 41 | # "ANN", # flake8-annotations 42 | # "ARG", # flake8-unused-arguments 43 | # "B", # flake8-bugbear 44 | # "COM", # flake8-commas 45 | # "CPY", # flake8-copyright 46 | # "D", # pydocstyle 47 | # "DTZ", # flake8-datetimez 48 | # "EM", # flake8-errmsg 49 | # "ERA", # eradicate 50 | # "FBT", # flake8-boolean-trap 51 | # "FIX", # flake8-fixme 52 | # "I", # isort 53 | # "INT", # flake8-gettext 54 | # "PTH", # flake8-use-pathlib 55 | # "PYI", # flake8-pyi 56 | # "Q", # flake8-quotes 57 | # "RET", # flake8-return 58 | # "RUF", # Ruff-specific rules 59 | # "SLF", # flake8-self 60 | # "T20", # flake8-print 61 | # "TD", # flake8-todos 62 | # "TRY", # tryceratops 63 | ] 64 | lint.ignore = [ 65 | "DJ001", 66 | "DJ006", 67 | "DJ008", 68 | "DJ012", 69 | "N801", 70 | "N802", 71 | "N803", 72 | "N806", 73 | "PIE790", 74 | "PT009", 75 | "PT027", 76 | "UP031", 77 | "UP032", 78 | ] 79 | lint.per-file-ignores."*/migrations/*" = [ 80 | "C405", 81 | "D", 82 | "E501", 83 | "I", 84 | "PGH004", 85 | ] 86 | lint.per-file-ignores."django_celery_beat/models.py" = [ 87 | "ISC002", 88 | ] 89 | lint.per-file-ignores."django_celery_beat/schedulers.py" = [ 90 | "PERF203", 91 | "SIM105", 92 | ] 93 | lint.per-file-ignores."django_celery_beat/validators.py" = [ 94 | "BLE001", 95 | ] 96 | lint.per-file-ignores."docker/base/celery.py" = [ 97 | "INP001", 98 | ] 99 | lint.per-file-ignores."docs/conf.py" = [ 100 | "INP001", 101 | ] 102 | lint.per-file-ignores."setup.py" = [ 103 | "EXE001", 104 | "FURB129", 105 | "SIM115", 106 | ] 107 | lint.per-file-ignores."t/*" = [ 108 | "S101", 109 | ] 110 | lint.per-file-ignores."t/proj/__init__.py" = [ 111 | "PGH004", 112 | ] 113 | lint.per-file-ignores."t/proj/settings.py" = [ 114 | "S105", 115 | ] 116 | lint.per-file-ignores."t/unit/conftest.py" = [ 117 | "F401", 118 | ] 119 | lint.per-file-ignores."t/unit/test_schedulers.py" = [ 120 | "C408", 121 | "PERF102", 122 | "PT018", 123 | ] 124 | lint.pylint.allow-magic-value-types = [ 125 | "float", 126 | "int", 127 | "str", 128 | ] 129 | lint.pylint.max-args = 8 # Default: 5 130 | 131 | [tool.coverage.run] 132 | branch = true 133 | cover_pylib = false 134 | include = [ "*django_celery_beat/*" ] 135 | omit = [ "django_celery_beat.tests.*" ] 136 | 137 | [tool.coverage.report] 138 | exclude_lines = [ 139 | "pragma: no cover", 140 | "if TYPE_CHECKING:", 141 | "except ImportError:", 142 | ] 143 | omit = [ 144 | "*/python?.?/*", 145 | "*/site-packages/*", 146 | "*/pypy/*", 147 | "*/.tox/*", 148 | "*/docker/*", 149 | "*/docs/*", 150 | "*/test_*.py", 151 | ] 152 | -------------------------------------------------------------------------------- /requirements/default.txt: -------------------------------------------------------------------------------- 1 | celery>=5.2.3,<6.0 2 | importlib-metadata<5.0; python_version<"3.8" # TODO: remove this when celery >= 5.3.0 3 | django-timezone-field>=5.0 4 | backports.zoneinfo; python_version<"3.9" 5 | tzdata 6 | python-crontab>=2.3.4 7 | cron-descriptor>=1.2.32 8 | -------------------------------------------------------------------------------- /requirements/docs.txt: -------------------------------------------------------------------------------- 1 | Django>=2.2,<6.0 2 | sphinxcontrib-django 3 | https://github.com/celery/sphinx_celery/archive/master.zip 4 | https://github.com/celery/kombu/zipball/main#egg=kombu 5 | https://github.com/celery/celery/zipball/main#egg=celery 6 | -r default.txt 7 | -------------------------------------------------------------------------------- /requirements/pkgutils.txt: -------------------------------------------------------------------------------- 1 | setuptools>=40.8.0 2 | wheel>=0.33.1 3 | flake8>=3.8.3 4 | tox>=2.3.1 5 | sphinx2rst>=1.0 6 | bumpversion 7 | pydocstyle 8 | -------------------------------------------------------------------------------- /requirements/runtime.txt: -------------------------------------------------------------------------------- 1 | Django>=2.2,<6.0 2 | -------------------------------------------------------------------------------- /requirements/test-ci.txt: -------------------------------------------------------------------------------- 1 | pytest-cov 2 | codecov 3 | -------------------------------------------------------------------------------- /requirements/test-django.txt: -------------------------------------------------------------------------------- 1 | Django>=3.2,<6.0 2 | -------------------------------------------------------------------------------- /requirements/test.txt: -------------------------------------------------------------------------------- 1 | # Base dependencies (common for all Python versions) 2 | ephem 3 | pytest-timeout 4 | 5 | # Conditional dependencies 6 | pytest>=6.2.5,<8.0; python_version < '3.9' # Python 3.8 only 7 | pytest>=6.2.5,<9.0; python_version >= '3.9' # Python 3.9+ only 8 | pytest-django>=4.5.2,<4.6.0; python_version < '3.9' # Python 3.8 only 9 | pytest-django>=4.5.2,<5.0; python_version >= '3.9' # Python 3.9+ only 10 | backports.zoneinfo; python_version < '3.9' # Python 3.8 only 11 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | testpaths = t/unit/ 3 | python_classes = test_* 4 | DJANGO_SETTINGS_MODULE=t.proj.settings 5 | 6 | [flake8] 7 | # classes can be lowercase, arguments and variables can be uppercase 8 | # whenever it makes the code more readable. 9 | ignore = N806, N802, N801, N803, W503, W504 10 | exclude = 11 | **/migrations/*.py 12 | max-line-length = 88 13 | 14 | [pep257] 15 | ignore = D102,D104,D203,D105,D213 16 | match-dir = [^migrations] 17 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import codecs 4 | import os 5 | import re 6 | 7 | import setuptools 8 | 9 | NAME = 'django-celery-beat' 10 | PACKAGE = 'django_celery_beat' 11 | 12 | # -*- Classifiers -*- 13 | 14 | classes = """ 15 | Development Status :: 5 - Production/Stable 16 | License :: OSI Approved :: BSD License 17 | Programming Language :: Python 18 | Programming Language :: Python :: 3 19 | Programming Language :: Python :: 3.8 20 | Programming Language :: Python :: 3.9 21 | Programming Language :: Python :: 3.10 22 | Programming Language :: Python :: 3.11 23 | Programming Language :: Python :: 3.12 24 | Programming Language :: Python :: 3.13 25 | Programming Language :: Python :: Implementation :: CPython 26 | Programming Language :: Python :: Implementation :: PyPy 27 | Framework :: Django 28 | Framework :: Django :: 3.2 29 | Framework :: Django :: 4.1 30 | Framework :: Django :: 4.2 31 | Framework :: Django :: 5.0 32 | Framework :: Django :: 5.1 33 | Framework :: Django :: 5.2 34 | Operating System :: OS Independent 35 | Topic :: Communications 36 | Topic :: System :: Distributed Computing 37 | Topic :: Software Development :: Libraries :: Python Modules 38 | """ 39 | classifiers = [s.strip() for s in classes.split('\n') if s] 40 | 41 | # -*- Distribution Meta -*- 42 | 43 | re_meta = re.compile(r'__(\w+?)__\s*=\s*(.*)') 44 | re_doc = re.compile(r'^"""(.+?)"""') 45 | 46 | 47 | def add_default(m): 48 | attr_name, attr_value = m.groups() 49 | return ((attr_name, attr_value.strip("\"'")),) 50 | 51 | 52 | def add_doc(m): 53 | return (('doc', m.groups()[0]),) 54 | 55 | 56 | pats = {re_meta: add_default, 57 | re_doc: add_doc} 58 | here = os.path.abspath(os.path.dirname(__file__)) 59 | with open(os.path.join(here, PACKAGE, '__init__.py')) as meta_fh: 60 | meta = {} 61 | for line in meta_fh: 62 | if line.strip() == '# -eof meta-': 63 | break 64 | for pattern, handler in pats.items(): 65 | m = pattern.match(line.strip()) 66 | if m: 67 | meta.update(handler(m)) 68 | 69 | # -*- Installation Requires -*- 70 | 71 | 72 | def strip_comments(line): 73 | return line.split('#', 1)[0].strip() 74 | 75 | 76 | def _pip_requirement(req): 77 | if req.startswith('-r '): 78 | _, path = req.split() 79 | return reqs(*path.split('/')) 80 | return [req] 81 | 82 | 83 | def _reqs(*f): 84 | return [ 85 | _pip_requirement(r) for r in ( 86 | strip_comments(line) for line in open( 87 | os.path.join(os.getcwd(), 'requirements', *f)).readlines() 88 | ) if r] 89 | 90 | 91 | def reqs(*f): 92 | return [req for subreq in _reqs(*f) for req in subreq] 93 | 94 | # -*- Long Description -*- 95 | 96 | 97 | if os.path.exists('README.rst'): 98 | long_description = codecs.open('README.rst', 'r', 'utf-8').read() 99 | long_description_content_type = 'text/x-rst' 100 | else: 101 | long_description = f'See http://pypi.python.org/pypi/{NAME}' 102 | long_description_content_type = 'text/markdown' 103 | 104 | # -*- %%% -*- 105 | 106 | setuptools.setup( 107 | name=NAME, 108 | packages=setuptools.find_packages(exclude=[ 109 | 'ez_setup', 't', 't.*', 110 | ]), 111 | version=meta['version'], 112 | description=meta['doc'], 113 | long_description=long_description, 114 | long_description_content_type=long_description_content_type, 115 | keywords='django celery beat periodic task database', 116 | author=meta['author'], 117 | author_email=meta['contact'], 118 | url=meta['homepage'], 119 | platforms=['any'], 120 | license='BSD', 121 | python_requires='>=3.8', 122 | install_requires=reqs('default.txt') + reqs('runtime.txt'), 123 | classifiers=classifiers, 124 | entry_points={ 125 | 'celery.beat_schedulers': [ 126 | 'django = django_celery_beat.schedulers:DatabaseScheduler', 127 | ], 128 | }, 129 | include_package_data=True, 130 | zip_safe=False, 131 | ) 132 | -------------------------------------------------------------------------------- /t/.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = 1 3 | cover_pylib = 0 4 | include=*thorn* 5 | omit = */tests/*;testproj/*;testapp/*;*/migrations/* 6 | 7 | [report] 8 | omit = 9 | */python?.?/* 10 | */site-packages/* 11 | */pypy/* 12 | */tests/* 13 | */testproj/* 14 | */testapp/* 15 | */migrations/* 16 | -------------------------------------------------------------------------------- /t/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celery/django-celery-beat/0ade27bc52f92499d8c9000bacfa90bedcba3129/t/__init__.py -------------------------------------------------------------------------------- /t/proj/__init__.py: -------------------------------------------------------------------------------- 1 | from .celery import app as celery_app # noqa 2 | -------------------------------------------------------------------------------- /t/proj/celery.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from celery import Celery 4 | 5 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 't.proj.settings') 6 | 7 | app = Celery('proj') 8 | 9 | # Using a string here means the worker doesn't have to serialize 10 | # the configuration object. 11 | app.config_from_object('django.conf:settings', namespace='CELERY') 12 | 13 | app.autodiscover_tasks() 14 | -------------------------------------------------------------------------------- /t/proj/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.7 on 2022-10-13 05:09 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ('django_celery_beat', '0016_alter_crontabschedule_timezone'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='O2OToPeriodicTasks', 18 | fields=[ 19 | ('periodictask_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='django_celery_beat.periodictask')), 20 | ], 21 | bases=('django_celery_beat.periodictask',), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /t/proj/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celery/django-celery-beat/0ade27bc52f92499d8c9000bacfa90bedcba3129/t/proj/migrations/__init__.py -------------------------------------------------------------------------------- /t/proj/models.py: -------------------------------------------------------------------------------- 1 | from django_celery_beat.models import PeriodicTask 2 | 3 | 4 | class O2OToPeriodicTasks(PeriodicTask): 5 | """ 6 | The test-case model of OneToOne relation. 7 | """ 8 | pass 9 | -------------------------------------------------------------------------------- /t/proj/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for Test project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.9.1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.9/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.9/ref/settings/ 11 | """ 12 | import os 13 | import sys 14 | 15 | CELERY_DEFAULT_EXCHANGE = 'testcelery' 16 | CELERY_DEFAULT_ROUTING_KEY = 'testcelery' 17 | CELERY_DEFAULT_QUEUE = 'testcelery' 18 | 19 | CELERY_QUEUES = {'testcelery': {'binding_key': 'testcelery'}} 20 | 21 | CELERY_ACCEPT_CONTENT = ['pickle', 'json'] 22 | CELERY_TASK_SERIALIZER = 'pickle' 23 | CELERY_RESULT_SERIALIZER = 'pickle' 24 | 25 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 26 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 27 | 28 | sys.path.insert(0, os.path.abspath(os.path.join(BASE_DIR, os.pardir))) 29 | 30 | # Quick-start development settings - unsuitable for production 31 | # See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ 32 | 33 | # SECURITY WARNING: keep the secret key used in production secret! 34 | SECRET_KEY = 'u($kbs9$irs0)436gbo9%!b&#zyd&70tx!n7!i&fl6qun@z1_l' 35 | 36 | # SECURITY WARNING: don't run with debug turned on in production! 37 | DEBUG = True 38 | 39 | ALLOWED_HOSTS = [] 40 | 41 | # Application definition 42 | 43 | INSTALLED_APPS = [ 44 | 'django.contrib.admin', 45 | 'django.contrib.auth', 46 | 'django.contrib.contenttypes', 47 | 'django.contrib.sessions', 48 | 'django.contrib.messages', 49 | 'django.contrib.staticfiles', 50 | 'django_celery_beat', 51 | 't.proj', 52 | ] 53 | 54 | MIDDLEWARE = [ 55 | 'django.middleware.security.SecurityMiddleware', 56 | 'django.contrib.sessions.middleware.SessionMiddleware', 57 | 'django.middleware.common.CommonMiddleware', 58 | 'django.middleware.csrf.CsrfViewMiddleware', 59 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 60 | 'django.contrib.messages.middleware.MessageMiddleware', 61 | ] 62 | 63 | ROOT_URLCONF = 't.proj.urls' 64 | 65 | TEMPLATES = [ 66 | { 67 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 68 | 'DIRS': [], 69 | 'APP_DIRS': True, 70 | 'OPTIONS': { 71 | 'context_processors': [ 72 | 'django.template.context_processors.debug', 73 | 'django.template.context_processors.request', 74 | 'django.contrib.auth.context_processors.auth', 75 | 'django.contrib.messages.context_processors.messages', 76 | ], 77 | }, 78 | }, 79 | ] 80 | 81 | WSGI_APPLICATION = 't.proj.wsgi.application' 82 | 83 | # Database 84 | # https://docs.djangoproject.com/en/1.9/ref/settings/#databases 85 | 86 | DATABASES = { 87 | 'default': { 88 | 'ENGINE': 'django.db.backends.sqlite3', 89 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 90 | 'OPTIONS': { 91 | 'timeout': 1000, 92 | }, 93 | } 94 | } 95 | 96 | # Password validation 97 | # https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators 98 | 99 | django_auth = 'django.contrib.auth.password_validation.' 100 | 101 | AUTH_PASSWORD_VALIDATORS = [ 102 | ] 103 | 104 | # Internationalization 105 | # https://docs.djangoproject.com/en/1.9/topics/i18n/ 106 | 107 | LANGUAGE_CODE = 'en-us' 108 | 109 | TIME_ZONE = 'UTC' 110 | 111 | USE_I18N = True 112 | 113 | USE_L10N = True 114 | 115 | USE_TZ = True 116 | 117 | # Static files (CSS, JavaScript, Images) 118 | # https://docs.djangoproject.com/en/1.9/howto/static-files/ 119 | 120 | STATIC_URL = '/static/' 121 | DJANGO_CELERY_BEAT_TZ_AWARE = True 122 | -------------------------------------------------------------------------------- /t/proj/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path 3 | 4 | urlpatterns = [ 5 | path('admin/', admin.site.urls), 6 | ] 7 | -------------------------------------------------------------------------------- /t/proj/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for Test project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/ 8 | """ 9 | import os 10 | 11 | from django.core.wsgi import get_wsgi_application 12 | 13 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "proj.settings") 14 | 15 | application = get_wsgi_application() 16 | -------------------------------------------------------------------------------- /t/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celery/django-celery-beat/0ade27bc52f92499d8c9000bacfa90bedcba3129/t/unit/__init__.py -------------------------------------------------------------------------------- /t/unit/conftest.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import MagicMock 2 | 3 | import pytest 4 | # we have to import the pytest plugin fixtures here, 5 | # in case user did not yet `pip install ".[develop]"`, 6 | # that installs the pytest plugin into the setuptools registry. 7 | from celery.contrib.pytest import (celery_app, celery_config, 8 | celery_enable_logging, celery_parameters, 9 | depends_on_current_app, use_celery_app_trap) 10 | from celery.contrib.testing.app import TestApp, Trap 11 | 12 | # Tricks flake8 into silencing redefining fixtures warnings. 13 | __all__ = ( 14 | 'celery_app', 'celery_enable_logging', 'depends_on_current_app', 15 | 'celery_parameters', 'celery_config', 'use_celery_app_trap' 16 | ) 17 | 18 | 19 | @pytest.fixture(scope='session', autouse=True) 20 | def setup_default_app_trap(): 21 | from celery._state import set_default_app 22 | set_default_app(Trap()) 23 | 24 | 25 | @pytest.fixture 26 | def app(celery_app): 27 | return celery_app 28 | 29 | 30 | @pytest.fixture(autouse=True) 31 | def test_cases_shortcuts(request, app, patching): 32 | if request.instance: 33 | @app.task 34 | def add(x, y): 35 | return x + y 36 | 37 | # IMPORTANT: We set an .app attribute for every test case class. 38 | request.instance.app = app 39 | request.instance.Celery = TestApp 40 | request.instance.add = add 41 | request.instance.patching = patching 42 | yield 43 | if request.instance: 44 | request.instance.app = None 45 | 46 | 47 | @pytest.fixture 48 | def patching(monkeypatch): 49 | def _patching(attr): 50 | monkeypatch.setattr(attr, MagicMock()) 51 | 52 | return _patching 53 | -------------------------------------------------------------------------------- /t/unit/test_admin.py: -------------------------------------------------------------------------------- 1 | from itertools import combinations 2 | from unittest import mock 3 | 4 | import pytest 5 | from django.core.exceptions import ValidationError 6 | from django.test import TestCase 7 | 8 | from django_celery_beat.admin import PeriodicTaskAdmin 9 | from django_celery_beat.models import (DAYS, ClockedSchedule, CrontabSchedule, 10 | IntervalSchedule, PeriodicTask, 11 | SolarSchedule) 12 | 13 | 14 | @pytest.mark.django_db 15 | class ActionsTests(TestCase): 16 | 17 | @classmethod 18 | def setUpTestData(cls): 19 | super().setUpTestData() 20 | cls.interval_schedule = IntervalSchedule.objects.create(every=10, 21 | period=DAYS) 22 | 23 | def test_toggle_action(self): 24 | PeriodicTask.objects.create(name='name1', task='task1', enabled=False, 25 | interval=self.interval_schedule) 26 | PeriodicTask.objects.create(name='name2', task='task2', enabled=True, 27 | interval=self.interval_schedule) 28 | PeriodicTask.objects.create(name='name3', task='task3', enabled=False, 29 | interval=self.interval_schedule) 30 | 31 | qs = PeriodicTask.objects.all() 32 | PeriodicTaskAdmin(PeriodicTask, None)._toggle_tasks_activity(qs) 33 | 34 | e1 = PeriodicTask.objects.get(name='name1', task='task1').enabled 35 | e2 = PeriodicTask.objects.get(name='name2', task='task2').enabled 36 | e3 = PeriodicTask.objects.get(name='name3', task='task3').enabled 37 | self.assertTrue(e1) 38 | self.assertFalse(e2) 39 | self.assertTrue(e3) 40 | 41 | def test_toggle_action_all_enabled(self): 42 | PeriodicTask.objects.create(name='name1', task='task1', enabled=True, 43 | interval=self.interval_schedule) 44 | PeriodicTask.objects.create(name='name2', task='task2', enabled=True, 45 | interval=self.interval_schedule) 46 | PeriodicTask.objects.create(name='name3', task='task3', enabled=True, 47 | interval=self.interval_schedule) 48 | 49 | qs = PeriodicTask.objects.all() 50 | PeriodicTaskAdmin(PeriodicTask, None)._toggle_tasks_activity(qs) 51 | 52 | e1 = PeriodicTask.objects.get(name='name1', task='task1').enabled 53 | e2 = PeriodicTask.objects.get(name='name2', task='task2').enabled 54 | e3 = PeriodicTask.objects.get(name='name3', task='task3').enabled 55 | self.assertFalse(e1) 56 | self.assertFalse(e2) 57 | self.assertFalse(e3) 58 | 59 | def test_toggle_action_all_disabled(self): 60 | 61 | PeriodicTask.objects.create(name='name1', task='task1', enabled=False, 62 | interval=self.interval_schedule) 63 | PeriodicTask.objects.create(name='name2', task='task2', enabled=False, 64 | interval=self.interval_schedule) 65 | PeriodicTask.objects.create(name='name3', task='task3', enabled=False, 66 | interval=self.interval_schedule) 67 | 68 | qs = PeriodicTask.objects.all() 69 | PeriodicTaskAdmin(PeriodicTask, None)._toggle_tasks_activity(qs) 70 | 71 | e1 = PeriodicTask.objects.get(name='name1', task='task1').enabled 72 | e2 = PeriodicTask.objects.get(name='name2', task='task2').enabled 73 | e3 = PeriodicTask.objects.get(name='name3', task='task3').enabled 74 | self.assertTrue(e1) 75 | self.assertTrue(e2) 76 | self.assertTrue(e3) 77 | 78 | 79 | @pytest.mark.django_db 80 | class ValidateUniqueTests(TestCase): 81 | 82 | def test_validate_unique_raises_if_schedule_not_set(self): 83 | with self.assertRaises(ValidationError) as cm: 84 | PeriodicTask(name='task0').validate_unique() 85 | self.assertEqual( 86 | cm.exception.args[0], 87 | 'One of clocked, interval, crontab, or solar must be set.', 88 | ) 89 | 90 | def test_validate_unique_raises_for_multiple_schedules(self): 91 | schedules = [ 92 | ('crontab', CrontabSchedule()), 93 | ('interval', IntervalSchedule()), 94 | ('solar', SolarSchedule()), 95 | ('clocked', ClockedSchedule()) 96 | ] 97 | expected_error_msg = ( 98 | 'Only one of clocked, interval, crontab, or solar ' 99 | 'must be set' 100 | ) 101 | for i, options in enumerate(combinations(schedules, 2)): 102 | name = f'task{i}' 103 | options_dict = dict(options) 104 | with self.assertRaises(ValidationError) as cm: 105 | PeriodicTask(name=name, **options_dict).validate_unique() 106 | errors = cm.exception.args[0] 107 | self.assertEqual(errors.keys(), options_dict.keys()) 108 | for error_msg in errors.values(): 109 | self.assertEqual(error_msg, [expected_error_msg]) 110 | 111 | def test_validate_unique_not_raises(self): 112 | PeriodicTask(crontab=CrontabSchedule()).validate_unique() 113 | PeriodicTask(interval=IntervalSchedule()).validate_unique() 114 | PeriodicTask(solar=SolarSchedule()).validate_unique() 115 | PeriodicTask(clocked=ClockedSchedule(), one_off=True).validate_unique() 116 | 117 | 118 | @pytest.mark.django_db 119 | class DisableTasksTest(TestCase): 120 | 121 | @classmethod 122 | def setUpTestData(cls): 123 | super().setUpTestData() 124 | cls.interval_schedule = IntervalSchedule.objects.create(every=10, 125 | period=DAYS) 126 | 127 | @mock.patch('django_celery_beat.admin.PeriodicTaskAdmin.message_user') 128 | def test_disable_tasks(self, mock_message_user): 129 | PeriodicTask.objects.create(name='name1', task='task1', enabled=True, 130 | interval=self.interval_schedule) 131 | PeriodicTask.objects.create(name='name2', task='task2', enabled=True, 132 | interval=self.interval_schedule) 133 | 134 | qs = PeriodicTask.objects.all() 135 | 136 | PeriodicTaskAdmin(PeriodicTask, None).disable_tasks(None, qs) 137 | 138 | for periodic_task in qs: 139 | self.assertFalse(periodic_task.enabled) 140 | self.assertIsNone(periodic_task.last_run_at) 141 | mock_message_user.assert_called_once() 142 | -------------------------------------------------------------------------------- /t/unit/test_models.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | 4 | try: 5 | from zoneinfo import ZoneInfo, available_timezones 6 | except ImportError: 7 | from backports.zoneinfo import available_timezones, ZoneInfo 8 | 9 | import pytest 10 | from celery import schedules 11 | from django.apps import apps 12 | from django.conf import settings 13 | from django.db.migrations.autodetector import MigrationAutodetector 14 | from django.db.migrations.loader import MigrationLoader 15 | from django.db.migrations.questioner import NonInteractiveMigrationQuestioner 16 | from django.db.migrations.state import ProjectState 17 | from django.test import TestCase, override_settings 18 | from django.utils import timezone 19 | 20 | from django_celery_beat import migrations as beat_migrations 21 | from django_celery_beat.models import (DAYS, ClockedSchedule, CrontabSchedule, 22 | IntervalSchedule, PeriodicTasks, 23 | SolarSchedule, 24 | crontab_schedule_celery_timezone) 25 | from t.proj.models import O2OToPeriodicTasks 26 | 27 | 28 | class MigrationTests(TestCase): 29 | def test_no_future_duplicate_migration_numbers(self): 30 | """Verify no duplicate migration numbers. 31 | 32 | Migration files with the same number can cause issues with 33 | backward migrations, so avoid them. 34 | """ 35 | path = os.path.dirname(beat_migrations.__file__) 36 | files = [f[:4] for f in os.listdir(path) if f.endswith('.py')] 37 | expected_duplicates = [ 38 | (3, '0006'), 39 | ] 40 | duplicates_extra = sum(count - 1 for count, _ in expected_duplicates) 41 | duplicates_numbers = [number for _, number in expected_duplicates] 42 | self.assertEqual( 43 | len(files), len(set(files)) + duplicates_extra, 44 | msg=('Detected migration files with the same migration number' 45 | ' (besides {})'.format(' and '.join(duplicates_numbers)))) 46 | 47 | def test_models_match_migrations(self): 48 | """Make sure that no model changes exist. 49 | 50 | This logic is taken from django's makemigrations.py file. 51 | Here just detect if model changes exist that require 52 | a migration, and if so we fail. 53 | """ 54 | app_labels = ['django_celery_beat'] 55 | loader = MigrationLoader(None, ignore_no_migrations=True) 56 | questioner = NonInteractiveMigrationQuestioner( 57 | specified_apps=app_labels, dry_run=False) 58 | autodetector = MigrationAutodetector( 59 | loader.project_state(), 60 | ProjectState.from_apps(apps), 61 | questioner, 62 | ) 63 | changes = autodetector.changes( 64 | graph=loader.graph, 65 | trim_to_apps=app_labels, 66 | convert_apps=app_labels, 67 | migration_name='fake_name', 68 | ) 69 | self.assertTrue( 70 | not changes, 71 | msg='Model changes exist that do not have a migration') 72 | 73 | 74 | class TestDuplicatesMixin: 75 | def _test_duplicate_schedules(self, cls, kwargs, schedule=None): 76 | sched1 = cls.objects.create(**kwargs) 77 | cls.objects.create(**kwargs) 78 | self.assertEqual(cls.objects.filter(**kwargs).count(), 2) 79 | # try to create a duplicate schedule from a celery schedule 80 | if schedule is None: 81 | schedule = sched1.schedule 82 | sched3 = cls.from_schedule(schedule) 83 | # the schedule should be the first of the 2 previous duplicates 84 | self.assertEqual(sched3, sched1) 85 | # and the duplicates should not be deleted ! 86 | self.assertEqual(cls.objects.filter(**kwargs).count(), 2) 87 | 88 | 89 | class CrontabScheduleTestCase(TestCase, TestDuplicatesMixin): 90 | FIRST_VALID_TIMEZONE = available_timezones().pop() 91 | 92 | def test_default_timezone_without_settings_config(self): 93 | assert crontab_schedule_celery_timezone() == "UTC" 94 | 95 | @override_settings(CELERY_TIMEZONE=FIRST_VALID_TIMEZONE) 96 | def test_default_timezone_with_settings_config(self): 97 | assert crontab_schedule_celery_timezone() == self.FIRST_VALID_TIMEZONE 98 | 99 | def test_duplicate_schedules(self): 100 | # See: https://github.com/celery/django-celery-beat/issues/322 101 | kwargs = { 102 | "minute": "*", 103 | "hour": "4", 104 | "day_of_week": "*", 105 | "day_of_month": "*", 106 | "month_of_year": "*", 107 | } 108 | schedule = schedules.crontab(hour="4") 109 | self._test_duplicate_schedules(CrontabSchedule, kwargs, schedule) 110 | 111 | 112 | class SolarScheduleTestCase(TestCase): 113 | EVENT_CHOICES = SolarSchedule._meta.get_field("event").choices 114 | 115 | def test_celery_solar_schedules_sorted(self): 116 | assert all( 117 | self.EVENT_CHOICES[i] <= self.EVENT_CHOICES[i + 1] 118 | for i in range(len(self.EVENT_CHOICES) - 1) 119 | ), "SolarSchedule event choices are unsorted" 120 | 121 | def test_celery_solar_schedules_included_as_event_choices(self): 122 | """Make sure that all Celery solar schedules are included 123 | in SolarSchedule `event` field choices, keeping synchronized 124 | Celery solar events with `django-celery-beat` supported solar 125 | events. 126 | 127 | This test is necessary because Celery solar schedules are 128 | hardcoded at models so that Django can discover their translations. 129 | """ 130 | event_choices_values = [value for value, tr in self.EVENT_CHOICES] 131 | for solar_event in schedules.solar._all_events: 132 | assert solar_event in event_choices_values 133 | 134 | for event_choice in event_choices_values: 135 | assert event_choice in schedules.solar._all_events 136 | 137 | 138 | class IntervalScheduleTestCase(TestCase, TestDuplicatesMixin): 139 | 140 | def test_duplicate_schedules(self): 141 | kwargs = {'every': 1, 'period': IntervalSchedule.SECONDS} 142 | schedule = schedules.schedule(run_every=1.0) 143 | self._test_duplicate_schedules(IntervalSchedule, kwargs, schedule) 144 | 145 | 146 | class ClockedScheduleTestCase(TestCase, TestDuplicatesMixin): 147 | 148 | def test_duplicate_schedules(self): 149 | kwargs = {'clocked_time': timezone.now()} 150 | self._test_duplicate_schedules(ClockedSchedule, kwargs) 151 | 152 | # IMPORTANT: we must have a valid timezone (not UTC) for accurate testing 153 | @override_settings(TIME_ZONE='Africa/Cairo') 154 | def test_timezone_format(self): 155 | """Ensure scheduled time is not shown in UTC when timezone is used""" 156 | tz_info = datetime.datetime.now(ZoneInfo(settings.TIME_ZONE)) 157 | schedule, created = ClockedSchedule.objects.get_or_create( 158 | clocked_time=tz_info) 159 | # testnig str(schedule) calls make_aware() internally 160 | assert str(schedule.clocked_time) == str(schedule) 161 | 162 | 163 | @pytest.mark.django_db 164 | class OneToOneRelTestCase(TestCase): 165 | """ 166 | Make sure that when OneToOne relation Model changed, 167 | the `PeriodicTasks.last_update` will be update. 168 | """ 169 | 170 | @classmethod 171 | def setUpTestData(cls): 172 | super().setUpTestData() 173 | cls.interval_schedule = IntervalSchedule.objects.create( 174 | every=10, period=DAYS 175 | ) 176 | 177 | def test_trigger_update_when_saved(self): 178 | o2o_to_periodic_tasks = O2OToPeriodicTasks.objects.create( 179 | name='name1', 180 | task='task1', 181 | enabled=True, 182 | interval=self.interval_schedule 183 | ) 184 | not_changed_dt = PeriodicTasks.last_change() 185 | o2o_to_periodic_tasks.enabled = True # Change something on instance. 186 | o2o_to_periodic_tasks.save() 187 | has_changed_dt = PeriodicTasks.last_change() 188 | self.assertTrue( 189 | not_changed_dt != has_changed_dt, 190 | 'The `PeriodicTasks.last_update` has not be update.' 191 | ) 192 | # Check the `PeriodicTasks` does be updated. 193 | 194 | def test_trigger_update_when_deleted(self): 195 | o2o_to_periodic_tasks = O2OToPeriodicTasks.objects.create( 196 | name='name1', 197 | task='task1', 198 | enabled=True, 199 | interval=self.interval_schedule 200 | ) 201 | not_changed_dt = PeriodicTasks.last_change() 202 | o2o_to_periodic_tasks.delete() 203 | has_changed_dt = PeriodicTasks.last_change() 204 | self.assertTrue( 205 | not_changed_dt != has_changed_dt, 206 | 'The `PeriodicTasks.last_update` has not be update.' 207 | ) 208 | # Check the `PeriodicTasks` does be updated. 209 | 210 | 211 | class HumanReadableTestCase(TestCase): 212 | def test_good(self): 213 | """Valid crontab display.""" 214 | cron = CrontabSchedule.objects.create( 215 | hour="2", 216 | minute="0", 217 | day_of_week="mon", 218 | ) 219 | self.assertNotEqual( 220 | cron.human_readable, "0 2 * * mon UTC" 221 | ) 222 | 223 | def test_invalid(self): 224 | """Invalid crontab display.""" 225 | cron = CrontabSchedule.objects.create( 226 | hour="2", 227 | minute="0", 228 | day_of_week="xxx", 229 | ) 230 | self.assertEqual( 231 | cron.human_readable, "0 2 * * xxx UTC" 232 | ) 233 | 234 | def test_long_name(self): 235 | """Long day name display.""" 236 | for day_day_of_week, expected in ( 237 | ("1", ", only on Monday"), 238 | ("mon", ", only on Monday"), 239 | ("Monday,tue", ", only on Monday and Tuesday"), 240 | ("sat-sun/2", ", only on Saturday"), 241 | ("mon-wed", ", only on Monday, Tuesday, and Wednesday"), 242 | ("*", ""), 243 | ("0-6", ""), 244 | ("2-1", ""), 245 | ("mon-sun", ""), 246 | ("tue-mon", ""), 247 | ): 248 | cron = CrontabSchedule.objects.create( 249 | hour="2", 250 | minute="0", 251 | day_of_week=day_day_of_week, 252 | ) 253 | 254 | self.assertEqual( 255 | cron.human_readable, 256 | f"At 02:00 AM{expected} UTC", 257 | day_day_of_week, 258 | ) 259 | -------------------------------------------------------------------------------- /t/unit/test_utils.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.test import TestCase, override_settings 3 | from django.utils import timezone 4 | 5 | from django_celery_beat.utils import aware_now 6 | 7 | 8 | @pytest.mark.django_db 9 | class TestUtils(TestCase): 10 | def test_aware_now_with_use_tz_true(self): 11 | """Test aware_now when USE_TZ is True""" 12 | with override_settings(USE_TZ=True): 13 | result = aware_now() 14 | assert timezone.is_aware(result) 15 | # Convert both timezones to string for comparison 16 | assert str(result.tzinfo) == str(timezone.get_current_timezone()) 17 | 18 | def test_aware_now_with_use_tz_false(self): 19 | """Test aware_now when USE_TZ is False""" 20 | with override_settings(USE_TZ=False, TIME_ZONE="Asia/Tokyo"): 21 | result = aware_now() 22 | assert timezone.is_aware(result) 23 | assert result.tzinfo.key == "Asia/Tokyo" 24 | 25 | def test_aware_now_with_use_tz_false_default_timezone(self): 26 | """Test aware_now when USE_TZ is False and default TIME_ZONE""" 27 | with override_settings(USE_TZ=False): # Let Django use its default UTC 28 | result = aware_now() 29 | assert timezone.is_aware(result) 30 | assert str(result.tzinfo) == "UTC" 31 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | ; https://docs.djangoproject.com/en/stable/faq/install/#what-python-version-can-i-use-with-django 2 | 3 | [gh-actions] 4 | python = 5 | 3.8: py38 6 | 3.9: py39, apicheck, linkcheck, flake8, pydocstyle, cov 7 | 3.10: py310 8 | 3.11: py311 9 | 3.12: py312 10 | 3.13: py313 11 | pypy-3.10: pypy3 12 | 13 | [gh-actions:env] 14 | DJANGO = 15 | 3.2: django32 16 | 4.2: django42 17 | 5.1: django51 18 | 5.2: django52 19 | 20 | [tox] 21 | envlist = 22 | py38-django{32,42} 23 | py39-django{32,42} 24 | py310-django{32,42,51} 25 | py311-django{42,51,52} 26 | py312-django{42,51,52} 27 | py313-django{52} 28 | pypy3-django{32,42,51,52} 29 | flake8 30 | apicheck 31 | linkcheck 32 | pydocstyle 33 | cov 34 | 35 | [testenv] 36 | deps= 37 | -r{toxinidir}/requirements/default.txt 38 | -r{toxinidir}/requirements/test.txt 39 | -r{toxinidir}/requirements/test-ci.txt 40 | 41 | cov: -r{toxinidir}/requirements/test-django.txt 42 | 43 | django32: Django ~= 3.2 44 | django42: Django ~= 4.2 45 | django51: Django ~= 5.1 46 | django52: Django ~= 5.2 47 | 48 | linkcheck,apicheck: -r{toxinidir}/requirements/docs.txt 49 | flake8,pydocstyle: -r{toxinidir}/requirements/pkgutils.txt 50 | sitepackages = False 51 | recreate = False 52 | commands = 53 | pip list 54 | pytest -xv {posargs} 55 | 56 | 57 | [testenv:apicheck] 58 | basepython = python3.9 59 | commands = 60 | sphinx-build -W -b apicheck -d {envtmpdir}/doctrees docs docs/_build/apicheck 61 | 62 | [testenv:linkcheck] 63 | basepython = python3.9 64 | commands = 65 | sphinx-build -W -b linkcheck -d {envtmpdir}/doctrees docs docs/_build/linkcheck 66 | 67 | [testenv:flake8] 68 | basepython = python3.9 69 | commands = 70 | python -m flake8 {toxinidir}/django_celery_beat {toxinidir}/t 71 | 72 | [testenv:pydocstyle] 73 | basepython = python3.9 74 | commands = 75 | pydocstyle {toxinidir}/django_celery_beat 76 | 77 | [testenv:cov] 78 | basepython = python3.9 79 | usedevelop = true 80 | commands = 81 | pip install --upgrade https://github.com/celery/celery/zipball/main#egg=celery 82 | pip install --upgrade https://github.com/celery/kombu/zipball/main#egg=kombu 83 | pip install Django pytest 84 | pytest -x --cov=django_celery_beat --cov-report=xml --no-cov-on-fail 85 | --------------------------------------------------------------------------------