django-qstash 0.0.11__py3-none-any.whl → 0.0.12__py3-none-any.whl
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.
Potentially problematic release.
This version of django-qstash might be problematic. Click here for more details.
- django_qstash/__init__.py +1 -1
- django_qstash/callbacks.py +6 -0
- django_qstash/cron.py +21 -0
- django_qstash/db/__init__.py +0 -0
- django_qstash/db/models.py +12 -0
- django_qstash/handlers.py +31 -6
- django_qstash/results/admin.py +4 -1
- django_qstash/results/migrations/0002_taskresult_function_path_alter_taskresult_status_and_more.py +46 -0
- django_qstash/results/models.py +7 -8
- django_qstash/results/services.py +28 -2
- django_qstash/results/tasks.py +19 -1
- django_qstash/schedules/exceptions.py +6 -0
- django_qstash/schedules/forms.py +7 -0
- django_qstash/schedules/models.py +2 -0
- django_qstash/schedules/validators.py +40 -0
- {django_qstash-0.0.11.dist-info → django_qstash-0.0.12.dist-info}/METADATA +100 -27
- {django_qstash-0.0.11.dist-info → django_qstash-0.0.12.dist-info}/RECORD +19 -15
- {django_qstash-0.0.11.dist-info → django_qstash-0.0.12.dist-info}/WHEEL +1 -1
- {django_qstash-0.0.11.dist-info → django_qstash-0.0.12.dist-info}/top_level.txt +0 -0
django_qstash/__init__.py
CHANGED
django_qstash/callbacks.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
4
|
+
|
|
3
5
|
from django_qstash.settings import DJANGO_QSTASH_DOMAIN
|
|
4
6
|
from django_qstash.settings import DJANGO_QSTASH_WEBHOOK_PATH
|
|
5
7
|
|
|
@@ -8,6 +10,10 @@ def get_callback_url() -> str:
|
|
|
8
10
|
"""
|
|
9
11
|
Get the callback URL based on the settings.
|
|
10
12
|
"""
|
|
13
|
+
if DJANGO_QSTASH_DOMAIN is None:
|
|
14
|
+
raise ImproperlyConfigured("DJANGO_QSTASH_DOMAIN is not set")
|
|
15
|
+
if DJANGO_QSTASH_WEBHOOK_PATH is None:
|
|
16
|
+
raise ImproperlyConfigured("DJANGO_QSTASH_WEBHOOK_PATH is not set")
|
|
11
17
|
callback_domain = DJANGO_QSTASH_DOMAIN.rstrip("/")
|
|
12
18
|
if not callback_domain.startswith(("http://", "https://")):
|
|
13
19
|
callback_domain = f"https://{callback_domain}"
|
django_qstash/cron.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
minute_re = re.compile(
|
|
6
|
+
r"^\*$|^[0-5]?\d$|^[0-5]?\d-[0-5]?\d$|^[0-5]?\d(,[0-5]?\d)+$|^\*/([1-9]|[1-5][0-9])$"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
hour_re = re.compile(
|
|
10
|
+
r"^\*$|^([01]?\d|2[0-3])$|^([01]?\d|2[0-3])-([01]?\d|2[0-3])$|^([01]?\d|2[0-3])(,([01]?\d|2[0-3]))+$|^\*/([1-9]|1[0-9]|2[0-3])$"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
day_of_month_re = re.compile(
|
|
14
|
+
r"^\*$|^([1-2]?\d|3[01])$|^([1-2]?\d|3[01])-([1-2]?\d|3[01])$|^([1-2]?\d|3[01])(,([1-2]?\d|3[01]))+$|^\*/([1-9]|[12]\d|3[01])$"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
month_re = re.compile(
|
|
18
|
+
r"^\*$|^([1-9]|1[0-2])$|^([1-9]|1[0-2])-([1-9]|1[0-2])$|^([1-9]|1[0-2])(,([1-9]|1[0-2]))+$|^\*/([1-9]|1[0-2])$"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
day_of_week_re = re.compile(r"^\*$|^[0-6]$|^[0-6]-[0-6]$|^[0-6](,[0-6])+$|^\*/[1-6]$")
|
|
File without changes
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from django.db import models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TaskStatus(models.TextChoices):
|
|
7
|
+
PENDING = "PENDING", "Pending"
|
|
8
|
+
SUCCESS = "SUCCESS", "Success"
|
|
9
|
+
EXECUTION_ERROR = "EXECUTION_ERROR", "Execution Error"
|
|
10
|
+
INTERNAL_ERROR = "INTERNAL_ERROR", "Internal Error"
|
|
11
|
+
OTHER_ERROR = "OTHER_ERROR", "Other Error"
|
|
12
|
+
UNKNOWN = "UNKNOWN", "Unknown"
|
django_qstash/handlers.py
CHANGED
|
@@ -9,6 +9,8 @@ from django.conf import settings
|
|
|
9
9
|
from django.http import HttpRequest
|
|
10
10
|
from qstash import Receiver
|
|
11
11
|
|
|
12
|
+
from django_qstash.db.models import TaskStatus
|
|
13
|
+
|
|
12
14
|
from . import utils
|
|
13
15
|
from .exceptions import PayloadError
|
|
14
16
|
from .exceptions import SignatureError
|
|
@@ -90,7 +92,9 @@ class QStashWebhook:
|
|
|
90
92
|
|
|
91
93
|
def handle_request(self, request: HttpRequest) -> tuple[dict, int]:
|
|
92
94
|
"""Process webhook request and return response data and status code."""
|
|
93
|
-
payload = None
|
|
95
|
+
payload = None
|
|
96
|
+
task_id = request.headers.get("Upstash-Message-Id")
|
|
97
|
+
|
|
94
98
|
try:
|
|
95
99
|
body = request.body.decode()
|
|
96
100
|
self.verify_signature(
|
|
@@ -101,14 +105,14 @@ class QStashWebhook:
|
|
|
101
105
|
|
|
102
106
|
payload = self.parse_payload(body)
|
|
103
107
|
result = self.execute_task(payload)
|
|
104
|
-
|
|
105
108
|
store_task_result(
|
|
106
|
-
task_id=
|
|
109
|
+
task_id=task_id,
|
|
107
110
|
task_name=payload.task_name,
|
|
108
|
-
status=
|
|
111
|
+
status=TaskStatus.SUCCESS,
|
|
109
112
|
result=result,
|
|
110
113
|
args=payload.args,
|
|
111
114
|
kwargs=payload.kwargs,
|
|
115
|
+
function_path=payload.function_path,
|
|
112
116
|
)
|
|
113
117
|
|
|
114
118
|
return {
|
|
@@ -125,16 +129,37 @@ class QStashWebhook:
|
|
|
125
129
|
"error": str(e),
|
|
126
130
|
"task_name": getattr(payload, "task_name", None),
|
|
127
131
|
}, 400
|
|
132
|
+
|
|
128
133
|
except TaskError as e:
|
|
129
134
|
logger.exception("Task execution error: %s", str(e))
|
|
135
|
+
store_task_result(
|
|
136
|
+
task_id=task_id,
|
|
137
|
+
task_name=payload.task_name,
|
|
138
|
+
status=TaskStatus.EXECUTION_ERROR,
|
|
139
|
+
traceback=str(e),
|
|
140
|
+
args=payload.args,
|
|
141
|
+
kwargs=payload.kwargs,
|
|
142
|
+
function_path=payload.function_path,
|
|
143
|
+
)
|
|
130
144
|
return {
|
|
131
145
|
"status": "error",
|
|
132
146
|
"error_type": e.__class__.__name__,
|
|
133
147
|
"error": str(e),
|
|
134
|
-
"task_name": payload.task_name
|
|
135
|
-
},
|
|
148
|
+
"task_name": payload.task_name,
|
|
149
|
+
}, 422
|
|
150
|
+
|
|
136
151
|
except Exception as e:
|
|
137
152
|
logger.exception("Unexpected error in webhook handler: %s", str(e))
|
|
153
|
+
if payload: # Store unexpected errors only if payload was parsed
|
|
154
|
+
store_task_result(
|
|
155
|
+
task_id=task_id,
|
|
156
|
+
task_name=payload.task_name,
|
|
157
|
+
status=TaskStatus.INTERNAL_ERROR,
|
|
158
|
+
traceback=str(e),
|
|
159
|
+
args=payload.args,
|
|
160
|
+
kwargs=payload.kwargs,
|
|
161
|
+
function_path=payload.function_path,
|
|
162
|
+
)
|
|
138
163
|
return {
|
|
139
164
|
"status": "error",
|
|
140
165
|
"error_type": "InternalServerError",
|
django_qstash/results/admin.py
CHANGED
|
@@ -17,5 +17,8 @@ class TaskResultAdmin(admin.ModelAdmin):
|
|
|
17
17
|
"kwargs",
|
|
18
18
|
"task_id",
|
|
19
19
|
"date_created",
|
|
20
|
+
"function_path",
|
|
20
21
|
]
|
|
21
|
-
|
|
22
|
+
search_fields = ["task_name", "task_id", "function_path"]
|
|
23
|
+
list_display = ["task_name", "function_path", "status", "date_done"]
|
|
24
|
+
list_filter = ["status", "date_done"]
|
django_qstash/results/migrations/0002_taskresult_function_path_alter_taskresult_status_and_more.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Generated by Django 5.1.4 on 2025-01-06 05:39
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from django.db import migrations
|
|
6
|
+
from django.db import models
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Migration(migrations.Migration):
|
|
10
|
+
dependencies = [
|
|
11
|
+
("django_qstash_results", "0001_initial"),
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
operations = [
|
|
15
|
+
migrations.AddField(
|
|
16
|
+
model_name="taskresult",
|
|
17
|
+
name="function_path",
|
|
18
|
+
field=models.TextField(blank=True, null=True),
|
|
19
|
+
),
|
|
20
|
+
migrations.AlterField(
|
|
21
|
+
model_name="taskresult",
|
|
22
|
+
name="status",
|
|
23
|
+
field=models.CharField(
|
|
24
|
+
choices=[
|
|
25
|
+
("PENDING", "Pending"),
|
|
26
|
+
("SUCCESS", "Success"),
|
|
27
|
+
("EXECUTION_ERROR", "Execution Error"),
|
|
28
|
+
("INTERNAL_ERROR", "Internal Error"),
|
|
29
|
+
("OTHER_ERROR", "Other Error"),
|
|
30
|
+
("UNKNOWN", "Unknown"),
|
|
31
|
+
],
|
|
32
|
+
default="PENDING",
|
|
33
|
+
max_length=50,
|
|
34
|
+
),
|
|
35
|
+
),
|
|
36
|
+
migrations.AlterField(
|
|
37
|
+
model_name="taskresult",
|
|
38
|
+
name="task_id",
|
|
39
|
+
field=models.CharField(db_index=True, max_length=255),
|
|
40
|
+
),
|
|
41
|
+
migrations.AlterField(
|
|
42
|
+
model_name="taskresult",
|
|
43
|
+
name="traceback",
|
|
44
|
+
field=models.TextField(blank=True, null=True),
|
|
45
|
+
),
|
|
46
|
+
]
|
django_qstash/results/models.py
CHANGED
|
@@ -5,26 +5,25 @@ import uuid
|
|
|
5
5
|
from django.db import models
|
|
6
6
|
from django.utils import timezone
|
|
7
7
|
|
|
8
|
+
from django_qstash.db.models import TaskStatus
|
|
9
|
+
|
|
8
10
|
|
|
9
11
|
class TaskResult(models.Model):
|
|
10
12
|
id = models.UUIDField(
|
|
11
13
|
primary_key=True, default=uuid.uuid1, editable=False, unique=True
|
|
12
14
|
)
|
|
13
|
-
task_id = models.CharField(max_length=255,
|
|
15
|
+
task_id = models.CharField(max_length=255, db_index=True)
|
|
14
16
|
task_name = models.CharField(max_length=255)
|
|
15
17
|
status = models.CharField(
|
|
16
18
|
max_length=50,
|
|
17
|
-
choices=
|
|
18
|
-
|
|
19
|
-
("SUCCESS", "Success"),
|
|
20
|
-
("FAILURE", "Failure"),
|
|
21
|
-
],
|
|
22
|
-
default="PENDING",
|
|
19
|
+
choices=TaskStatus.choices,
|
|
20
|
+
default=TaskStatus.PENDING,
|
|
23
21
|
)
|
|
24
22
|
date_created = models.DateTimeField(default=timezone.now)
|
|
25
23
|
date_done = models.DateTimeField(null=True)
|
|
26
24
|
result = models.JSONField(null=True)
|
|
27
|
-
traceback = models.TextField(null=True)
|
|
25
|
+
traceback = models.TextField(blank=True, null=True)
|
|
26
|
+
function_path = models.TextField(blank=True, null=True)
|
|
28
27
|
args = models.JSONField(null=True)
|
|
29
28
|
kwargs = models.JSONField(null=True)
|
|
30
29
|
|
|
@@ -1,17 +1,42 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
import logging
|
|
5
|
+
from typing import Any
|
|
4
6
|
|
|
5
7
|
from django.apps import apps
|
|
6
8
|
from django.utils import timezone
|
|
7
9
|
|
|
10
|
+
from django_qstash.db.models import TaskStatus
|
|
11
|
+
|
|
8
12
|
logger = logging.getLogger(__name__)
|
|
9
13
|
|
|
10
14
|
|
|
15
|
+
def function_result_to_json(result: Any) -> str:
|
|
16
|
+
"""Convert a function result to a JSON string"""
|
|
17
|
+
try:
|
|
18
|
+
data = {"result": result}
|
|
19
|
+
return json.dumps(data)
|
|
20
|
+
except Exception as e:
|
|
21
|
+
logger.exception("Failed to convert function result to JSON: %s", str(e))
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
|
|
11
25
|
def store_task_result(
|
|
12
|
-
task_id,
|
|
26
|
+
task_id,
|
|
27
|
+
task_name,
|
|
28
|
+
status,
|
|
29
|
+
result=None,
|
|
30
|
+
traceback=None,
|
|
31
|
+
args=None,
|
|
32
|
+
kwargs=None,
|
|
33
|
+
error=None,
|
|
34
|
+
function_path=None,
|
|
13
35
|
):
|
|
14
36
|
"""Helper function to store task results if the results app is installed"""
|
|
37
|
+
if status not in TaskStatus.values:
|
|
38
|
+
status = TaskStatus.UNKNOWN
|
|
39
|
+
|
|
15
40
|
try:
|
|
16
41
|
TaskResult = apps.get_model("django_qstash_results", "TaskResult")
|
|
17
42
|
task_result = TaskResult.objects.create(
|
|
@@ -19,10 +44,11 @@ def store_task_result(
|
|
|
19
44
|
task_name=task_name,
|
|
20
45
|
status=status,
|
|
21
46
|
date_done=timezone.now(),
|
|
22
|
-
result=result,
|
|
47
|
+
result=function_result_to_json(result),
|
|
23
48
|
traceback=traceback,
|
|
24
49
|
args=args,
|
|
25
50
|
kwargs=kwargs,
|
|
51
|
+
function_path=function_path,
|
|
26
52
|
)
|
|
27
53
|
return task_result
|
|
28
54
|
except LookupError:
|
django_qstash/results/tasks.py
CHANGED
|
@@ -8,6 +8,7 @@ from django.conf import settings
|
|
|
8
8
|
from django.utils import timezone
|
|
9
9
|
|
|
10
10
|
from django_qstash import stashed_task
|
|
11
|
+
from django_qstash.db.models import TaskStatus
|
|
11
12
|
|
|
12
13
|
DJANGO_QSTASH_RESULT_TTL = getattr(settings, "DJANGO_QSTASH_RESULT_TTL", 604800)
|
|
13
14
|
|
|
@@ -16,7 +17,7 @@ logger = logging.getLogger(__name__)
|
|
|
16
17
|
|
|
17
18
|
@stashed_task(name="Cleanup Task Results")
|
|
18
19
|
def clear_stale_results_task(
|
|
19
|
-
since=None, stdout=None, user_confirm=False, *args, **options
|
|
20
|
+
since=None, stdout=None, user_confirm=False, exclude_errors=True, *args, **options
|
|
20
21
|
):
|
|
21
22
|
delta_seconds = since or DJANGO_QSTASH_RESULT_TTL
|
|
22
23
|
cutoff_date = timezone.now() - timedelta(seconds=delta_seconds)
|
|
@@ -30,6 +31,14 @@ def clear_stale_results_task(
|
|
|
30
31
|
logger.exception(msg)
|
|
31
32
|
raise e
|
|
32
33
|
qs_to_delete = TaskResult.objects.filter(date_done__lt=cutoff_date)
|
|
34
|
+
if exclude_errors:
|
|
35
|
+
qs_to_delete = qs_to_delete.exclude(
|
|
36
|
+
status__in=[
|
|
37
|
+
TaskStatus.EXECUTION_ERROR,
|
|
38
|
+
TaskStatus.INTERNAL_ERROR,
|
|
39
|
+
TaskStatus.OTHER_ERROR,
|
|
40
|
+
]
|
|
41
|
+
)
|
|
33
42
|
|
|
34
43
|
if user_confirm:
|
|
35
44
|
user_input = input("Are you sure? (Y/n): ")
|
|
@@ -65,3 +74,12 @@ def clear_stale_results_task(
|
|
|
65
74
|
stdout.write(msg)
|
|
66
75
|
logger.exception(msg)
|
|
67
76
|
raise e
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@stashed_task(name="Clear Task Error Results")
|
|
80
|
+
def clear_task_errors_task(
|
|
81
|
+
since=None, stdout=None, user_confirm=False, *args, **options
|
|
82
|
+
):
|
|
83
|
+
clear_stale_results_task(
|
|
84
|
+
since=since, stdout=stdout, user_confirm=user_confirm, exclude_errors=False
|
|
85
|
+
)
|
django_qstash/schedules/forms.py
CHANGED
|
@@ -22,3 +22,10 @@ class TaskScheduleForm(forms.ModelForm):
|
|
|
22
22
|
"retries",
|
|
23
23
|
"timeout",
|
|
24
24
|
]
|
|
25
|
+
|
|
26
|
+
def clean(self):
|
|
27
|
+
cleaned_data = super().clean()
|
|
28
|
+
# If task_name is not provided, use the task value
|
|
29
|
+
if not cleaned_data.get("task_name") and cleaned_data.get("task"):
|
|
30
|
+
cleaned_data["task_name"] = cleaned_data["task"]
|
|
31
|
+
return cleaned_data
|
|
@@ -8,6 +8,7 @@ from django.db import models
|
|
|
8
8
|
from django.utils import timezone
|
|
9
9
|
|
|
10
10
|
from django_qstash.discovery.models import TaskField
|
|
11
|
+
from django_qstash.schedules.validators import validate_cron_expression
|
|
11
12
|
from django_qstash.schedules.validators import validate_duration_string
|
|
12
13
|
|
|
13
14
|
DJANGO_QSTASH_DOMAIN = getattr(settings, "DJANGO_QSTASH_DOMAIN", None)
|
|
@@ -56,6 +57,7 @@ class TaskSchedule(models.Model):
|
|
|
56
57
|
cron = models.CharField(
|
|
57
58
|
max_length=255,
|
|
58
59
|
default="*/5 * * * *",
|
|
60
|
+
validators=[validate_cron_expression],
|
|
59
61
|
verbose_name="Cron Expression",
|
|
60
62
|
help_text="Cron expression for scheduling the task",
|
|
61
63
|
)
|
|
@@ -2,6 +2,10 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
4
|
|
|
5
|
+
from django.utils.safestring import mark_safe
|
|
6
|
+
|
|
7
|
+
from django_qstash import cron
|
|
8
|
+
from django_qstash.schedules.exceptions import InvalidCronStringValidationError
|
|
5
9
|
from django_qstash.schedules.exceptions import InvalidDurationStringValidationError
|
|
6
10
|
|
|
7
11
|
|
|
@@ -27,3 +31,39 @@ def validate_duration_string(value):
|
|
|
27
31
|
raise InvalidDurationStringValidationError(
|
|
28
32
|
"Duration too long. Maximum allowed: 7 days (equivalent to: 604800s, 10080m, 168h, 7d)"
|
|
29
33
|
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def validate_cron_expression(value: str) -> None:
|
|
37
|
+
"""Validates a standard cron expression with 5 fields (minute, hour, day of month, month, day of week)."""
|
|
38
|
+
parts = value.split()
|
|
39
|
+
if len(parts) != 5:
|
|
40
|
+
raise InvalidCronStringValidationError(
|
|
41
|
+
'Invalid cron format. Must contain 5 fields: "minute hour day_of_month month day_of_week"'
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
field_descriptions = {
|
|
45
|
+
"minute": "0-59",
|
|
46
|
+
"hour": "0-23",
|
|
47
|
+
"day_of_month": "1-31",
|
|
48
|
+
"month": "1-12",
|
|
49
|
+
"day_of_week": "0-6 (0=Sunday, 6=Saturday)",
|
|
50
|
+
}
|
|
51
|
+
field_label = {
|
|
52
|
+
"minute": "minute",
|
|
53
|
+
"hour": "hour",
|
|
54
|
+
"day_of_month": "day of the month",
|
|
55
|
+
"month": "month",
|
|
56
|
+
"day_of_week": "day of the week",
|
|
57
|
+
}
|
|
58
|
+
patterns = {
|
|
59
|
+
"minute": cron.minute_re,
|
|
60
|
+
"hour": cron.hour_re,
|
|
61
|
+
"day_of_month": cron.day_of_month_re,
|
|
62
|
+
"month": cron.month_re,
|
|
63
|
+
"day_of_week": cron.day_of_week_re,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for sub_value, (field, pattern) in zip(parts, patterns.items()):
|
|
67
|
+
if not re.match(pattern, sub_value):
|
|
68
|
+
invalid_msg = f"""<b>{sub_value}</b> is not a valid {field_label[field]}. <br/>Must be in range {field_descriptions[field]}. <br/><a target="_blank" href="https://crontab.guru/">crontab.guru</a> is also helpful."""
|
|
69
|
+
raise InvalidCronStringValidationError(mark_safe(invalid_msg))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: django-qstash
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.12
|
|
4
4
|
Summary: A drop-in replacement for Celery's shared_task with Upstash QStash.
|
|
5
5
|
Author-email: Justin Mitchel <justin@codingforentrepreneurs.com>
|
|
6
6
|
Project-URL: Changelog, https://github.com/jmitchel3/django-qstash
|
|
@@ -79,18 +79,22 @@ This allows us to:
|
|
|
79
79
|
- [Usage](#usage)
|
|
80
80
|
- [Define a Task](#define-a-task)
|
|
81
81
|
- [Regular Task Call](#regular-task-call)
|
|
82
|
-
- [
|
|
82
|
+
- [Background Task](#background-task)
|
|
83
83
|
- [`.delay()`](#delay)
|
|
84
84
|
- [`.apply_async()`](#apply_async)
|
|
85
85
|
- [`.apply_async()` With Time Delay](#apply_async-with-time-delay)
|
|
86
|
-
- [JSON-ready
|
|
86
|
+
- [Arguments Must be JSON-ready](#arguments-must-be-json-ready)
|
|
87
87
|
- [Example Task](#example-task)
|
|
88
88
|
- [Management Commands](#management-commands)
|
|
89
|
-
- [Development
|
|
89
|
+
- [Public Domain In Development](#public-domain-in-development)
|
|
90
90
|
- [Django Settings Configuration](#django-settings-configuration)
|
|
91
|
+
- [`DJANGO_QSTASH_DOMAIN`](#django_qstash_domain)
|
|
92
|
+
- [`DJANGO_QSTASH_WEBHOOK_PATH`](#django_qstash_webhook_path)
|
|
93
|
+
- [`DJANGO_QSTASH_FORCE_HTTPS`](#django_qstash_force_https)
|
|
94
|
+
- [Example Django Settings](#example-django-settings)
|
|
91
95
|
- [Schedule Tasks (Optional)](#schedule-tasks-optional)
|
|
92
96
|
- [Installation](#installation-1)
|
|
93
|
-
|
|
97
|
+
- [Schedule a Task](#schedule-a-task)
|
|
94
98
|
- [Store Task Results (Optional)](#store-task-results-optional)
|
|
95
99
|
- [Clear Stale Results](#clear-stale-results)
|
|
96
100
|
- [Definitions](#definitions)
|
|
@@ -161,7 +165,7 @@ There is a sample project in [sample_project/](sample_project/) that shows how a
|
|
|
161
165
|
|
|
162
166
|
## Usage
|
|
163
167
|
|
|
164
|
-
Django-QStash revolves around the `stashed_task` decorator. The goal is to be a drop-in replacement for Celery's `
|
|
168
|
+
Django-QStash revolves around the `stashed_task` decorator. The goal is to be a drop-in replacement for Celery's `shared_task` decorator.
|
|
165
169
|
|
|
166
170
|
Here's how it works:
|
|
167
171
|
- Define a Task
|
|
@@ -169,6 +173,8 @@ Here's how it works:
|
|
|
169
173
|
|
|
170
174
|
### Define a Task
|
|
171
175
|
```python
|
|
176
|
+
# from celery import shared_task
|
|
177
|
+
from django_qstash import shared_task
|
|
172
178
|
from django_qstash import stashed_task
|
|
173
179
|
|
|
174
180
|
|
|
@@ -178,8 +184,19 @@ def hello_world(name: str, age: int = None, activity: str = None):
|
|
|
178
184
|
print(f"Hello {name}! I see you're {activity}.")
|
|
179
185
|
return
|
|
180
186
|
print(f"Hello {name}! I see you're {activity} at {age} years old.")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@shared_task
|
|
190
|
+
def hello_world_redux(name: str, age: int = None, activity: str = None):
|
|
191
|
+
if age is None:
|
|
192
|
+
print(f"Hello {name}! I see you're {activity}.")
|
|
193
|
+
return
|
|
194
|
+
print(f"Hello {name}! I see you're {activity} at {age} years old.")
|
|
181
195
|
```
|
|
182
196
|
|
|
197
|
+
- `hello_world` and `hello_world_redux` work the same with django-qstash.
|
|
198
|
+
- If you use Celery's `@shared_task` instead, Celery would handle only `hello_world_redux` and django-qstash would handle only `hello_world`.
|
|
199
|
+
|
|
183
200
|
### Regular Task Call
|
|
184
201
|
Nothing special here. Just call the function like any other to verify it works.
|
|
185
202
|
|
|
@@ -188,9 +205,11 @@ Nothing special here. Just call the function like any other to verify it works.
|
|
|
188
205
|
hello_world("Tony Stark", age=40, activity="building in a cave with a box of scraps.")
|
|
189
206
|
```
|
|
190
207
|
|
|
191
|
-
###
|
|
208
|
+
### Background Task
|
|
209
|
+
|
|
210
|
+
Using `.delay()` or `.apply_async()` is how you trigger a background task. These background tasks are actually setting up a QStash message that will be delivered via webhook to your Django application. django-qstash handles the webhook and the message delivery assuming installed correctly.
|
|
192
211
|
|
|
193
|
-
|
|
212
|
+
This functionality is modeled after Celery and it works as you'd expect.
|
|
194
213
|
|
|
195
214
|
|
|
196
215
|
#### `.delay()`
|
|
@@ -223,10 +242,11 @@ hello_world.apply_async(
|
|
|
223
242
|
)
|
|
224
243
|
```
|
|
225
244
|
|
|
226
|
-
### JSON-ready
|
|
245
|
+
### Arguments Must be JSON-ready
|
|
227
246
|
|
|
228
|
-
|
|
247
|
+
Arguments to django-qstash managed functions must be _JSON_ serializable.
|
|
229
248
|
|
|
249
|
+
The way you find out:
|
|
230
250
|
```python
|
|
231
251
|
import json
|
|
232
252
|
|
|
@@ -237,6 +257,11 @@ data = {
|
|
|
237
257
|
print(json.dumps(data))
|
|
238
258
|
# no errors, you're good to go.
|
|
239
259
|
```
|
|
260
|
+
If you have `errors` you'll need to fix them. Here's a few common errors you might see:
|
|
261
|
+
|
|
262
|
+
- Using a Django queryset directly as an argument
|
|
263
|
+
- Using a Django model instance directly as an argument
|
|
264
|
+
- Using a datetime object directly as an argument (e.g. `datetime.datetime` or `datetime.date`) instead of a timestamp or date string (e.g. `datetime.datetime.now().timestamp()` or `datetime.datetime.now.strftime("%Y-%m-%d")`)
|
|
240
265
|
|
|
241
266
|
### Example Task
|
|
242
267
|
|
|
@@ -281,38 +306,83 @@ The `.delay()` method does not support a countdown parameter because it simply p
|
|
|
281
306
|
|
|
282
307
|
## Management Commands
|
|
283
308
|
|
|
284
|
-
- `python manage.py available_tasks` to view all available tasks
|
|
309
|
+
- `python manage.py available_tasks` to view all available tasks found by django-qstash. Unlike Celery, django-qstash does not assign tasks to a specific Celery app (e.g. `app = Celery()`).
|
|
285
310
|
|
|
286
311
|
_Requires `django_qstash.schedules` installed._
|
|
287
312
|
- `python manage.py task_schedules --list` see all schedules relate to the `DJANGO_QSTASH_DOMAIN`
|
|
288
313
|
- `python manage.py task_schedules --sync` sync schedules based on the `DJANGO_QSTASH_DOMAIN` to store in the Django Admin.
|
|
289
314
|
|
|
290
|
-
## Development
|
|
315
|
+
## Public Domain In Development
|
|
291
316
|
|
|
292
|
-
django-qstash
|
|
317
|
+
django-qstash _requires_ a publicly accessible domain to work (e.g. `https://djangoqstash.com`). There are many ways to do this, we recommend:
|
|
293
318
|
|
|
294
319
|
- [Cloudflare Tunnels](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) with a domain name you control.
|
|
295
320
|
- [ngrok](https://ngrok.com/)
|
|
296
321
|
|
|
297
322
|
Once you have a domain name, you can configure the `DJANGO_QSTASH_DOMAIN` setting in your Django settings.
|
|
298
323
|
|
|
299
|
-
|
|
300
324
|
## Django Settings Configuration
|
|
301
325
|
|
|
302
|
-
|
|
326
|
+
Various options are available to configure django-qstash.
|
|
327
|
+
|
|
328
|
+
### `DJANGO_QSTASH_DOMAIN`
|
|
329
|
+
- Required: Yes
|
|
330
|
+
- Default:`None`
|
|
331
|
+
- Description: Must be a valid and publicly accessible domain. For example `https://djangoqstash.com`. Review [Development usage](#development-usage) for setting up a domain name during development.
|
|
303
332
|
|
|
304
|
-
|
|
333
|
+
### `DJANGO_QSTASH_WEBHOOK_PATH`
|
|
334
|
+
- Required: Yes
|
|
335
|
+
- Default:`/qstash/webhook/`
|
|
336
|
+
- Description: The path where QStash will send webhooks to your Django application.
|
|
305
337
|
|
|
306
|
-
|
|
338
|
+
### `DJANGO_QSTASH_FORCE_HTTPS`
|
|
339
|
+
- Required: No
|
|
340
|
+
- Default: `True`
|
|
341
|
+
- Description: Whether to force HTTPS for the webhook.
|
|
307
342
|
|
|
308
|
-
|
|
343
|
+
###`DJANGO_QSTASH_RESULT_TTL`
|
|
344
|
+
- Required: No
|
|
345
|
+
- Default:`604800`
|
|
346
|
+
- Description: A number of seconds after which task result data can be safely deleted. Defaults to 604800 seconds (7 days or 7 * 24 * 60 * 60).
|
|
309
347
|
|
|
310
|
-
|
|
348
|
+
|
|
349
|
+
### Example Django Settings
|
|
350
|
+
|
|
351
|
+
For a complete example, review [sample_project/settings.py](sample_project/settings.py) where [python-decouple](https://github.com/henriquebastos/python-decouple) is used to set the environment variables via the `.env` file or system environment variables (for production use).
|
|
352
|
+
|
|
353
|
+
Using `os.environ`:
|
|
354
|
+
```python
|
|
355
|
+
import os
|
|
356
|
+
|
|
357
|
+
###########################
|
|
358
|
+
# django settings
|
|
359
|
+
###########################
|
|
360
|
+
DJANGO_DEBUG = str(os.environ.get("DJANGO_DEBUG")) == "1"
|
|
361
|
+
DJANGO_SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY")
|
|
362
|
+
ALLOWED_HOSTS = [os.environ.get("ALLOWED_HOST")]
|
|
363
|
+
CSRF_TRUSTED_ORIGINS = [os.environ.get("CSRF_TRUSTED_ORIGIN")]
|
|
364
|
+
###########################
|
|
365
|
+
# qstash-py settings
|
|
366
|
+
###########################
|
|
367
|
+
QSTASH_TOKEN = os.environ.get("QSTASH_TOKEN")
|
|
368
|
+
QSTASH_CURRENT_SIGNING_KEY = os.environ.get("QSTASH_CURRENT_SIGNING_KEY")
|
|
369
|
+
QSTASH_NEXT_SIGNING_KEY = os.environ.get("QSTASH_NEXT_SIGNING_KEY")
|
|
370
|
+
|
|
371
|
+
###########################
|
|
372
|
+
# django_qstash settings
|
|
373
|
+
###########################
|
|
374
|
+
DJANGO_QSTASH_DOMAIN = os.environ.get("DJANGO_QSTASH_DOMAIN")
|
|
375
|
+
DJANGO_QSTASH_WEBHOOK_PATH = os.environ.get("DJANGO_QSTASH_WEBHOOK_PATH")
|
|
376
|
+
DJANGO_QSTASH_FORCE_HTTPS = True
|
|
377
|
+
DJANGO_QSTASH_RESULT_TTL = 604800
|
|
378
|
+
```
|
|
311
379
|
|
|
312
380
|
|
|
313
381
|
## Schedule Tasks (Optional)
|
|
314
382
|
|
|
315
|
-
|
|
383
|
+
Run background tasks on a CRON schedule.
|
|
384
|
+
|
|
385
|
+
The `django_qstash.schedules` app schedules tasks using Upstash [QStash Schedules](https://upstash.com/docs/qstash/features/schedules) via `@shared_task` or `@stashed_task` decorators along with the `TaskSchedule` model.
|
|
316
386
|
|
|
317
387
|
### Installation
|
|
318
388
|
|
|
@@ -332,8 +402,7 @@ Run migrations:
|
|
|
332
402
|
python manage.py migrate django_qstash_schedules
|
|
333
403
|
```
|
|
334
404
|
|
|
335
|
-
|
|
336
|
-
## Schedule a Task
|
|
405
|
+
### Schedule a Task
|
|
337
406
|
|
|
338
407
|
Tasks must exist before you can schedule them. Review [Define a Task](#define-a-task) for more information.
|
|
339
408
|
|
|
@@ -341,9 +410,6 @@ Here's how you can schedule a task:
|
|
|
341
410
|
- Django Admin (`/admin/django_qstash_schedules/taskschedule/add/`)
|
|
342
411
|
- Django shell (`python manage.py shell`)
|
|
343
412
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
413
|
```python
|
|
348
414
|
from django_qstash.schedules.models import TaskSchedule
|
|
349
415
|
from django_qstash.discovery.utils import discover_tasks
|
|
@@ -372,10 +438,11 @@ TaskSchedule.objects.create(
|
|
|
372
438
|
- `cron` is the cron schedule to run the task. Use [contrab.guru](https://crontab.guru/) for writing the cron format.
|
|
373
439
|
|
|
374
440
|
|
|
375
|
-
|
|
376
441
|
## Store Task Results (Optional)
|
|
377
442
|
|
|
378
|
-
|
|
443
|
+
Retain the results of background tasks in the database with clear-out functionality.
|
|
444
|
+
|
|
445
|
+
In `django_qstash.results.models` we have the `TaskResult` model class that can be used to track async task results. These entries are created via the django-qstash webhook view handler (`qstash_webhook_view`).
|
|
379
446
|
|
|
380
447
|
To install it, just add `django_qstash.results` to your `INSTALLED_APPS` setting.
|
|
381
448
|
|
|
@@ -393,6 +460,12 @@ Run migrations:
|
|
|
393
460
|
python manage.py migrate django_qstash_results
|
|
394
461
|
```
|
|
395
462
|
|
|
463
|
+
Key configuration:
|
|
464
|
+
|
|
465
|
+
- [DJANGO_QSTASH_WEBHOOK_PATH](#django-settings-configuration)
|
|
466
|
+
- [DJANGO_QSTASH_DOMAIN](#django-settings-configuration)
|
|
467
|
+
- [DJANGO_QSTASH_RESULT_TTL](#django-settings-configuration)
|
|
468
|
+
|
|
396
469
|
### Clear Stale Results
|
|
397
470
|
|
|
398
471
|
We recommend purging the `TaskResult` model after a certain amount of time.
|
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
django_qstash/__init__.py,sha256=
|
|
2
|
-
django_qstash/callbacks.py,sha256=
|
|
1
|
+
django_qstash/__init__.py,sha256=ENXrTf63lzkaRO580z6PPjYDS7bgC2bNaK24S-uqnbo,188
|
|
2
|
+
django_qstash/callbacks.py,sha256=IQ-D8sPlSRuREZ1zwkRyd2GtxfmrJJh2x4jLd39rZCE,813
|
|
3
3
|
django_qstash/client.py,sha256=cgHf-g6lDAltY_Vt6GUVJNY2JSz1czBOHL-WVkkLs2M,149
|
|
4
|
+
django_qstash/cron.py,sha256=13OzTMGXFgjNEXjs2Et20WGZYtW9lKlu79BjbRySnVc,716
|
|
4
5
|
django_qstash/exceptions.py,sha256=pH6kKRJFIVFkDHUJQ9yRWmtGdBBSXpNAwMSFuNzMgPw,392
|
|
5
|
-
django_qstash/handlers.py,sha256=
|
|
6
|
+
django_qstash/handlers.py,sha256=TtYZ-Sr858ajISBpDuHIT7-qaVsoVfTE6vVFJ9-kpPE,5820
|
|
6
7
|
django_qstash/settings.py,sha256=YvpXMo1AdIWvbotISWJmhg0vrW3A3UQ4BieNzMfRC7Y,524
|
|
7
8
|
django_qstash/utils.py,sha256=wrTU30cobO2di18BNEFtKD4ih2euf7eQNpg6p6TkQ1Y,1185
|
|
8
9
|
django_qstash/views.py,sha256=H32f_jGnlwOTO0YG9znNo2b-GRYZ8TM-Wt0T62SGdXM,639
|
|
9
10
|
django_qstash/app/__init__.py,sha256=kmoCVoESInzCZ_oGUiPVY4GsFQwBC07cqFJCyn9Loyk,240
|
|
10
11
|
django_qstash/app/base.py,sha256=gM7GIJh_omZcxbmsrwAEadA-N6EuUJbPzh0CflOIVRg,3864
|
|
11
12
|
django_qstash/app/decorators.py,sha256=Zkr0dLhW5-7yGmj7JunLGcgzOwsONRyz3YkrD957DqY,1170
|
|
13
|
+
django_qstash/db/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
django_qstash/db/models.py,sha256=UTmjw76h49HT4hPKaCwnOkHph70GOJ6mMDZt8aKmHH8,372
|
|
12
15
|
django_qstash/discovery/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
16
|
django_qstash/discovery/fields.py,sha256=h-31sysbIU05KGKGBAu7uQo9bZnZg3kgjN_ZhPPMTGU,1260
|
|
14
17
|
django_qstash/discovery/models.py,sha256=9ml9lTKEqEKx2uqYvejZw_BjdnowgFOPE7rYNt_8E9A,685
|
|
@@ -20,27 +23,28 @@ django_qstash/management/commands/available_tasks.py,sha256=l-do7Mry83NxbCdyMLcL
|
|
|
20
23
|
django_qstash/management/commands/clear_stale_results.py,sha256=mxXXqIy6pnvsN8JVE0xe3mypqtkaZbpqdBjpox-MDik,1402
|
|
21
24
|
django_qstash/management/commands/task_schedules.py,sha256=b9lJ1vjQKHyGzWAo9csGwE_oaKfgcSC8bPFLt9Ry6WE,4278
|
|
22
25
|
django_qstash/results/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
|
-
django_qstash/results/admin.py,sha256=
|
|
26
|
+
django_qstash/results/admin.py,sha256=Vr1O6E7vucSnkZ1PwHQ7RYkwOxiX4WHPAKaA7L8Cn-4,580
|
|
24
27
|
django_qstash/results/apps.py,sha256=4me4cg5yeoeSJTphkHYzGMJUfGucT47FNIUMYu5gmIo,275
|
|
25
|
-
django_qstash/results/models.py,sha256=
|
|
26
|
-
django_qstash/results/services.py,sha256=
|
|
27
|
-
django_qstash/results/tasks.py,sha256=
|
|
28
|
+
django_qstash/results/models.py,sha256=AV7-nVEMq-Xt2QVY6D7NQc4_3E7v-6NA2xngTU8DfXA,1062
|
|
29
|
+
django_qstash/results/services.py,sha256=vax0nsH9VO8nWTbS1Ki762-w-15ADeWSN6Sohpx7Dlg,1590
|
|
30
|
+
django_qstash/results/tasks.py,sha256=kd49OOZyOg6OG3RSyywtZPtpGaPlycY2OXl1BXaoxVM,2746
|
|
28
31
|
django_qstash/results/migrations/0001_initial.py,sha256=A90SKgWmBf4SIJYG1Jh6-b_81Ia1zIzGj3Bfl1O4-kg,1902
|
|
32
|
+
django_qstash/results/migrations/0002_taskresult_function_path_alter_taskresult_status_and_more.py,sha256=FevtPlzkKHjRD1tcnXskigY5jr2X3gYv7KE2TcdEAxU,1374
|
|
29
33
|
django_qstash/results/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
30
34
|
django_qstash/schedules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
31
35
|
django_qstash/schedules/admin.py,sha256=ToFkdGfN3smNctTduLQpwxZ5yW20jW4HKRr7x1UilIk,1673
|
|
32
36
|
django_qstash/schedules/apps.py,sha256=zTyTH6xTZPKsEC2MlJcQBDp2ZiRWdMfq2j2jGbpBLH0,360
|
|
33
|
-
django_qstash/schedules/exceptions.py,sha256=
|
|
37
|
+
django_qstash/schedules/exceptions.py,sha256=lrX8kKF5H93FIQNaeZR9mlJBEEmtta6T69CevoYk_xA,295
|
|
34
38
|
django_qstash/schedules/formatters.py,sha256=KMt1457z4Mp0Mob2IxgTBHmye_mRfcLn7Rdf7ThvbEg,1157
|
|
35
|
-
django_qstash/schedules/forms.py,sha256=
|
|
36
|
-
django_qstash/schedules/models.py,sha256=
|
|
39
|
+
django_qstash/schedules/forms.py,sha256=A0TW5xM73Kp7gzieprqaf7leQlqMB7NZFsNn3r_7Jp4,808
|
|
40
|
+
django_qstash/schedules/models.py,sha256=ZcwuIZAn9jC8ZQDfqKTYGjo1C-0WfKf4Mddp_EfoBU4,4476
|
|
37
41
|
django_qstash/schedules/services.py,sha256=U3c2cZgdKGWgpmhRD0j0wT_T43w7K4pXm552M2sTr_4,2186
|
|
38
42
|
django_qstash/schedules/signals.py,sha256=g1aRAbZx-stnvD589mZagR6I27E56064fUyWsxKitR4,696
|
|
39
|
-
django_qstash/schedules/validators.py,sha256=
|
|
43
|
+
django_qstash/schedules/validators.py,sha256=w1dLgf_rAujurcktGPgu1aXejT5BhH5ianKSr7cEYKU,2467
|
|
40
44
|
django_qstash/schedules/migrations/0001_initial.py,sha256=66cA8xnJV3h7QgzCaOiv-Nu3Xl9IdZQPgQKhxyW3bs4,4516
|
|
41
45
|
django_qstash/schedules/migrations/0002_taskschedule_updated_at.py,sha256=6hZO0a9P2ZpOROkk7O5UXBhahghU0QfxZl4E-c3HKGw,459
|
|
42
46
|
django_qstash/schedules/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
43
|
-
django_qstash-0.0.
|
|
44
|
-
django_qstash-0.0.
|
|
45
|
-
django_qstash-0.0.
|
|
46
|
-
django_qstash-0.0.
|
|
47
|
+
django_qstash-0.0.12.dist-info/METADATA,sha256=NIxqfsPXtnoculriqbmMK9d_4PkFwvX-D2ey4U43hY0,19422
|
|
48
|
+
django_qstash-0.0.12.dist-info/WHEEL,sha256=A3WOREP4zgxI0fKrHUG8DC8013e3dK3n7a6HDbcEIwE,91
|
|
49
|
+
django_qstash-0.0.12.dist-info/top_level.txt,sha256=AlV3WSK1A0ZvKuCLsINtIJhJW8zo7SEB-D3_RAjZ0hI,14
|
|
50
|
+
django_qstash-0.0.12.dist-info/RECORD,,
|
|
File without changes
|