django-qstash 0.0.11__tar.gz → 0.0.12__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of django-qstash might be problematic. Click here for more details.

Files changed (63) hide show
  1. {django_qstash-0.0.11 → django_qstash-0.0.12}/PKG-INFO +100 -27
  2. {django_qstash-0.0.11 → django_qstash-0.0.12}/README.md +99 -26
  3. {django_qstash-0.0.11 → django_qstash-0.0.12}/pyproject.toml +1 -1
  4. {django_qstash-0.0.11 → django_qstash-0.0.12}/src/django_qstash/__init__.py +1 -1
  5. {django_qstash-0.0.11 → django_qstash-0.0.12}/src/django_qstash/callbacks.py +6 -0
  6. django_qstash-0.0.12/src/django_qstash/cron.py +21 -0
  7. django_qstash-0.0.12/src/django_qstash/db/models.py +12 -0
  8. {django_qstash-0.0.11 → django_qstash-0.0.12}/src/django_qstash/handlers.py +31 -6
  9. {django_qstash-0.0.11 → django_qstash-0.0.12}/src/django_qstash/results/admin.py +4 -1
  10. django_qstash-0.0.12/src/django_qstash/results/migrations/0002_taskresult_function_path_alter_taskresult_status_and_more.py +46 -0
  11. {django_qstash-0.0.11 → django_qstash-0.0.12}/src/django_qstash/results/models.py +7 -8
  12. {django_qstash-0.0.11 → django_qstash-0.0.12}/src/django_qstash/results/services.py +28 -2
  13. {django_qstash-0.0.11 → django_qstash-0.0.12}/src/django_qstash/results/tasks.py +19 -1
  14. {django_qstash-0.0.11 → django_qstash-0.0.12}/src/django_qstash/schedules/exceptions.py +6 -0
  15. {django_qstash-0.0.11 → django_qstash-0.0.12}/src/django_qstash/schedules/forms.py +7 -0
  16. django_qstash-0.0.12/src/django_qstash/schedules/migrations/__init__.py +0 -0
  17. {django_qstash-0.0.11 → django_qstash-0.0.12}/src/django_qstash/schedules/models.py +2 -0
  18. django_qstash-0.0.12/src/django_qstash/schedules/validators.py +69 -0
  19. {django_qstash-0.0.11 → django_qstash-0.0.12}/src/django_qstash.egg-info/PKG-INFO +100 -27
  20. {django_qstash-0.0.11 → django_qstash-0.0.12}/src/django_qstash.egg-info/SOURCES.txt +4 -0
  21. {django_qstash-0.0.11 → django_qstash-0.0.12}/tests/test_handlers.py +1 -1
  22. django_qstash-0.0.11/src/django_qstash/schedules/validators.py +0 -29
  23. {django_qstash-0.0.11 → django_qstash-0.0.12}/setup.cfg +0 -0
  24. {django_qstash-0.0.11 → django_qstash-0.0.12}/src/django_qstash/app/__init__.py +0 -0
  25. {django_qstash-0.0.11 → django_qstash-0.0.12}/src/django_qstash/app/base.py +0 -0
  26. {django_qstash-0.0.11 → django_qstash-0.0.12}/src/django_qstash/app/decorators.py +0 -0
  27. {django_qstash-0.0.11 → django_qstash-0.0.12}/src/django_qstash/client.py +0 -0
  28. {django_qstash-0.0.11/src/django_qstash/discovery → django_qstash-0.0.12/src/django_qstash/db}/__init__.py +0 -0
  29. {django_qstash-0.0.11/src/django_qstash/management → django_qstash-0.0.12/src/django_qstash/discovery}/__init__.py +0 -0
  30. {django_qstash-0.0.11 → django_qstash-0.0.12}/src/django_qstash/discovery/fields.py +0 -0
  31. {django_qstash-0.0.11 → django_qstash-0.0.12}/src/django_qstash/discovery/models.py +0 -0
  32. {django_qstash-0.0.11 → django_qstash-0.0.12}/src/django_qstash/discovery/utils.py +0 -0
  33. {django_qstash-0.0.11 → django_qstash-0.0.12}/src/django_qstash/discovery/validators.py +0 -0
  34. {django_qstash-0.0.11 → django_qstash-0.0.12}/src/django_qstash/exceptions.py +0 -0
  35. {django_qstash-0.0.11/src/django_qstash/management/commands → django_qstash-0.0.12/src/django_qstash/management}/__init__.py +0 -0
  36. {django_qstash-0.0.11/src/django_qstash/results → django_qstash-0.0.12/src/django_qstash/management/commands}/__init__.py +0 -0
  37. {django_qstash-0.0.11 → django_qstash-0.0.12}/src/django_qstash/management/commands/available_tasks.py +0 -0
  38. {django_qstash-0.0.11 → django_qstash-0.0.12}/src/django_qstash/management/commands/clear_stale_results.py +0 -0
  39. {django_qstash-0.0.11 → django_qstash-0.0.12}/src/django_qstash/management/commands/task_schedules.py +0 -0
  40. {django_qstash-0.0.11/src/django_qstash/results/migrations → django_qstash-0.0.12/src/django_qstash/results}/__init__.py +0 -0
  41. {django_qstash-0.0.11 → django_qstash-0.0.12}/src/django_qstash/results/apps.py +0 -0
  42. {django_qstash-0.0.11 → django_qstash-0.0.12}/src/django_qstash/results/migrations/0001_initial.py +0 -0
  43. {django_qstash-0.0.11/src/django_qstash/schedules → django_qstash-0.0.12/src/django_qstash/results/migrations}/__init__.py +0 -0
  44. {django_qstash-0.0.11/src/django_qstash/schedules/migrations → django_qstash-0.0.12/src/django_qstash/schedules}/__init__.py +0 -0
  45. {django_qstash-0.0.11 → django_qstash-0.0.12}/src/django_qstash/schedules/admin.py +0 -0
  46. {django_qstash-0.0.11 → django_qstash-0.0.12}/src/django_qstash/schedules/apps.py +0 -0
  47. {django_qstash-0.0.11 → django_qstash-0.0.12}/src/django_qstash/schedules/formatters.py +0 -0
  48. {django_qstash-0.0.11 → django_qstash-0.0.12}/src/django_qstash/schedules/migrations/0001_initial.py +0 -0
  49. {django_qstash-0.0.11 → django_qstash-0.0.12}/src/django_qstash/schedules/migrations/0002_taskschedule_updated_at.py +0 -0
  50. {django_qstash-0.0.11 → django_qstash-0.0.12}/src/django_qstash/schedules/services.py +0 -0
  51. {django_qstash-0.0.11 → django_qstash-0.0.12}/src/django_qstash/schedules/signals.py +0 -0
  52. {django_qstash-0.0.11 → django_qstash-0.0.12}/src/django_qstash/settings.py +0 -0
  53. {django_qstash-0.0.11 → django_qstash-0.0.12}/src/django_qstash/utils.py +0 -0
  54. {django_qstash-0.0.11 → django_qstash-0.0.12}/src/django_qstash/views.py +0 -0
  55. {django_qstash-0.0.11 → django_qstash-0.0.12}/src/django_qstash.egg-info/dependency_links.txt +0 -0
  56. {django_qstash-0.0.11 → django_qstash-0.0.12}/src/django_qstash.egg-info/requires.txt +0 -0
  57. {django_qstash-0.0.11 → django_qstash-0.0.12}/src/django_qstash.egg-info/top_level.txt +0 -0
  58. {django_qstash-0.0.11 → django_qstash-0.0.12}/tests/test_callbacks.py +0 -0
  59. {django_qstash-0.0.11 → django_qstash-0.0.12}/tests/test_exceptions.py +0 -0
  60. {django_qstash-0.0.11 → django_qstash-0.0.12}/tests/test_results_models.py +0 -0
  61. {django_qstash-0.0.11 → django_qstash-0.0.12}/tests/test_settings.py +0 -0
  62. {django_qstash-0.0.11 → django_qstash-0.0.12}/tests/test_utils.py +0 -0
  63. {django_qstash-0.0.11 → django_qstash-0.0.12}/tests/test_views.py +0 -0
