django-database-task 0.2.2__tar.gz → 0.2.4__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 (37) hide show
  1. {django_database_task-0.2.2/django_database_task.egg-info → django_database_task-0.2.4}/PKG-INFO +58 -5
  2. {django_database_task-0.2.2 → django_database_task-0.2.4}/README.md +57 -4
  3. {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task/__init__.py +1 -1
  4. {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task/backends.py +28 -6
  5. {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task/cloudtasks/auth.py +28 -1
  6. {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task/views.py +68 -17
  7. {django_database_task-0.2.2 → django_database_task-0.2.4/django_database_task.egg-info}/PKG-INFO +58 -5
  8. {django_database_task-0.2.2 → django_database_task-0.2.4}/pyproject.toml +1 -1
  9. {django_database_task-0.2.2 → django_database_task-0.2.4}/tests/test_views.py +59 -3
  10. {django_database_task-0.2.2 → django_database_task-0.2.4}/LICENSE +0 -0
  11. {django_database_task-0.2.2 → django_database_task-0.2.4}/MANIFEST.in +0 -0
  12. {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task/admin.py +0 -0
  13. {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task/apps.py +0 -0
  14. {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task/cloudtasks/__init__.py +0 -0
  15. {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task/cloudtasks/backend.py +0 -0
  16. {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task/cloudtasks/detection.py +0 -0
  17. {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task/executor.py +0 -0
  18. {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task/locale/ja/LC_MESSAGES/django.mo +0 -0
  19. {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task/locale/ja/LC_MESSAGES/django.po +0 -0
  20. {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task/management/__init__.py +0 -0
  21. {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task/management/commands/__init__.py +0 -0
  22. {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task/management/commands/purge_completed_database_tasks.py +0 -0
  23. {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task/management/commands/run_database_tasks.py +0 -0
  24. {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task/migrations/0001_initial.py +0 -0
  25. {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task/migrations/__init__.py +0 -0
  26. {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task/models.py +0 -0
  27. {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task/urls.py +0 -0
  28. {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task.egg-info/SOURCES.txt +0 -0
  29. {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task.egg-info/dependency_links.txt +0 -0
  30. {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task.egg-info/requires.txt +0 -0
  31. {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task.egg-info/top_level.txt +0 -0
  32. {django_database_task-0.2.2 → django_database_task-0.2.4}/setup.cfg +0 -0
  33. {django_database_task-0.2.2 → django_database_task-0.2.4}/tests/test_admin.py +0 -0
  34. {django_database_task-0.2.2 → django_database_task-0.2.4}/tests/test_backend.py +0 -0
  35. {django_database_task-0.2.2 → django_database_task-0.2.4}/tests/test_commands.py +0 -0
  36. {django_database_task-0.2.2 → django_database_task-0.2.4}/tests/test_executor.py +0 -0
  37. {django_database_task-0.2.2 → django_database_task-0.2.4}/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.2.2
3
+ Version: 0.2.4
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>
@@ -358,7 +358,7 @@ urlpatterns = [
358
358
  | `/tasks/run-one/` | POST | Process a single pending task |
359
359
  | `/tasks/status/` | GET | Get pending task count |
360
360
  | `/tasks/execute/<uuid>/` | POST | Execute a specific task by ID |
361
- | `/tasks/purge/` | POST | Delete completed tasks |
361
+ | `/tasks/purge/` | GET, POST | Delete completed tasks |
362
362
 
363
363
  ### Request Parameters
364
364
 
@@ -434,10 +434,14 @@ Response (task not found):
434
434
  {"error": "Task not found"} // HTTP 404
435
435
  ```
436
436
 
437
- #### POST `/tasks/purge/`
437
+ #### GET/POST `/tasks/purge/`
438
438
 
439
439
  Delete completed tasks from the database. Useful for cron-based cleanup.
440
440
 
441
+ **Note:** GET method is supported for GAE cron compatibility (GAE cron only supports GET requests).
442
+
443
+ **POST parameters (JSON body):**
444
+
441
445
  | Parameter | Type | Default | Description |
442
446
  |-----------|------|---------|-------------|
443
447
  | `days` | int | 0 | Delete tasks completed more than N days ago (0=all) |
@@ -445,6 +449,15 @@ Delete completed tasks from the database. Useful for cron-based cleanup.
445
449
  | `batch_size` | int | 1000 | Number of tasks to delete at once (max: 10000) |
446
450
  | `dry_run` | bool | false | If true, return count without deleting |
447
451
 
452
+ **GET query parameters:**
453
+
454
+ | Parameter | Type | Default | Description |
455
+ |-----------|------|---------|-------------|
456
+ | `days` | int | 0 | Delete tasks completed more than N days ago (0=all) |
457
+ | `status` | string | "SUCCESSFUL,FAILED" | Target statuses, comma-separated |
458
+ | `batch_size` | int | 1000 | Number of tasks to delete at once (max: 10000) |
459
+ | `dry_run` | string | "false" | If "true", return count without deleting |
460
+
448
461
  Response:
449
462
  ```json
450
463
  {"deleted": 150, "dry_run": false}
@@ -471,15 +484,21 @@ curl -X POST http://localhost:8000/tasks/run/ \
471
484
  # Get pending task count
472
485
  curl http://localhost:8000/tasks/status/
473
486
 
474
- # Delete tasks completed more than 7 days ago
487
+ # Delete tasks completed more than 7 days ago (POST)
475
488
  curl -X POST http://localhost:8000/tasks/purge/ \
476
489
  -H "Content-Type: application/json" \
477
490
  -d '{"days": 7}'
478
491
 
492
+ # Delete tasks completed more than 7 days ago (GET - for GAE cron)
493
+ curl "http://localhost:8000/tasks/purge/?days=7"
494
+
479
495
  # Dry run to check how many tasks would be deleted
480
496
  curl -X POST http://localhost:8000/tasks/purge/ \
481
497
  -H "Content-Type: application/json" \
482
498
  -d '{"days": 30, "dry_run": true}'
499
+
500
+ # Dry run via GET
501
+ curl "http://localhost:8000/tasks/purge/?days=30&dry_run=true"
483
502
  ```
484
503
 
485
504
  ### Use Cases
@@ -717,6 +736,40 @@ TASKS = {
717
736
 
718
737
  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.
719
738
 
739
+ #### Required IAM Roles
740
+
741
+ To use OIDC authentication, the following IAM roles are required:
742
+
743
+ | Role | Description |
744
+ |------|-------------|
745
+ | `roles/cloudtasks.enqueuer` | Required to create tasks in Cloud Tasks queues |
746
+ | `roles/iam.serviceAccountUser` | Required to specify the OIDC service account when creating tasks |
747
+
748
+ **Setup:**
749
+
750
+ 1. Create a service account for OIDC token generation:
751
+ ```bash
752
+ gcloud iam service-accounts create cloud-tasks-invoker \
753
+ --display-name="Cloud Tasks Invoker"
754
+ ```
755
+
756
+ 2. Grant the Cloud Tasks Enqueuer role to the service account running your application (e.g., App Engine default service account):
757
+ ```bash
758
+ gcloud projects add-iam-policy-binding PROJECT_ID \
759
+ --member="serviceAccount:PROJECT_ID@appspot.gserviceaccount.com" \
760
+ --role="roles/cloudtasks.enqueuer"
761
+ ```
762
+
763
+ 3. Grant the Service Account User role to allow impersonation of the OIDC service account:
764
+ ```bash
765
+ gcloud iam service-accounts add-iam-policy-binding \
766
+ cloud-tasks-invoker@PROJECT_ID.iam.gserviceaccount.com \
767
+ --member="serviceAccount:PROJECT_ID@appspot.gserviceaccount.com" \
768
+ --role="roles/iam.serviceAccountUser"
769
+ ```
770
+
771
+ **Note:** The OIDC service account specified in `OIDC_SERVICE_ACCOUNT_EMAIL` does not need any additional roles. It is only used to generate the OIDC token that is included in the HTTP request to your task handler.
772
+
720
773
  ```python
721
774
  # settings.py - Automatic OIDC verification
722
775
  TASKS = {
@@ -724,7 +777,7 @@ TASKS = {
724
777
  "BACKEND": "django_database_task.cloudtasks.CloudTasksDatabaseBackend",
725
778
  "QUEUES": [], # Allow all queue names
726
779
  "OPTIONS": {
727
- "OIDC_SERVICE_ACCOUNT_EMAIL": "my-sa@project.iam.gserviceaccount.com",
780
+ "OIDC_SERVICE_ACCOUNT_EMAIL": "cloud-tasks-invoker@PROJECT_ID.iam.gserviceaccount.com",
728
781
  # OIDC_AUDIENCE is auto-detected from handler URL if not set
729
782
  },
730
783
  },
@@ -322,7 +322,7 @@ urlpatterns = [
322
322
  | `/tasks/run-one/` | POST | Process a single pending task |
323
323
  | `/tasks/status/` | GET | Get pending task count |
324
324
  | `/tasks/execute/<uuid>/` | POST | Execute a specific task by ID |
325
- | `/tasks/purge/` | POST | Delete completed tasks |
325
+ | `/tasks/purge/` | GET, POST | Delete completed tasks |
326
326
 
327
327
  ### Request Parameters
328
328
 
@@ -398,10 +398,14 @@ Response (task not found):
398
398
  {"error": "Task not found"} // HTTP 404
399
399
  ```
400
400
 
401
- #### POST `/tasks/purge/`
401
+ #### GET/POST `/tasks/purge/`
402
402
 
403
403
  Delete completed tasks from the database. Useful for cron-based cleanup.
404
404
 
405
+ **Note:** GET method is supported for GAE cron compatibility (GAE cron only supports GET requests).
406
+
407
+ **POST parameters (JSON body):**
408
+
405
409
  | Parameter | Type | Default | Description |
406
410
  |-----------|------|---------|-------------|
407
411
  | `days` | int | 0 | Delete tasks completed more than N days ago (0=all) |
@@ -409,6 +413,15 @@ Delete completed tasks from the database. Useful for cron-based cleanup.
409
413
  | `batch_size` | int | 1000 | Number of tasks to delete at once (max: 10000) |
410
414
  | `dry_run` | bool | false | If true, return count without deleting |
411
415
 
416
+ **GET query parameters:**
417
+
418
+ | Parameter | Type | Default | Description |
419
+ |-----------|------|---------|-------------|
420
+ | `days` | int | 0 | Delete tasks completed more than N days ago (0=all) |
421
+ | `status` | string | "SUCCESSFUL,FAILED" | Target statuses, comma-separated |
422
+ | `batch_size` | int | 1000 | Number of tasks to delete at once (max: 10000) |
423
+ | `dry_run` | string | "false" | If "true", return count without deleting |
424
+
412
425
  Response:
413
426
  ```json
414
427
  {"deleted": 150, "dry_run": false}
@@ -435,15 +448,21 @@ curl -X POST http://localhost:8000/tasks/run/ \
435
448
  # Get pending task count
436
449
  curl http://localhost:8000/tasks/status/
437
450
 
438
- # Delete tasks completed more than 7 days ago
451
+ # Delete tasks completed more than 7 days ago (POST)
439
452
  curl -X POST http://localhost:8000/tasks/purge/ \
440
453
  -H "Content-Type: application/json" \
441
454
  -d '{"days": 7}'
442
455
 
456
+ # Delete tasks completed more than 7 days ago (GET - for GAE cron)
457
+ curl "http://localhost:8000/tasks/purge/?days=7"
458
+
443
459
  # Dry run to check how many tasks would be deleted
444
460
  curl -X POST http://localhost:8000/tasks/purge/ \
445
461
  -H "Content-Type: application/json" \
446
462
  -d '{"days": 30, "dry_run": true}'
463
+
464
+ # Dry run via GET
465
+ curl "http://localhost:8000/tasks/purge/?days=30&dry_run=true"
447
466
  ```
448
467
 
449
468
  ### Use Cases
@@ -681,6 +700,40 @@ TASKS = {
681
700
 
682
701
  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.
683
702
 
703
+ #### Required IAM Roles
704
+
705
+ To use OIDC authentication, the following IAM roles are required:
706
+
707
+ | Role | Description |
708
+ |------|-------------|
709
+ | `roles/cloudtasks.enqueuer` | Required to create tasks in Cloud Tasks queues |
710
+ | `roles/iam.serviceAccountUser` | Required to specify the OIDC service account when creating tasks |
711
+
712
+ **Setup:**
713
+
714
+ 1. Create a service account for OIDC token generation:
715
+ ```bash
716
+ gcloud iam service-accounts create cloud-tasks-invoker \
717
+ --display-name="Cloud Tasks Invoker"
718
+ ```
719
+
720
+ 2. Grant the Cloud Tasks Enqueuer role to the service account running your application (e.g., App Engine default service account):
721
+ ```bash
722
+ gcloud projects add-iam-policy-binding PROJECT_ID \
723
+ --member="serviceAccount:PROJECT_ID@appspot.gserviceaccount.com" \
724
+ --role="roles/cloudtasks.enqueuer"
725
+ ```
726
+
727
+ 3. Grant the Service Account User role to allow impersonation of the OIDC service account:
728
+ ```bash
729
+ gcloud iam service-accounts add-iam-policy-binding \
730
+ cloud-tasks-invoker@PROJECT_ID.iam.gserviceaccount.com \
731
+ --member="serviceAccount:PROJECT_ID@appspot.gserviceaccount.com" \
732
+ --role="roles/iam.serviceAccountUser"
733
+ ```
734
+
735
+ **Note:** The OIDC service account specified in `OIDC_SERVICE_ACCOUNT_EMAIL` does not need any additional roles. It is only used to generate the OIDC token that is included in the HTTP request to your task handler.
736
+
684
737
  ```python
685
738
  # settings.py - Automatic OIDC verification
686
739
  TASKS = {
@@ -688,7 +741,7 @@ TASKS = {
688
741
  "BACKEND": "django_database_task.cloudtasks.CloudTasksDatabaseBackend",
689
742
  "QUEUES": [], # Allow all queue names
690
743
  "OPTIONS": {
691
- "OIDC_SERVICE_ACCOUNT_EMAIL": "my-sa@project.iam.gserviceaccount.com",
744
+ "OIDC_SERVICE_ACCOUNT_EMAIL": "cloud-tasks-invoker@PROJECT_ID.iam.gserviceaccount.com",
692
745
  # OIDC_AUDIENCE is auto-detected from handler URL if not set
693
746
  },
694
747
  },
@@ -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.2.2"
5
+ __version__ = "0.2.4"
6
6
 
7
7
 
8
8
  def __getattr__(name):
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ import logging
2
3
  import traceback
3
4
  from importlib import import_module
4
5
  from inspect import iscoroutinefunction
@@ -10,6 +11,8 @@ from django.tasks.signals import task_enqueued, task_finished, task_started
10
11
  from django.utils import timezone
11
12
  from django.utils.json import normalize_json
12
13
 
14
+ logger = logging.getLogger("django_database_task")
15
+
13
16
 
14
17
  class DatabaseTaskBackend(BaseTaskBackend):
15
18
  """A task backend that persists tasks in the database."""
@@ -200,6 +203,20 @@ class DatabaseTaskBackend(BaseTaskBackend):
200
203
  ]
201
204
  )
202
205
 
206
+ # Send signal for success
207
+ db_task.refresh_from_db()
208
+ final_result = self._db_task_to_result(db_task, task)
209
+ # Note: Django's task_finished handler logs "NoneType: None" after this
210
+ # due to exc_info=sys.exc_info() being called outside exception context.
211
+ # This is a known Django issue and will be fixed upstream.
212
+ logger.info(
213
+ "Task completed successfully: id=%s path=%s",
214
+ final_result.id,
215
+ db_task.task_path,
216
+ )
217
+ task_finished.send(sender=self.__class__, task_result=final_result)
218
+ return final_result
219
+
203
220
  except Exception as e:
204
221
  # Failure
205
222
  error = TaskError(
@@ -221,9 +238,14 @@ class DatabaseTaskBackend(BaseTaskBackend):
221
238
  update_fields=["status", "errors_json", "finished_at", "updated_at"]
222
239
  )
223
240
 
224
- # Get final result and send signal
225
- db_task.refresh_from_db()
226
- final_result = self._db_task_to_result(db_task, task)
227
- task_finished.send(sender=self.__class__, task_result=final_result)
228
-
229
- return final_result
241
+ # Send signal for failure (with exception context for Django's logging)
242
+ db_task.refresh_from_db()
243
+ final_result = self._db_task_to_result(db_task, task)
244
+ logger.error(
245
+ "Task failed: id=%s path=%s error=%s",
246
+ final_result.id,
247
+ db_task.task_path,
248
+ error.exception_class_path,
249
+ )
250
+ task_finished.send(sender=self.__class__, task_result=final_result)
251
+ return final_result
@@ -5,12 +5,16 @@ Provides authentication handler and decorator for verifying OIDC tokens
5
5
  sent by Cloud Tasks.
6
6
  """
7
7
 
8
+ import logging
9
+ import traceback
8
10
  from functools import wraps
9
11
 
10
12
  from django.http import JsonResponse
11
13
  from google.auth.transport import requests as google_requests
12
14
  from google.oauth2 import id_token
13
15
 
16
+ logger = logging.getLogger("django_database_task.cloudtasks.auth")
17
+
14
18
 
15
19
  def create_oidc_auth_handler(audience):
16
20
  """
@@ -64,8 +68,31 @@ def create_oidc_auth_handler(audience):
64
68
  # Attach claims to request for use in view
65
69
  request.cloud_tasks_claims = claims
66
70
 
71
+ # Log successful verification
72
+ service_account = claims.get("email", "unknown")
73
+ logger.info(
74
+ "OIDC token verified successfully: service_account=%s audience=%s",
75
+ service_account,
76
+ audience,
77
+ )
78
+
67
79
  except Exception as e:
68
- return JsonResponse({"error": f"Invalid token: {e}"}, status=401)
80
+ # Log the full exception for debugging
81
+ # Use print as fallback since logging config may not capture this logger
82
+ error_type = type(e).__name__
83
+ error_msg = str(e) if str(e) else "No details available"
84
+ tb = traceback.format_exc()
85
+ full_error = (
86
+ f"OIDC token verification failed: {error_type}: {error_msg}\n{tb}"
87
+ )
88
+ logger.error(full_error)
89
+ # Also print to stderr for environments where logging is not configured
90
+ import sys
91
+
92
+ print(full_error, file=sys.stderr)
93
+ return JsonResponse(
94
+ {"error": f"Invalid token: {error_type}: {error_msg}"}, status=401
95
+ )
69
96
 
70
97
  return None
71
98
 
@@ -366,11 +366,17 @@ class ExecuteTaskView(View):
366
366
  @method_decorator(csrf_exempt, name="dispatch")
367
367
  class PurgeCompletedTasksView(View):
368
368
  """
369
- Delete completed tasks via HTTP POST.
369
+ Delete completed tasks via HTTP GET or POST.
370
370
 
371
371
  This endpoint is useful for cron-based cleanup of completed tasks
372
372
  when management commands cannot be executed directly.
373
373
 
374
+ GET query parameters:
375
+ days: Delete tasks completed more than N days ago (0=all, default: 0)
376
+ status: Target statuses, comma-separated (default: "SUCCESSFUL,FAILED")
377
+ batch_size: Number of tasks to delete at once (default: 1000, max: 10000)
378
+ dry_run: If "true", return count without deleting (default: "false")
379
+
374
380
  POST parameters (JSON body):
375
381
  days: Delete tasks completed more than N days ago (0=all, default: 0)
376
382
  status: Target statuses, comma-separated (default: "SUCCESSFUL,FAILED")
@@ -395,14 +401,57 @@ class PurgeCompletedTasksView(View):
395
401
  }
396
402
 
397
403
  Security:
398
- - Only accepts POST requests
404
+ - Accepts GET and POST requests
399
405
  - CSRF exempt (intended for API/cron use)
400
406
  - Consider adding authentication in your URL configuration
401
- """
402
407
 
403
- http_method_names = ["post"]
408
+ GAE Cron:
409
+ GAE cron only supports GET requests. Use query parameters:
410
+ GET /tasks/purge/?days=7&status=SUCCESSFUL,FAILED
411
+ """
404
412
 
405
- def post(self, request):
413
+ http_method_names = ["get", "post"]
414
+
415
+ def _get_params(self, request):
416
+ """Extract parameters from GET query string or POST JSON body."""
417
+ if request.method == "GET":
418
+ days_str = request.GET.get("days", "0")
419
+ try:
420
+ days = int(days_str)
421
+ except ValueError:
422
+ return None, {"error": "days must be an integer"}
423
+
424
+ batch_size_str = request.GET.get("batch_size", "1000")
425
+ try:
426
+ batch_size = int(batch_size_str)
427
+ except ValueError:
428
+ return None, {"error": "batch_size must be an integer"}
429
+
430
+ return {
431
+ "days": days,
432
+ "status": request.GET.get("status", "SUCCESSFUL,FAILED"),
433
+ "batch_size": batch_size,
434
+ "dry_run": request.GET.get("dry_run", "").lower() == "true",
435
+ }, None
436
+ else:
437
+ # POST - parse JSON body
438
+ try:
439
+ if request.body:
440
+ data = json.loads(request.body)
441
+ else:
442
+ data = {}
443
+ except json.JSONDecodeError:
444
+ return None, {"error": "Invalid JSON"}
445
+
446
+ return {
447
+ "days": data.get("days", 0),
448
+ "status": data.get("status", "SUCCESSFUL,FAILED"),
449
+ "batch_size": data.get("batch_size", 1000),
450
+ "dry_run": data.get("dry_run", False),
451
+ }, None
452
+
453
+ def _purge(self, request):
454
+ """Common purge logic for GET and POST."""
406
455
  backend_name = request.GET.get("backend_name", "default")
407
456
 
408
457
  # Get backend and check for auth handler
@@ -414,19 +463,15 @@ class PurgeCompletedTasksView(View):
414
463
  if error_response:
415
464
  return error_response
416
465
 
417
- # Parse JSON body if present
418
- try:
419
- if request.body:
420
- data = json.loads(request.body)
421
- else:
422
- data = {}
423
- except json.JSONDecodeError:
424
- return JsonResponse({"error": "Invalid JSON"}, status=400)
466
+ # Get parameters
467
+ params, error = self._get_params(request)
468
+ if error:
469
+ return JsonResponse(error, status=400)
425
470
 
426
- days = data.get("days", 0)
427
- status_str = data.get("status", "SUCCESSFUL,FAILED")
428
- batch_size = data.get("batch_size", 1000)
429
- dry_run = data.get("dry_run", False)
471
+ days = params["days"]
472
+ status_str = params["status"]
473
+ batch_size = params["batch_size"]
474
+ dry_run = params["dry_run"]
430
475
 
431
476
  # Validate parameters
432
477
  if not isinstance(days, int) or days < 0:
@@ -475,3 +520,9 @@ class PurgeCompletedTasksView(View):
475
520
  deleted_total += deleted_count
476
521
 
477
522
  return JsonResponse({"deleted": deleted_total, "dry_run": False})
523
+
524
+ def get(self, request):
525
+ return self._purge(request)
526
+
527
+ def post(self, request):
528
+ return self._purge(request)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-database-task
3
- Version: 0.2.2
3
+ Version: 0.2.4
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>
@@ -358,7 +358,7 @@ urlpatterns = [
358
358
  | `/tasks/run-one/` | POST | Process a single pending task |
359
359
  | `/tasks/status/` | GET | Get pending task count |
360
360
  | `/tasks/execute/<uuid>/` | POST | Execute a specific task by ID |
361
- | `/tasks/purge/` | POST | Delete completed tasks |
361
+ | `/tasks/purge/` | GET, POST | Delete completed tasks |
362
362
 
363
363
  ### Request Parameters
364
364
 
@@ -434,10 +434,14 @@ Response (task not found):
434
434
  {"error": "Task not found"} // HTTP 404
435
435
  ```
436
436
 
437
- #### POST `/tasks/purge/`
437
+ #### GET/POST `/tasks/purge/`
438
438
 
439
439
  Delete completed tasks from the database. Useful for cron-based cleanup.
440
440
 
441
+ **Note:** GET method is supported for GAE cron compatibility (GAE cron only supports GET requests).
442
+
443
+ **POST parameters (JSON body):**
444
+
441
445
  | Parameter | Type | Default | Description |
442
446
  |-----------|------|---------|-------------|
443
447
  | `days` | int | 0 | Delete tasks completed more than N days ago (0=all) |
@@ -445,6 +449,15 @@ Delete completed tasks from the database. Useful for cron-based cleanup.
445
449
  | `batch_size` | int | 1000 | Number of tasks to delete at once (max: 10000) |
446
450
  | `dry_run` | bool | false | If true, return count without deleting |
447
451
 
452
+ **GET query parameters:**
453
+
454
+ | Parameter | Type | Default | Description |
455
+ |-----------|------|---------|-------------|
456
+ | `days` | int | 0 | Delete tasks completed more than N days ago (0=all) |
457
+ | `status` | string | "SUCCESSFUL,FAILED" | Target statuses, comma-separated |
458
+ | `batch_size` | int | 1000 | Number of tasks to delete at once (max: 10000) |
459
+ | `dry_run` | string | "false" | If "true", return count without deleting |
460
+
448
461
  Response:
449
462
  ```json
450
463
  {"deleted": 150, "dry_run": false}
@@ -471,15 +484,21 @@ curl -X POST http://localhost:8000/tasks/run/ \
471
484
  # Get pending task count
472
485
  curl http://localhost:8000/tasks/status/
473
486
 
474
- # Delete tasks completed more than 7 days ago
487
+ # Delete tasks completed more than 7 days ago (POST)
475
488
  curl -X POST http://localhost:8000/tasks/purge/ \
476
489
  -H "Content-Type: application/json" \
477
490
  -d '{"days": 7}'
478
491
 
492
+ # Delete tasks completed more than 7 days ago (GET - for GAE cron)
493
+ curl "http://localhost:8000/tasks/purge/?days=7"
494
+
479
495
  # Dry run to check how many tasks would be deleted
480
496
  curl -X POST http://localhost:8000/tasks/purge/ \
481
497
  -H "Content-Type: application/json" \
482
498
  -d '{"days": 30, "dry_run": true}'
499
+
500
+ # Dry run via GET
501
+ curl "http://localhost:8000/tasks/purge/?days=30&dry_run=true"
483
502
  ```
484
503
 
485
504
  ### Use Cases
@@ -717,6 +736,40 @@ TASKS = {
717
736
 
718
737
  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.
719
738
 
739
+ #### Required IAM Roles
740
+
741
+ To use OIDC authentication, the following IAM roles are required:
742
+
743
+ | Role | Description |
744
+ |------|-------------|
745
+ | `roles/cloudtasks.enqueuer` | Required to create tasks in Cloud Tasks queues |
746
+ | `roles/iam.serviceAccountUser` | Required to specify the OIDC service account when creating tasks |
747
+
748
+ **Setup:**
749
+
750
+ 1. Create a service account for OIDC token generation:
751
+ ```bash
752
+ gcloud iam service-accounts create cloud-tasks-invoker \
753
+ --display-name="Cloud Tasks Invoker"
754
+ ```
755
+
756
+ 2. Grant the Cloud Tasks Enqueuer role to the service account running your application (e.g., App Engine default service account):
757
+ ```bash
758
+ gcloud projects add-iam-policy-binding PROJECT_ID \
759
+ --member="serviceAccount:PROJECT_ID@appspot.gserviceaccount.com" \
760
+ --role="roles/cloudtasks.enqueuer"
761
+ ```
762
+
763
+ 3. Grant the Service Account User role to allow impersonation of the OIDC service account:
764
+ ```bash
765
+ gcloud iam service-accounts add-iam-policy-binding \
766
+ cloud-tasks-invoker@PROJECT_ID.iam.gserviceaccount.com \
767
+ --member="serviceAccount:PROJECT_ID@appspot.gserviceaccount.com" \
768
+ --role="roles/iam.serviceAccountUser"
769
+ ```
770
+
771
+ **Note:** The OIDC service account specified in `OIDC_SERVICE_ACCOUNT_EMAIL` does not need any additional roles. It is only used to generate the OIDC token that is included in the HTTP request to your task handler.
772
+
720
773
  ```python
721
774
  # settings.py - Automatic OIDC verification
722
775
  TASKS = {
@@ -724,7 +777,7 @@ TASKS = {
724
777
  "BACKEND": "django_database_task.cloudtasks.CloudTasksDatabaseBackend",
725
778
  "QUEUES": [], # Allow all queue names
726
779
  "OPTIONS": {
727
- "OIDC_SERVICE_ACCOUNT_EMAIL": "my-sa@project.iam.gserviceaccount.com",
780
+ "OIDC_SERVICE_ACCOUNT_EMAIL": "cloud-tasks-invoker@PROJECT_ID.iam.gserviceaccount.com",
728
781
  # OIDC_AUDIENCE is auto-detected from handler URL if not set
729
782
  },
730
783
  },
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "django-database-task"
7
- version = "0.2.2"
7
+ version = "0.2.4"
8
8
  description = "A database-backed task queue backend for Django 6.0's built-in task framework"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -738,10 +738,66 @@ class TestPurgeCompletedTasksView:
738
738
  data = response.json()
739
739
  assert data["deleted"] == 0
740
740
 
741
- def test_purge_rejects_get(self, client):
742
- """Test that GET method is not allowed."""
741
+ def test_purge_via_get(self, client):
742
+ """Test that GET method works for GAE cron compatibility."""
743
+ # Create a completed task
744
+ DatabaseTask.objects.create(
745
+ task_path="tests.test_executor.sample_task",
746
+ queue_name="default",
747
+ priority=0,
748
+ args_json=[1, 2],
749
+ kwargs_json={},
750
+ status=TaskResultStatus.SUCCESSFUL,
751
+ enqueued_at=timezone.now(),
752
+ finished_at=timezone.now(),
753
+ backend_name="default",
754
+ )
743
755
  response = client.get(reverse("django_database_task:purge_completed_tasks"))
744
- assert response.status_code == 405
756
+ assert response.status_code == 200
757
+ data = response.json()
758
+ assert data["deleted"] == 1
759
+ assert data["dry_run"] is False
760
+
761
+ def test_purge_via_get_with_params(self, client):
762
+ """Test GET method with query parameters."""
763
+ # Create a completed task
764
+ DatabaseTask.objects.create(
765
+ task_path="tests.test_executor.sample_task",
766
+ queue_name="default",
767
+ priority=0,
768
+ args_json=[1, 2],
769
+ kwargs_json={},
770
+ status=TaskResultStatus.SUCCESSFUL,
771
+ enqueued_at=timezone.now(),
772
+ finished_at=timezone.now(),
773
+ backend_name="default",
774
+ )
775
+ response = client.get(
776
+ reverse("django_database_task:purge_completed_tasks"),
777
+ {"days": "0", "status": "SUCCESSFUL", "dry_run": "true"},
778
+ )
779
+ assert response.status_code == 200
780
+ data = response.json()
781
+ assert data["count"] == 1
782
+ assert data["dry_run"] is True
783
+
784
+ def test_purge_via_get_invalid_days(self, client):
785
+ """Test GET method with invalid days parameter."""
786
+ response = client.get(
787
+ reverse("django_database_task:purge_completed_tasks"),
788
+ {"days": "abc"},
789
+ )
790
+ assert response.status_code == 400
791
+ assert "days must be an integer" in response.json()["error"]
792
+
793
+ def test_purge_via_get_invalid_batch_size(self, client):
794
+ """Test GET method with invalid batch_size parameter."""
795
+ response = client.get(
796
+ reverse("django_database_task:purge_completed_tasks"),
797
+ {"batch_size": "abc"},
798
+ )
799
+ assert response.status_code == 400
800
+ assert "batch_size must be an integer" in response.json()["error"]
745
801
 
746
802
  def test_purge_rejects_invalid_json(self, client):
747
803
  """Test that invalid JSON returns 400."""