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.
Files changed (39) hide show
  1. {django_database_task-0.1.0/django_database_task.egg-info → django_database_task-0.2.1}/PKG-INFO +291 -4
  2. {django_database_task-0.1.0 → django_database_task-0.2.1}/README.md +287 -3
  3. {django_database_task-0.1.0 → django_database_task-0.2.1}/django_database_task/__init__.py +3 -1
  4. {django_database_task-0.1.0 → django_database_task-0.2.1}/django_database_task/backends.py +24 -6
  5. django_database_task-0.2.1/django_database_task/cloudtasks/__init__.py +83 -0
  6. django_database_task-0.2.1/django_database_task/cloudtasks/auth.py +128 -0
  7. django_database_task-0.2.1/django_database_task/cloudtasks/backend.py +239 -0
  8. django_database_task-0.2.1/django_database_task/cloudtasks/detection.py +160 -0
  9. {django_database_task-0.1.0 → django_database_task-0.2.1}/django_database_task/executor.py +69 -0
  10. {django_database_task-0.1.0 → django_database_task-0.2.1}/django_database_task/urls.py +9 -1
  11. django_database_task-0.2.1/django_database_task/views.py +477 -0
  12. {django_database_task-0.1.0 → django_database_task-0.2.1/django_database_task.egg-info}/PKG-INFO +291 -4
  13. {django_database_task-0.1.0 → django_database_task-0.2.1}/django_database_task.egg-info/SOURCES.txt +4 -0
  14. {django_database_task-0.1.0 → django_database_task-0.2.1}/django_database_task.egg-info/requires.txt +4 -0
  15. {django_database_task-0.1.0 → django_database_task-0.2.1}/pyproject.toml +5 -1
  16. django_database_task-0.2.1/tests/test_views.py +799 -0
  17. django_database_task-0.1.0/django_database_task/views.py +0 -205
  18. django_database_task-0.1.0/tests/test_views.py +0 -261
  19. {django_database_task-0.1.0 → django_database_task-0.2.1}/LICENSE +0 -0
  20. {django_database_task-0.1.0 → django_database_task-0.2.1}/MANIFEST.in +0 -0
  21. {django_database_task-0.1.0 → django_database_task-0.2.1}/django_database_task/admin.py +0 -0
  22. {django_database_task-0.1.0 → django_database_task-0.2.1}/django_database_task/apps.py +0 -0
  23. {django_database_task-0.1.0 → django_database_task-0.2.1}/django_database_task/locale/ja/LC_MESSAGES/django.mo +0 -0
  24. {django_database_task-0.1.0 → django_database_task-0.2.1}/django_database_task/locale/ja/LC_MESSAGES/django.po +0 -0
  25. {django_database_task-0.1.0 → django_database_task-0.2.1}/django_database_task/management/__init__.py +0 -0
  26. {django_database_task-0.1.0 → django_database_task-0.2.1}/django_database_task/management/commands/__init__.py +0 -0
  27. {django_database_task-0.1.0 → django_database_task-0.2.1}/django_database_task/management/commands/purge_completed_database_tasks.py +0 -0
  28. {django_database_task-0.1.0 → django_database_task-0.2.1}/django_database_task/management/commands/run_database_tasks.py +0 -0
  29. {django_database_task-0.1.0 → django_database_task-0.2.1}/django_database_task/migrations/0001_initial.py +0 -0
  30. {django_database_task-0.1.0 → django_database_task-0.2.1}/django_database_task/migrations/__init__.py +0 -0
  31. {django_database_task-0.1.0 → django_database_task-0.2.1}/django_database_task/models.py +0 -0
  32. {django_database_task-0.1.0 → django_database_task-0.2.1}/django_database_task.egg-info/dependency_links.txt +0 -0
  33. {django_database_task-0.1.0 → django_database_task-0.2.1}/django_database_task.egg-info/top_level.txt +0 -0
  34. {django_database_task-0.1.0 → django_database_task-0.2.1}/setup.cfg +0 -0
  35. {django_database_task-0.1.0 → django_database_task-0.2.1}/tests/test_admin.py +0 -0
  36. {django_database_task-0.1.0 → django_database_task-0.2.1}/tests/test_backend.py +0 -0
  37. {django_database_task-0.1.0 → django_database_task-0.2.1}/tests/test_commands.py +0 -0
  38. {django_database_task-0.1.0 → django_database_task-0.2.1}/tests/test_executor.py +0 -0
  39. {django_database_task-0.1.0 → django_database_task-0.2.1}/tests/test_models.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-database-task
3
- Version: 0.1.0
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 process_one_task, process_tasks, get_pending_task_count
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 RunTasksView, RunOneTaskView, TaskStatusView
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 task status:
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.