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.
- {django_database_task-0.2.2/django_database_task.egg-info → django_database_task-0.2.4}/PKG-INFO +58 -5
- {django_database_task-0.2.2 → django_database_task-0.2.4}/README.md +57 -4
- {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task/__init__.py +1 -1
- {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task/backends.py +28 -6
- {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task/cloudtasks/auth.py +28 -1
- {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task/views.py +68 -17
- {django_database_task-0.2.2 → django_database_task-0.2.4/django_database_task.egg-info}/PKG-INFO +58 -5
- {django_database_task-0.2.2 → django_database_task-0.2.4}/pyproject.toml +1 -1
- {django_database_task-0.2.2 → django_database_task-0.2.4}/tests/test_views.py +59 -3
- {django_database_task-0.2.2 → django_database_task-0.2.4}/LICENSE +0 -0
- {django_database_task-0.2.2 → django_database_task-0.2.4}/MANIFEST.in +0 -0
- {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task/admin.py +0 -0
- {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task/apps.py +0 -0
- {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task/cloudtasks/__init__.py +0 -0
- {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task/cloudtasks/backend.py +0 -0
- {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task/cloudtasks/detection.py +0 -0
- {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task/executor.py +0 -0
- {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task/locale/ja/LC_MESSAGES/django.mo +0 -0
- {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task/locale/ja/LC_MESSAGES/django.po +0 -0
- {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task/management/__init__.py +0 -0
- {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task/management/commands/__init__.py +0 -0
- {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task/management/commands/purge_completed_database_tasks.py +0 -0
- {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task/management/commands/run_database_tasks.py +0 -0
- {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task/migrations/0001_initial.py +0 -0
- {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task/migrations/__init__.py +0 -0
- {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task/models.py +0 -0
- {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task/urls.py +0 -0
- {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task.egg-info/SOURCES.txt +0 -0
- {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task.egg-info/dependency_links.txt +0 -0
- {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task.egg-info/requires.txt +0 -0
- {django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task.egg-info/top_level.txt +0 -0
- {django_database_task-0.2.2 → django_database_task-0.2.4}/setup.cfg +0 -0
- {django_database_task-0.2.2 → django_database_task-0.2.4}/tests/test_admin.py +0 -0
- {django_database_task-0.2.2 → django_database_task-0.2.4}/tests/test_backend.py +0 -0
- {django_database_task-0.2.2 → django_database_task-0.2.4}/tests/test_commands.py +0 -0
- {django_database_task-0.2.2 → django_database_task-0.2.4}/tests/test_executor.py +0 -0
- {django_database_task-0.2.2 → django_database_task-0.2.4}/tests/test_models.py +0 -0
{django_database_task-0.2.2/django_database_task.egg-info → django_database_task-0.2.4}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-database-task
|
|
3
|
-
Version: 0.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": "
|
|
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": "
|
|
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
|
},
|
|
@@ -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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
{django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task/cloudtasks/auth.py
RENAMED
|
@@ -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
|
-
|
|
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
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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 =
|
|
427
|
-
status_str =
|
|
428
|
-
batch_size =
|
|
429
|
-
dry_run =
|
|
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)
|
{django_database_task-0.2.2 → django_database_task-0.2.4/django_database_task.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-database-task
|
|
3
|
-
Version: 0.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": "
|
|
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
|
},
|
|
@@ -738,10 +738,66 @@ class TestPurgeCompletedTasksView:
|
|
|
738
738
|
data = response.json()
|
|
739
739
|
assert data["deleted"] == 0
|
|
740
740
|
|
|
741
|
-
def
|
|
742
|
-
"""Test that GET method
|
|
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 ==
|
|
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."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task/cloudtasks/backend.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{django_database_task-0.2.2 → django_database_task-0.2.4}/django_database_task.egg-info/requires.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|