django-periodic-tasks 0.1.0a3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. django_periodic_tasks-0.1.0a3/.gitignore +224 -0
  2. django_periodic_tasks-0.1.0a3/.gitlab-ci.yml +59 -0
  3. django_periodic_tasks-0.1.0a3/.pre-commit-config.yaml +44 -0
  4. django_periodic_tasks-0.1.0a3/CHANGELOG.md +48 -0
  5. django_periodic_tasks-0.1.0a3/CLAUDE.md +50 -0
  6. django_periodic_tasks-0.1.0a3/Dockerfile +8 -0
  7. django_periodic_tasks-0.1.0a3/LICENSE +15 -0
  8. django_periodic_tasks-0.1.0a3/PKG-INFO +65 -0
  9. django_periodic_tasks-0.1.0a3/README.md +47 -0
  10. django_periodic_tasks-0.1.0a3/bin/publish.sh +22 -0
  11. django_periodic_tasks-0.1.0a3/django_periodic_tasks/__init__.py +15 -0
  12. django_periodic_tasks-0.1.0a3/django_periodic_tasks/admin.py +86 -0
  13. django_periodic_tasks-0.1.0a3/django_periodic_tasks/apps.py +39 -0
  14. django_periodic_tasks-0.1.0a3/django_periodic_tasks/cron.py +41 -0
  15. django_periodic_tasks-0.1.0a3/django_periodic_tasks/decorators.py +59 -0
  16. django_periodic_tasks-0.1.0a3/django_periodic_tasks/management/__init__.py +0 -0
  17. django_periodic_tasks-0.1.0a3/django_periodic_tasks/management/commands/__init__.py +0 -0
  18. django_periodic_tasks-0.1.0a3/django_periodic_tasks/management/commands/run_scheduler.py +60 -0
  19. django_periodic_tasks-0.1.0a3/django_periodic_tasks/migrations/0001_initial.py +37 -0
  20. django_periodic_tasks-0.1.0a3/django_periodic_tasks/migrations/0002_remove_redundant_next_run_at_db_index.py +17 -0
  21. django_periodic_tasks-0.1.0a3/django_periodic_tasks/migrations/0003_add_task_execution.py +31 -0
  22. django_periodic_tasks-0.1.0a3/django_periodic_tasks/migrations/__init__.py +0 -0
  23. django_periodic_tasks-0.1.0a3/django_periodic_tasks/models.py +146 -0
  24. django_periodic_tasks-0.1.0a3/django_periodic_tasks/registry.py +131 -0
  25. django_periodic_tasks-0.1.0a3/django_periodic_tasks/scheduler.py +200 -0
  26. django_periodic_tasks-0.1.0a3/django_periodic_tasks/sync.py +57 -0
  27. django_periodic_tasks-0.1.0a3/django_periodic_tasks/task_resolver.py +22 -0
  28. django_periodic_tasks-0.1.0a3/docker-compose.yml +12 -0
  29. django_periodic_tasks-0.1.0a3/docs/.pages +5 -0
  30. django_periodic_tasks-0.1.0a3/docs/api/.pages +6 -0
  31. django_periodic_tasks-0.1.0a3/docs/api/decorators.md +6 -0
  32. django_periodic_tasks-0.1.0a3/docs/api/index.md +3 -0
  33. django_periodic_tasks-0.1.0a3/docs/api/models.md +25 -0
  34. django_periodic_tasks-0.1.0a3/docs/api/registry.md +17 -0
  35. django_periodic_tasks-0.1.0a3/docs/api/settings.md +31 -0
  36. django_periodic_tasks-0.1.0a3/docs/guides/.pages +6 -0
  37. django_periodic_tasks-0.1.0a3/docs/guides/defining-schedules.md +179 -0
  38. django_periodic_tasks-0.1.0a3/docs/guides/exactly-once.md +87 -0
  39. django_periodic_tasks-0.1.0a3/docs/guides/getting-started.md +74 -0
  40. django_periodic_tasks-0.1.0a3/docs/guides/index.md +3 -0
  41. django_periodic_tasks-0.1.0a3/docs/guides/running.md +75 -0
  42. django_periodic_tasks-0.1.0a3/docs/index.md +43 -0
  43. django_periodic_tasks-0.1.0a3/docs/styles.css +58 -0
  44. django_periodic_tasks-0.1.0a3/mise.toml +44 -0
  45. django_periodic_tasks-0.1.0a3/mkdocs.yml +51 -0
  46. django_periodic_tasks-0.1.0a3/public/404.html +730 -0
  47. django_periodic_tasks-0.1.0a3/public/api/decorators.html +859 -0
  48. django_periodic_tasks-0.1.0a3/public/api/index.html +768 -0
  49. django_periodic_tasks-0.1.0a3/public/api/models.html +943 -0
  50. django_periodic_tasks-0.1.0a3/public/api/registry.html +949 -0
  51. django_periodic_tasks-0.1.0a3/public/api/settings.html +910 -0
  52. django_periodic_tasks-0.1.0a3/public/assets/images/favicon.png +0 -0
  53. django_periodic_tasks-0.1.0a3/public/assets/javascripts/bundle.79ae519e.min.js +16 -0
  54. django_periodic_tasks-0.1.0a3/public/assets/javascripts/bundle.79ae519e.min.js.map +7 -0
  55. django_periodic_tasks-0.1.0a3/public/assets/javascripts/lunr/min/lunr.ar.min.js +1 -0
  56. django_periodic_tasks-0.1.0a3/public/assets/javascripts/lunr/min/lunr.da.min.js +18 -0
  57. django_periodic_tasks-0.1.0a3/public/assets/javascripts/lunr/min/lunr.de.min.js +18 -0
  58. django_periodic_tasks-0.1.0a3/public/assets/javascripts/lunr/min/lunr.du.min.js +18 -0
  59. django_periodic_tasks-0.1.0a3/public/assets/javascripts/lunr/min/lunr.el.min.js +1 -0
  60. django_periodic_tasks-0.1.0a3/public/assets/javascripts/lunr/min/lunr.es.min.js +18 -0
  61. django_periodic_tasks-0.1.0a3/public/assets/javascripts/lunr/min/lunr.fi.min.js +18 -0
  62. django_periodic_tasks-0.1.0a3/public/assets/javascripts/lunr/min/lunr.fr.min.js +18 -0
  63. django_periodic_tasks-0.1.0a3/public/assets/javascripts/lunr/min/lunr.he.min.js +1 -0
  64. django_periodic_tasks-0.1.0a3/public/assets/javascripts/lunr/min/lunr.hi.min.js +1 -0
  65. django_periodic_tasks-0.1.0a3/public/assets/javascripts/lunr/min/lunr.hu.min.js +18 -0
  66. django_periodic_tasks-0.1.0a3/public/assets/javascripts/lunr/min/lunr.hy.min.js +1 -0
  67. django_periodic_tasks-0.1.0a3/public/assets/javascripts/lunr/min/lunr.it.min.js +18 -0
  68. django_periodic_tasks-0.1.0a3/public/assets/javascripts/lunr/min/lunr.ja.min.js +1 -0
  69. django_periodic_tasks-0.1.0a3/public/assets/javascripts/lunr/min/lunr.jp.min.js +1 -0
  70. django_periodic_tasks-0.1.0a3/public/assets/javascripts/lunr/min/lunr.kn.min.js +1 -0
  71. django_periodic_tasks-0.1.0a3/public/assets/javascripts/lunr/min/lunr.ko.min.js +1 -0
  72. django_periodic_tasks-0.1.0a3/public/assets/javascripts/lunr/min/lunr.multi.min.js +1 -0
  73. django_periodic_tasks-0.1.0a3/public/assets/javascripts/lunr/min/lunr.nl.min.js +18 -0
  74. django_periodic_tasks-0.1.0a3/public/assets/javascripts/lunr/min/lunr.no.min.js +18 -0
  75. django_periodic_tasks-0.1.0a3/public/assets/javascripts/lunr/min/lunr.pt.min.js +18 -0
  76. django_periodic_tasks-0.1.0a3/public/assets/javascripts/lunr/min/lunr.ro.min.js +18 -0
  77. django_periodic_tasks-0.1.0a3/public/assets/javascripts/lunr/min/lunr.ru.min.js +18 -0
  78. django_periodic_tasks-0.1.0a3/public/assets/javascripts/lunr/min/lunr.sa.min.js +1 -0
  79. django_periodic_tasks-0.1.0a3/public/assets/javascripts/lunr/min/lunr.stemmer.support.min.js +1 -0
  80. django_periodic_tasks-0.1.0a3/public/assets/javascripts/lunr/min/lunr.sv.min.js +18 -0
  81. django_periodic_tasks-0.1.0a3/public/assets/javascripts/lunr/min/lunr.ta.min.js +1 -0
  82. django_periodic_tasks-0.1.0a3/public/assets/javascripts/lunr/min/lunr.te.min.js +1 -0
  83. django_periodic_tasks-0.1.0a3/public/assets/javascripts/lunr/min/lunr.th.min.js +1 -0
  84. django_periodic_tasks-0.1.0a3/public/assets/javascripts/lunr/min/lunr.tr.min.js +18 -0
  85. django_periodic_tasks-0.1.0a3/public/assets/javascripts/lunr/min/lunr.vi.min.js +1 -0
  86. django_periodic_tasks-0.1.0a3/public/assets/javascripts/lunr/min/lunr.zh.min.js +1 -0
  87. django_periodic_tasks-0.1.0a3/public/assets/javascripts/lunr/tinyseg.js +206 -0
  88. django_periodic_tasks-0.1.0a3/public/assets/javascripts/lunr/wordcut.js +6708 -0
  89. django_periodic_tasks-0.1.0a3/public/assets/javascripts/workers/search.2c215733.min.js +42 -0
  90. django_periodic_tasks-0.1.0a3/public/assets/javascripts/workers/search.2c215733.min.js.map +7 -0
  91. django_periodic_tasks-0.1.0a3/public/assets/stylesheets/main.484c7ddc.min.css +1 -0
  92. django_periodic_tasks-0.1.0a3/public/assets/stylesheets/main.484c7ddc.min.css.map +1 -0
  93. django_periodic_tasks-0.1.0a3/public/assets/stylesheets/palette.ab4e12ef.min.css +1 -0
  94. django_periodic_tasks-0.1.0a3/public/assets/stylesheets/palette.ab4e12ef.min.css.map +1 -0
  95. django_periodic_tasks-0.1.0a3/public/guides/defining-schedules.html +1262 -0
  96. django_periodic_tasks-0.1.0a3/public/guides/exactly-once.html +1020 -0
  97. django_periodic_tasks-0.1.0a3/public/guides/getting-started.html +983 -0
  98. django_periodic_tasks-0.1.0a3/public/guides/index.html +768 -0
  99. django_periodic_tasks-0.1.0a3/public/guides/running.html +1047 -0
  100. django_periodic_tasks-0.1.0a3/public/index.html +913 -0
  101. django_periodic_tasks-0.1.0a3/public/search/search_index.json +1 -0
  102. django_periodic_tasks-0.1.0a3/public/sitemap.xml +3 -0
  103. django_periodic_tasks-0.1.0a3/public/sitemap.xml.gz +0 -0
  104. django_periodic_tasks-0.1.0a3/public/styles.css +58 -0
  105. django_periodic_tasks-0.1.0a3/pyproject.toml +125 -0
  106. django_periodic_tasks-0.1.0a3/renovate.json +4 -0
  107. django_periodic_tasks-0.1.0a3/sandbox/__init__.py +0 -0
  108. django_periodic_tasks-0.1.0a3/sandbox/manage.py +14 -0
  109. django_periodic_tasks-0.1.0a3/sandbox/settings.py +74 -0
  110. django_periodic_tasks-0.1.0a3/sandbox/settings_docgen.py +24 -0
  111. django_periodic_tasks-0.1.0a3/sandbox/testapp/__init__.py +0 -0
  112. django_periodic_tasks-0.1.0a3/sandbox/testapp/apps.py +6 -0
  113. django_periodic_tasks-0.1.0a3/sandbox/testapp/models.py +0 -0
  114. django_periodic_tasks-0.1.0a3/sandbox/testapp/tasks.py +19 -0
  115. django_periodic_tasks-0.1.0a3/sandbox/urls.py +6 -0
  116. django_periodic_tasks-0.1.0a3/tests/__init__.py +0 -0
  117. django_periodic_tasks-0.1.0a3/tests/test_admin.py +94 -0
  118. django_periodic_tasks-0.1.0a3/tests/test_apps.py +64 -0
  119. django_periodic_tasks-0.1.0a3/tests/test_commands.py +36 -0
  120. django_periodic_tasks-0.1.0a3/tests/test_cron.py +101 -0
  121. django_periodic_tasks-0.1.0a3/tests/test_decorators.py +113 -0
  122. django_periodic_tasks-0.1.0a3/tests/test_integration.py +279 -0
  123. django_periodic_tasks-0.1.0a3/tests/test_models.py +284 -0
  124. django_periodic_tasks-0.1.0a3/tests/test_registry.py +127 -0
  125. django_periodic_tasks-0.1.0a3/tests/test_scheduler.py +670 -0
  126. django_periodic_tasks-0.1.0a3/tests/test_sync.py +185 -0
  127. django_periodic_tasks-0.1.0a3/tests/test_task_resolver.py +32 -0
  128. django_periodic_tasks-0.1.0a3/tox.ini +17 -0
  129. django_periodic_tasks-0.1.0a3/uv.lock +1125 -0
