django-qstash 0.0.5__tar.gz → 0.0.7__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 (56) hide show
  1. {django_qstash-0.0.5 → django_qstash-0.0.7}/PKG-INFO +1 -1
  2. {django_qstash-0.0.5 → django_qstash-0.0.7}/pyproject.toml +3 -1
  3. {django_qstash-0.0.5 → django_qstash-0.0.7}/src/django_qstash/__init__.py +1 -1
  4. django_qstash-0.0.7/src/django_qstash/callbacks.py +15 -0
  5. django_qstash-0.0.7/src/django_qstash/client.py +7 -0
  6. django_qstash-0.0.7/src/django_qstash/discovery/fields.py +38 -0
  7. django_qstash-0.0.7/src/django_qstash/discovery/models.py +25 -0
  8. django_qstash-0.0.7/src/django_qstash/discovery/utils.py +93 -0
  9. django_qstash-0.0.7/src/django_qstash/discovery/validators.py +24 -0
  10. django_qstash-0.0.7/src/django_qstash/management/commands/task_schedules.py +116 -0
  11. django_qstash-0.0.7/src/django_qstash/results/__init__.py +0 -0
  12. django_qstash-0.0.7/src/django_qstash/results/migrations/__init__.py +0 -0
  13. django_qstash-0.0.7/src/django_qstash/schedules/__init__.py +0 -0
  14. django_qstash-0.0.7/src/django_qstash/schedules/admin.py +66 -0
  15. django_qstash-0.0.7/src/django_qstash/schedules/apps.py +13 -0
  16. django_qstash-0.0.7/src/django_qstash/schedules/exceptions.py +9 -0
  17. django_qstash-0.0.7/src/django_qstash/schedules/formatters.py +37 -0
  18. django_qstash-0.0.7/src/django_qstash/schedules/forms.py +24 -0
  19. django_qstash-0.0.7/src/django_qstash/schedules/migrations/0001_initial.py +122 -0
  20. django_qstash-0.0.7/src/django_qstash/schedules/migrations/0002_taskschedule_updated_at.py +20 -0
  21. django_qstash-0.0.7/src/django_qstash/schedules/migrations/__init__.py +0 -0
  22. django_qstash-0.0.7/src/django_qstash/schedules/models.py +120 -0
  23. django_qstash-0.0.7/src/django_qstash/schedules/services.py +64 -0
  24. django_qstash-0.0.7/src/django_qstash/schedules/signals.py +21 -0
  25. django_qstash-0.0.7/src/django_qstash/schedules/validators.py +29 -0
  26. django_qstash-0.0.7/src/django_qstash/settings.py +17 -0
  27. {django_qstash-0.0.5 → django_qstash-0.0.7}/src/django_qstash/tasks.py +5 -25
  28. {django_qstash-0.0.5 → django_qstash-0.0.7}/src/django_qstash.egg-info/PKG-INFO +1 -1
  29. django_qstash-0.0.7/src/django_qstash.egg-info/SOURCES.txt +53 -0
  30. django_qstash-0.0.7/tests/test_callbacks.py +62 -0
  31. django_qstash-0.0.7/tests/test_settings.py +63 -0
  32. django_qstash-0.0.5/src/django_qstash.egg-info/SOURCES.txt +0 -28
  33. {django_qstash-0.0.5 → django_qstash-0.0.7}/README.md +0 -0
  34. {django_qstash-0.0.5 → django_qstash-0.0.7}/setup.cfg +0 -0
  35. {django_qstash-0.0.5/src/django_qstash/management/commands → django_qstash-0.0.7/src/django_qstash/discovery}/__init__.py +0 -0
  36. {django_qstash-0.0.5 → django_qstash-0.0.7}/src/django_qstash/exceptions.py +0 -0
  37. {django_qstash-0.0.5 → django_qstash-0.0.7}/src/django_qstash/handlers.py +0 -0
  38. {django_qstash-0.0.5/src/django_qstash/results → django_qstash-0.0.7/src/django_qstash/management}/__init__.py +0 -0
  39. {django_qstash-0.0.5/src/django_qstash/results/migrations → django_qstash-0.0.7/src/django_qstash/management/commands}/__init__.py +0 -0
  40. {django_qstash-0.0.5 → django_qstash-0.0.7}/src/django_qstash/management/commands/clear_stale_results.py +0 -0
  41. {django_qstash-0.0.5 → django_qstash-0.0.7}/src/django_qstash/results/admin.py +0 -0
  42. {django_qstash-0.0.5 → django_qstash-0.0.7}/src/django_qstash/results/apps.py +0 -0
  43. {django_qstash-0.0.5 → django_qstash-0.0.7}/src/django_qstash/results/migrations/0001_initial.py +0 -0
  44. {django_qstash-0.0.5 → django_qstash-0.0.7}/src/django_qstash/results/models.py +0 -0
  45. {django_qstash-0.0.5 → django_qstash-0.0.7}/src/django_qstash/results/services.py +0 -0
  46. {django_qstash-0.0.5 → django_qstash-0.0.7}/src/django_qstash/utils.py +0 -0
  47. {django_qstash-0.0.5 → django_qstash-0.0.7}/src/django_qstash/views.py +0 -0
  48. {django_qstash-0.0.5 → django_qstash-0.0.7}/src/django_qstash.egg-info/dependency_links.txt +0 -0
  49. {django_qstash-0.0.5 → django_qstash-0.0.7}/src/django_qstash.egg-info/requires.txt +0 -0
  50. {django_qstash-0.0.5 → django_qstash-0.0.7}/src/django_qstash.egg-info/top_level.txt +0 -0
  51. {django_qstash-0.0.5 → django_qstash-0.0.7}/tests/test_exceptions.py +0 -0
  52. {django_qstash-0.0.5 → django_qstash-0.0.7}/tests/test_handlers.py +0 -0
  53. {django_qstash-0.0.5 → django_qstash-0.0.7}/tests/test_results_models.py +0 -0
  54. {django_qstash-0.0.5 → django_qstash-0.0.7}/tests/test_tasks.py +0 -0
  55. {django_qstash-0.0.5 → django_qstash-0.0.7}/tests/test_utils.py +0 -0
  56. {django_qstash-0.0.5 → django_qstash-0.0.7}/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.5
