django-database-task 0.1.0__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 (33) hide show
  1. django_database_task-0.1.0/LICENSE +21 -0
  2. django_database_task-0.1.0/MANIFEST.in +3 -0
  3. django_database_task-0.1.0/PKG-INFO +506 -0
  4. django_database_task-0.1.0/README.md +473 -0
  5. django_database_task-0.1.0/django_database_task/__init__.py +27 -0
  6. django_database_task-0.1.0/django_database_task/admin.py +211 -0
  7. django_database_task-0.1.0/django_database_task/apps.py +7 -0
  8. django_database_task-0.1.0/django_database_task/backends.py +211 -0
  9. django_database_task-0.1.0/django_database_task/executor.py +181 -0
  10. django_database_task-0.1.0/django_database_task/locale/ja/LC_MESSAGES/django.mo +0 -0
  11. django_database_task-0.1.0/django_database_task/locale/ja/LC_MESSAGES/django.po +170 -0
  12. django_database_task-0.1.0/django_database_task/management/__init__.py +0 -0
  13. django_database_task-0.1.0/django_database_task/management/commands/__init__.py +0 -0
  14. django_database_task-0.1.0/django_database_task/management/commands/purge_completed_database_tasks.py +88 -0
  15. django_database_task-0.1.0/django_database_task/management/commands/run_database_tasks.py +98 -0
  16. django_database_task-0.1.0/django_database_task/migrations/0001_initial.py +190 -0
  17. django_database_task-0.1.0/django_database_task/migrations/__init__.py +0 -0
  18. django_database_task-0.1.0/django_database_task/models.py +120 -0
  19. django_database_task-0.1.0/django_database_task/urls.py +67 -0
  20. django_database_task-0.1.0/django_database_task/views.py +205 -0
  21. django_database_task-0.1.0/django_database_task.egg-info/PKG-INFO +506 -0
  22. django_database_task-0.1.0/django_database_task.egg-info/SOURCES.txt +31 -0
  23. django_database_task-0.1.0/django_database_task.egg-info/dependency_links.txt +1 -0
  24. django_database_task-0.1.0/django_database_task.egg-info/requires.txt +6 -0
  25. django_database_task-0.1.0/django_database_task.egg-info/top_level.txt +1 -0
  26. django_database_task-0.1.0/pyproject.toml +89 -0
  27. django_database_task-0.1.0/setup.cfg +4 -0
  28. django_database_task-0.1.0/tests/test_admin.py +233 -0
  29. django_database_task-0.1.0/tests/test_backend.py +247 -0
  30. django_database_task-0.1.0/tests/test_commands.py +211 -0
  31. django_database_task-0.1.0/tests/test_executor.py +367 -0
  32. django_database_task-0.1.0/tests/test_models.py +107 -0
  33. django_database_task-0.1.0/tests/test_views.py +261 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Shinya Okano
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,3 @@
1
+ include LICENSE
2
+ include README.md
3
+ recursive-include django_database_task/locale *.po *.mo
@@ -0,0 +1,506 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-database-task
3
+ Version: 0.1.0
4
+ Summary: A database-backed task queue backend for Django 6.0's built-in task framework
5
+ Author-email: Shinya Okano <tokibito@gmail.com>
6
+ Maintainer-email: Shinya Okano <tokibito@gmail.com>
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/tokibito/django-database-task
9
+ Project-URL: Repository, https://github.com/tokibito/django-database-task
10
+ Project-URL: Issues, https://github.com/tokibito/django-database-task/issues
11
+ Keywords: django,task,queue,background,database,async
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Web Environment
14
+ Classifier: Framework :: Django
15
+ Classifier: Framework :: Django :: 6.0
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Requires-Python: >=3.12
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: Django>=6.0
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest>=7.0; extra == "dev"
30
+ Requires-Dist: pytest-django>=4.5; extra == "dev"
31
+ Requires-Dist: ruff>=0.8; extra == "dev"
32
+ Dynamic: license-file
33
+
34
+ # django-database-task
35
+
36
+ A database-backed task queue backend for Django 6.0's built-in task framework.
37
+
38
+ ## Features
39
+
40
+ - **No external dependencies** - Uses your existing database, no Redis or message broker required
41
+ - **Priority support** - Tasks can have priorities from -100 to 100
42
+ - **Delayed execution** - Schedule tasks to run at a specific time with `run_after`
43
+ - **Exclusive locking** - Prevents duplicate task execution with `SELECT FOR UPDATE SKIP LOCKED`
44
+ - **Django Admin integration** - View and manage tasks from the admin interface
45
+ - **Async support** - Supports async task functions
46
+
47
+ ## Architecture
48
+
49
+ ```mermaid
50
+ sequenceDiagram
51
+ participant App as Application
52
+ participant Backend as DatabaseTaskBackend
53
+ participant DB as Database
54
+ participant Worker as Worker Process
55
+
56
+ Note over App,Worker: Task Enqueue
57
+ App->>Backend: task.enqueue(args, kwargs)
58
+ Backend->>Backend: Validate & serialize args
59
+ Backend->>DB: INSERT task (status=READY)
60
+ DB-->>Backend: Task ID
61
+ Backend-->>App: TaskResult (id, status=READY)
62
+
63
+ Note over App,Worker: Task Execution
64
+ Worker->>DB: SELECT FOR UPDATE SKIP LOCKED<br/>(status=READY, run_after <= now)
65
+ DB-->>Worker: Task record (with lock)
66
+ Worker->>DB: UPDATE status=RUNNING
67
+ Worker->>Worker: Execute task function
68
+ alt Success
69
+ Worker->>DB: UPDATE status=SUCCESSFUL,<br/>return_value, finished_at
70
+ else Failure
71
+ Worker->>DB: UPDATE status=FAILED,<br/>errors, finished_at
72
+ end
73
+
74
+ Note over App,Worker: Result Retrieval (Optional)
75
+ App->>Backend: backend.get_result(task_id)
76
+ Backend->>DB: SELECT task
77
+ DB-->>Backend: Task record
78
+ Backend-->>App: TaskResult (status, return_value, errors)
79
+ ```
80
+
81
+ ## Requirements
82
+
83
+ - Python 3.12+
84
+ - Django 6.0+
85
+
86
+ ### Supported Databases
87
+
88
+ Django 6.0 officially supports the following database versions:
89
+
90
+ | Database | Minimum Version | Notes |
91
+ |----------|-----------------|-------|
92
+ | PostgreSQL | 14+ | Recommended for production. Full `SELECT FOR UPDATE SKIP LOCKED` support. |
93
+ | MySQL | 8.0.11+ | Full `SELECT FOR UPDATE SKIP LOCKED` support. |
94
+ | MariaDB | 10.6+ | Full `SELECT FOR UPDATE SKIP LOCKED` support. |
95
+ | SQLite | 3.31.0+ | Works for development/testing, but no row-level locking. |
96
+ | Oracle | 19c+ | Supported but not tested with this package. |
97
+
98
+ **Note**: `SELECT FOR UPDATE SKIP LOCKED` is used to prevent duplicate task execution in multi-worker environments. SQLite does not support row-level locking, so it is only recommended for development or single-worker deployments.
99
+
100
+ ## Installation
101
+
102
+ ```bash
103
+ pip install django-database-task
104
+ ```
105
+
106
+ ## Quick Start
107
+
108
+ ### 1. Add to INSTALLED_APPS
109
+
110
+ ```python
111
+ INSTALLED_APPS = [
112
+ # ...
113
+ 'django_database_task',
114
+ ]
115
+ ```
116
+
117
+ ### 2. Configure the task backend
118
+
119
+ ```python
120
+ TASKS = {
121
+ 'default': {
122
+ 'BACKEND': 'django_database_task.backends.DatabaseTaskBackend',
123
+ 'QUEUES': [], # Empty list means all queues
124
+ 'OPTIONS': {},
125
+ },
126
+ }
127
+ ```
128
+
129
+ ### 3. Run migrations
130
+
131
+ ```bash
132
+ python manage.py migrate django_database_task
133
+ ```
134
+
135
+ ### 4. Define a task
136
+
137
+ ```python
138
+ from django.tasks import task
139
+
140
+ @task
141
+ def send_welcome_email(user_id):
142
+ user = User.objects.get(id=user_id)
143
+ # Send email...
144
+ return f"Email sent to {user.email}"
145
+ ```
146
+
147
+ ### 5. Enqueue the task
148
+
149
+ ```python
150
+ result = send_welcome_email.enqueue(user_id=123)
151
+ print(f"Task ID: {result.id}")
152
+ ```
153
+
154
+ ### 6. Run the worker
155
+
156
+ ```bash
157
+ # Run once (exit when no tasks)
158
+ python manage.py run_database_tasks
159
+
160
+ # Run continuously (poll every 5 seconds)
161
+ python manage.py run_database_tasks --continuous --interval 5
162
+ ```
163
+
164
+ ## Usage
165
+
166
+ ### Important: JSON-Serializable Parameters
167
+
168
+ Task arguments, keyword arguments, and return values **must be JSON-serializable**.
169
+
170
+ Supported types:
171
+ - `str`, `int`, `float`, `bool`, `None`
172
+ - `dict` (with JSON-serializable keys and values)
173
+ - `list`, `tuple` (with JSON-serializable elements)
174
+ - `bytes` (UTF-8 decodable only)
175
+
176
+ **Not supported** (will raise `TypeError`):
177
+ - `datetime`, `date`, `time` - convert to ISO string: `dt.isoformat()`
178
+ - `UUID` - convert to string: `str(uuid)`
179
+ - `Decimal` - convert to float or string
180
+ - Custom objects - serialize manually
181
+
182
+ ```python
183
+ from django.tasks import task
184
+
185
+ # ❌ This will raise TypeError
186
+ @task
187
+ def bad_task(user_id, created_at):
188
+ pass
189
+ bad_task.enqueue(123, datetime.now()) # TypeError!
190
+
191
+ # ✅ Convert to JSON-serializable types
192
+ @task
193
+ def good_task(user_id, created_at_iso):
194
+ created_at = datetime.fromisoformat(created_at_iso)
195
+ # ...
196
+ good_task.enqueue(123, datetime.now().isoformat()) # OK
197
+ ```
198
+
199
+ ### Task with priority
200
+
201
+ ```python
202
+ @task(priority=10) # Higher priority, runs first
203
+ def urgent_task():
204
+ pass
205
+
206
+ @task(priority=-10) # Lower priority
207
+ def background_task():
208
+ pass
209
+ ```
210
+
211
+ ### Delayed execution
212
+
213
+ ```python
214
+ from datetime import timedelta
215
+ from django.utils import timezone
216
+
217
+ # Run 1 hour from now
218
+ delayed_task = my_task.using(run_after=timezone.now() + timedelta(hours=1))
219
+ result = delayed_task.enqueue()
220
+ ```
221
+
222
+ ### Task with context
223
+
224
+ ```python
225
+ @task(takes_context=True)
226
+ def task_with_context(context, message):
227
+ task_id = context.task_result.id
228
+ attempt = context.attempt
229
+ return f"Task {task_id} (attempt {attempt}): {message}"
230
+ ```
231
+
232
+ ### Async tasks
233
+
234
+ ```python
235
+ @task
236
+ async def fetch_data(url):
237
+ async with aiohttp.ClientSession() as session:
238
+ async with session.get(url) as response:
239
+ return await response.text()
240
+
241
+ # Enqueue like normal tasks
242
+ result = fetch_data.enqueue("https://example.com/api")
243
+ ```
244
+
245
+ ### Queue-specific tasks
246
+
247
+ ```python
248
+ @task(queue_name="emails")
249
+ def send_newsletter():
250
+ pass
251
+
252
+ # Run worker for specific queue
253
+ # python manage.py run_database_tasks --queue emails
254
+ ```
255
+
256
+ ## Management Commands
257
+
258
+ ### run_database_tasks
259
+
260
+ Execute tasks queued in the database.
261
+
262
+ ```bash
263
+ python manage.py run_database_tasks [options]
264
+ ```
265
+
266
+ | Option | Description |
267
+ |--------|-------------|
268
+ | `--queue` | Queue name to process (all queues if not specified) |
269
+ | `--backend` | Backend name (default: "default") |
270
+ | `--continuous` | Keep polling even when no tasks |
271
+ | `--interval` | Polling interval in seconds (default: 5) |
272
+ | `--max-tasks` | Maximum number of tasks to process (0=unlimited) |
273
+
274
+ ### purge_completed_database_tasks
275
+
276
+ Delete completed task records from the database.
277
+
278
+ ```bash
279
+ python manage.py purge_completed_database_tasks [options]
280
+ ```
281
+
282
+ | Option | Description |
283
+ |--------|-------------|
284
+ | `--days` | Delete tasks completed more than N days ago (0=all) |
285
+ | `--status` | Target statuses, comma-separated (default: "SUCCESSFUL,FAILED") |
286
+ | `--batch-size` | Number of tasks to delete at once (default: 1000) |
287
+ | `--dry-run` | Show count only without deleting |
288
+
289
+ ## Programmatic API
290
+
291
+ You can also process tasks programmatically without management commands:
292
+
293
+ ```python
294
+ from django_database_task import process_one_task, process_tasks, get_pending_task_count
295
+
296
+ # Process a single task
297
+ result = process_one_task()
298
+ if result:
299
+ print(f"Processed: {result.id}, status: {result.status}")
300
+
301
+ # Process multiple tasks
302
+ results = process_tasks(max_tasks=10)
303
+ print(f"Processed {len(results)} tasks")
304
+
305
+ # Process tasks from a specific queue
306
+ results = process_tasks(queue_name="emails", max_tasks=5)
307
+
308
+ # Get pending task count
309
+ count = get_pending_task_count()
310
+ print(f"Pending tasks: {count}")
311
+ ```
312
+
313
+ ## HTTP Endpoints (Optional)
314
+
315
+ For environments where cron or direct command execution is not available
316
+ (e.g., serverless, PaaS), you can use HTTP endpoints to trigger task processing.
317
+
318
+ ### Setup
319
+
320
+ Include the URLs in your project:
321
+
322
+ ```python
323
+ # urls.py
324
+ from django.urls import path, include
325
+
326
+ urlpatterns = [
327
+ path("tasks/", include("django_database_task.urls")),
328
+ ]
329
+ ```
330
+
331
+ ### Available Endpoints
332
+
333
+ | Endpoint | Method | Description |
334
+ |----------|--------|-------------|
335
+ | `/tasks/run/` | POST | Process multiple pending tasks |
336
+ | `/tasks/run-one/` | POST | Process a single pending task |
337
+ | `/tasks/status/` | GET | Get pending task count |
338
+
339
+ ### Request Parameters
340
+
341
+ #### POST `/tasks/run/`
342
+
343
+ | Parameter | Type | Default | Description |
344
+ |-----------|------|---------|-------------|
345
+ | `max_tasks` | int | 10 | Maximum tasks to process (1-100) |
346
+ | `queue_name` | string | null | Filter by queue name |
347
+ | `backend_name` | string | "default" | Task backend name |
348
+
349
+ Response:
350
+ ```json
351
+ {
352
+ "processed": 3,
353
+ "results": [
354
+ {"id": "uuid", "status": "SUCCESSFUL", "task_path": "myapp.tasks.send_email"},
355
+ {"id": "uuid", "status": "FAILED", "task_path": "myapp.tasks.process_data"}
356
+ ]
357
+ }
358
+ ```
359
+
360
+ #### POST `/tasks/run-one/`
361
+
362
+ | Parameter | Type | Default | Description |
363
+ |-----------|------|---------|-------------|
364
+ | `queue_name` | string | null | Filter by queue name |
365
+ | `backend_name` | string | "default" | Task backend name |
366
+
367
+ Response:
368
+ ```json
369
+ {"processed": true, "result": {"id": "uuid", "status": "SUCCESSFUL", "task_path": "..."}}
370
+ ```
371
+ or
372
+ ```json
373
+ {"processed": false, "result": null}
374
+ ```
375
+
376
+ #### GET `/tasks/status/`
377
+
378
+ | Parameter | Type | Default | Description |
379
+ |-----------|------|---------|-------------|
380
+ | `queue_name` | string | null | Filter by queue name |
381
+ | `backend_name` | string | "default" | Task backend name |
382
+
383
+ Response:
384
+ ```json
385
+ {"pending_count": 5}
386
+ ```
387
+
388
+ ### Example Usage
389
+
390
+ ```bash
391
+ # Process up to 10 tasks
392
+ curl -X POST http://localhost:8000/tasks/run/ \
393
+ -H "Content-Type: application/json" \
394
+ -d '{"max_tasks": 10}'
395
+
396
+ # Process tasks from a specific queue
397
+ curl -X POST http://localhost:8000/tasks/run/ \
398
+ -H "Content-Type: application/json" \
399
+ -d '{"queue_name": "emails", "max_tasks": 5}'
400
+
401
+ # Get pending task count
402
+ curl http://localhost:8000/tasks/status/
403
+ ```
404
+
405
+ ### Use Cases
406
+
407
+ #### Cloud Scheduler / Cron Job
408
+
409
+ Call the endpoint periodically to process tasks:
410
+
411
+ ```bash
412
+ # Every minute via cron or Cloud Scheduler
413
+ curl -X POST https://your-app.com/tasks/run/ \
414
+ -H "Authorization: Bearer $TOKEN" \
415
+ -H "Content-Type: application/json" \
416
+ -d '{"max_tasks": 50}'
417
+ ```
418
+
419
+ #### Webhook Trigger
420
+
421
+ Trigger task processing after an event:
422
+
423
+ ```python
424
+ # In your webhook handler
425
+ import requests
426
+
427
+ def handle_webhook(request):
428
+ # ... process webhook ...
429
+
430
+ # Trigger background task processing
431
+ requests.post(
432
+ "http://localhost:8000/tasks/run/",
433
+ json={"max_tasks": 10}
434
+ )
435
+ ```
436
+
437
+ #### Health Check with Task Status
438
+
439
+ Monitor pending task count:
440
+
441
+ ```bash
442
+ # Alert if too many pending tasks
443
+ count=$(curl -s http://localhost:8000/tasks/status/ | jq '.pending_count')
444
+ if [ "$count" -gt 100 ]; then
445
+ echo "Warning: $count pending tasks"
446
+ fi
447
+ ```
448
+
449
+ ### Security
450
+
451
+ The endpoints are CSRF-exempt for API/webhook use. **Always add authentication in production:**
452
+
453
+ ```python
454
+ from django.contrib.admin.views.decorators import staff_member_required
455
+ from django_database_task.views import RunTasksView, RunOneTaskView, TaskStatusView
456
+
457
+ urlpatterns = [
458
+ path(
459
+ "tasks/run/",
460
+ staff_member_required(RunTasksView.as_view()),
461
+ name="run_tasks",
462
+ ),
463
+ path(
464
+ "tasks/run-one/",
465
+ staff_member_required(RunOneTaskView.as_view()),
466
+ name="run_one_task",
467
+ ),
468
+ path(
469
+ "tasks/status/",
470
+ staff_member_required(TaskStatusView.as_view()),
471
+ name="task_status",
472
+ ),
473
+ ]
474
+ ```
475
+
476
+ Or use token-based authentication:
477
+
478
+ ```python
479
+ from django.http import HttpResponseForbidden
480
+ from django.conf import settings
481
+
482
+ def require_api_token(view_func):
483
+ def wrapper(request, *args, **kwargs):
484
+ token = request.headers.get("Authorization", "").replace("Bearer ", "")
485
+ if token != settings.TASK_API_TOKEN:
486
+ return HttpResponseForbidden("Invalid token")
487
+ return view_func(request, *args, **kwargs)
488
+ return wrapper
489
+
490
+ urlpatterns = [
491
+ path("tasks/run/", require_api_token(RunTasksView.as_view())),
492
+ ]
493
+ ```
494
+
495
+ ## Django Admin
496
+
497
+ The package includes a Django Admin integration to view task status:
498
+
499
+ - Task list with status badges
500
+ - Filter by status, queue, backend
501
+ - Search by task ID or path
502
+ - View task arguments and results
503
+
504
+ ## License
505
+
506
+ MIT License - see [LICENSE](LICENSE) for details.