django-qstash 0.0.6__tar.gz → 0.0.8__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.
- {django_qstash-0.0.6 → django_qstash-0.0.8}/PKG-INFO +1 -1
- {django_qstash-0.0.6 → django_qstash-0.0.8}/pyproject.toml +3 -4
- {django_qstash-0.0.6 → django_qstash-0.0.8}/src/django_qstash/__init__.py +2 -2
- django_qstash-0.0.8/src/django_qstash/app/__init__.py +7 -0
- django_qstash-0.0.6/src/django_qstash/tasks.py → django_qstash-0.0.8/src/django_qstash/app/base.py +0 -23
- django_qstash-0.0.8/src/django_qstash/app/decorators.py +29 -0
- {django_qstash-0.0.6 → django_qstash-0.0.8}/src/django_qstash/discovery/utils.py +2 -2
- django_qstash-0.0.8/src/django_qstash/management/commands/clear_stale_results.py +42 -0
- {django_qstash-0.0.6 → django_qstash-0.0.8}/src/django_qstash/management/commands/task_schedules.py +13 -9
- django_qstash-0.0.8/src/django_qstash/results/tasks.py +67 -0
- django_qstash-0.0.8/src/django_qstash/schedules/migrations/__init__.py +0 -0
- {django_qstash-0.0.6 → django_qstash-0.0.8}/src/django_qstash.egg-info/PKG-INFO +1 -1
- {django_qstash-0.0.6 → django_qstash-0.0.8}/src/django_qstash.egg-info/SOURCES.txt +5 -2
- django_qstash-0.0.8/tests/test_settings.py +63 -0
- django_qstash-0.0.6/src/django_qstash/management/commands/clear_stale_results.py +0 -61
- django_qstash-0.0.6/tests/test_settings.py +0 -29
- django_qstash-0.0.6/tests/test_tasks.py +0 -53
- {django_qstash-0.0.6 → django_qstash-0.0.8}/README.md +0 -0
- {django_qstash-0.0.6 → django_qstash-0.0.8}/setup.cfg +0 -0
- {django_qstash-0.0.6 → django_qstash-0.0.8}/src/django_qstash/callbacks.py +0 -0
- {django_qstash-0.0.6 → django_qstash-0.0.8}/src/django_qstash/client.py +0 -0
- {django_qstash-0.0.6 → django_qstash-0.0.8}/src/django_qstash/discovery/__init__.py +0 -0
- {django_qstash-0.0.6 → django_qstash-0.0.8}/src/django_qstash/discovery/fields.py +0 -0
- {django_qstash-0.0.6 → django_qstash-0.0.8}/src/django_qstash/discovery/models.py +0 -0
- {django_qstash-0.0.6 → django_qstash-0.0.8}/src/django_qstash/discovery/validators.py +0 -0
- {django_qstash-0.0.6 → django_qstash-0.0.8}/src/django_qstash/exceptions.py +0 -0
- {django_qstash-0.0.6 → django_qstash-0.0.8}/src/django_qstash/handlers.py +0 -0
- {django_qstash-0.0.6/src/django_qstash/management/commands → django_qstash-0.0.8/src/django_qstash/management}/__init__.py +0 -0
- {django_qstash-0.0.6/src/django_qstash/results → django_qstash-0.0.8/src/django_qstash/management/commands}/__init__.py +0 -0
- {django_qstash-0.0.6/src/django_qstash/results/migrations → django_qstash-0.0.8/src/django_qstash/results}/__init__.py +0 -0
- {django_qstash-0.0.6 → django_qstash-0.0.8}/src/django_qstash/results/admin.py +0 -0
- {django_qstash-0.0.6 → django_qstash-0.0.8}/src/django_qstash/results/apps.py +0 -0
- {django_qstash-0.0.6 → django_qstash-0.0.8}/src/django_qstash/results/migrations/0001_initial.py +0 -0
- {django_qstash-0.0.6/src/django_qstash/schedules → django_qstash-0.0.8/src/django_qstash/results/migrations}/__init__.py +0 -0
- {django_qstash-0.0.6 → django_qstash-0.0.8}/src/django_qstash/results/models.py +0 -0
- {django_qstash-0.0.6 → django_qstash-0.0.8}/src/django_qstash/results/services.py +0 -0
- {django_qstash-0.0.6/src/django_qstash/schedules/migrations → django_qstash-0.0.8/src/django_qstash/schedules}/__init__.py +0 -0
- {django_qstash-0.0.6 → django_qstash-0.0.8}/src/django_qstash/schedules/admin.py +0 -0
- {django_qstash-0.0.6 → django_qstash-0.0.8}/src/django_qstash/schedules/apps.py +0 -0
- {django_qstash-0.0.6 → django_qstash-0.0.8}/src/django_qstash/schedules/exceptions.py +0 -0
- {django_qstash-0.0.6 → django_qstash-0.0.8}/src/django_qstash/schedules/formatters.py +0 -0
- {django_qstash-0.0.6 → django_qstash-0.0.8}/src/django_qstash/schedules/forms.py +0 -0
- {django_qstash-0.0.6 → django_qstash-0.0.8}/src/django_qstash/schedules/migrations/0001_initial.py +0 -0
- {django_qstash-0.0.6 → django_qstash-0.0.8}/src/django_qstash/schedules/migrations/0002_taskschedule_updated_at.py +0 -0
- {django_qstash-0.0.6 → django_qstash-0.0.8}/src/django_qstash/schedules/models.py +0 -0
- {django_qstash-0.0.6 → django_qstash-0.0.8}/src/django_qstash/schedules/services.py +0 -0
- {django_qstash-0.0.6 → django_qstash-0.0.8}/src/django_qstash/schedules/signals.py +0 -0
- {django_qstash-0.0.6 → django_qstash-0.0.8}/src/django_qstash/schedules/validators.py +0 -0
- {django_qstash-0.0.6 → django_qstash-0.0.8}/src/django_qstash/settings.py +0 -0
- {django_qstash-0.0.6 → django_qstash-0.0.8}/src/django_qstash/utils.py +0 -0
- {django_qstash-0.0.6 → django_qstash-0.0.8}/src/django_qstash/views.py +0 -0
- {django_qstash-0.0.6 → django_qstash-0.0.8}/src/django_qstash.egg-info/dependency_links.txt +0 -0
- {django_qstash-0.0.6 → django_qstash-0.0.8}/src/django_qstash.egg-info/requires.txt +0 -0
- {django_qstash-0.0.6 → django_qstash-0.0.8}/src/django_qstash.egg-info/top_level.txt +0 -0
- {django_qstash-0.0.6 → django_qstash-0.0.8}/tests/test_callbacks.py +0 -0
- {django_qstash-0.0.6 → django_qstash-0.0.8}/tests/test_exceptions.py +0 -0
- {django_qstash-0.0.6 → django_qstash-0.0.8}/tests/test_handlers.py +0 -0
- {django_qstash-0.0.6 → django_qstash-0.0.8}/tests/test_results_models.py +0 -0
- {django_qstash-0.0.6 → django_qstash-0.0.8}/tests/test_utils.py +0 -0
- {django_qstash-0.0.6 → django_qstash-0.0.8}/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.
|
|
3
|
+
Version: 0.0.8
|
|
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
|
|
@@ -6,7 +6,7 @@ requires = [
|
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "django-qstash"
|
|
9
|
-
version = "0.0.
|
|
9
|
+
version = "0.0.8"
|
|
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" }
|
|
@@ -70,7 +70,6 @@ branch = true
|
|
|
70
70
|
parallel = true
|
|
71
71
|
source = [
|
|
72
72
|
"django_qstash",
|
|
73
|
-
"tests",
|
|
74
73
|
]
|
|
75
74
|
omit = [
|
|
76
75
|
"*/migrations/*",
|
|
@@ -80,8 +79,8 @@ omit = [
|
|
|
80
79
|
|
|
81
80
|
[tool.coverage.paths]
|
|
82
81
|
source = [
|
|
83
|
-
"src",
|
|
84
|
-
".tox
|
|
82
|
+
"src/django_qstash",
|
|
83
|
+
".tox/*/lib/python*/site-packages/django_qstash",
|
|
85
84
|
]
|
|
86
85
|
|
|
87
86
|
[tool.coverage.report]
|
django_qstash-0.0.6/src/django_qstash/tasks.py → django_qstash-0.0.8/src/django_qstash/app/base.py
RENAMED
|
@@ -118,26 +118,3 @@ class AsyncResult:
|
|
|
118
118
|
@property
|
|
119
119
|
def id(self) -> str:
|
|
120
120
|
return self.task_id
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
def shared_task(
|
|
124
|
-
func: Callable | None = None,
|
|
125
|
-
name: str | None = None,
|
|
126
|
-
deduplicated: bool = False,
|
|
127
|
-
**options: dict[str, Any],
|
|
128
|
-
) -> QStashTask:
|
|
129
|
-
"""
|
|
130
|
-
Decorator that mimics Celery's shared_task
|
|
131
|
-
|
|
132
|
-
Can be used as:
|
|
133
|
-
@shared_task
|
|
134
|
-
def my_task():
|
|
135
|
-
pass
|
|
136
|
-
|
|
137
|
-
@shared_task(name="custom_name", deduplicated=True)
|
|
138
|
-
def my_task():
|
|
139
|
-
pass
|
|
140
|
-
"""
|
|
141
|
-
if func is not None:
|
|
142
|
-
return QStashTask(func, name=name, deduplicated=deduplicated, **options)
|
|
143
|
-
return lambda f: QStashTask(f, name=name, deduplicated=deduplicated, **options)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
from typing import Callable
|
|
5
|
+
|
|
6
|
+
from django_qstash.app.base import QStashTask
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def shared_task(
|
|
10
|
+
func: Callable | None = None,
|
|
11
|
+
name: str | None = None,
|
|
12
|
+
deduplicated: bool = False,
|
|
13
|
+
**options: dict[str, Any],
|
|
14
|
+
) -> QStashTask:
|
|
15
|
+
"""
|
|
16
|
+
Decorator that mimics Celery's shared_task
|
|
17
|
+
|
|
18
|
+
Can be used as:
|
|
19
|
+
@shared_task
|
|
20
|
+
def my_task():
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
@shared_task(name="custom_name", deduplicated=True)
|
|
24
|
+
def my_task():
|
|
25
|
+
pass
|
|
26
|
+
"""
|
|
27
|
+
if func is not None:
|
|
28
|
+
return QStashTask(func, name=name, deduplicated=deduplicated, **options)
|
|
29
|
+
return lambda f: QStashTask(f, name=name, deduplicated=deduplicated, **options)
|
|
@@ -32,7 +32,7 @@ def discover_tasks() -> list[tuple[str, str]]:
|
|
|
32
32
|
('other_app.tasks.custom_task', 'special_name')
|
|
33
33
|
]
|
|
34
34
|
"""
|
|
35
|
-
from django_qstash.
|
|
35
|
+
from django_qstash.app import QStashTask
|
|
36
36
|
|
|
37
37
|
discovered_tasks = []
|
|
38
38
|
packages = []
|
|
@@ -71,7 +71,7 @@ def discover_tasks() -> list[tuple[str, str]]:
|
|
|
71
71
|
if attr.name == attr_name:
|
|
72
72
|
label = value
|
|
73
73
|
else:
|
|
74
|
-
label = f"{attr.name} ({
|
|
74
|
+
label = f"{attr.name} ({package}.tasks)"
|
|
75
75
|
discovered_tasks.append((value, label))
|
|
76
76
|
except Exception as e:
|
|
77
77
|
warnings.warn(
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from django.conf import settings
|
|
4
|
+
from django.core.management.base import BaseCommand
|
|
5
|
+
|
|
6
|
+
from django_qstash.results.tasks import clear_stale_results_task
|
|
7
|
+
|
|
8
|
+
DJANGO_QSTASH_RESULT_TTL = getattr(settings, "DJANGO_QSTASH_RESULT_TTL", 604800)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Command(BaseCommand):
|
|
12
|
+
help = f"""Clears stale task results older than\n
|
|
13
|
+
{DJANGO_QSTASH_RESULT_TTL} seconds (settings.DJANGO_QSTASH_RESULT_TTL)"""
|
|
14
|
+
|
|
15
|
+
def add_arguments(self, parser):
|
|
16
|
+
parser.add_argument(
|
|
17
|
+
"--no-input",
|
|
18
|
+
action="store_true",
|
|
19
|
+
help="Do not ask for confirmation",
|
|
20
|
+
)
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"--since",
|
|
23
|
+
type=int,
|
|
24
|
+
help="The number of seconds ago to clear results for",
|
|
25
|
+
)
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
"--delay", action="store_true", help="Offload request using django_qstash"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def handle(self, *args, **options):
|
|
31
|
+
delay = options["delay"]
|
|
32
|
+
no_input = options["no_input"]
|
|
33
|
+
since = options.get("since") or DJANGO_QSTASH_RESULT_TTL
|
|
34
|
+
user_confirm = not no_input
|
|
35
|
+
if not delay:
|
|
36
|
+
clear_stale_results_task(
|
|
37
|
+
since=since, user_confirm=user_confirm, stdout=self.stdout
|
|
38
|
+
)
|
|
39
|
+
else:
|
|
40
|
+
clear_stale_results_task.delay(
|
|
41
|
+
since=since, user_confirm=user_confirm, stdout=self.stdout
|
|
42
|
+
)
|
{django_qstash-0.0.6 → django_qstash-0.0.8}/src/django_qstash/management/commands/task_schedules.py
RENAMED
|
@@ -7,7 +7,8 @@ from django.apps import apps
|
|
|
7
7
|
from django.core.management.base import BaseCommand
|
|
8
8
|
from django.db import models
|
|
9
9
|
|
|
10
|
-
from django_qstash.
|
|
10
|
+
from django_qstash.callbacks import get_callback_url
|
|
11
|
+
from django_qstash.client import qstash_client
|
|
11
12
|
|
|
12
13
|
logger = logging.getLogger(__name__)
|
|
13
14
|
|
|
@@ -28,6 +29,11 @@ class Command(BaseCommand):
|
|
|
28
29
|
action="store_true",
|
|
29
30
|
help="Sync schedules from QStash to local database",
|
|
30
31
|
)
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"--no-input",
|
|
34
|
+
action="store_true",
|
|
35
|
+
help="Do not ask for confirmation",
|
|
36
|
+
)
|
|
31
37
|
|
|
32
38
|
def get_task_schedule_model(self) -> models.Model | None:
|
|
33
39
|
"""Get the TaskSchedule model if available."""
|
|
@@ -40,13 +46,10 @@ class Command(BaseCommand):
|
|
|
40
46
|
"Add `django_qstash.schedules` to INSTALLED_APPS and run migrations."
|
|
41
47
|
)
|
|
42
48
|
)
|
|
43
|
-
return None
|
|
44
49
|
|
|
45
50
|
def sync_schedules(self, schedules: list) -> None:
|
|
46
51
|
"""Sync remote schedules to local database."""
|
|
47
52
|
TaskSchedule = self.get_task_schedule_model()
|
|
48
|
-
if not TaskSchedule:
|
|
49
|
-
return
|
|
50
53
|
|
|
51
54
|
for schedule in schedules:
|
|
52
55
|
try:
|
|
@@ -72,6 +75,7 @@ class Command(BaseCommand):
|
|
|
72
75
|
logger.exception("Failed to sync schedule %s", schedule.schedule_id)
|
|
73
76
|
|
|
74
77
|
def handle(self, *args, **options) -> None:
|
|
78
|
+
auto_confirm = options.get("no_input")
|
|
75
79
|
if not (options.get("sync") or options.get("list")):
|
|
76
80
|
self.stdout.write(
|
|
77
81
|
self.style.ERROR("Please specify either --list or --sync option")
|
|
@@ -79,9 +83,8 @@ class Command(BaseCommand):
|
|
|
79
83
|
return
|
|
80
84
|
|
|
81
85
|
try:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
schedules = client.list_schedules()
|
|
86
|
+
destination = get_callback_url()
|
|
87
|
+
schedules = qstash_client.schedule.list()
|
|
85
88
|
|
|
86
89
|
self.stdout.write(
|
|
87
90
|
self.style.SUCCESS(
|
|
@@ -105,8 +108,9 @@ class Command(BaseCommand):
|
|
|
105
108
|
|
|
106
109
|
if options.get("sync"):
|
|
107
110
|
user_input = input("Do you want to sync remote schedules? (y/n): ")
|
|
108
|
-
if user_input.lower() == "y":
|
|
111
|
+
if user_input.lower() == "y" or auto_confirm:
|
|
109
112
|
self.sync_schedules(schedules)
|
|
110
|
-
|
|
113
|
+
else:
|
|
114
|
+
self.stdout.write(self.style.ERROR("Sync cancelled"))
|
|
111
115
|
except Exception as e:
|
|
112
116
|
self.stdout.write(self.style.ERROR(f"An error occurred: {str(e)}"))
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from datetime import timedelta
|
|
5
|
+
|
|
6
|
+
from django.apps import apps
|
|
7
|
+
from django.conf import settings
|
|
8
|
+
from django.utils import timezone
|
|
9
|
+
|
|
10
|
+
from django_qstash import shared_task
|
|
11
|
+
|
|
12
|
+
DJANGO_QSTASH_RESULT_TTL = getattr(settings, "DJANGO_QSTASH_RESULT_TTL", 604800)
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@shared_task(name="Cleanup Task Results")
|
|
18
|
+
def clear_stale_results_task(
|
|
19
|
+
since=None, stdout=None, user_confirm=False, *args, **options
|
|
20
|
+
):
|
|
21
|
+
delta_seconds = since or DJANGO_QSTASH_RESULT_TTL
|
|
22
|
+
cutoff_date = timezone.now() - timedelta(seconds=delta_seconds)
|
|
23
|
+
TaskResult = None
|
|
24
|
+
try:
|
|
25
|
+
TaskResult = apps.get_model("django_qstash_results", "TaskResult")
|
|
26
|
+
except LookupError as e:
|
|
27
|
+
msg = "Django QStash Results not installed.\nAdd `django_qstash.results` to INSTALLED_APPS and run migrations."
|
|
28
|
+
if stdout is not None:
|
|
29
|
+
stdout.write(msg)
|
|
30
|
+
logger.exception(msg)
|
|
31
|
+
raise e
|
|
32
|
+
qs_to_delete = TaskResult.objects.filter(date_done__lt=cutoff_date)
|
|
33
|
+
|
|
34
|
+
if user_confirm:
|
|
35
|
+
user_input = input("Are you sure? (Y/n): ")
|
|
36
|
+
if f"{user_input}".lower() != "y":
|
|
37
|
+
msg = "Skipping deletion"
|
|
38
|
+
if stdout is not None:
|
|
39
|
+
stdout.write(msg)
|
|
40
|
+
logger.info(msg)
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
if not qs_to_delete.exists():
|
|
44
|
+
msg = "No stale Django QStash task results found"
|
|
45
|
+
if stdout is not None:
|
|
46
|
+
stdout.write(msg)
|
|
47
|
+
else:
|
|
48
|
+
logger.info(msg)
|
|
49
|
+
return
|
|
50
|
+
delete_msg = f"Deleting {qs_to_delete.count()} task results older than {cutoff_date} ({DJANGO_QSTASH_RESULT_TTL} seconds)"
|
|
51
|
+
if stdout is not None:
|
|
52
|
+
stdout.write(delete_msg)
|
|
53
|
+
else:
|
|
54
|
+
logger.info(delete_msg)
|
|
55
|
+
try:
|
|
56
|
+
deleted_count, _ = qs_to_delete.delete()
|
|
57
|
+
msg = f"Successfully deleted {deleted_count} stale results."
|
|
58
|
+
if stdout is not None:
|
|
59
|
+
stdout.write(msg)
|
|
60
|
+
else:
|
|
61
|
+
logger.info(msg)
|
|
62
|
+
except Exception as e:
|
|
63
|
+
msg = f"Error deleting stale results: {e}"
|
|
64
|
+
if stdout is not None:
|
|
65
|
+
stdout.write(msg)
|
|
66
|
+
logger.exception(msg)
|
|
67
|
+
raise e
|
|
File without changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: django-qstash
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.8
|
|
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
|
|
@@ -6,7 +6,6 @@ src/django_qstash/client.py
|
|
|
6
6
|
src/django_qstash/exceptions.py
|
|
7
7
|
src/django_qstash/handlers.py
|
|
8
8
|
src/django_qstash/settings.py
|
|
9
|
-
src/django_qstash/tasks.py
|
|
10
9
|
src/django_qstash/utils.py
|
|
11
10
|
src/django_qstash/views.py
|
|
12
11
|
src/django_qstash.egg-info/PKG-INFO
|
|
@@ -14,11 +13,15 @@ src/django_qstash.egg-info/SOURCES.txt
|
|
|
14
13
|
src/django_qstash.egg-info/dependency_links.txt
|
|
15
14
|
src/django_qstash.egg-info/requires.txt
|
|
16
15
|
src/django_qstash.egg-info/top_level.txt
|
|
16
|
+
src/django_qstash/app/__init__.py
|
|
17
|
+
src/django_qstash/app/base.py
|
|
18
|
+
src/django_qstash/app/decorators.py
|
|
17
19
|
src/django_qstash/discovery/__init__.py
|
|
18
20
|
src/django_qstash/discovery/fields.py
|
|
19
21
|
src/django_qstash/discovery/models.py
|
|
20
22
|
src/django_qstash/discovery/utils.py
|
|
21
23
|
src/django_qstash/discovery/validators.py
|
|
24
|
+
src/django_qstash/management/__init__.py
|
|
22
25
|
src/django_qstash/management/commands/__init__.py
|
|
23
26
|
src/django_qstash/management/commands/clear_stale_results.py
|
|
24
27
|
src/django_qstash/management/commands/task_schedules.py
|
|
@@ -27,6 +30,7 @@ src/django_qstash/results/admin.py
|
|
|
27
30
|
src/django_qstash/results/apps.py
|
|
28
31
|
src/django_qstash/results/models.py
|
|
29
32
|
src/django_qstash/results/services.py
|
|
33
|
+
src/django_qstash/results/tasks.py
|
|
30
34
|
src/django_qstash/results/migrations/0001_initial.py
|
|
31
35
|
src/django_qstash/results/migrations/__init__.py
|
|
32
36
|
src/django_qstash/schedules/__init__.py
|
|
@@ -47,6 +51,5 @@ tests/test_exceptions.py
|
|
|
47
51
|
tests/test_handlers.py
|
|
48
52
|
tests/test_results_models.py
|
|
49
53
|
tests/test_settings.py
|
|
50
|
-
tests/test_tasks.py
|
|
51
54
|
tests/test_utils.py
|
|
52
55
|
tests/test_views.py
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import warnings
|
|
4
|
+
|
|
5
|
+
from django.test import TestCase
|
|
6
|
+
from django.test import override_settings
|
|
7
|
+
|
|
8
|
+
from django_qstash.settings import DJANGO_QSTASH_WEBHOOK_PATH
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SettingsTestCase(TestCase):
|
|
12
|
+
def test_default_webhook_path(self):
|
|
13
|
+
"""Test that default webhook path is set correctly"""
|
|
14
|
+
self.assertEqual(DJANGO_QSTASH_WEBHOOK_PATH, "/qstash/webhook/")
|
|
15
|
+
|
|
16
|
+
def test_warning_when_required_settings_missing(self):
|
|
17
|
+
"""Test that warning is raised when required settings are missing"""
|
|
18
|
+
with override_settings(QSTASH_TOKEN=None, DJANGO_QSTASH_DOMAIN=None):
|
|
19
|
+
with warnings.catch_warnings(record=True) as w:
|
|
20
|
+
warnings.simplefilter("always")
|
|
21
|
+
|
|
22
|
+
# Force reload of settings to trigger warning
|
|
23
|
+
from importlib import reload
|
|
24
|
+
|
|
25
|
+
import django_qstash.settings
|
|
26
|
+
|
|
27
|
+
reload(django_qstash.settings)
|
|
28
|
+
|
|
29
|
+
self.assertEqual(len(w), 1)
|
|
30
|
+
self.assertTrue(issubclass(w[0].category, RuntimeWarning))
|
|
31
|
+
self.assertIn(
|
|
32
|
+
"QSTASH_TOKEN and DJANGO_QSTASH_DOMAIN should be set",
|
|
33
|
+
str(w[0].message),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
@override_settings(QSTASH_TOKEN="test-token", DJANGO_QSTASH_DOMAIN="example.com")
|
|
37
|
+
def test_no_warning_when_settings_present(self):
|
|
38
|
+
"""Test that no warning is raised when required settings are present"""
|
|
39
|
+
with warnings.catch_warnings(record=True) as w:
|
|
40
|
+
warnings.simplefilter("always")
|
|
41
|
+
|
|
42
|
+
# Force reload of settings
|
|
43
|
+
from importlib import reload
|
|
44
|
+
|
|
45
|
+
import django_qstash.settings
|
|
46
|
+
|
|
47
|
+
reload(django_qstash.settings)
|
|
48
|
+
|
|
49
|
+
self.assertEqual(len(w), 0)
|
|
50
|
+
|
|
51
|
+
@override_settings(DJANGO_QSTASH_WEBHOOK_PATH="/custom/webhook/path/")
|
|
52
|
+
def test_custom_webhook_path(self):
|
|
53
|
+
"""Test that custom webhook path can be set"""
|
|
54
|
+
# Force reload of settings to get new webhook path
|
|
55
|
+
from importlib import reload
|
|
56
|
+
|
|
57
|
+
import django_qstash.settings
|
|
58
|
+
|
|
59
|
+
reload(django_qstash.settings)
|
|
60
|
+
|
|
61
|
+
self.assertEqual(
|
|
62
|
+
django_qstash.settings.DJANGO_QSTASH_WEBHOOK_PATH, "/custom/webhook/path/"
|
|
63
|
+
)
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from datetime import timedelta
|
|
4
|
-
|
|
5
|
-
from django.apps import apps
|
|
6
|
-
from django.conf import settings
|
|
7
|
-
from django.core.management.base import BaseCommand
|
|
8
|
-
from django.utils import timezone
|
|
9
|
-
|
|
10
|
-
DJANGO_QSTASH_RESULT_TTL = getattr(settings, "DJANGO_QSTASH_RESULT_TTL", 604800)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class Command(BaseCommand):
|
|
14
|
-
help = f"""Clears stale task results older than\n
|
|
15
|
-
{DJANGO_QSTASH_RESULT_TTL} seconds (settings.DJANGO_QSTASH_RESULT_TTL)"""
|
|
16
|
-
|
|
17
|
-
def add_arguments(self, parser):
|
|
18
|
-
parser.add_argument(
|
|
19
|
-
"--no-input",
|
|
20
|
-
action="store_true",
|
|
21
|
-
help="Do not ask for confirmation",
|
|
22
|
-
)
|
|
23
|
-
parser.add_argument(
|
|
24
|
-
"--since",
|
|
25
|
-
type=int,
|
|
26
|
-
help="The number of seconds ago to clear results for",
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
def handle(self, *args, **options):
|
|
30
|
-
no_input = options["no_input"]
|
|
31
|
-
since = options.get("since") or DJANGO_QSTASH_RESULT_TTL
|
|
32
|
-
cutoff_date = timezone.now() - timedelta(seconds=since)
|
|
33
|
-
try:
|
|
34
|
-
TaskResult = apps.get_model("django_qstash_results", "TaskResult")
|
|
35
|
-
except LookupError:
|
|
36
|
-
self.stdout.write(
|
|
37
|
-
self.style.ERROR(
|
|
38
|
-
"Django QStash Results not installed.\nAdd `django_qstash.results` to INSTALLED_APPS and run migrations."
|
|
39
|
-
)
|
|
40
|
-
)
|
|
41
|
-
return
|
|
42
|
-
to_delete = TaskResult.objects.filter(date_done__lt=cutoff_date)
|
|
43
|
-
|
|
44
|
-
if not to_delete.exists():
|
|
45
|
-
self.stdout.write("No stale Django QStash task results found")
|
|
46
|
-
return
|
|
47
|
-
|
|
48
|
-
# use input to confirm deletion
|
|
49
|
-
self.stdout.write(
|
|
50
|
-
f"Deleting {to_delete.count()} task results older than {cutoff_date} ({DJANGO_QSTASH_RESULT_TTL} seconds)"
|
|
51
|
-
)
|
|
52
|
-
if not no_input:
|
|
53
|
-
if input("Are you sure? (y/n): ") != "y":
|
|
54
|
-
self.stdout.write("Skipping deletion")
|
|
55
|
-
return
|
|
56
|
-
|
|
57
|
-
deleted_count, _ = to_delete.delete()
|
|
58
|
-
|
|
59
|
-
self.stdout.write(
|
|
60
|
-
self.style.SUCCESS(f"Successfully deleted {deleted_count} stale results.")
|
|
61
|
-
)
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
# from django.conf import settings
|
|
2
|
-
# from django.test import TestCase
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
from __future__ import annotations
|
|
6
|
-
|
|
7
|
-
# class SettingsTestCase(TestCase):
|
|
8
|
-
# def test_required_settings_present(self):
|
|
9
|
-
# """Test that all required QStash settings are present"""
|
|
10
|
-
# self.assertTrue(hasattr(settings, "QSTASH_TOKEN"))
|
|
11
|
-
# self.assertTrue(hasattr(settings, "DJANGO_QSTASH_DOMAIN"))
|
|
12
|
-
# self.assertTrue(hasattr(settings, "QSTASH_CURRENT_SIGNING_KEY"))
|
|
13
|
-
# self.assertTrue(hasattr(settings, "QSTASH_NEXT_SIGNING_KEY"))
|
|
14
|
-
# def test_settings_values(self):
|
|
15
|
-
# """Test that settings have expected test values"""
|
|
16
|
-
# self.assertEqual(settings.QSTASH_TOKEN, "test-token")
|
|
17
|
-
# self.assertEqual(settings.DJANGO_QSTASH_DOMAIN, "example.com")
|
|
18
|
-
# self.assertEqual(settings.QSTASH_CURRENT_SIGNING_KEY, "current-key")
|
|
19
|
-
# self.assertEqual(settings.QSTASH_NEXT_SIGNING_KEY, "next-key")
|
|
20
|
-
|
|
21
|
-
# def test_required_apps_installed(self):
|
|
22
|
-
# """Test that required apps are in INSTALLED_APPS"""
|
|
23
|
-
# required_apps = [
|
|
24
|
-
# "django_qstash",
|
|
25
|
-
# "django_qstash.results",
|
|
26
|
-
# "django_qstash.schedules",
|
|
27
|
-
# ]
|
|
28
|
-
# for app in required_apps:
|
|
29
|
-
# self.assertIn(app, settings.INSTALLED_APPS)
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
from unittest.mock import Mock, patch
|
|
2
|
-
|
|
3
|
-
import pytest
|
|
4
|
-
|
|
5
|
-
from django_qstash.tasks import shared_task
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
@shared_task
|
|
9
|
-
def sample_task(x, y):
|
|
10
|
-
return x + y
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
@shared_task(name="custom_task", deduplicated=True)
|
|
14
|
-
def sample_task_with_options(x, y):
|
|
15
|
-
return x * y
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
@pytest.mark.django_db
|
|
19
|
-
class TestQStashTasks:
|
|
20
|
-
def test_basic_task_execution(self):
|
|
21
|
-
"""Test that tasks can be executed directly"""
|
|
22
|
-
result = sample_task(2, 3)
|
|
23
|
-
assert result == 5
|
|
24
|
-
|
|
25
|
-
def test_task_with_options(self):
|
|
26
|
-
"""Test that tasks with custom options work"""
|
|
27
|
-
result = sample_task_with_options(4, 5)
|
|
28
|
-
assert result == 20
|
|
29
|
-
|
|
30
|
-
@patch("django_qstash.tasks.qstash_client")
|
|
31
|
-
def test_task_delay(self, mock_client):
|
|
32
|
-
"""Test that delay() sends task to QStash"""
|
|
33
|
-
mock_response = Mock()
|
|
34
|
-
mock_response.message_id = "test-id-123"
|
|
35
|
-
mock_client.message.publish_json.return_value = mock_response
|
|
36
|
-
|
|
37
|
-
result = sample_task.delay(2, 3)
|
|
38
|
-
|
|
39
|
-
assert result.task_id == "test-id-123"
|
|
40
|
-
mock_client.message.publish_json.assert_called_once()
|
|
41
|
-
|
|
42
|
-
@patch("django_qstash.tasks.qstash_client")
|
|
43
|
-
def test_task_apply_async(self, mock_client):
|
|
44
|
-
"""Test that apply_async() works with countdown"""
|
|
45
|
-
mock_response = Mock()
|
|
46
|
-
mock_response.message_id = "test-id-456"
|
|
47
|
-
mock_client.message.publish_json.return_value = mock_response
|
|
48
|
-
|
|
49
|
-
result = sample_task.apply_async(args=(2, 3), countdown=60)
|
|
50
|
-
|
|
51
|
-
assert result.task_id == "test-id-456"
|
|
52
|
-
call_kwargs = mock_client.message.publish_json.call_args[1]
|
|
53
|
-
assert call_kwargs["delay"] == "60s"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_qstash-0.0.6 → django_qstash-0.0.8}/src/django_qstash/results/migrations/0001_initial.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_qstash-0.0.6 → django_qstash-0.0.8}/src/django_qstash/schedules/migrations/0001_initial.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|