django-database-task 0.1.0__tar.gz → 0.2.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.
- {django_database_task-0.1.0/django_database_task.egg-info → django_database_task-0.2.0}/PKG-INFO +244 -4
- {django_database_task-0.1.0 → django_database_task-0.2.0}/README.md +240 -3
- {django_database_task-0.1.0 → django_database_task-0.2.0}/django_database_task/__init__.py +3 -1
- {django_database_task-0.1.0 → django_database_task-0.2.0}/django_database_task/backends.py +14 -0
- django_database_task-0.2.0/django_database_task/cloudtasks/__init__.py +83 -0
- django_database_task-0.2.0/django_database_task/cloudtasks/auth.py +128 -0
- django_database_task-0.2.0/django_database_task/cloudtasks/backend.py +235 -0
- django_database_task-0.2.0/django_database_task/cloudtasks/detection.py +123 -0
- {django_database_task-0.1.0 → django_database_task-0.2.0}/django_database_task/executor.py +69 -0
- {django_database_task-0.1.0 → django_database_task-0.2.0}/django_database_task/urls.py +9 -1
- django_database_task-0.2.0/django_database_task/views.py +477 -0
- {django_database_task-0.1.0 → django_database_task-0.2.0/django_database_task.egg-info}/PKG-INFO +244 -4
- {django_database_task-0.1.0 → django_database_task-0.2.0}/django_database_task.egg-info/SOURCES.txt +4 -0
- {django_database_task-0.1.0 → django_database_task-0.2.0}/django_database_task.egg-info/requires.txt +4 -0
- {django_database_task-0.1.0 → django_database_task-0.2.0}/pyproject.toml +5 -1
- django_database_task-0.2.0/tests/test_views.py +799 -0
- django_database_task-0.1.0/django_database_task/views.py +0 -205
- django_database_task-0.1.0/tests/test_views.py +0 -261
- {django_database_task-0.1.0 → django_database_task-0.2.0}/LICENSE +0 -0
- {django_database_task-0.1.0 → django_database_task-0.2.0}/MANIFEST.in +0 -0
- {django_database_task-0.1.0 → django_database_task-0.2.0}/django_database_task/admin.py +0 -0
- {django_database_task-0.1.0 → django_database_task-0.2.0}/django_database_task/apps.py +0 -0
- {django_database_task-0.1.0 → django_database_task-0.2.0}/django_database_task/locale/ja/LC_MESSAGES/django.mo +0 -0
- {django_database_task-0.1.0 → django_database_task-0.2.0}/django_database_task/locale/ja/LC_MESSAGES/django.po +0 -0
- {django_database_task-0.1.0 → django_database_task-0.2.0}/django_database_task/management/__init__.py +0 -0
- {django_database_task-0.1.0 → django_database_task-0.2.0}/django_database_task/management/commands/__init__.py +0 -0
- {django_database_task-0.1.0 → django_database_task-0.2.0}/django_database_task/management/commands/purge_completed_database_tasks.py +0 -0
- {django_database_task-0.1.0 → django_database_task-0.2.0}/django_database_task/management/commands/run_database_tasks.py +0 -0
- {django_database_task-0.1.0 → django_database_task-0.2.0}/django_database_task/migrations/0001_initial.py +0 -0
- {django_database_task-0.1.0 → django_database_task-0.2.0}/django_database_task/migrations/__init__.py +0 -0
- {django_database_task-0.1.0 → django_database_task-0.2.0}/django_database_task/models.py +0 -0
- {django_database_task-0.1.0 → django_database_task-0.2.0}/django_database_task.egg-info/dependency_links.txt +0 -0
- {django_database_task-0.1.0 → django_database_task-0.2.0}/django_database_task.egg-info/top_level.txt +0 -0
- {django_database_task-0.1.0 → django_database_task-0.2.0}/setup.cfg +0 -0
- {django_database_task-0.1.0 → django_database_task-0.2.0}/tests/test_admin.py +0 -0
- {django_database_task-0.1.0 → django_database_task-0.2.0}/tests/test_backend.py +0 -0
- {django_database_task-0.1.0 → django_database_task-0.2.0}/tests/test_commands.py +0 -0
- {django_database_task-0.1.0 → django_database_task-0.2.0}/tests/test_executor.py +0 -0
- {django_database_task-0.1.0 → django_database_task-0.2.0}/tests/test_models.py +0 -0
{django_database_task-0.1.0/django_database_task.egg-info → django_database_task-0.2.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-database-task
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A database-backed task queue backend for Django 6.0's built-in task framework
|
|
5
5
|
Author-email: Shinya Okano <tokibito@gmail.com>
|
|
6
6
|
Maintainer-email: Shinya Okano <tokibito@gmail.com>
|
|
@@ -25,6 +25,9 @@ Requires-Python: >=3.12
|
|
|
25
25
|
Description-Content-Type: text/markdown
|
|
26
26
|
License-File: LICENSE
|
|
27
27
|
Requires-Dist: Django>=6.0
|
|
28
|
+
Provides-Extra: cloudtasks
|
|
29
|
+
Requires-Dist: google-cloud-tasks>=2.0.0; extra == "cloudtasks"
|
|
30
|
+
Requires-Dist: google-auth>=2.0.0; extra == "cloudtasks"
|
|
28
31
|
Provides-Extra: dev
|
|
29
32
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
30
33
|
Requires-Dist: pytest-django>=4.5; extra == "dev"
|
|
@@ -43,6 +46,7 @@ A database-backed task queue backend for Django 6.0's built-in task framework.
|
|
|
43
46
|
- **Exclusive locking** - Prevents duplicate task execution with `SELECT FOR UPDATE SKIP LOCKED`
|
|
44
47
|
- **Django Admin integration** - View and manage tasks from the admin interface
|
|
45
48
|
- **Async support** - Supports async task functions
|
|
49
|
+
- **Google Cloud Tasks integration** - Optional backend for GAE/Cloud Run with auto-detection
|
|
46
50
|
|
|
47
51
|
## Architecture
|
|
48
52
|
|
|
@@ -291,7 +295,12 @@ python manage.py purge_completed_database_tasks [options]
|
|
|
291
295
|
You can also process tasks programmatically without management commands:
|
|
292
296
|
|
|
293
297
|
```python
|
|
294
|
-
from django_database_task import
|
|
298
|
+
from django_database_task import (
|
|
299
|
+
process_one_task,
|
|
300
|
+
process_tasks,
|
|
301
|
+
get_pending_task_count,
|
|
302
|
+
run_task_by_id,
|
|
303
|
+
)
|
|
295
304
|
|
|
296
305
|
# Process a single task
|
|
297
306
|
result = process_one_task()
|
|
@@ -308,6 +317,14 @@ results = process_tasks(queue_name="emails", max_tasks=5)
|
|
|
308
317
|
# Get pending task count
|
|
309
318
|
count = get_pending_task_count()
|
|
310
319
|
print(f"Pending tasks: {count}")
|
|
320
|
+
|
|
321
|
+
# Execute a specific task by ID
|
|
322
|
+
result = run_task_by_id("550e8400-e29b-41d4-a716-446655440000")
|
|
323
|
+
if result:
|
|
324
|
+
print(f"Executed: {result.id}, status: {result.status}")
|
|
325
|
+
|
|
326
|
+
# Retry a failed task
|
|
327
|
+
result = run_task_by_id("...", allow_retry=True)
|
|
311
328
|
```
|
|
312
329
|
|
|
313
330
|
## HTTP Endpoints (Optional)
|
|
@@ -335,6 +352,8 @@ urlpatterns = [
|
|
|
335
352
|
| `/tasks/run/` | POST | Process multiple pending tasks |
|
|
336
353
|
| `/tasks/run-one/` | POST | Process a single pending task |
|
|
337
354
|
| `/tasks/status/` | GET | Get pending task count |
|
|
355
|
+
| `/tasks/execute/<uuid>/` | POST | Execute a specific task by ID |
|
|
356
|
+
| `/tasks/purge/` | POST | Delete completed tasks |
|
|
338
357
|
|
|
339
358
|
### Request Parameters
|
|
340
359
|
|
|
@@ -385,6 +404,52 @@ Response:
|
|
|
385
404
|
{"pending_count": 5}
|
|
386
405
|
```
|
|
387
406
|
|
|
407
|
+
#### POST `/tasks/execute/<uuid>/`
|
|
408
|
+
|
|
409
|
+
Execute a specific task by ID. This endpoint is designed for external trigger systems
|
|
410
|
+
(e.g., Cloud Tasks, webhooks) that need to execute a specific task.
|
|
411
|
+
|
|
412
|
+
| Parameter | Type | Default | Description |
|
|
413
|
+
|-----------|------|---------|-------------|
|
|
414
|
+
| `fail_on_error` | query string | "false" | Return HTTP 500 on task failure |
|
|
415
|
+
| `allow_retry` | query string | "false" | Allow re-execution of FAILED tasks |
|
|
416
|
+
|
|
417
|
+
Response (success):
|
|
418
|
+
```json
|
|
419
|
+
{"executed": true, "result": {"id": "uuid", "status": "SUCCESSFUL", "task_path": "..."}}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
Response (task not in executable status):
|
|
423
|
+
```json
|
|
424
|
+
{"executed": false, "reason": "Task is not in READY status"}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
Response (task not found):
|
|
428
|
+
```json
|
|
429
|
+
{"error": "Task not found"} // HTTP 404
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
#### POST `/tasks/purge/`
|
|
433
|
+
|
|
434
|
+
Delete completed tasks from the database. Useful for cron-based cleanup.
|
|
435
|
+
|
|
436
|
+
| Parameter | Type | Default | Description |
|
|
437
|
+
|-----------|------|---------|-------------|
|
|
438
|
+
| `days` | int | 0 | Delete tasks completed more than N days ago (0=all) |
|
|
439
|
+
| `status` | string | "SUCCESSFUL,FAILED" | Target statuses, comma-separated |
|
|
440
|
+
| `batch_size` | int | 1000 | Number of tasks to delete at once (max: 10000) |
|
|
441
|
+
| `dry_run` | bool | false | If true, return count without deleting |
|
|
442
|
+
|
|
443
|
+
Response:
|
|
444
|
+
```json
|
|
445
|
+
{"deleted": 150, "dry_run": false}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
Response (dry run):
|
|
449
|
+
```json
|
|
450
|
+
{"count": 150, "dry_run": true}
|
|
451
|
+
```
|
|
452
|
+
|
|
388
453
|
### Example Usage
|
|
389
454
|
|
|
390
455
|
```bash
|
|
@@ -400,6 +465,16 @@ curl -X POST http://localhost:8000/tasks/run/ \
|
|
|
400
465
|
|
|
401
466
|
# Get pending task count
|
|
402
467
|
curl http://localhost:8000/tasks/status/
|
|
468
|
+
|
|
469
|
+
# Delete tasks completed more than 7 days ago
|
|
470
|
+
curl -X POST http://localhost:8000/tasks/purge/ \
|
|
471
|
+
-H "Content-Type: application/json" \
|
|
472
|
+
-d '{"days": 7}'
|
|
473
|
+
|
|
474
|
+
# Dry run to check how many tasks would be deleted
|
|
475
|
+
curl -X POST http://localhost:8000/tasks/purge/ \
|
|
476
|
+
-H "Content-Type: application/json" \
|
|
477
|
+
-d '{"days": 30, "dry_run": true}'
|
|
403
478
|
```
|
|
404
479
|
|
|
405
480
|
### Use Cases
|
|
@@ -446,13 +521,31 @@ if [ "$count" -gt 100 ]; then
|
|
|
446
521
|
fi
|
|
447
522
|
```
|
|
448
523
|
|
|
524
|
+
#### Scheduled Cleanup
|
|
525
|
+
|
|
526
|
+
Use cron or Cloud Scheduler to delete old completed tasks:
|
|
527
|
+
|
|
528
|
+
```bash
|
|
529
|
+
# Daily cleanup via cron or Cloud Scheduler
|
|
530
|
+
# Delete tasks completed more than 30 days ago
|
|
531
|
+
curl -X POST https://your-app.com/tasks/purge/ \
|
|
532
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
533
|
+
-H "Content-Type: application/json" \
|
|
534
|
+
-d '{"days": 30}'
|
|
535
|
+
```
|
|
536
|
+
|
|
449
537
|
### Security
|
|
450
538
|
|
|
451
539
|
The endpoints are CSRF-exempt for API/webhook use. **Always add authentication in production:**
|
|
452
540
|
|
|
453
541
|
```python
|
|
454
542
|
from django.contrib.admin.views.decorators import staff_member_required
|
|
455
|
-
from django_database_task.views import
|
|
543
|
+
from django_database_task.views import (
|
|
544
|
+
RunTasksView,
|
|
545
|
+
RunOneTaskView,
|
|
546
|
+
TaskStatusView,
|
|
547
|
+
PurgeCompletedTasksView,
|
|
548
|
+
)
|
|
456
549
|
|
|
457
550
|
urlpatterns = [
|
|
458
551
|
path(
|
|
@@ -470,6 +563,11 @@ urlpatterns = [
|
|
|
470
563
|
staff_member_required(TaskStatusView.as_view()),
|
|
471
564
|
name="task_status",
|
|
472
565
|
),
|
|
566
|
+
path(
|
|
567
|
+
"tasks/purge/",
|
|
568
|
+
staff_member_required(PurgeCompletedTasksView.as_view()),
|
|
569
|
+
name="purge_completed_tasks",
|
|
570
|
+
),
|
|
473
571
|
]
|
|
474
572
|
```
|
|
475
573
|
|
|
@@ -492,15 +590,157 @@ urlpatterns = [
|
|
|
492
590
|
]
|
|
493
591
|
```
|
|
494
592
|
|
|
593
|
+
## Google Cloud Tasks Integration
|
|
594
|
+
|
|
595
|
+
For serverless environments like Google App Engine or Cloud Run, you can use the Cloud Tasks backend to automatically create Cloud Tasks when tasks are enqueued.
|
|
596
|
+
|
|
597
|
+
### Installation
|
|
598
|
+
|
|
599
|
+
```bash
|
|
600
|
+
pip install django-database-task[cloudtasks]
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
### Quick Setup
|
|
604
|
+
|
|
605
|
+
```python
|
|
606
|
+
# settings.py
|
|
607
|
+
TASKS = {
|
|
608
|
+
"default": {
|
|
609
|
+
"BACKEND": "django_database_task.cloudtasks.CloudTasksDatabaseBackend",
|
|
610
|
+
"OPTIONS": {
|
|
611
|
+
"CLOUD_TASKS_QUEUE": "default", # Only required setting
|
|
612
|
+
},
|
|
613
|
+
},
|
|
614
|
+
}
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
Project ID, location, and handler URL are auto-detected from GAE/Cloud Run environment variables.
|
|
618
|
+
|
|
619
|
+
### How It Works
|
|
620
|
+
|
|
621
|
+
```
|
|
622
|
+
1. App enqueues task → Task saved to database + Cloud Task created
|
|
623
|
+
2. Cloud Task triggers HTTP request → /tasks/execute/<task_id>/
|
|
624
|
+
3. Handler retrieves task from database and executes it
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
The Cloud Task only contains the task ID. All task parameters are stored in the database, ensuring:
|
|
628
|
+
- **Blue/Green deployment support**: Tasks execute on the same version that enqueued them
|
|
629
|
+
- **Database as source of truth**: Task parameters are never lost
|
|
630
|
+
- **Automatic retry**: Cloud Tasks handles retry with the task ID
|
|
631
|
+
|
|
632
|
+
### Configuration Options
|
|
633
|
+
|
|
634
|
+
```python
|
|
635
|
+
TASKS = {
|
|
636
|
+
"default": {
|
|
637
|
+
"BACKEND": "django_database_task.cloudtasks.CloudTasksDatabaseBackend",
|
|
638
|
+
"OPTIONS": {
|
|
639
|
+
# Required
|
|
640
|
+
"CLOUD_TASKS_QUEUE": "default",
|
|
641
|
+
|
|
642
|
+
# Auto-detected (override if needed)
|
|
643
|
+
# "CLOUD_TASKS_PROJECT": "my-project",
|
|
644
|
+
# "CLOUD_TASKS_LOCATION": "asia-northeast1",
|
|
645
|
+
# "TASK_HANDLER_URL": "https://myapp.example.com/tasks/execute/{task_id}/",
|
|
646
|
+
# "TASK_HANDLER_PATH": "/tasks/execute/{task_id}/",
|
|
647
|
+
|
|
648
|
+
# OIDC authentication (optional)
|
|
649
|
+
# "OIDC_SERVICE_ACCOUNT_EMAIL": "...",
|
|
650
|
+
# "OIDC_AUDIENCE": "https://...",
|
|
651
|
+
},
|
|
652
|
+
},
|
|
653
|
+
}
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
### Auto-Detection
|
|
657
|
+
|
|
658
|
+
| Setting | Environment Variable | Description |
|
|
659
|
+
|---------|---------------------|-------------|
|
|
660
|
+
| Project | `GOOGLE_CLOUD_PROJECT` | GCP project ID |
|
|
661
|
+
| Location | `CLOUD_RUN_REGION` / `GAE_REGION` | Cloud Tasks region |
|
|
662
|
+
| Handler URL | Built from `K_SERVICE`, `GAE_SERVICE`, `GAE_VERSION` | Task execution endpoint |
|
|
663
|
+
|
|
664
|
+
### OIDC Authentication
|
|
665
|
+
|
|
666
|
+
When `OIDC_SERVICE_ACCOUNT_EMAIL` is configured, Cloud Tasks will send OIDC tokens with each request. The backend automatically verifies these tokens on the `/tasks/execute/` and `/tasks/purge/` endpoints.
|
|
667
|
+
|
|
668
|
+
```python
|
|
669
|
+
# settings.py - Automatic OIDC verification
|
|
670
|
+
TASKS = {
|
|
671
|
+
"default": {
|
|
672
|
+
"BACKEND": "django_database_task.cloudtasks.CloudTasksDatabaseBackend",
|
|
673
|
+
"OPTIONS": {
|
|
674
|
+
"CLOUD_TASKS_QUEUE": "default",
|
|
675
|
+
"OIDC_SERVICE_ACCOUNT_EMAIL": "my-sa@project.iam.gserviceaccount.com",
|
|
676
|
+
# OIDC_AUDIENCE is auto-detected from handler URL if not set
|
|
677
|
+
},
|
|
678
|
+
},
|
|
679
|
+
}
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
Alternatively, you can use the decorator directly on your URL configuration:
|
|
683
|
+
|
|
684
|
+
```python
|
|
685
|
+
# urls.py
|
|
686
|
+
from django.urls import path
|
|
687
|
+
from django_database_task.views import ExecuteTaskView
|
|
688
|
+
from django_database_task.cloudtasks import verify_cloud_tasks_oidc
|
|
689
|
+
|
|
690
|
+
urlpatterns = [
|
|
691
|
+
path(
|
|
692
|
+
"tasks/execute/<uuid:task_id>/",
|
|
693
|
+
verify_cloud_tasks_oidc(
|
|
694
|
+
ExecuteTaskView.as_view(),
|
|
695
|
+
audience="https://myapp.example.com"
|
|
696
|
+
),
|
|
697
|
+
name="execute_task",
|
|
698
|
+
),
|
|
699
|
+
]
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
### Detection Utilities
|
|
703
|
+
|
|
704
|
+
You can use the detection functions directly:
|
|
705
|
+
|
|
706
|
+
```python
|
|
707
|
+
from django_database_task.cloudtasks import (
|
|
708
|
+
detect_gcp_project,
|
|
709
|
+
detect_gcp_location,
|
|
710
|
+
detect_task_handler_host,
|
|
711
|
+
is_cloud_run,
|
|
712
|
+
is_app_engine,
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
if is_cloud_run():
|
|
716
|
+
print(f"Running on Cloud Run in {detect_gcp_location()}")
|
|
717
|
+
elif is_app_engine():
|
|
718
|
+
print(f"Running on App Engine in project {detect_gcp_project()}")
|
|
719
|
+
```
|
|
720
|
+
|
|
495
721
|
## Django Admin
|
|
496
722
|
|
|
497
|
-
The package includes a Django Admin integration to view
|
|
723
|
+
The package includes a Django Admin integration to view and manage tasks:
|
|
498
724
|
|
|
499
725
|
- Task list with status badges
|
|
500
726
|
- Filter by status, queue, backend
|
|
501
727
|
- Search by task ID or path
|
|
502
728
|
- View task arguments and results
|
|
503
729
|
|
|
730
|
+
### Admin Actions
|
|
731
|
+
|
|
732
|
+
The admin interface provides the following bulk actions:
|
|
733
|
+
|
|
734
|
+
| Action | Description |
|
|
735
|
+
|--------|-------------|
|
|
736
|
+
| **Run selected tasks** | Execute selected tasks that are in READY status |
|
|
737
|
+
| **Retry failed tasks** | Reset FAILED tasks to READY status and re-execute them |
|
|
738
|
+
|
|
739
|
+
These actions are useful for:
|
|
740
|
+
- Manually triggering task execution from the admin
|
|
741
|
+
- Retrying failed tasks after fixing issues
|
|
742
|
+
- Testing task execution during development
|
|
743
|
+
|
|
504
744
|
## License
|
|
505
745
|
|
|
506
746
|
MIT License - see [LICENSE](LICENSE) for details.
|
|
@@ -10,6 +10,7 @@ A database-backed task queue backend for Django 6.0's built-in task framework.
|
|
|
10
10
|
- **Exclusive locking** - Prevents duplicate task execution with `SELECT FOR UPDATE SKIP LOCKED`
|
|
11
11
|
- **Django Admin integration** - View and manage tasks from the admin interface
|
|
12
12
|
- **Async support** - Supports async task functions
|
|
13
|
+
- **Google Cloud Tasks integration** - Optional backend for GAE/Cloud Run with auto-detection
|
|
13
14
|
|
|
14
15
|
## Architecture
|
|
15
16
|
|
|
@@ -258,7 +259,12 @@ python manage.py purge_completed_database_tasks [options]
|
|
|
258
259
|
You can also process tasks programmatically without management commands:
|
|
259
260
|
|
|
260
261
|
```python
|
|
261
|
-
from django_database_task import
|
|
262
|
+
from django_database_task import (
|
|
263
|
+
process_one_task,
|
|
264
|
+
process_tasks,
|
|
265
|
+
get_pending_task_count,
|
|
266
|
+
run_task_by_id,
|
|
267
|
+
)
|
|
262
268
|
|
|
263
269
|
# Process a single task
|
|
264
270
|
result = process_one_task()
|
|
@@ -275,6 +281,14 @@ results = process_tasks(queue_name="emails", max_tasks=5)
|
|
|
275
281
|
# Get pending task count
|
|
276
282
|
count = get_pending_task_count()
|
|
277
283
|
print(f"Pending tasks: {count}")
|
|
284
|
+
|
|
285
|
+
# Execute a specific task by ID
|
|
286
|
+
result = run_task_by_id("550e8400-e29b-41d4-a716-446655440000")
|
|
287
|
+
if result:
|
|
288
|
+
print(f"Executed: {result.id}, status: {result.status}")
|
|
289
|
+
|
|
290
|
+
# Retry a failed task
|
|
291
|
+
result = run_task_by_id("...", allow_retry=True)
|
|
278
292
|
```
|
|
279
293
|
|
|
280
294
|
## HTTP Endpoints (Optional)
|
|
@@ -302,6 +316,8 @@ urlpatterns = [
|
|
|
302
316
|
| `/tasks/run/` | POST | Process multiple pending tasks |
|
|
303
317
|
| `/tasks/run-one/` | POST | Process a single pending task |
|
|
304
318
|
| `/tasks/status/` | GET | Get pending task count |
|
|
319
|
+
| `/tasks/execute/<uuid>/` | POST | Execute a specific task by ID |
|
|
320
|
+
| `/tasks/purge/` | POST | Delete completed tasks |
|
|
305
321
|
|
|
306
322
|
### Request Parameters
|
|
307
323
|
|
|
@@ -352,6 +368,52 @@ Response:
|
|
|
352
368
|
{"pending_count": 5}
|
|
353
369
|
```
|
|
354
370
|
|
|
371
|
+
#### POST `/tasks/execute/<uuid>/`
|
|
372
|
+
|
|
373
|
+
Execute a specific task by ID. This endpoint is designed for external trigger systems
|
|
374
|
+
(e.g., Cloud Tasks, webhooks) that need to execute a specific task.
|
|
375
|
+
|
|
376
|
+
| Parameter | Type | Default | Description |
|
|
377
|
+
|-----------|------|---------|-------------|
|
|
378
|
+
| `fail_on_error` | query string | "false" | Return HTTP 500 on task failure |
|
|
379
|
+
| `allow_retry` | query string | "false" | Allow re-execution of FAILED tasks |
|
|
380
|
+
|
|
381
|
+
Response (success):
|
|
382
|
+
```json
|
|
383
|
+
{"executed": true, "result": {"id": "uuid", "status": "SUCCESSFUL", "task_path": "..."}}
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
Response (task not in executable status):
|
|
387
|
+
```json
|
|
388
|
+
{"executed": false, "reason": "Task is not in READY status"}
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
Response (task not found):
|
|
392
|
+
```json
|
|
393
|
+
{"error": "Task not found"} // HTTP 404
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
#### POST `/tasks/purge/`
|
|
397
|
+
|
|
398
|
+
Delete completed tasks from the database. Useful for cron-based cleanup.
|
|
399
|
+
|
|
400
|
+
| Parameter | Type | Default | Description |
|
|
401
|
+
|-----------|------|---------|-------------|
|
|
402
|
+
| `days` | int | 0 | Delete tasks completed more than N days ago (0=all) |
|
|
403
|
+
| `status` | string | "SUCCESSFUL,FAILED" | Target statuses, comma-separated |
|
|
404
|
+
| `batch_size` | int | 1000 | Number of tasks to delete at once (max: 10000) |
|
|
405
|
+
| `dry_run` | bool | false | If true, return count without deleting |
|
|
406
|
+
|
|
407
|
+
Response:
|
|
408
|
+
```json
|
|
409
|
+
{"deleted": 150, "dry_run": false}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
Response (dry run):
|
|
413
|
+
```json
|
|
414
|
+
{"count": 150, "dry_run": true}
|
|
415
|
+
```
|
|
416
|
+
|
|
355
417
|
### Example Usage
|
|
356
418
|
|
|
357
419
|
```bash
|
|
@@ -367,6 +429,16 @@ curl -X POST http://localhost:8000/tasks/run/ \
|
|
|
367
429
|
|
|
368
430
|
# Get pending task count
|
|
369
431
|
curl http://localhost:8000/tasks/status/
|
|
432
|
+
|
|
433
|
+
# Delete tasks completed more than 7 days ago
|
|
434
|
+
curl -X POST http://localhost:8000/tasks/purge/ \
|
|
435
|
+
-H "Content-Type: application/json" \
|
|
436
|
+
-d '{"days": 7}'
|
|
437
|
+
|
|
438
|
+
# Dry run to check how many tasks would be deleted
|
|
439
|
+
curl -X POST http://localhost:8000/tasks/purge/ \
|
|
440
|
+
-H "Content-Type: application/json" \
|
|
441
|
+
-d '{"days": 30, "dry_run": true}'
|
|
370
442
|
```
|
|
371
443
|
|
|
372
444
|
### Use Cases
|
|
@@ -413,13 +485,31 @@ if [ "$count" -gt 100 ]; then
|
|
|
413
485
|
fi
|
|
414
486
|
```
|
|
415
487
|
|
|
488
|
+
#### Scheduled Cleanup
|
|
489
|
+
|
|
490
|
+
Use cron or Cloud Scheduler to delete old completed tasks:
|
|
491
|
+
|
|
492
|
+
```bash
|
|
493
|
+
# Daily cleanup via cron or Cloud Scheduler
|
|
494
|
+
# Delete tasks completed more than 30 days ago
|
|
495
|
+
curl -X POST https://your-app.com/tasks/purge/ \
|
|
496
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
497
|
+
-H "Content-Type: application/json" \
|
|
498
|
+
-d '{"days": 30}'
|
|
499
|
+
```
|
|
500
|
+
|
|
416
501
|
### Security
|
|
417
502
|
|
|
418
503
|
The endpoints are CSRF-exempt for API/webhook use. **Always add authentication in production:**
|
|
419
504
|
|
|
420
505
|
```python
|
|
421
506
|
from django.contrib.admin.views.decorators import staff_member_required
|
|
422
|
-
from django_database_task.views import
|
|
507
|
+
from django_database_task.views import (
|
|
508
|
+
RunTasksView,
|
|
509
|
+
RunOneTaskView,
|
|
510
|
+
TaskStatusView,
|
|
511
|
+
PurgeCompletedTasksView,
|
|
512
|
+
)
|
|
423
513
|
|
|
424
514
|
urlpatterns = [
|
|
425
515
|
path(
|
|
@@ -437,6 +527,11 @@ urlpatterns = [
|
|
|
437
527
|
staff_member_required(TaskStatusView.as_view()),
|
|
438
528
|
name="task_status",
|
|
439
529
|
),
|
|
530
|
+
path(
|
|
531
|
+
"tasks/purge/",
|
|
532
|
+
staff_member_required(PurgeCompletedTasksView.as_view()),
|
|
533
|
+
name="purge_completed_tasks",
|
|
534
|
+
),
|
|
440
535
|
]
|
|
441
536
|
```
|
|
442
537
|
|
|
@@ -459,15 +554,157 @@ urlpatterns = [
|
|
|
459
554
|
]
|
|
460
555
|
```
|
|
461
556
|
|
|
557
|
+
## Google Cloud Tasks Integration
|
|
558
|
+
|
|
559
|
+
For serverless environments like Google App Engine or Cloud Run, you can use the Cloud Tasks backend to automatically create Cloud Tasks when tasks are enqueued.
|
|
560
|
+
|
|
561
|
+
### Installation
|
|
562
|
+
|
|
563
|
+
```bash
|
|
564
|
+
pip install django-database-task[cloudtasks]
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
### Quick Setup
|
|
568
|
+
|
|
569
|
+
```python
|
|
570
|
+
# settings.py
|
|
571
|
+
TASKS = {
|
|
572
|
+
"default": {
|
|
573
|
+
"BACKEND": "django_database_task.cloudtasks.CloudTasksDatabaseBackend",
|
|
574
|
+
"OPTIONS": {
|
|
575
|
+
"CLOUD_TASKS_QUEUE": "default", # Only required setting
|
|
576
|
+
},
|
|
577
|
+
},
|
|
578
|
+
}
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
Project ID, location, and handler URL are auto-detected from GAE/Cloud Run environment variables.
|
|
582
|
+
|
|
583
|
+
### How It Works
|
|
584
|
+
|
|
585
|
+
```
|
|
586
|
+
1. App enqueues task → Task saved to database + Cloud Task created
|
|
587
|
+
2. Cloud Task triggers HTTP request → /tasks/execute/<task_id>/
|
|
588
|
+
3. Handler retrieves task from database and executes it
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
The Cloud Task only contains the task ID. All task parameters are stored in the database, ensuring:
|
|
592
|
+
- **Blue/Green deployment support**: Tasks execute on the same version that enqueued them
|
|
593
|
+
- **Database as source of truth**: Task parameters are never lost
|
|
594
|
+
- **Automatic retry**: Cloud Tasks handles retry with the task ID
|
|
595
|
+
|
|
596
|
+
### Configuration Options
|
|
597
|
+
|
|
598
|
+
```python
|
|
599
|
+
TASKS = {
|
|
600
|
+
"default": {
|
|
601
|
+
"BACKEND": "django_database_task.cloudtasks.CloudTasksDatabaseBackend",
|
|
602
|
+
"OPTIONS": {
|
|
603
|
+
# Required
|
|
604
|
+
"CLOUD_TASKS_QUEUE": "default",
|
|
605
|
+
|
|
606
|
+
# Auto-detected (override if needed)
|
|
607
|
+
# "CLOUD_TASKS_PROJECT": "my-project",
|
|
608
|
+
# "CLOUD_TASKS_LOCATION": "asia-northeast1",
|
|
609
|
+
# "TASK_HANDLER_URL": "https://myapp.example.com/tasks/execute/{task_id}/",
|
|
610
|
+
# "TASK_HANDLER_PATH": "/tasks/execute/{task_id}/",
|
|
611
|
+
|
|
612
|
+
# OIDC authentication (optional)
|
|
613
|
+
# "OIDC_SERVICE_ACCOUNT_EMAIL": "...",
|
|
614
|
+
# "OIDC_AUDIENCE": "https://...",
|
|
615
|
+
},
|
|
616
|
+
},
|
|
617
|
+
}
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
### Auto-Detection
|
|
621
|
+
|
|
622
|
+
| Setting | Environment Variable | Description |
|
|
623
|
+
|---------|---------------------|-------------|
|
|
624
|
+
| Project | `GOOGLE_CLOUD_PROJECT` | GCP project ID |
|
|
625
|
+
| Location | `CLOUD_RUN_REGION` / `GAE_REGION` | Cloud Tasks region |
|
|
626
|
+
| Handler URL | Built from `K_SERVICE`, `GAE_SERVICE`, `GAE_VERSION` | Task execution endpoint |
|
|
627
|
+
|
|
628
|
+
### OIDC Authentication
|
|
629
|
+
|
|
630
|
+
When `OIDC_SERVICE_ACCOUNT_EMAIL` is configured, Cloud Tasks will send OIDC tokens with each request. The backend automatically verifies these tokens on the `/tasks/execute/` and `/tasks/purge/` endpoints.
|
|
631
|
+
|
|
632
|
+
```python
|
|
633
|
+
# settings.py - Automatic OIDC verification
|
|
634
|
+
TASKS = {
|
|
635
|
+
"default": {
|
|
636
|
+
"BACKEND": "django_database_task.cloudtasks.CloudTasksDatabaseBackend",
|
|
637
|
+
"OPTIONS": {
|
|
638
|
+
"CLOUD_TASKS_QUEUE": "default",
|
|
639
|
+
"OIDC_SERVICE_ACCOUNT_EMAIL": "my-sa@project.iam.gserviceaccount.com",
|
|
640
|
+
# OIDC_AUDIENCE is auto-detected from handler URL if not set
|
|
641
|
+
},
|
|
642
|
+
},
|
|
643
|
+
}
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
Alternatively, you can use the decorator directly on your URL configuration:
|
|
647
|
+
|
|
648
|
+
```python
|
|
649
|
+
# urls.py
|
|
650
|
+
from django.urls import path
|
|
651
|
+
from django_database_task.views import ExecuteTaskView
|
|
652
|
+
from django_database_task.cloudtasks import verify_cloud_tasks_oidc
|
|
653
|
+
|
|
654
|
+
urlpatterns = [
|
|
655
|
+
path(
|
|
656
|
+
"tasks/execute/<uuid:task_id>/",
|
|
657
|
+
verify_cloud_tasks_oidc(
|
|
658
|
+
ExecuteTaskView.as_view(),
|
|
659
|
+
audience="https://myapp.example.com"
|
|
660
|
+
),
|
|
661
|
+
name="execute_task",
|
|
662
|
+
),
|
|
663
|
+
]
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
### Detection Utilities
|
|
667
|
+
|
|
668
|
+
You can use the detection functions directly:
|
|
669
|
+
|
|
670
|
+
```python
|
|
671
|
+
from django_database_task.cloudtasks import (
|
|
672
|
+
detect_gcp_project,
|
|
673
|
+
detect_gcp_location,
|
|
674
|
+
detect_task_handler_host,
|
|
675
|
+
is_cloud_run,
|
|
676
|
+
is_app_engine,
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
if is_cloud_run():
|
|
680
|
+
print(f"Running on Cloud Run in {detect_gcp_location()}")
|
|
681
|
+
elif is_app_engine():
|
|
682
|
+
print(f"Running on App Engine in project {detect_gcp_project()}")
|
|
683
|
+
```
|
|
684
|
+
|
|
462
685
|
## Django Admin
|
|
463
686
|
|
|
464
|
-
The package includes a Django Admin integration to view
|
|
687
|
+
The package includes a Django Admin integration to view and manage tasks:
|
|
465
688
|
|
|
466
689
|
- Task list with status badges
|
|
467
690
|
- Filter by status, queue, backend
|
|
468
691
|
- Search by task ID or path
|
|
469
692
|
- View task arguments and results
|
|
470
693
|
|
|
694
|
+
### Admin Actions
|
|
695
|
+
|
|
696
|
+
The admin interface provides the following bulk actions:
|
|
697
|
+
|
|
698
|
+
| Action | Description |
|
|
699
|
+
|--------|-------------|
|
|
700
|
+
| **Run selected tasks** | Execute selected tasks that are in READY status |
|
|
701
|
+
| **Retry failed tasks** | Reset FAILED tasks to READY status and re-execute them |
|
|
702
|
+
|
|
703
|
+
These actions are useful for:
|
|
704
|
+
- Manually triggering task execution from the admin
|
|
705
|
+
- Retrying failed tasks after fixing issues
|
|
706
|
+
- Testing task execution during development
|
|
707
|
+
|
|
471
708
|
## License
|
|
472
709
|
|
|
473
710
|
MIT License - see [LICENSE](LICENSE) for details.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
django-database-task: A database-backed task queue backend for Django 6.0's task framework.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
__version__ = "0.
|
|
5
|
+
__version__ = "0.2.0"
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def __getattr__(name):
|
|
@@ -12,6 +12,7 @@ def __getattr__(name):
|
|
|
12
12
|
"get_pending_task_count",
|
|
13
13
|
"process_one_task",
|
|
14
14
|
"process_tasks",
|
|
15
|
+
"run_task_by_id",
|
|
15
16
|
):
|
|
16
17
|
from . import executor
|
|
17
18
|
|
|
@@ -24,4 +25,5 @@ __all__ = [
|
|
|
24
25
|
"get_pending_task_count",
|
|
25
26
|
"process_one_task",
|
|
26
27
|
"process_tasks",
|
|
28
|
+
"run_task_by_id",
|
|
27
29
|
]
|
|
@@ -19,6 +19,20 @@ class DatabaseTaskBackend(BaseTaskBackend):
|
|
|
19
19
|
supports_get_result = True
|
|
20
20
|
supports_priority = True
|
|
21
21
|
|
|
22
|
+
def get_auth_handler(self):
|
|
23
|
+
"""
|
|
24
|
+
Get the authentication handler for task execution endpoints.
|
|
25
|
+
|
|
26
|
+
Subclasses can override this to provide custom authentication.
|
|
27
|
+
The handler should be a callable that takes a request and returns:
|
|
28
|
+
- None if authentication succeeds
|
|
29
|
+
- A JsonResponse with error details if authentication fails
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Callable or None
|
|
33
|
+
"""
|
|
34
|
+
return None
|
|
35
|
+
|
|
22
36
|
def enqueue(self, task, args, kwargs):
|
|
23
37
|
"""Enqueue a task to the database.
|
|
24
38
|
|