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 CHANGED
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.0.11"
3
+ __version__ = "0.0.12"
4
4
 
5
5
  from django_qstash.app import shared_task
6
6
  from django_qstash.app import stashed_task
@@ -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 # Initialize payload as 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=request.headers.get("Upstash-Message-Id"),
109
+ task_id=task_id,
107
110
  task_name=payload.task_name,
108
- status="SUCCESS",
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 if payload else None,
135
- }, 500
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",
@@ -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
- list_display = ["task_name", "status", "date_done"]
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"]
@@ -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
+ ]
@@ -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, unique=True, db_index=True)
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
- ("PENDING", "Pending"),
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, task_name, status, result=None, traceback=None, args=None, kwargs=None
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:
@@ -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
+ )
@@ -7,3 +7,9 @@ class InvalidDurationStringValidationError(ValidationError):
7
7
  """Invalid duration string."""
8
8
 
9
9
  pass
10
+
11
+
12
+ class InvalidCronStringValidationError(ValidationError):
13
+ """Invalid cron string."""
14
+
15
+ pass
@@ -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.11
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
- - [Async Task](#async-task)
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 Arguments](#json-ready-arguments)
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 Usage](#development-usage)
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
- - [Schedule a Task](#schedule-a-task)
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 `stashed_task` decorator.
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
- ### Async Task
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
- Using `.delay()` or `.apply_async()` is how you call an async task. This is modeled after Celery and it works as you'd expect.
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 Arguments
245
+ ### Arguments Must be JSON-ready
227
246
 
228
- Each argument needs to be _JSON_ serializable. The way you find out:
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 Usage
315
+ ## Public Domain In Development
291
316
 
292
- django-qstash requires a public domain to work (e.g. `https://djangoqstash.com`). There are many ways to do this, we recommend:
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
- In Django settings, you can configure the following:
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
- - `DJANGO_QSTASH_DOMAIN`: 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.
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
- - `DJANGO_QSTASH_WEBHOOK_PATH` (default:`/qstash/webhook/`): The path where QStash will send webhooks to your Django application.
338
+ ### `DJANGO_QSTASH_FORCE_HTTPS`
339
+ - Required: No
340
+ - Default: `True`
341
+ - Description: Whether to force HTTPS for the webhook.
307
342
 
308
- - `DJANGO_QSTASH_FORCE_HTTPS` (default:`True`): Whether to force HTTPS for the webhook.
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
- - `DJANGO_QSTASH_RESULT_TTL` (default:`604800`): A number of seconds after which task result data can be safely deleted. Defaults to 604800 seconds (7 days or 7 * 24 * 60 * 60).
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
- The `django_qstash.schedules` app schedules tasks using Upstash [QStash Schedules](https://upstash.com/docs/qstash/features/schedules) and the django-qstash `@stashed_task` decorator.
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
- 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 webhooks.
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=IpryDJmo4VtbkyGvYNbMeNbhF7Cg0fvpj5d3SLT7wac,188
2
- django_qstash/callbacks.py,sha256=VG5tGdNzAmUEh7NlpghrxhWvnpRNXZucWmWwxaemw0M,530
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=mmm8TJOqV3j1rQXooNOa128gtmALXFNCAaDZ5xwIcuw,4950
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=q9fn3lfn0gviMfiimYij0wBCYww7FxyrOfGPr1NvntA,434
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=aEiAhGJOuLRtjibUw6xdQqUt3eYKLqY2as4I4QSrF5U,1047
26
- django_qstash/results/services.py,sha256=HvNp5D1tQ__nz4LVUTAGxuyLl_dnlBps4pJ6E9HD2kA,991
27
- django_qstash/results/tasks.py,sha256=5o0Lb2XjWDVQOHPnZnW97stzdydCeq_UZzdE2j2jvEs,2168
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=fvXArrK24ZBGITijuJ_I5xixURmFrJpQ6Dbi2bLNqCQ,195
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=S5Wy2mZha2-th4oAQLjkC9JytfXFNJcL1j18u6rslxI,524
36
- django_qstash/schedules/models.py,sha256=WS4b5ssh07ekiTe5_y4phLKGl7uTXHJBiF4W47vkGec,4357
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=i8IEjnRVk-iysmqvT_kbPYpxTKCQWoX9P1JHcL3zkhI,925
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.11.dist-info/METADATA,sha256=FoYduoYeCoobQqnx2__vsz-yXnYsysOcGRLnIfVAjxE,15971
44
- django_qstash-0.0.11.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
45
- django_qstash-0.0.11.dist-info/top_level.txt,sha256=AlV3WSK1A0ZvKuCLsINtIJhJW8zo7SEB-D3_RAjZ0hI,14
46
- django_qstash-0.0.11.dist-info/RECORD,,
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.6.0)
2
+ Generator: setuptools (75.7.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5