django-qstash 0.0.10__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 +3 -2
- django_qstash/app/__init__.py +2 -1
- django_qstash/app/decorators.py +22 -3
- 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 +21 -3
- 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.10.dist-info → django_qstash-0.0.12.dist-info}/METADATA +129 -36
- {django_qstash-0.0.10.dist-info → django_qstash-0.0.12.dist-info}/RECORD +21 -17
- {django_qstash-0.0.10.dist-info → django_qstash-0.0.12.dist-info}/WHEEL +1 -1
- {django_qstash-0.0.10.dist-info → django_qstash-0.0.12.dist-info}/top_level.txt +0 -0
django_qstash/__init__.py
CHANGED
django_qstash/app/__init__.py
CHANGED
|
@@ -3,5 +3,6 @@ from __future__ import annotations
|
|
|
3
3
|
from .base import AsyncResult
|
|
4
4
|
from .base import QStashTask
|
|
5
5
|
from .decorators import shared_task
|
|
6
|
+
from .decorators import stashed_task
|
|
6
7
|
|
|
7
|
-
__all__ = ["AsyncResult", "QStashTask", "shared_task"]
|
|
8
|
+
__all__ = ["AsyncResult", "QStashTask", "stashed_task", "shared_task"]
|
django_qstash/app/decorators.py
CHANGED
|
@@ -6,24 +6,43 @@ from typing import Callable
|
|
|
6
6
|
from django_qstash.app.base import QStashTask
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
def
|
|
9
|
+
def stashed_task(
|
|
10
10
|
func: Callable | None = None,
|
|
11
11
|
name: str | None = None,
|
|
12
12
|
deduplicated: bool = False,
|
|
13
13
|
**options: dict[str, Any],
|
|
14
14
|
) -> QStashTask:
|
|
15
15
|
"""
|
|
16
|
-
Decorator that mimics Celery's shared_task
|
|
16
|
+
Decorator that mimics Celery's shared_task that maintains
|
|
17
|
+
Celery compatibility.
|
|
17
18
|
|
|
18
19
|
Can be used as:
|
|
20
|
+
|
|
21
|
+
from django_qstash import shared_task
|
|
22
|
+
|
|
19
23
|
@shared_task
|
|
20
24
|
def my_task():
|
|
21
25
|
pass
|
|
22
26
|
|
|
23
|
-
@
|
|
27
|
+
@stashed_task(name="custom_name", deduplicated=True)
|
|
24
28
|
def my_task():
|
|
25
29
|
pass
|
|
26
30
|
"""
|
|
27
31
|
if func is not None:
|
|
28
32
|
return QStashTask(func, name=name, deduplicated=deduplicated, **options)
|
|
29
33
|
return lambda f: QStashTask(f, name=name, deduplicated=deduplicated, **options)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def shared_task(func: Callable | None = None, **options: dict[str, Any]) -> QStashTask:
|
|
37
|
+
"""
|
|
38
|
+
Decorator that is a drop-in replacement for Celery's shared_task.
|
|
39
|
+
|
|
40
|
+
Can be used as:
|
|
41
|
+
|
|
42
|
+
from django_qstash import shared_task
|
|
43
|
+
|
|
44
|
+
@shared_task
|
|
45
|
+
def my_task():
|
|
46
|
+
pass
|
|
47
|
+
"""
|
|
48
|
+
return stashed_task(func, **options)
|
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
|
@@ -7,16 +7,17 @@ from django.apps import apps
|
|
|
7
7
|
from django.conf import settings
|
|
8
8
|
from django.utils import timezone
|
|
9
9
|
|
|
10
|
-
from django_qstash import
|
|
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
|
|
|
14
15
|
logger = logging.getLogger(__name__)
|
|
15
16
|
|
|
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
|
|
@@ -33,16 +33,32 @@ Requires-Dist: requests>=2.30
|
|
|
33
33
|
|
|
34
34
|
_django-qstash_ is a drop-in replacement for Celery's `shared_task`.
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
|
|
37
|
+
## How it works
|
|
38
|
+
|
|
39
|
+
In `tasks.py` in your apps:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from django_qstash import shared_task
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@shared_task
|
|
46
|
+
def my_task():
|
|
47
|
+
pass
|
|
48
|
+
```
|
|
49
|
+
> To use Celery too, you can use `@stashed_task` instead of `@shared_task` more below.
|
|
50
|
+
|
|
51
|
+
To do this we need:
|
|
37
52
|
|
|
38
53
|
- [Upstash QStash](https://upstash.com/docs/qstash/overall/getstarted)
|
|
39
|
-
- A single public _webhook_ to call `@
|
|
54
|
+
- A single public _webhook_ to call `@stashed_task` functions automatically
|
|
40
55
|
|
|
41
56
|
This allows us to:
|
|
42
57
|
|
|
58
|
+
- Nearly identical usage to Celery's `@shared_task` with far less configuration and overhead
|
|
43
59
|
- Focus just on Django
|
|
44
|
-
- Drop Celery
|
|
45
|
-
-
|
|
60
|
+
- Drop Celery completely, scale it down, or use it as normal. django-qstash can work hand-in-hand with Celery
|
|
61
|
+
- Unlock true serverless and scale-to-zero for Django
|
|
46
62
|
- Run background tasks through webhooks
|
|
47
63
|
- Cut costs
|
|
48
64
|
- Trigger GitHub Actions Workflows or GitLab CI/CD pipelines for handling other kinds of background tasks based on our project's code.
|
|
@@ -51,6 +67,7 @@ This allows us to:
|
|
|
51
67
|
## Table of Contents
|
|
52
68
|
|
|
53
69
|
- [django-qstash](#django-qstash)
|
|
70
|
+
- [How it works](#how-it-works)
|
|
54
71
|
- [Table of Contents](#table-of-contents)
|
|
55
72
|
- [Installation](#installation)
|
|
56
73
|
- [Using Pip](#using-pip)
|
|
@@ -62,18 +79,22 @@ This allows us to:
|
|
|
62
79
|
- [Usage](#usage)
|
|
63
80
|
- [Define a Task](#define-a-task)
|
|
64
81
|
- [Regular Task Call](#regular-task-call)
|
|
65
|
-
- [
|
|
82
|
+
- [Background Task](#background-task)
|
|
66
83
|
- [`.delay()`](#delay)
|
|
67
84
|
- [`.apply_async()`](#apply_async)
|
|
68
85
|
- [`.apply_async()` With Time Delay](#apply_async-with-time-delay)
|
|
69
|
-
- [JSON-ready
|
|
86
|
+
- [Arguments Must be JSON-ready](#arguments-must-be-json-ready)
|
|
70
87
|
- [Example Task](#example-task)
|
|
71
88
|
- [Management Commands](#management-commands)
|
|
72
|
-
- [Development
|
|
89
|
+
- [Public Domain In Development](#public-domain-in-development)
|
|
73
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)
|
|
74
95
|
- [Schedule Tasks (Optional)](#schedule-tasks-optional)
|
|
75
96
|
- [Installation](#installation-1)
|
|
76
|
-
|
|
97
|
+
- [Schedule a Task](#schedule-a-task)
|
|
77
98
|
- [Store Task Results (Optional)](#store-task-results-optional)
|
|
78
99
|
- [Clear Stale Results](#clear-stale-results)
|
|
79
100
|
- [Definitions](#definitions)
|
|
@@ -98,9 +119,9 @@ INSTALLED_APPS = [
|
|
|
98
119
|
##...
|
|
99
120
|
]
|
|
100
121
|
```
|
|
101
|
-
- `django_qstash` Includes the `@shared_task`
|
|
122
|
+
- `django_qstash` Includes the `@shared_task` and `@stashed_task` decorators and webhook view
|
|
102
123
|
- `django_qstash.results` (Optional): Store task results in Django DB
|
|
103
|
-
- `django_qstash.schedules` (Optional): Use QStash Schedules to run your `django_qstash` tasks. Out of the box support for _django_qstash_ `@
|
|
124
|
+
- `django_qstash.schedules` (Optional): Use QStash Schedules to run your `django_qstash` tasks. Out of the box support for _django_qstash_ `@stashed_task`. Schedule tasks using _cron_ (e.g. `0 0 * * *`) format which is required based on [QStash Schedules](https://upstash.com/docs/qstash/features/schedules). use [contrab.guru](https://crontab.guru/) for writing the cron format.
|
|
104
125
|
|
|
105
126
|
### Configure Webhook URL
|
|
106
127
|
|
|
@@ -144,7 +165,7 @@ There is a sample project in [sample_project/](sample_project/) that shows how a
|
|
|
144
165
|
|
|
145
166
|
## Usage
|
|
146
167
|
|
|
147
|
-
Django-QStash revolves around the `
|
|
168
|
+
Django-QStash revolves around the `stashed_task` decorator. The goal is to be a drop-in replacement for Celery's `shared_task` decorator.
|
|
148
169
|
|
|
149
170
|
Here's how it works:
|
|
150
171
|
- Define a Task
|
|
@@ -152,17 +173,30 @@ Here's how it works:
|
|
|
152
173
|
|
|
153
174
|
### Define a Task
|
|
154
175
|
```python
|
|
176
|
+
# from celery import shared_task
|
|
155
177
|
from django_qstash import shared_task
|
|
178
|
+
from django_qstash import stashed_task
|
|
156
179
|
|
|
157
180
|
|
|
158
|
-
@
|
|
181
|
+
@stashed_task
|
|
159
182
|
def hello_world(name: str, age: int = None, activity: str = None):
|
|
160
183
|
if age is None:
|
|
161
184
|
print(f"Hello {name}! I see you're {activity}.")
|
|
162
185
|
return
|
|
163
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.")
|
|
164
195
|
```
|
|
165
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
|
+
|
|
166
200
|
### Regular Task Call
|
|
167
201
|
Nothing special here. Just call the function like any other to verify it works.
|
|
168
202
|
|
|
@@ -171,9 +205,11 @@ Nothing special here. Just call the function like any other to verify it works.
|
|
|
171
205
|
hello_world("Tony Stark", age=40, activity="building in a cave with a box of scraps.")
|
|
172
206
|
```
|
|
173
207
|
|
|
174
|
-
###
|
|
208
|
+
### Background Task
|
|
175
209
|
|
|
176
|
-
Using `.delay()` or `.apply_async()` is how you
|
|
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.
|
|
211
|
+
|
|
212
|
+
This functionality is modeled after Celery and it works as you'd expect.
|
|
177
213
|
|
|
178
214
|
|
|
179
215
|
#### `.delay()`
|
|
@@ -206,10 +242,11 @@ hello_world.apply_async(
|
|
|
206
242
|
)
|
|
207
243
|
```
|
|
208
244
|
|
|
209
|
-
### JSON-ready
|
|
245
|
+
### Arguments Must be JSON-ready
|
|
210
246
|
|
|
211
|
-
|
|
247
|
+
Arguments to django-qstash managed functions must be _JSON_ serializable.
|
|
212
248
|
|
|
249
|
+
The way you find out:
|
|
213
250
|
```python
|
|
214
251
|
import json
|
|
215
252
|
|
|
@@ -220,15 +257,23 @@ data = {
|
|
|
220
257
|
print(json.dumps(data))
|
|
221
258
|
# no errors, you're good to go.
|
|
222
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")`)
|
|
223
265
|
|
|
224
266
|
### Example Task
|
|
225
267
|
|
|
226
268
|
```python
|
|
227
269
|
# from celery import shared_task
|
|
228
|
-
|
|
270
|
+
# becomes
|
|
271
|
+
# from django_qstash import shared_task
|
|
272
|
+
# or
|
|
273
|
+
from django_qstash import stashed_task
|
|
229
274
|
|
|
230
275
|
|
|
231
|
-
@
|
|
276
|
+
@stashed_task
|
|
232
277
|
def math_add_task(a, b, save_to_file=False, *args, **kwargs):
|
|
233
278
|
logger.info(f"Adding {a} and {b}")
|
|
234
279
|
if save_to_file:
|
|
@@ -261,38 +306,83 @@ The `.delay()` method does not support a countdown parameter because it simply p
|
|
|
261
306
|
|
|
262
307
|
## Management Commands
|
|
263
308
|
|
|
264
|
-
- `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()`).
|
|
265
310
|
|
|
266
311
|
_Requires `django_qstash.schedules` installed._
|
|
267
312
|
- `python manage.py task_schedules --list` see all schedules relate to the `DJANGO_QSTASH_DOMAIN`
|
|
268
313
|
- `python manage.py task_schedules --sync` sync schedules based on the `DJANGO_QSTASH_DOMAIN` to store in the Django Admin.
|
|
269
314
|
|
|
270
|
-
## Development
|
|
315
|
+
## Public Domain In Development
|
|
271
316
|
|
|
272
|
-
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:
|
|
273
318
|
|
|
274
319
|
- [Cloudflare Tunnels](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) with a domain name you control.
|
|
275
320
|
- [ngrok](https://ngrok.com/)
|
|
276
321
|
|
|
277
322
|
Once you have a domain name, you can configure the `DJANGO_QSTASH_DOMAIN` setting in your Django settings.
|
|
278
323
|
|
|
279
|
-
|
|
280
324
|
## Django Settings Configuration
|
|
281
325
|
|
|
282
|
-
|
|
326
|
+
Various options are available to configure django-qstash.
|
|
283
327
|
|
|
284
|
-
|
|
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.
|
|
285
332
|
|
|
286
|
-
|
|
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.
|
|
287
337
|
|
|
288
|
-
|
|
338
|
+
### `DJANGO_QSTASH_FORCE_HTTPS`
|
|
339
|
+
- Required: No
|
|
340
|
+
- Default: `True`
|
|
341
|
+
- Description: Whether to force HTTPS for the webhook.
|
|
289
342
|
|
|
290
|
-
|
|
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).
|
|
347
|
+
|
|
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
|
+
```
|
|
291
379
|
|
|
292
380
|
|
|
293
381
|
## Schedule Tasks (Optional)
|
|
294
382
|
|
|
295
|
-
|
|
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.
|
|
296
386
|
|
|
297
387
|
### Installation
|
|
298
388
|
|
|
@@ -312,8 +402,7 @@ Run migrations:
|
|
|
312
402
|
python manage.py migrate django_qstash_schedules
|
|
313
403
|
```
|
|
314
404
|
|
|
315
|
-
|
|
316
|
-
## Schedule a Task
|
|
405
|
+
### Schedule a Task
|
|
317
406
|
|
|
318
407
|
Tasks must exist before you can schedule them. Review [Define a Task](#define-a-task) for more information.
|
|
319
408
|
|
|
@@ -321,9 +410,6 @@ Here's how you can schedule a task:
|
|
|
321
410
|
- Django Admin (`/admin/django_qstash_schedules/taskschedule/add/`)
|
|
322
411
|
- Django shell (`python manage.py shell`)
|
|
323
412
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
413
|
```python
|
|
328
414
|
from django_qstash.schedules.models import TaskSchedule
|
|
329
415
|
from django_qstash.discovery.utils import discover_tasks
|
|
@@ -352,10 +438,11 @@ TaskSchedule.objects.create(
|
|
|
352
438
|
- `cron` is the cron schedule to run the task. Use [contrab.guru](https://crontab.guru/) for writing the cron format.
|
|
353
439
|
|
|
354
440
|
|
|
355
|
-
|
|
356
441
|
## Store Task Results (Optional)
|
|
357
442
|
|
|
358
|
-
|
|
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`).
|
|
359
446
|
|
|
360
447
|
To install it, just add `django_qstash.results` to your `INSTALLED_APPS` setting.
|
|
361
448
|
|
|
@@ -373,6 +460,12 @@ Run migrations:
|
|
|
373
460
|
python manage.py migrate django_qstash_results
|
|
374
461
|
```
|
|
375
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
|
+
|
|
376
469
|
### Clear Stale Results
|
|
377
470
|
|
|
378
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
|
-
django_qstash/app/__init__.py,sha256=
|
|
10
|
+
django_qstash/app/__init__.py,sha256=kmoCVoESInzCZ_oGUiPVY4GsFQwBC07cqFJCyn9Loyk,240
|
|
10
11
|
django_qstash/app/base.py,sha256=gM7GIJh_omZcxbmsrwAEadA-N6EuUJbPzh0CflOIVRg,3864
|
|
11
|
-
django_qstash/app/decorators.py,sha256=
|
|
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
|