3
+ Version: 0.0.7
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.5"
9
+ version = "0.0.7"
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" }
@@ -74,6 +74,8 @@ source = [
74
74
  ]
75
75
  omit = [
76
76
  "*/migrations/*",
77
+ "*/admin.py",
78
+ "tests/*",
77
79
  ]
78
80
 
79
81
  [tool.coverage.paths]
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.0.5"
3
+ __version__ = "0.0.7"
4
4
 
5
5
  from .tasks import shared_task
6
6
 
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from django_qstash.settings import DJANGO_QSTASH_DOMAIN
4
+ from django_qstash.settings import DJANGO_QSTASH_WEBHOOK_PATH
5
+
6
+
7
+ def get_callback_url() -> str:
8
+ """
9
+ Get the callback URL based on the settings.
10
+ """
11
+ callback_domain = DJANGO_QSTASH_DOMAIN.rstrip("/")
12
+ if not callback_domain.startswith(("http://", "https://")):
13
+ callback_domain = f"https://{callback_domain}"
14
+ webhook_path = DJANGO_QSTASH_WEBHOOK_PATH.strip("/")
15
+ return f"{callback_domain}/{webhook_path}/"
@@ -0,0 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+ from qstash import QStash
4
+
5
+ from django_qstash.settings import QSTASH_TOKEN
6
+
7
+ qstash_client = QStash(QSTASH_TOKEN)
@@ -0,0 +1,38 @@
1
+ from __future__ import annotations
2
+
3
+ from django import forms
4
+
5
+ from django_qstash.discovery.utils import discover_tasks
6
+ from django_qstash.discovery.validators import task_exists_validator
7
+
8
+
9
+ class TaskChoiceField(forms.ChoiceField):
10
+ """
11
+ A form field that provides choices from discovered QStash tasks
12
+ """
13
+
14
+ def __init__(self, *args, **kwargs):
15
+ # Remove max_length if it's present since ChoiceField doesn't use it
16
+ kwargs.pop("max_length", None)
17
+
18
+ # Get tasks before calling parent to set choices
19
+ tasks = discover_tasks()
20
+
21
+ # Convert tasks to choices using (task_name, task_name) format
22
+ task_choices = [(task_value, task_label) for task_value, task_label in tasks]
23
+
24
+ kwargs["choices"] = task_choices
25
+ kwargs["validators"] = [task_exists_validator] + kwargs.get("validators", [])
26
+ super().__init__(*args, **kwargs)
27
+
28
+ def get_task(self):
29
+ """
30
+ Returns the actual task dot notation path for the selected value
31
+ """
32
+ if self.data:
33
+ tasks = discover_tasks()
34
+
35
+ for task_value, task_label in tasks:
36
+ if task_label == self.data:
37
+ return task_value
38
+ return None
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ from django.db import models
4
+
5
+ from django_qstash.discovery.fields import TaskChoiceField
6
+
7
+
8
+ class TaskField(models.CharField):
9
+ """
10
+ A model field for storing QStash task references
11
+ """
12
+
13
+ def __init__(self, *args, **kwargs):
14
+ # Set a reasonable max_length for task names if not provided
15
+ if "max_length" not in kwargs:
16
+ kwargs["max_length"] = 255
17
+ super().__init__(*args, **kwargs)
18
+
19
+ def formfield(self, **kwargs):
20
+ # Use our custom form field
21
+ defaults = {
22
+ "form_class": TaskChoiceField,
23
+ }
24
+ defaults.update(kwargs)
25
+ return super().formfield(**defaults)
@@ -0,0 +1,93 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import os
5
+ import warnings
6
+ from functools import lru_cache
7
+ from importlib import import_module
8
+
9
+ from django.apps import apps
10
+ from django.conf import settings
11
+ from django.core.signals import request_started
12
+ from django.utils.module_loading import module_has_submodule
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ DJANGO_QSTASH_DISCOVER_INCLUDE_SETTINGS_DIR = getattr(
17
+ settings, "DJANGO_QSTASH_DISCOVER_INCLUDE_SETTINGS_DIR", True
18
+ )
19
+
20
+
21
+ @lru_cache(maxsize=None)
22
+ def discover_tasks() -> list[tuple[str, str]]:
23
+ """
24
+ Automatically discover tasks in Django apps and return them as a list of tuples.
25
+ Each tuple contains (dot_notation_path, task_name).
26
+ If no custom task name is specified, both values will be the dot notation path.
27
+
28
+ Returns:
29
+ List of tuples: [(dot_notation_path, task_name), ...]
30
+ Example: [
31
+ ('example_app.tasks.my_task', 'example_app.tasks.my_task'),
32
+ ('other_app.tasks.custom_task', 'special_name')
33
+ ]
34
+ """
35
+ from django_qstash.tasks import QStashTask
36
+
37
+ discovered_tasks = []
38
+ packages = []
39
+
40
+ # Add Django apps that contain tasks.py
41
+ for app_config in apps.get_app_configs():
42
+ if module_has_submodule(app_config.module, "tasks"):
43
+ packages.append(app_config.name)
44
+
45
+ # Add the directory containing settings.py if it has a tasks.py module
46
+ if DJANGO_QSTASH_DISCOVER_INCLUDE_SETTINGS_DIR:
47
+ settings_module = os.environ.get("DJANGO_SETTINGS_MODULE", "")
48
+ if settings_module:
49
+ settings_package = settings_module.rsplit(".", 1)[0]
50
+ try:
51
+ settings_module_obj = import_module(settings_package)
52
+ if module_has_submodule(settings_module_obj, "tasks"):
53
+ packages.append(settings_package)
54
+ except ImportError:
55
+ warnings.warn(
56
+ f"Could not import settings package {settings_package} for task discovery",
57
+ RuntimeWarning,
58
+ stacklevel=2,
59
+ )
60
+
61
+ # Rest of the discovery logic
62
+ for package in packages:
63
+ try:
64
+ tasks_module = import_module(f"{package}.tasks")
65
+ # Find all attributes that are QstashTask instances
66
+ for attr_name in dir(tasks_module):
67
+ attr = getattr(tasks_module, attr_name)
68
+
69
+ if isinstance(attr, QStashTask):
70
+ value = f"{package}.tasks.{attr_name}"
71
+ if attr.name == attr_name:
72
+ label = value
73
+ else:
74
+ label = f"{attr.name} ({value})"
75
+ discovered_tasks.append((value, label))
76
+ except Exception as e:
77
+ warnings.warn(
78
+ f"Failed to import tasks from {package}: {str(e)}",
79
+ RuntimeWarning,
80
+ stacklevel=2,
81
+ )
82
+ return discovered_tasks
83
+
84
+
85
+ def clear_discover_tasks_cache(sender, **kwargs):
86
+ logger.info("Clearing Django QStash discovered tasks cache")
87
+ discover_tasks.cache_clear()
88
+
89
+
90
+ request_started.connect(
91
+ clear_discover_tasks_cache,
92
+ dispatch_uid="clear_django_qstash_discovered_tasks_cache",
93
+ )
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from django.core.exceptions import ValidationError
4
+
5
+ from django_qstash.discovery.utils import discover_tasks
6
+
7
+
8
+ def task_exists_validator(task_name):
9
+ """
10
+ Validates that a task name exists in the discovered tasks
11
+
12
+ Args:
13
+ task_name: The name of the task to validate
14
+
15
+ Raises:
16
+ ValidationError: If the task cannot be found
17
+ """
18
+ tasks = discover_tasks()
19
+ available_tasks = [task[0] for task in tasks]
20
+
21
+ if task_name not in available_tasks:
22
+ raise ValidationError(
23
+ f"Task '{task_name}' not found. Available tasks: {', '.join(available_tasks)}"
24
+ )
@@ -0,0 +1,116 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+
6
+ from django.apps import apps
7
+ from django.core.management.base import BaseCommand
8
+ from django.db import models
9
+
10
+ from django_qstash.callbacks import get_callback_url
11
+ from django_qstash.client import qstash_client
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class Command(BaseCommand):
17
+ """Management command to list and sync QStash schedules."""
18
+
19
+ help = "List and sync schedules from QStash"
20
+
21
+ def add_arguments(self, parser) -> None:
22
+ parser.add_argument(
23
+ "--list",
24
+ action="store_true",
25
+ help="List schedules from QStash",
26
+ )
27
+ parser.add_argument(
28
+ "--sync",
29
+ action="store_true",
30
+ help="Sync schedules from QStash to local database",
31
+ )
32
+ parser.add_argument(
33
+ "--no-input",
34
+ action="store_true",
35
+ help="Do not ask for confirmation",
36
+ )
37
+
38
+ def get_task_schedule_model(self) -> models.Model | None:
39
+ """Get the TaskSchedule model if available."""
40
+ try:
41
+ return apps.get_model("django_qstash_schedules", "TaskSchedule")
42
+ except LookupError:
43
+ self.stdout.write(
44
+ self.style.ERROR(
45
+ "Django QStash Schedules not installed.\n"
46
+ "Add `django_qstash.schedules` to INSTALLED_APPS and run migrations."
47
+ )
48
+ )
49
+
50
+ def sync_schedules(self, schedules: list) -> None:
51
+ """Sync remote schedules to local database."""
52
+ TaskSchedule = self.get_task_schedule_model()
53
+
54
+ for schedule in schedules:
55
+ try:
56
+ body = json.loads(schedule.body)
57
+ task_name = body.get("task_name", "Unnamed Task")
58
+ function = f"{body['module']}.{body['function']}"
59
+
60
+ obj, created = TaskSchedule.objects.update_or_create(
61
+ schedule_id=schedule.schedule_id,
62
+ defaults={
63
+ "name": task_name,
64
+ "task": function,
65
+ "cron": schedule.cron,
66
+ "args": body.get("args", []),
67
+ "kwargs": body.get("kwargs", {}),
68
+ },
69
+ )
70
+ status = "Created" if created else "Updated"
71
+ logger.info(
72
+ "%s schedule: %s (%s)", status, task_name, schedule.schedule_id
73
+ )
74
+ except Exception:
75
+ logger.exception("Failed to sync schedule %s", schedule.schedule_id)
76
+
77
+ def handle(self, *args, **options) -> None:
78
+ auto_confirm = options.get("no_input")
79
+ if not (options.get("sync") or options.get("list")):
80
+ self.stdout.write(
81
+ self.style.ERROR("Please specify either --list or --sync option")
82
+ )
83
+ return
84
+
85
+ try:
86
+ destination = get_callback_url()
87
+ schedules = qstash_client.schedule.list()
88
+
89
+ self.stdout.write(
90
+ self.style.SUCCESS(
91
+ f"Found {len(schedules)} remote schedules based on destination: {destination}"
92
+ )
93
+ )
94
+
95
+ for schedule in schedules:
96
+ body = json.loads(schedule.body)
97
+ task_name = body.get("task_name", "Unnamed Task")
98
+ function = f"{body['module']}.{body['function']}"
99
+
100
+ self.stdout.write(
101
+ f"\nSchedule ID: {schedule.schedule_id}"
102
+ f"\n Task: {task_name} ({function})"
103
+ f"\n Cron: {schedule.cron}"
104
+ f"\n Destination: {schedule.destination}"
105
+ f"\n Retries: {schedule.retries}"
106
+ f"\n Status: {'Paused' if schedule.paused else 'Active'}"
107
+ )
108
+
109
+ if options.get("sync"):
110
+ user_input = input("Do you want to sync remote schedules? (y/n): ")
111
+ if user_input.lower() == "y" or auto_confirm:
112
+ self.sync_schedules(schedules)
113
+ else:
114
+ self.stdout.write(self.style.ERROR("Sync cancelled"))
115
+ except Exception as e:
116
+ self.stdout.write(self.style.ERROR(f"An error occurred: {str(e)}"))
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ from django.contrib import admin
4
+
5
+ from django_qstash.schedules import services
6
+ from django_qstash.schedules.forms import TaskScheduleForm
7
+ from django_qstash.schedules.models import TaskSchedule
8
+
9
+
10
+ @admin.register(TaskSchedule)
11
+ class TaskScheduleAdmin(admin.ModelAdmin):
12
+ list_display = ["schedule_id", "task_name"]
13
+ readonly_fields = [
14
+ "schedule_id",
15
+ "task_name",
16
+ "get_qstash_schedule_details",
17
+ "paused_at",
18
+ "resumed_at",
19
+ "active_at",
20
+ ]
21
+ form = TaskScheduleForm
22
+
23
+ fieldsets = [
24
+ (
25
+ "Name",
26
+ {
27
+ "fields": ["name", "schedule_id", "is_active"],
28
+ },
29
+ ),
30
+ (
31
+ "Task Selection",
32
+ {
33
+ "fields": ["task", "task_name"],
34
+ },
35
+ ),
36
+ (
37
+ "Arguments",
38
+ {
39
+ "fields": ["args", "kwargs"],
40
+ },
41
+ ),
42
+ (
43
+ "Schedule",
44
+ {
45
+ "fields": ["cron", "retries", "timeout"],
46
+ },
47
+ ),
48
+ (
49
+ "QStash Metadata",
50
+ {
51
+ "fields": [
52
+ "paused_at",
53
+ "resumed_at",
54
+ "active_at",
55
+ "get_qstash_schedule_details",
56
+ ],
57
+ "classes": ["collapse"],
58
+ },
59
+ ),
60
+ ]
61
+
62
+ @admin.display(description="Raw")
63
+ def get_qstash_schedule_details(self, obj: TaskSchedule) -> dict:
64
+ if not obj.schedule_id:
65
+ return "No schedule ID yet"
66
+ return services.get_task_schedule_from_qstash(obj, as_dict=True)
@@ -0,0 +1,13 @@
1
+ from __future__ import annotations
2
+
3
+ from django.apps import AppConfig
4
+
5
+
6
+ class SchedulesConfig(AppConfig):
7
+ name = "django_qstash.schedules"
8
+ label = "django_qstash_schedules"
9
+ verbose_name = "django_qstash_schedules"
10
+ default_auto_field = "django.db.models.BigAutoField"
11
+
12
+ def ready(self):
13
+ import django_qstash.schedules.signals # noqa
@@ -0,0 +1,9 @@
1
+ from __future__ import annotations
2
+
3
+ from django.core.exceptions import ValidationError
4
+
5
+
6
+ class InvalidDurationStringValidationError(ValidationError):
7
+ """Invalid duration string."""
8
+
9
+ pass
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any
5
+
6
+ from django_qstash.callbacks import get_callback_url
7
+ from django_qstash.schedules.models import TaskSchedule
8
+
9
+
10
+ def prepare_qstash_payload(instance: TaskSchedule) -> dict[str, Any]:
11
+ """Prepare the task payload for QStash"""
12
+ return {
13
+ "function": instance.task_name.split(".")[-1], # Get function name
14
+ "module": ".".join(instance.task_name.split(".")[:-1]), # Get module path
15
+ "args": instance.args,
16
+ "kwargs": instance.kwargs,
17
+ "task_name": instance.name,
18
+ "options": {
19
+ "max_retries": instance.retries,
20
+ "timeout": instance.timeout,
21
+ },
22
+ }
23
+
24
+
25
+ def format_task_schedule_for_qstash(instance: TaskSchedule) -> dict[str, Any]:
26
+ payload = prepare_qstash_payload(instance)
27
+ callback_url = get_callback_url()
28
+ data = {
29
+ "destination": callback_url,
30
+ "body": json.dumps(payload),
31
+ "cron": instance.cron,
32
+ "retries": instance.retries,
33
+ "timeout": instance.timeout,
34
+ }
35
+ if instance.schedule_id:
36
+ data["schedule_id"] = instance.schedule_id
37
+ return data
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from django import forms
4
+
5
+ from django_qstash.discovery.fields import TaskChoiceField
6
+ from django_qstash.schedules.models import TaskSchedule
7
+
8
+
9
+ class TaskScheduleForm(forms.ModelForm):
10
+ task = TaskChoiceField()
11
+
12
+ class Meta:
13
+ model = TaskSchedule
14
+ fields = [
15
+ "name",
16
+ "task",
17
+ "task_name",
18
+ "args",
19
+ "kwargs",
20
+ "schedule_id",
21
+ "cron",
22
+ "retries",
23
+ "timeout",
24
+ ]
@@ -0,0 +1,122 @@
1
+ # Generated by Django 5.1.4 on 2025-01-02 06:28
2
+
3
+ from __future__ import annotations
4
+
5
+ import django.core.validators
6
+ import django.utils.timezone
7
+ from django.db import migrations
8
+ from django.db import models
9
+
10
+ import django_qstash.discovery.models
11
+ import django_qstash.schedules.validators
12
+
13
+
14
+ class Migration(migrations.Migration):
15
+ initial = True
16
+
17
+ dependencies = []
18
+
19
+ operations = [
20
+ migrations.CreateModel(
21
+ name="TaskSchedule",
22
+ fields=[
23
+ (
24
+ "id",
25
+ models.BigAutoField(
26
+ auto_created=True,
27
+ primary_key=True,
28
+ serialize=False,
29
+ verbose_name="ID",
30
+ ),
31
+ ),
32
+ (
33
+ "schedule_id",
34
+ models.CharField(
35
+ blank=True,
36
+ db_index=True,
37
+ help_text="The schedule ID stored in QStash",
38
+ max_length=255,
39
+ null=True,
40
+ unique=True,
41
+ verbose_name="Schedule ID",
42
+ ),
43
+ ),
44
+ (
45
+ "name",
46
+ models.CharField(
47
+ help_text="Short Description For This Task Schedule",
48
+ max_length=200,
49
+ verbose_name="Name",
50
+ ),
51
+ ),
52
+ ("task", django_qstash.discovery.models.TaskField(max_length=255)),
53
+ (
54
+ "task_name",
55
+ models.CharField(
56
+ blank=True,
57
+ help_text="Original Python location of task",
58
+ max_length=255,
59
+ ),
60
+ ),
61
+ (
62
+ "args",
63
+ models.JSONField(
64
+ blank=True,
65
+ default=list,
66
+ help_text='JSON encoded positional arguments (Example: ["arg1", "arg2"])',
67
+ verbose_name="Positional Arguments",
68
+ ),
69
+ ),
70
+ (
71
+ "kwargs",
72
+ models.JSONField(
73
+ blank=True,
74
+ default=dict,
75
+ help_text='JSON encoded keyword arguments (Example: {"argument": "value"})',
76
+ verbose_name="Keyword Arguments",
77
+ ),
78
+ ),
79
+ (
80
+ "cron",
81
+ models.CharField(
82
+ default="*/5 * * * *",
83
+ help_text="Cron expression for scheduling the task",
84
+ max_length=255,
85
+ verbose_name="Cron Expression",
86
+ ),
87
+ ),
88
+ (
89
+ "retries",
90
+ models.IntegerField(
91
+ default=3,
92
+ help_text="Number of times to retry the task if it fails",
93
+ validators=[django.core.validators.MaxValueValidator(5)],
94
+ verbose_name="Retries",
95
+ ),
96
+ ),
97
+ (
98
+ "timeout",
99
+ models.CharField(
100
+ default="60s",
101
+ help_text="Duration string for task timeout (e.g., '1s', '5m', '2h'). See Max HTTP Connection Timeout on QStash pricing page for allowed values for your Upstash account.",
102
+ max_length=10,
103
+ validators=[
104
+ django_qstash.schedules.validators.validate_duration_string
105
+ ],
106
+ verbose_name="Timeout",
107
+ ),
108
+ ),
109
+ ("is_active", models.BooleanField(default=True)),
110
+ (
111
+ "active_at",
112
+ models.DateTimeField(
113
+ blank=True, default=django.utils.timezone.now, null=True
114
+ ),
115
+ ),
116
+ ("is_paused", models.BooleanField(default=False)),
117
+ ("paused_at", models.DateTimeField(blank=True, null=True)),
118
+ ("is_resumed", models.BooleanField(default=False)),
119
+ ("resumed_at", models.DateTimeField(blank=True, null=True)),
120
+ ],
121
+ ),
122
+ ]
@@ -0,0 +1,20 @@
1
+ # Generated by Django 5.1.4 on 2025-01-02 07:44
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_schedules", "0001_initial"),
12
+ ]
13
+
14
+ operations = [
15
+ migrations.AddField(
16
+ model_name="taskschedule",
17
+ name="updated_at",
18
+ field=models.DateTimeField(auto_now=True),
19
+ ),
20
+ ]