@@ -0,0 +1,224 @@
1
+ # Created by https://www.toptal.com/developers/gitignore/api/python,linux,macos
2
+ # Edit at https://www.toptal.com/developers/gitignore?templates=python,linux,macos
3
+
4
+ ### Linux ###
5
+ *~
6
+
7
+ # temporary files which can be created if a process still has a handle open of a deleted file
8
+ .fuse_hidden*
9
+
10
+ # KDE directory preferences
11
+ .directory
12
+
13
+ # Linux trash folder which might appear on any partition or disk
14
+ .Trash-*
15
+
16
+ # .nfs files are created when an open file is removed but is still being accessed
17
+ .nfs*
18
+
19
+ ### macOS ###
20
+ # General
21
+ .DS_Store
22
+ .AppleDouble
23
+ .LSOverride
24
+
25
+ # Icon must end with two \r
26
+ Icon
27
+
28
+
29
+ # Thumbnails
30
+ ._*
31
+
32
+ # Files that might appear in the root of a volume
33
+ .DocumentRevisions-V100
34
+ .fseventsd
35
+ .Spotlight-V100
36
+ .TemporaryItems
37
+ .Trashes
38
+ .VolumeIcon.icns
39
+ .com.apple.timemachine.donotpresent
40
+
41
+ # Directories potentially created on remote AFP share
42
+ .AppleDB
43
+ .AppleDesktop
44
+ Network Trash Folder
45
+ Temporary Items
46
+ .apdisk
47
+
48
+ ### macOS Patch ###
49
+ # iCloud generated files
50
+ *.icloud
51
+
52
+ ### Python ###
53
+ # Byte-compiled / optimized / DLL files
54
+ __pycache__/
55
+ *.py[cod]
56
+ *$py.class
57
+
58
+ # C extensions
59
+ *.so
60
+
61
+ # Distribution / packaging
62
+ .Python
63
+ build/
64
+ develop-eggs/
65
+ dist/
66
+ downloads/
67
+ eggs/
68
+ .eggs/
69
+ lib/
70
+ lib64/
71
+ parts/
72
+ sdist/
73
+ var/
74
+ wheels/
75
+ share/python-wheels/
76
+ *.egg-info/
77
+ .installed.cfg
78
+ *.egg
79
+ MANIFEST
80
+
81
+ # PyInstaller
82
+ # Usually these files are written by a python script from a template
83
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
84
+ *.manifest
85
+ *.spec
86
+
87
+ # Installer logs
88
+ pip-log.txt
89
+ pip-delete-this-directory.txt
90
+
91
+ # Unit test / coverage reports
92
+ htmlcov/
93
+ .tox/
94
+ .nox/
95
+ .coverage
96
+ .coverage.*
97
+ .cache
98
+ nosetests.xml
99
+ coverage.xml
100
+ *.cover
101
+ *.py,cover
102
+ .hypothesis/
103
+ .pytest_cache/
104
+ cover/
105
+
106
+ # Translations
107
+ *.mo
108
+ *.pot
109
+
110
+ # Django stuff:
111
+ *.log
112
+ local_settings.py
113
+ db.sqlite3
114
+ db.sqlite3-journal
115
+
116
+ # Flask stuff:
117
+ instance/
118
+ .webassets-cache
119
+
120
+ # Scrapy stuff:
121
+ .scrapy
122
+
123
+ # Sphinx documentation
124
+ docs/_build/
125
+
126
+ # PyBuilder
127
+ .pybuilder/
128
+ target/
129
+
130
+ # Jupyter Notebook
131
+ .ipynb_checkpoints
132
+
133
+ # IPython
134
+ profile_default/
135
+ ipython_config.py
136
+
137
+ # pyenv
138
+ # For a library or package, you might want to ignore these files since the code is
139
+ # intended to run in multiple environments; otherwise, check them in:
140
+ # .python-version
141
+
142
+ # pipenv
143
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
144
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
145
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
146
+ # install all needed dependencies.
147
+ #Pipfile.lock
148
+
149
+ # poetry
150
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
151
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
152
+ # commonly ignored for libraries.
153
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
154
+ #poetry.lock
155
+
156
+ # pdm
157
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
158
+ #pdm.lock
159
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
160
+ # in version control.
161
+ # https://pdm.fming.dev/#use-with-ide
162
+ .pdm.toml
163
+
164
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
165
+ __pypackages__/
166
+
167
+ # Celery stuff
168
+ celerybeat-schedule
169
+ celerybeat.pid
170
+
171
+ # SageMath parsed files
172
+ *.sage.py
173
+
174
+ # Environments
175
+ .env
176
+ .venv
177
+ env/
178
+ venv/
179
+ ENV/
180
+ env.bak/
181
+ venv.bak/
182
+
183
+ # Spyder project settings
184
+ .spyderproject
185
+ .spyproject
186
+
187
+ # Rope project settings
188
+ .ropeproject
189
+
190
+ # mkdocs documentation
191
+ /site
192
+
193
+ # mypy
194
+ .mypy_cache/
195
+ .dmypy.json
196
+ dmypy.json
197
+
198
+ # Pyre type checker
199
+ .pyre/
200
+
201
+ # pytype static type analyzer
202
+ .pytype/
203
+
204
+ # Cython debug symbols
205
+ cython_debug/
206
+
207
+ # PyCharm
208
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
209
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
210
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
211
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
212
+ #.idea/
213
+
214
+ ### Python Patch ###
215
+ # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
216
+ poetry.toml
217
+
218
+ # ruff
219
+ .ruff_cache/
220
+
221
+ # LSP config files
222
+ pyrightconfig.json
223
+
224
+ # End of https://www.toptal.com/developers/gitignore/api/python,linux,macos
@@ -0,0 +1,59 @@
1
+ workflow:
2
+ rules:
3
+ - if: $CI_COMMIT_REF_PROTECTED == "true"
4
+ - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
5
+
6
+ stages:
7
+ - test
8
+ - docs
9
+ - release
10
+
11
+ cache:
12
+ key: "$CI_PROJECT_NAME"
13
+ paths:
14
+ - $HOME/.cache/pip
15
+
16
+ include:
17
+ - component: gitlab.com/thelabnyc/thelab-ci-components/review@0.7.1
18
+ - component: gitlab.com/thelabnyc/thelab-ci-components/precommit@0.7.1
19
+ - component: gitlab.com/thelabnyc/thelab-ci-components/publish-gitlab-release@0.7.1
20
+ - component: gitlab.com/thelabnyc/thelab-ci-components/publish-to-pypi@0.7.1
21
+
22
+ test:
23
+ stage: test
24
+ image: "registry.gitlab.com/thelabnyc/python:${IMAGE}"
25
+ variables:
26
+ POSTGRES_DB: postgres
27
+ POSTGRES_USER: postgres
28
+ POSTGRES_PASSWORD: ""
29
+ POSTGRES_HOST_AUTH_METHOD: "trust"
30
+ services:
31
+ - postgres:latest
32
+ script:
33
+ - uv sync --all-extras
34
+ - uv run tox
35
+ coverage: '/^TOTAL.+?(\d+\%)$/'
36
+ parallel:
37
+ matrix:
38
+ - IMAGE: "3.13"
39
+ TOX_SKIP_ENV: "^(?!py313-)"
40
+ - IMAGE: "3.14"
41
+ TOX_SKIP_ENV: "^(?!py314-)"
42
+
43
+ pages:
44
+ stage: docs
45
+ image: "registry.gitlab.com/thelabnyc/python:3.14"
46
+ rules:
47
+ - if: $CI_COMMIT_REF_PROTECTED == "true"
48
+ needs:
49
+ - test
50
+ variables:
51
+ DJANGO_SETTINGS_MODULE: sandbox.settings_docgen
52
+ script:
53
+ - uv sync --all-extras
54
+ - uv run mkdocs build --strict
55
+ - rm -rf ./public
56
+ - mv ./build/mkdocs ./public
57
+ artifacts:
58
+ paths:
59
+ - public
@@ -0,0 +1,44 @@
1
+ repos:
2
+ - repo: https://gitlab.com/thelabnyc/thelab-pre-commit-hooks
3
+ rev: v0.0.3
4
+ hooks:
5
+ - id: update-copyright-year
6
+
7
+ - repo: https://github.com/pre-commit/pre-commit-hooks
8
+ rev: v6.0.0
9
+ hooks:
10
+ - id: check-json
11
+ - id: check-merge-conflict
12
+ - id: check-symlinks
13
+ - id: check-toml
14
+ - id: check-yaml
15
+ args: [--unsafe]
16
+ - id: end-of-file-fixer
17
+ - id: trailing-whitespace
18
+
19
+ - repo: https://github.com/asottile/pyupgrade
20
+ rev: v3.21.2
21
+ hooks:
22
+ - id: pyupgrade
23
+ args: [--py313-plus]
24
+
25
+ - repo: https://github.com/adamchainz/django-upgrade
26
+ rev: "1.29.1"
27
+ hooks:
28
+ - id: django-upgrade
29
+
30
+ - repo: https://github.com/pycqa/isort
31
+ rev: "7.0.0"
32
+ hooks:
33
+ - id: isort
34
+
35
+ - repo: https://github.com/astral-sh/ruff-pre-commit
36
+ rev: v0.14.10
37
+ hooks:
38
+ - id: ruff
39
+ - id: ruff-format
40
+
41
+ - repo: https://github.com/commitizen-tools/commitizen
42
+ rev: v4.10.1
43
+ hooks:
44
+ - id: commitizen
@@ -0,0 +1,48 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## v0.1.0a3 (2026-02-05)
9
+
10
+ ## v0.1.0a2 (2026-02-05)
11
+
12
+ ### Fix
13
+
14
+ - gitignored uv.lock
15
+
16
+ ## v0.1.0a1 (2026-02-05)
17
+
18
+ ## v0.1.0a0 (2026-02-05)
19
+
20
+ ### Feat
21
+
22
+ - auto-discover tasks.py modules in installed apps
23
+ - add exactly-once docs, fluentcron examples, remove scheduler_db_worker
24
+ - add bug fixes and exactly-once decorator for scheduled tasks
25
+ - add MkDocs documentation with auto-generated API reference
26
+ - add integration tests
27
+ - add Django admin interface
28
+ - add management commands
29
+ - add PeriodicTaskScheduler with Docker test infrastructure
30
+ - add code-to-DB schedule sync
31
+ - add task path resolver
32
+ - add schedule registry and @scheduled_task decorator
33
+ - add ScheduledTask model
34
+ - add cron expression utilities
35
+ - initial project scaffolding
36
+
37
+ ### Fix
38
+
39
+ - race condition in scheduler re-queuing
40
+ - uv commands
41
+ - remove dead code around TaskExec SKIPPED status
42
+ - hold row locks for entire tick to prevent duplicate task enqueue
43
+
44
+ ### Refactor
45
+
46
+ - consolidate mocked tests and remove redundant test cases
47
+ - cleanup imports
48
+ - tweak how scheduler start works
@@ -0,0 +1,50 @@
1
+ # django-periodic-tasks
2
+
3
+ Periodic/cron task scheduling for django-tasks. Backend-agnostic replacement for celery-beat.
4
+
5
+ ## Project Structure
6
+
7
+ - `django_periodic_tasks/` - Main package
8
+ - `models.py` - ScheduledTask model
9
+ - `registry.py` - ScheduleRegistry, ScheduleEntry, @scheduled_task decorator
10
+ - `cron.py` - Cron expression utilities (croniter wrapper)
11
+ - `sync.py` - Code-to-DB sync
12
+ - `scheduler.py` - PeriodicTaskScheduler (daemon thread)
13
+ - `task_resolver.py` - Resolve task_path -> Task object
14
+ - `admin.py` - Django admin (read-only for code-defined tasks)
15
+ - `management/commands/` - run_scheduler
16
+ - `sandbox/` - Development/test Django project
17
+ - `tests/` - Test suite
18
+
19
+ ## Development
20
+
21
+ Tests run inside Docker (PostgreSQL required for SELECT FOR UPDATE SKIP LOCKED):
22
+
23
+ ```bash
24
+ mise run test # Run test suite
25
+ mise run mypy # Type checking
26
+ mise run coverage # Tests with coverage
27
+ mise run tox # Full tox matrix
28
+ ```
29
+
30
+ Or directly with docker compose:
31
+
32
+ ```bash
33
+ docker compose run --rm test uv run python sandbox/manage.py test --noinput -v 2 tests
34
+ ```
35
+
36
+ ## Settings
37
+
38
+ | Setting | Type | Default | Description |
39
+ |---------|------|---------|-------------|
40
+ | `PERIODIC_TASKS_AUTOSTART` | `bool` | `False` | Start scheduler daemon thread on `AppConfig.ready()` |
41
+ | `PERIODIC_TASKS_SCHEDULER_INTERVAL` | `int` | `15` | Seconds between scheduler ticks |
42
+
43
+ ## Key Conventions
44
+
45
+ - Python 3.13+ required
46
+ - All code must pass mypy strict mode
47
+ - Tests use django-tasks DummyBackend with ENQUEUE_ON_COMMIT=False
48
+ - ScheduledTask.save() auto-computes next_run_at when it's None and task is enabled
49
+ - Code-defined schedules use source=CODE, admin-defined use source=DATABASE
50
+ - Scheduler uses SELECT FOR UPDATE SKIP LOCKED for safe multi-worker deployment
@@ -0,0 +1,8 @@
1
+ FROM registry.gitlab.com/thelabnyc/python:3.14@sha256:1be853e6f27965f57ba0fc6bdc2651ecbc3bcb02becc38b983b6af363cbc39ed
2
+ ENV PYTHONUNBUFFERED 1
3
+
4
+ RUN mkdir /code
5
+ WORKDIR /code
6
+
7
+ ADD . /code/
8
+ RUN uv sync
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2025, thelab
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
14
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15
+ PERFORMANCE OF THIS SOFTWARE.
@@ -0,0 +1,65 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-periodic-tasks
3
+ Version: 0.1.0a3
4
+ Summary: Periodic/cron task scheduling for django-tasks. Backend-agnostic replacement for celery-beat.
5
+ Project-URL: Homepage, https://gitlab.com/thelabnyc/django-periodic-tasks
6
+ Project-URL: Repository, https://gitlab.com/thelabnyc/django-periodic-tasks
7
+ Author-email: thelab <thelabdev@thelab.co>
8
+ License: ISC
9
+ License-File: LICENSE
10
+ Requires-Python: >=3.13
11
+ Requires-Dist: croniter>=1.0
12
+ Requires-Dist: django-tasks>=0.7
13
+ Requires-Dist: django>=5.2
14
+ Provides-Extra: rq
15
+ Requires-Dist: django-rq; extra == 'rq'
16
+ Requires-Dist: django-tasks[rq]; extra == 'rq'
17
+ Description-Content-Type: text/markdown
18
+
19
+ # django-periodic-tasks
20
+
21
+ Periodic/cron task scheduling for [django-tasks](https://github.com/RealOrangeOne/django-tasks). Backend-agnostic replacement for celery-beat + django-celery-beat.
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ pip install django-periodic-tasks
27
+ ```
28
+
29
+ Add to `INSTALLED_APPS`:
30
+
31
+ ```python
32
+ INSTALLED_APPS = [
33
+ ...
34
+ "django_periodic_tasks",
35
+ ]
36
+ ```
37
+
38
+ ## Usage
39
+
40
+ ### Define scheduled tasks
41
+
42
+ ```python
43
+ from django_tasks import task
44
+ from django_periodic_tasks import scheduled_task
45
+
46
+ @scheduled_task(cron="0 5 * * *", name="daily-report")
47
+ @task()
48
+ def daily_report() -> None:
49
+ ...
50
+ ```
51
+
52
+ ### Run the scheduler
53
+
54
+ Enable the autostart setting so the scheduler runs as a daemon thread inside your Django process:
55
+
56
+ ```python
57
+ # settings.py
58
+ PERIODIC_TASKS_AUTOSTART = True
59
+ ```
60
+
61
+ Or run it as a standalone process:
62
+
63
+ ```bash
64
+ python manage.py run_scheduler
65
+ ```
@@ -0,0 +1,47 @@
1
+ # django-periodic-tasks
2
+
3
+ Periodic/cron task scheduling for [django-tasks](https://github.com/RealOrangeOne/django-tasks). Backend-agnostic replacement for celery-beat + django-celery-beat.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install django-periodic-tasks
9
+ ```
10
+
11
+ Add to `INSTALLED_APPS`:
12
+
13
+ ```python
14
+ INSTALLED_APPS = [
15
+ ...
16
+ "django_periodic_tasks",
17
+ ]
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ### Define scheduled tasks
23
+
24
+ ```python
25
+ from django_tasks import task
26
+ from django_periodic_tasks import scheduled_task
27
+
28
+ @scheduled_task(cron="0 5 * * *", name="daily-report")
29
+ @task()
30
+ def daily_report() -> None:
31
+ ...
32
+ ```
33
+
34
+ ### Run the scheduler
35
+
36
+ Enable the autostart setting so the scheduler runs as a daemon thread inside your Django process:
37
+
38
+ ```python
39
+ # settings.py
40
+ PERIODIC_TASKS_AUTOSTART = True
41
+ ```
42
+
43
+ Or run it as a standalone process:
44
+
45
+ ```bash
46
+ python manage.py run_scheduler
47
+ ```
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -euxo pipefail
4
+
5
+ # Check git status
6
+ git fetch --all
7
+ CURRENT_BRANCH=$(git branch --show-current)
8
+ if [ "$CURRENT_BRANCH" != "master" ]; then
9
+ echo "This script must be run only when the master branch is checked out, but the current branch is ${CURRENT_BRANCH}. Abort!"
10
+ exit 1
11
+ fi
12
+
13
+ NUM_BEHIND=$(git log ..origin/master | wc -l | awk '{print $1}')
14
+ if [ "$NUM_BEHIND" == "0" ]; then
15
+ echo ""
16
+ else
17
+ echo "Your branch is NOT up to date with origin/master. Abort! Please fetch and rebase first."
18
+ exit 1
19
+ fi
20
+
21
+ # Update version and publish via commitizen
22
+ cz bump "$@"
@@ -0,0 +1,15 @@
1
+ from django_periodic_tasks.decorators import exactly_once
2
+ from django_periodic_tasks.registry import (
3
+ ScheduleEntry,
4
+ ScheduleRegistry,
5
+ schedule_registry,
6
+ scheduled_task,
7
+ )
8
+
9
+ __all__ = [
10
+ "ScheduleEntry",
11
+ "ScheduleRegistry",
12
+ "exactly_once",
13
+ "schedule_registry",
14
+ "scheduled_task",
15
+ ]
@@ -0,0 +1,86 @@
1
+ from typing import Any
2
+
3
+ from django.contrib import admin
4
+ from django.http import HttpRequest
5
+
6
+ from django_periodic_tasks.models import ScheduledTask, TaskExecution
7
+
8
+ # Fields that are always read-only (computed/tracking)
9
+ TRACKING_READONLY = ("source", "last_run_at", "next_run_at", "total_run_count", "created_at", "updated_at")
10
+
11
+ # All editable model fields
12
+ ALL_FIELDS = (
13
+ "name",
14
+ "task_path",
15
+ "cron_expression",
16
+ "timezone",
17
+ "args",
18
+ "kwargs",
19
+ "source",
20
+ "enabled",
21
+ "queue_name",
22
+ "priority",
23
+ "backend",
24
+ "last_run_at",
25
+ "next_run_at",
26
+ "total_run_count",
27
+ "created_at",
28
+ "updated_at",
29
+ )
30
+
31
+
32
+ @admin.register(ScheduledTask)
33
+ class ScheduledTaskAdmin(admin.ModelAdmin[ScheduledTask]):
34
+ list_display = (
35
+ "name",
36
+ "task_path",
37
+ "cron_expression",
38
+ "source",
39
+ "enabled",
40
+ "last_run_at",
41
+ "next_run_at",
42
+ "total_run_count",
43
+ )
44
+ list_filter = ("source", "enabled")
45
+ search_fields = ("name", "task_path")
46
+ ordering = ("name",)
47
+
48
+ def get_readonly_fields(
49
+ self,
50
+ request: HttpRequest,
51
+ obj: ScheduledTask | None = None,
52
+ ) -> tuple[str, ...]:
53
+ if obj is None:
54
+ # New task being created
55
+ return TRACKING_READONLY
56
+ if obj.source == ScheduledTask.Source.CODE:
57
+ # Code-defined tasks: everything is read-only
58
+ return ALL_FIELDS
59
+ # DB-defined tasks: only tracking fields are read-only
60
+ return TRACKING_READONLY
61
+
62
+ def has_delete_permission(
63
+ self,
64
+ request: HttpRequest,
65
+ obj: Any = None,
66
+ ) -> bool:
67
+ if obj is not None and obj.source == ScheduledTask.Source.CODE:
68
+ return False
69
+ return super().has_delete_permission(request, obj)
70
+
71
+
72
+ @admin.register(TaskExecution)
73
+ class TaskExecutionAdmin(admin.ModelAdmin[TaskExecution]):
74
+ list_display = ("id", "scheduled_task", "status", "created_at", "completed_at")
75
+ list_filter = ("status",)
76
+ search_fields = ("scheduled_task__name",)
77
+ ordering = ("-created_at",)
78
+
79
+ def has_add_permission(self, request: HttpRequest) -> bool:
80
+ return False
81
+
82
+ def has_change_permission(self, request: HttpRequest, obj: Any = None) -> bool:
83
+ return False
84
+
85
+ def has_delete_permission(self, request: HttpRequest, obj: Any = None) -> bool:
86
+ return False