django-database-task 0.1.0__tar.gz → 0.2.1__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.1}/PKG-INFO +291 -4
- {django_database_task-0.1.0 → django_database_task-0.2.1}/README.md +287 -3
- {django_database_task-0.1.0 → django_database_task-0.2.1}/django_database_task/__init__.py +3 -1
- {django_database_task-0.1.0 → django_database_task-0.2.1}/django_database_task/backends.py +24 -6
- django_database_task-0.2.1/django_database_task/cloudtasks/__init__.py +83 -0
- django_database_task-0.2.1/django_database_task/cloudtasks/auth.py +128 -0
- django_database_task-0.2.1/django_database_task/cloudtasks/backend.py +239 -0
- django_database_task-0.2.1/django_database_task/cloudtasks/detection.py +160 -0
- {django_database_task-0.1.0 → django_database_task-0.2.1}/django_database_task/executor.py +69 -0
- {django_database_task-0.1.0 → django_database_task-0.2.1}/django_database_task/urls.py +9 -1
- django_database_task-0.2.1/django_database_task/views.py +477 -0
- {django_database_task-0.1.0 → django_database_task-0.2.1/django_database_task.egg-info}/PKG-INFO +291 -4
- {django_database_task-0.1.0 → django_database_task-0.2.1}/django_database_task.egg-info/SOURCES.txt +4 -0
- {django_database_task-0.1.0 → django_database_task-0.2.1}/django_database_task.egg-info/requires.txt +4 -0
- {django_database_task-0.1.0 → django_database_task-0.2.1}/pyproject.toml +5 -1
- django_database_task-0.2.1/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.1}/LICENSE +0 -0
- {django_database_task-0.1.0 → django_database_task-0.2.1}/MANIFEST.in +0 -0
- {django_database_task-0.1.0 → django_database_task-0.2.1}/django_database_task/admin.py +0 -0
- {django_database_task-0.1.0 → django_database_task-0.2.1}/django_database_task/apps.py +0 -0
- {django_database_task-0.1.0 → django_database_task-0.2.1}/django_database_task/locale/ja/LC_MESSAGES/django.mo +0 -0
- {django_database_task-0.1.0 → django_database_task-0.2.1}/django_database_task/locale/ja/LC_MESSAGES/django.po +0 -0
- {django_database_task-0.1.0 → django_database_task-0.2.1}/django_database_task/management/__init__.py +0 -0
- {django_database_task-0.1.0 → django_database_task-0.2.1}/django_database_task/management/commands/__init__.py +0 -0
- {django_database_task-0.1.0 → django_database_task-0.2.1}/django_database_task/management/commands/purge_completed_database_tasks.py +0 -0
- {django_database_task-0.1.0 → django_database_task-0.2.1}/django_database_task/management/commands/run_database_tasks.py +0 -0
- {django_database_task-0.1.0 → django_database_task-0.2.1}/django_database_task/migrations/0001_initial.py +0 -0
- {django_database_task-0.1.0 → django_database_task-0.2.1}/django_database_task/migrations/__init__.py +0 -0
- {django_database_task-0.1.0 → django_database_task-0.2.1}/django_database_task/models.py +0 -0
- {django_database_task-0.1.0 → django_database_task-0.2.1}/django_database_task.egg-info/dependency_links.txt +0 -0
- {django_database_task-0.1.0 → django_database_task-0.2.1}/django_database_task.egg-info/top_level.txt +0 -0
- {django_database_task-0.1.0 → django_database_task-0.2.1}/setup.cfg +0 -0
- {django_database_task-0.1.0 → django_database_task-0.2.1}/tests/test_admin.py +0 -0
- {django_database_task-0.1.0 → django_database_task-0.2.1}/tests/test_backend.py +0 -0
- {django_database_task-0.1.0 → django_database_task-0.2.1}/tests/test_commands.py +0 -0
- {django_database_task-0.1.0 → django_database_task-0.2.1}/tests/test_executor.py +0 -0
- {django_database_task-0.1.0 → django_database_task-0.2.1}/tests/test_models.py +0 -0
{django_database_task-0.1.0/django_database_task.egg-info → django_database_task-0.2.1}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-database-task
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
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,204 @@ 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
|
+
"QUEUES": [], # Allow all queue names
|
|
611
|
+
},
|
|
612
|
+
}
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
Project ID, location, and handler URL are auto-detected from GAE/Cloud Run environment.
|
|
616
|
+
|
|
617
|
+
**Important**: Set `QUEUES: []` to allow any queue name, or list the queues you use:
|
|
618
|
+
```python
|
|
619
|
+
"QUEUES": ["default", "emails", "batch"], # Only these queues allowed
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
The Cloud Tasks queue name is determined by the task's `queue_name` attribute:
|
|
623
|
+
|
|
624
|
+
```python
|
|
625
|
+
@task # Uses "default" queue
|
|
626
|
+
def normal_task():
|
|
627
|
+
pass
|
|
628
|
+
|
|
629
|
+
@task(queue="batch") # Uses "batch" queue
|
|
630
|
+
def batch_task():
|
|
631
|
+
pass
|
|
632
|
+
|
|
633
|
+
@task(queue="high-priority") # Uses "high-priority" queue
|
|
634
|
+
def urgent_task():
|
|
635
|
+
pass
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
This allows you to configure different rate limits and concurrency settings per queue in Cloud Tasks.
|
|
639
|
+
|
|
640
|
+
### How It Works
|
|
641
|
+
|
|
642
|
+
```mermaid
|
|
643
|
+
sequenceDiagram
|
|
644
|
+
participant App as Application
|
|
645
|
+
participant Backend as CloudTasksDatabaseBackend
|
|
646
|
+
participant DB as Database
|
|
647
|
+
participant CT as Cloud Tasks
|
|
648
|
+
participant Handler as /tasks/execute/
|
|
649
|
+
|
|
650
|
+
Note over App,Handler: Task Enqueue
|
|
651
|
+
App->>Backend: task.enqueue(args, kwargs)
|
|
652
|
+
Backend->>DB: INSERT task (status=READY)
|
|
653
|
+
DB-->>Backend: Task ID
|
|
654
|
+
Backend->>CT: Create Cloud Task (task_id only)
|
|
655
|
+
CT-->>Backend: OK
|
|
656
|
+
Backend-->>App: TaskResult (id, status=READY)
|
|
657
|
+
|
|
658
|
+
Note over App,Handler: Task Execution (triggered by Cloud Tasks)
|
|
659
|
+
CT->>Handler: POST /tasks/execute/<task_id>/<br/>(with OIDC token if configured)
|
|
660
|
+
Handler->>Handler: Verify OIDC token (optional)
|
|
661
|
+
Handler->>DB: SELECT task by ID
|
|
662
|
+
DB-->>Handler: Task record
|
|
663
|
+
Handler->>DB: UPDATE status=RUNNING
|
|
664
|
+
Handler->>Handler: Execute task function
|
|
665
|
+
alt Success
|
|
666
|
+
Handler->>DB: UPDATE status=SUCCESSFUL
|
|
667
|
+
Handler-->>CT: HTTP 200
|
|
668
|
+
else Failure
|
|
669
|
+
Handler->>DB: UPDATE status=FAILED
|
|
670
|
+
Handler-->>CT: HTTP 500 (triggers retry)
|
|
671
|
+
end
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
The Cloud Task only contains the task ID. All task parameters are stored in the database, ensuring:
|
|
675
|
+
- **Blue/Green deployment support**: Tasks execute on the same version that enqueued them
|
|
676
|
+
- **Database as source of truth**: Task parameters are never lost
|
|
677
|
+
- **Automatic retry**: Cloud Tasks handles retry with the task ID
|
|
678
|
+
|
|
679
|
+
### Configuration Options
|
|
680
|
+
|
|
681
|
+
```python
|
|
682
|
+
TASKS = {
|
|
683
|
+
"default": {
|
|
684
|
+
"BACKEND": "django_database_task.cloudtasks.CloudTasksDatabaseBackend",
|
|
685
|
+
"OPTIONS": {
|
|
686
|
+
# All settings are optional - auto-detected from environment
|
|
687
|
+
|
|
688
|
+
# Override auto-detection if needed
|
|
689
|
+
# "CLOUD_TASKS_PROJECT": "my-project",
|
|
690
|
+
# "CLOUD_TASKS_LOCATION": "asia-northeast1",
|
|
691
|
+
# "TASK_HANDLER_URL": "https://myapp.example.com/tasks/execute/{task_id}/",
|
|
692
|
+
# "TASK_HANDLER_PATH": "/tasks/execute/{task_id}/",
|
|
693
|
+
|
|
694
|
+
# OIDC authentication (optional)
|
|
695
|
+
# "OIDC_SERVICE_ACCOUNT_EMAIL": "...",
|
|
696
|
+
# "OIDC_AUDIENCE": "https://...",
|
|
697
|
+
},
|
|
698
|
+
},
|
|
699
|
+
}
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
### Auto-Detection
|
|
703
|
+
|
|
704
|
+
| Setting | Detection Method | Description |
|
|
705
|
+
|---------|------------------|-------------|
|
|
706
|
+
| Project | `GOOGLE_CLOUD_PROJECT` env var | GCP project ID |
|
|
707
|
+
| Location | `CLOUD_RUN_REGION` env var, or metadata server | Cloud Tasks region |
|
|
708
|
+
| Handler URL | Built from `K_SERVICE`, `GAE_SERVICE`, `GAE_VERSION` | Task execution endpoint |
|
|
709
|
+
| Queue name | Task's `queue_name` attribute | Defaults to "default" |
|
|
710
|
+
|
|
711
|
+
### OIDC Authentication
|
|
712
|
+
|
|
713
|
+
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.
|
|
714
|
+
|
|
715
|
+
```python
|
|
716
|
+
# settings.py - Automatic OIDC verification
|
|
717
|
+
TASKS = {
|
|
718
|
+
"default": {
|
|
719
|
+
"BACKEND": "django_database_task.cloudtasks.CloudTasksDatabaseBackend",
|
|
720
|
+
"QUEUES": [], # Allow all queue names
|
|
721
|
+
"OPTIONS": {
|
|
722
|
+
"OIDC_SERVICE_ACCOUNT_EMAIL": "my-sa@project.iam.gserviceaccount.com",
|
|
723
|
+
# OIDC_AUDIENCE is auto-detected from handler URL if not set
|
|
724
|
+
},
|
|
725
|
+
},
|
|
726
|
+
}
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
Alternatively, you can use the decorator directly on your URL configuration:
|
|
730
|
+
|
|
731
|
+
```python
|
|
732
|
+
# urls.py
|
|
733
|
+
from django.urls import path
|
|
734
|
+
from django_database_task.views import ExecuteTaskView
|
|
735
|
+
from django_database_task.cloudtasks import verify_cloud_tasks_oidc
|
|
736
|
+
|
|
737
|
+
urlpatterns = [
|
|
738
|
+
path(
|
|
739
|
+
"tasks/execute/<uuid:task_id>/",
|
|
740
|
+
verify_cloud_tasks_oidc(
|
|
741
|
+
ExecuteTaskView.as_view(),
|
|
742
|
+
audience="https://myapp.example.com"
|
|
743
|
+
),
|
|
744
|
+
name="execute_task",
|
|
745
|
+
),
|
|
746
|
+
]
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
### Detection Utilities
|
|
750
|
+
|
|
751
|
+
You can use the detection functions directly:
|
|
752
|
+
|
|
753
|
+
```python
|
|
754
|
+
from django_database_task.cloudtasks import (
|
|
755
|
+
detect_gcp_project,
|
|
756
|
+
detect_gcp_location,
|
|
757
|
+
detect_task_handler_host,
|
|
758
|
+
is_cloud_run,
|
|
759
|
+
is_app_engine,
|
|
760
|
+
)
|
|
761
|
+
|
|
762
|
+
if is_cloud_run():
|
|
763
|
+
print(f"Running on Cloud Run in {detect_gcp_location()}")
|
|
764
|
+
elif is_app_engine():
|
|
765
|
+
print(f"Running on App Engine in project {detect_gcp_project()}")
|
|
766
|
+
```
|
|
767
|
+
|
|
495
768
|
## Django Admin
|
|
496
769
|
|
|
497
|
-
The package includes a Django Admin integration to view
|
|
770
|
+
The package includes a Django Admin integration to view and manage tasks:
|
|
498
771
|
|
|
499
772
|
- Task list with status badges
|
|
500
773
|
- Filter by status, queue, backend
|
|
501
774
|
- Search by task ID or path
|
|
502
775
|
- View task arguments and results
|
|
503
776
|
|
|
777
|
+
### Admin Actions
|
|
778
|
+
|
|
779
|
+
The admin interface provides the following bulk actions:
|
|
780
|
+
|
|
781
|
+
| Action | Description |
|
|
782
|
+
|--------|-------------|
|
|
783
|
+
| **Run selected tasks** | Execute selected tasks that are in READY status |
|
|
784
|
+
| **Retry failed tasks** | Reset FAILED tasks to READY status and re-execute them |
|
|
785
|
+
|
|
786
|
+
These actions are useful for:
|
|
787
|
+
- Manually triggering task execution from the admin
|
|
788
|
+
- Retrying failed tasks after fixing issues
|
|
789
|
+
- Testing task execution during development
|
|
790
|
+
|
|
504
791
|
## License
|
|
505
792
|
|
|
506
793
|
MIT License - see [LICENSE](LICENSE) for details.
|