@@ -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.
@@ -50,18 +50,22 @@ This allows us to:
50
50
  - [Usage](#usage)
51
51
  - [Define a Task](#define-a-task)
52
52
  - [Regular Task Call](#regular-task-call)
53
- - [Async Task](#async-task)
53
+ - [Background Task](#background-task)
54
54
  - [`.delay()`](#delay)
55
55
  - [`.apply_async()`](#apply_async)
56
56
  - [`.apply_async()` With Time Delay](#apply_async-with-time-delay)
57
- - [JSON-ready Arguments](#json-ready-arguments)
57
+ - [Arguments Must be JSON-ready](#arguments-must-be-json-ready)
58
58
  - [Example Task](#example-task)
59
59
  - [Management Commands](#management-commands)
60
- - [Development Usage](#development-usage)
60
+ - [Public Domain In Development](#public-domain-in-development)
61
61
  - [Django Settings Configuration](#django-settings-configuration)
62
+ - [`DJANGO_QSTASH_DOMAIN`](#django_qstash_domain)
63
+ - [`DJANGO_QSTASH_WEBHOOK_PATH`](#django_qstash_webhook_path)
64
+ - [`DJANGO_QSTASH_FORCE_HTTPS`](#django_qstash_force_https)
65
+ - [Example Django Settings](#example-django-settings)
62
66
  - [Schedule Tasks (Optional)](#schedule-tasks-optional)
63
67
  - [Installation](#installation-1)
64
- - [Schedule a Task](#schedule-a-task)
68
+ - [Schedule a Task](#schedule-a-task)
65
69
  - [Store Task Results (Optional)](#store-task-results-optional)
66
70
  - [Clear Stale Results](#clear-stale-results)
67
71
  - [Definitions](#definitions)
@@ -132,7 +136,7 @@ There is a sample project in [sample_project/](sample_project/) that shows how a
132
136
 
133
137
  ## Usage
134
138
 
135
- Django-QStash revolves around the `stashed_task` decorator. The goal is to be a drop-in replacement for Celery's `stashed_task` decorator.
139
+ Django-QStash revolves around the `stashed_task` decorator. The goal is to be a drop-in replacement for Celery's `shared_task` decorator.
136
140
 
137
141
  Here's how it works:
138
142
  - Define a Task
@@ -140,6 +144,8 @@ Here's how it works:
140
144
 
141
145
  ### Define a Task
142
146
  ```python
147
+ # from celery import shared_task
148
+ from django_qstash import shared_task
143
149
  from django_qstash import stashed_task
144
150
 
145
151
 
@@ -149,8 +155,19 @@ def hello_world(name: str, age: int = None, activity: str = None):
149
155
  print(f"Hello {name}! I see you're {activity}.")
150
156
  return
151
157
  print(f"Hello {name}! I see you're {activity} at {age} years old.")
158
+
159
+
160
+ @shared_task
161
+ def hello_world_redux(name: str, age: int = None, activity: str = None):
162
+ if age is None:
163
+ print(f"Hello {name}! I see you're {activity}.")
164
+ return
165
+ print(f"Hello {name}! I see you're {activity} at {age} years old.")
152
166
  ```
153
167
 
168
+ - `hello_world` and `hello_world_redux` work the same with django-qstash.
169
+ - If you use Celery's `@shared_task` instead, Celery would handle only `hello_world_redux` and django-qstash would handle only `hello_world`.
170
+
154
171
  ### Regular Task Call
155
172
  Nothing special here. Just call the function like any other to verify it works.
156
173
 
@@ -159,9 +176,11 @@ Nothing special here. Just call the function like any other to verify it works.
159
176
  hello_world("Tony Stark", age=40, activity="building in a cave with a box of scraps.")
160
177
  ```
161
178
 
162
- ### Async Task
179
+ ### Background Task
180
+
181
+ 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.
163
182
 
164
- Using `.delay()` or `.apply_async()` is how you call an async task. This is modeled after Celery and it works as you'd expect.
183
+ This functionality is modeled after Celery and it works as you'd expect.
165
184
 
166
185
 
167
186
  #### `.delay()`
@@ -194,10 +213,11 @@ hello_world.apply_async(
194
213
  )
195
214
  ```
196
215
 
197
- ### JSON-ready Arguments
216
+ ### Arguments Must be JSON-ready
198
217
 
199
- Each argument needs to be _JSON_ serializable. The way you find out:
218
+ Arguments to django-qstash managed functions must be _JSON_ serializable.
200
219
 
220
+ The way you find out:
201
221
  ```python
202
222
  import json
203
223
 
@@ -208,6 +228,11 @@ data = {
208
228
  print(json.dumps(data))
209
229
  # no errors, you're good to go.
210
230
  ```
231
+ If you have `errors` you'll need to fix them. Here's a few common errors you might see:
232
+
233
+ - Using a Django queryset directly as an argument
234
+ - Using a Django model instance directly as an argument
235
+ - 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")`)
211
236
 
212
237
  ### Example Task
213
238
 
@@ -252,38 +277,83 @@ The `.delay()` method does not support a countdown parameter because it simply p
252
277
 
253
278
  ## Management Commands
254
279
 
255
- - `python manage.py available_tasks` to view all available tasks
280
+ - `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()`).
256
281
 
257
282
  _Requires `django_qstash.schedules` installed._
258
283
  - `python manage.py task_schedules --list` see all schedules relate to the `DJANGO_QSTASH_DOMAIN`
259
284
  - `python manage.py task_schedules --sync` sync schedules based on the `DJANGO_QSTASH_DOMAIN` to store in the Django Admin.
260
285
 
261
- ## Development Usage
286
+ ## Public Domain In Development
262
287
 
263
- django-qstash requires a public domain to work (e.g. `https://djangoqstash.com`). There are many ways to do this, we recommend:
288
+ django-qstash _requires_ a publicly accessible domain to work (e.g. `https://djangoqstash.com`). There are many ways to do this, we recommend:
264
289
 
265
290
  - [Cloudflare Tunnels](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) with a domain name you control.
266
291
  - [ngrok](https://ngrok.com/)
267
292
 
268
293
  Once you have a domain name, you can configure the `DJANGO_QSTASH_DOMAIN` setting in your Django settings.
269
294
 
270
-
271
295
  ## Django Settings Configuration
272
296
 
273
- In Django settings, you can configure the following:
297
+ Various options are available to configure django-qstash.
298
+
299
+ ### `DJANGO_QSTASH_DOMAIN`
300
+ - Required: Yes
301
+ - Default:`None`
302
+ - 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.
274
303
 
275
- - `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.
304
+ ### `DJANGO_QSTASH_WEBHOOK_PATH`
305
+ - Required: Yes
306
+ - Default:`/qstash/webhook/`
307
+ - Description: The path where QStash will send webhooks to your Django application.
276
308
 
277
- - `DJANGO_QSTASH_WEBHOOK_PATH` (default:`/qstash/webhook/`): The path where QStash will send webhooks to your Django application.
309
+ ### `DJANGO_QSTASH_FORCE_HTTPS`
310
+ - Required: No
311
+ - Default: `True`
312
+ - Description: Whether to force HTTPS for the webhook.
278
313
 
279
- - `DJANGO_QSTASH_FORCE_HTTPS` (default:`True`): Whether to force HTTPS for the webhook.
314
+ ###`DJANGO_QSTASH_RESULT_TTL`
315
+ - Required: No
316
+ - Default:`604800`
317
+ - 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).
280
318
 
281
- - `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).
319
+
320
+ ### Example Django Settings
321
+
322
+ 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).
323
+
324
+ Using `os.environ`:
325
+ ```python
326
+ import os
327
+
328
+ ###########################
329
+ # django settings
330
+ ###########################
331
+ DJANGO_DEBUG = str(os.environ.get("DJANGO_DEBUG")) == "1"
332
+ DJANGO_SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY")
333
+ ALLOWED_HOSTS = [os.environ.get("ALLOWED_HOST")]
334
+ CSRF_TRUSTED_ORIGINS = [os.environ.get("CSRF_TRUSTED_ORIGIN")]
335
+ ###########################
336
+ # qstash-py settings
337
+ ###########################
338
+ QSTASH_TOKEN = os.environ.get("QSTASH_TOKEN")
339
+ QSTASH_CURRENT_SIGNING_KEY = os.environ.get("QSTASH_CURRENT_SIGNING_KEY")
340
+ QSTASH_NEXT_SIGNING_KEY = os.environ.get("QSTASH_NEXT_SIGNING_KEY")
341
+
342
+ ###########################
343
+ # django_qstash settings
344
+ ###########################
345
+ DJANGO_QSTASH_DOMAIN = os.environ.get("DJANGO_QSTASH_DOMAIN")
346
+ DJANGO_QSTASH_WEBHOOK_PATH = os.environ.get("DJANGO_QSTASH_WEBHOOK_PATH")
347
+ DJANGO_QSTASH_FORCE_HTTPS = True
348
+ DJANGO_QSTASH_RESULT_TTL = 604800
349
+ ```
282
350
 
283
351
 
284
352
  ## Schedule Tasks (Optional)
285
353
 
286
- 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.
354
+ Run background tasks on a CRON schedule.
355
+
356
+ 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.
287
357
 
288
358
  ### Installation
289
359
 
@@ -303,8 +373,7 @@ Run migrations:
303
373
  python manage.py migrate django_qstash_schedules
304
374
  ```
305
375
 
306
-
307
- ## Schedule a Task
376
+ ### Schedule a Task
308
377
 
309
378
  Tasks must exist before you can schedule them. Review [Define a Task](#define-a-task) for more information.
310
379
 
@@ -312,9 +381,6 @@ Here's how you can schedule a task:
312
381
  - Django Admin (`/admin/django_qstash_schedules/taskschedule/add/`)
313
382
  - Django shell (`python manage.py shell`)
314
383
 
315
-
316
-
317
-
318
384
  ```python
319
385
  from django_qstash.schedules.models import TaskSchedule
320
386
  from django_qstash.discovery.utils import discover_tasks
@@ -343,10 +409,11 @@ TaskSchedule.objects.create(
343
409
  - `cron` is the cron schedule to run the task. Use [contrab.guru](https://crontab.guru/) for writing the cron format.
344
410
 
345
411
 
346
-
347
412
  ## Store Task Results (Optional)
348
413
 
349
- 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.
414
+ Retain the results of background tasks in the database with clear-out functionality.
415
+
416
+ 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`).
350
417
 
351
418
  To install it, just add `django_qstash.results` to your `INSTALLED_APPS` setting.
352
419
 
@@ -364,6 +431,12 @@ Run migrations:
364
431
  python manage.py migrate django_qstash_results
365
432
  ```
366
433
 
434
+ Key configuration:
435
+
436
+ - [DJANGO_QSTASH_WEBHOOK_PATH](#django-settings-configuration)
437
+ - [DJANGO_QSTASH_DOMAIN](#django-settings-configuration)
438
+ - [DJANGO_QSTASH_RESULT_TTL](#django-settings-configuration)
439
+
367
440
  ### Clear Stale Results
368
441
 
369
442
  We recommend purging the `TaskResult` model after a certain amount of time.
@@ -6,7 +6,7 @@ requires = [
6
6
 
7
7
  [project]
8
8
  name = "django-qstash"
9
- version = "0.0.11"
9
+ version = "0.0.12"
10
10
  description = "A drop-in replacement for Celery's shared_task with Upstash QStash."
11
11
  readme = "README.md"
12
12
  license = { file = "LICENSE" }
@@ -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}"
@@ -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]$")
@@ -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"
@@ -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"]