django-qstash 0.0.1__py3-none-any.whl → 0.0.3__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,4 +1,6 @@
1
- __version__ = "0.1.0"
1
+ from __future__ import annotations
2
+
3
+ __version__ = "0.0.3"
2
4
 
3
5
  from .tasks import shared_task
4
6
 
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class WebhookError(Exception):
5
+ """Base exception for webhook handling errors."""
6
+
7
+ pass
8
+
9
+
10
+ class SignatureError(WebhookError):
11
+ """Invalid or missing signature."""
12
+
13
+ pass
14
+
15
+
16
+ class PayloadError(WebhookError):
17
+ """Invalid payload structure or content."""
18
+
19
+ pass
20
+
21
+
22
+ class TaskError(WebhookError):
23
+ """Error in task execution."""
24
+
25
+ pass
@@ -0,0 +1,143 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ from dataclasses import dataclass
6
+ from typing import Any
7
+
8
+ from django.conf import settings
9
+ from django.http import HttpRequest
10
+ from qstash import Receiver
11
+
12
+ from . import utils
13
+ from .exceptions import PayloadError
14
+ from .exceptions import SignatureError
15
+ from .exceptions import TaskError
16
+ from .results.services import store_task_result
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ @dataclass
22
+ class TaskPayload:
23
+ function: str
24
+ module: str
25
+ args: list
26
+ kwargs: dict
27
+ task_name: str
28
+ function_path: str
29
+
30
+ @classmethod
31
+ def from_dict(cls, data: dict) -> TaskPayload:
32
+ """Create TaskPayload from dictionary."""
33
+ is_valid, error = utils.validate_task_payload(data)
34
+ if not is_valid:
35
+ raise PayloadError(error)
36
+
37
+ function_path = f"{data['module']}.{data['function']}"
38
+ return cls(
39
+ function=data["function"],
40
+ module=data["module"],
41
+ args=data["args"],
42
+ kwargs=data["kwargs"],
43
+ task_name=data.get("task_name", function_path),
44
+ function_path=function_path,
45
+ )
46
+
47
+
48
+ class QStashWebhook:
49
+ def __init__(self):
50
+ self.receiver = Receiver(
51
+ current_signing_key=settings.QSTASH_CURRENT_SIGNING_KEY,
52
+ next_signing_key=settings.QSTASH_NEXT_SIGNING_KEY,
53
+ )
54
+ self.force_https = getattr(settings, "DJANGO_QSTASH_FORCE_HTTPS", True)
55
+
56
+ def verify_signature(self, body: str, signature: str, url: str) -> None:
57
+ """Verify QStash signature."""
58
+ if not signature:
59
+ raise SignatureError("Missing Upstash-Signature header")
60
+
61
+ if self.force_https and not url.startswith("https://"):
62
+ url = url.replace("http://", "https://")
63
+
64
+ try:
65
+ self.receiver.verify(body=body, signature=signature, url=url)
66
+ except Exception as e:
67
+ raise SignatureError(f"Invalid signature: {e}")
68
+
69
+ def parse_payload(self, body: str) -> TaskPayload:
70
+ """Parse and validate webhook payload."""
71
+ try:
72
+ data = json.loads(body)
73
+ return TaskPayload.from_dict(data)
74
+ except json.JSONDecodeError as e:
75
+ raise PayloadError(f"Invalid JSON payload: {e}")
76
+
77
+ def execute_task(self, payload: TaskPayload) -> Any:
78
+ """Import and execute the task function."""
79
+ try:
80
+ task_func = utils.import_string(payload.function_path)
81
+ except ImportError as e:
82
+ raise TaskError(f"Could not import task function: {e}")
83
+
84
+ try:
85
+ if callable(task_func) and hasattr(task_func, "actual_func"):
86
+ return task_func.actual_func(*payload.args, **payload.kwargs)
87
+ return task_func(*payload.args, **payload.kwargs)
88
+ except Exception as e:
89
+ raise TaskError(f"Task execution failed: {e}")
90
+
91
+ def handle_request(self, request: HttpRequest) -> tuple[dict, int]:
92
+ """Process webhook request and return response data and status code."""
93
+ payload = None # Initialize payload as None
94
+ try:
95
+ body = request.body.decode()
96
+ self.verify_signature(
97
+ body=body,
98
+ signature=request.headers.get("Upstash-Signature"),
99
+ url=request.build_absolute_uri(),
100
+ )
101
+
102
+ payload = self.parse_payload(body)
103
+ result = self.execute_task(payload)
104
+
105
+ store_task_result(
106
+ task_id=request.headers.get("Upstash-Message-Id"),
107
+ task_name=payload.task_name,
108
+ status="SUCCESS",
109
+ result=result,
110
+ args=payload.args,
111
+ kwargs=payload.kwargs,
112
+ )
113
+
114
+ return {
115
+ "status": "success",
116
+ "task_name": payload.task_name,
117
+ "result": result if result is not None else "null",
118
+ }, 200
119
+
120
+ except (SignatureError, PayloadError) as e:
121
+ logger.exception("Authentication error: %s", str(e))
122
+ return {
123
+ "status": "error",
124
+ "error_type": e.__class__.__name__,
125
+ "error": str(e),
126
+ "task_name": getattr(payload, "task_name", None),
127
+ }, 400
128
+ except TaskError as e:
129
+ logger.exception("Task execution error: %s", str(e))
130
+ return {
131
+ "status": "error",
132
+ "error_type": e.__class__.__name__,
133
+ "error": str(e),
134
+ "task_name": payload.task_name if payload else None,
135
+ }, 500
136
+ except Exception as e:
137
+ logger.exception("Unexpected error in webhook handler: %s", str(e))
138
+ return {
139
+ "status": "error",
140
+ "error_type": "InternalServerError",
141
+ "error": "An unexpected error occurred",
142
+ "task_name": getattr(payload, "task_name", None),
143
+ }, 500
File without changes
@@ -0,0 +1,61 @@
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
+ )
File without changes
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ from django.contrib import admin
4
+
5
+ from .models import TaskResult
6
+
7
+
8
+ @admin.register(TaskResult)
9
+ class TaskResultAdmin(admin.ModelAdmin):
10
+ readonly_fields = [
11
+ "task_name",
12
+ "status",
13
+ "date_done",
14
+ "result",
15
+ "traceback",
16
+ "args",
17
+ "kwargs",
18
+ "task_id",
19
+ "date_created",
20
+ ]
21
+ list_display = ["task_name", "status", "date_done"]
@@ -0,0 +1,10 @@
1
+ from __future__ import annotations
2
+
3
+ from django.apps import AppConfig
4
+
5
+
6
+ class ResultsConfig(AppConfig):
7
+ name = "django_qstash.results"
8
+ label = "django_qstash_results"
9
+ verbose_name = "django_qstash_results"
10
+ default_auto_field = "django.db.models.BigAutoField"
@@ -0,0 +1,62 @@
1
+ # Generated by Django 5.1.4 on 2024-12-30 23:31
2
+
3
+ from __future__ import annotations
4
+
5
+ import uuid
6
+
7
+ import django.utils.timezone
8
+ from django.db import migrations
9
+ from django.db import models
10
+
11
+
12
+ class Migration(migrations.Migration):
13
+ initial = True
14
+
15
+ dependencies = []
16
+
17
+ operations = [
18
+ migrations.CreateModel(
19
+ name="TaskResult",
20
+ fields=[
21
+ (
22
+ "id",
23
+ models.UUIDField(
24
+ default=uuid.uuid1,
25
+ editable=False,
26
+ primary_key=True,
27
+ serialize=False,
28
+ unique=True,
29
+ ),
30
+ ),
31
+ (
32
+ "task_id",
33
+ models.CharField(db_index=True, max_length=255, unique=True),
34
+ ),
35
+ ("task_name", models.CharField(max_length=255)),
36
+ (
37
+ "status",
38
+ models.CharField(
39
+ choices=[
40
+ ("PENDING", "Pending"),
41
+ ("SUCCESS", "Success"),
42
+ ("FAILURE", "Failure"),
43
+ ],
44
+ default="PENDING",
45
+ max_length=50,
46
+ ),
47
+ ),
48
+ (
49
+ "date_created",
50
+ models.DateTimeField(default=django.utils.timezone.now),
51
+ ),
52
+ ("date_done", models.DateTimeField(null=True)),
53
+ ("result", models.JSONField(null=True)),
54
+ ("traceback", models.TextField(null=True)),
55
+ ("args", models.JSONField(null=True)),
56
+ ("kwargs", models.JSONField(null=True)),
57
+ ],
58
+ options={
59
+ "ordering": ["-date_done"],
60
+ },
61
+ ),
62
+ ]
File without changes
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ import uuid
4
+
5
+ from django.db import models
6
+ from django.utils import timezone
7
+
8
+
9
+ class TaskResult(models.Model):
10
+ id = models.UUIDField(
11
+ primary_key=True, default=uuid.uuid1, editable=False, unique=True
12
+ )
13
+ task_id = models.CharField(max_length=255, unique=True, db_index=True)
14
+ task_name = models.CharField(max_length=255)
15
+ status = models.CharField(
16
+ max_length=50,
17
+ choices=[
18
+ ("PENDING", "Pending"),
19
+ ("SUCCESS", "Success"),
20
+ ("FAILURE", "Failure"),
21
+ ],
22
+ default="PENDING",
23
+ )
24
+ date_created = models.DateTimeField(default=timezone.now)
25
+ date_done = models.DateTimeField(null=True)
26
+ result = models.JSONField(null=True)
27
+ traceback = models.TextField(null=True)
28
+ args = models.JSONField(null=True)
29
+ kwargs = models.JSONField(null=True)
30
+
31
+ class Meta:
32
+ app_label = "django_qstash_results"
33
+ ordering = ["-date_done"]
34
+
35
+ def __str__(self):
36
+ return f"{self.task_name} ({self.task_id})"
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+
5
+ from django.apps import apps
6
+ from django.utils import timezone
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ def store_task_result(
12
+ task_id, task_name, status, result=None, traceback=None, args=None, kwargs=None
13
+ ):
14
+ """Helper function to store task results if the results app is installed"""
15
+ try:
16
+ TaskResult = apps.get_model("django_qstash_results", "TaskResult")
17
+ task_result = TaskResult.objects.create(
18
+ task_id=task_id,
19
+ task_name=task_name,
20
+ status=status,
21
+ date_done=timezone.now(),
22
+ result=result,
23
+ traceback=traceback,
24
+ args=args,
25
+ kwargs=kwargs,
26
+ )
27
+ return task_result
28
+ except LookupError:
29
+ # Model isn't installed, skip storage
30
+ logger.debug(
31
+ "Django QStash Results not installed. Add `django_qstash.results` to INSTALLED_APPS and run migrations."
32
+ )
33
+ return None
django_qstash/views.py CHANGED
@@ -1,124 +1,21 @@
1
+ from __future__ import annotations
2
+
1
3
  import json
2
- import logging
3
4
 
4
- from django.conf import settings
5
- from django.http import (
6
- HttpRequest,
7
- HttpResponse,
8
- HttpResponseBadRequest,
9
- HttpResponseForbidden,
10
- )
5
+ from django.http import HttpRequest
6
+ from django.http import HttpResponse
11
7
  from django.views.decorators.csrf import csrf_exempt
12
8
  from django.views.decorators.http import require_http_methods
13
- from qstash import Receiver
14
-
15
- from . import utils
16
-
17
- DJANGO_QSTASH_FORCE_HTTPS = getattr(settings, "DJANGO_QSTASH_FORCE_HTTPS", True)
18
-
19
- logger = logging.getLogger(__name__)
20
9
 
21
-
22
- # Initialize the QStash receiver
23
- receiver = Receiver(
24
- current_signing_key=settings.QSTASH_CURRENT_SIGNING_KEY,
25
- next_signing_key=settings.QSTASH_NEXT_SIGNING_KEY,
26
- )
10
+ from .handlers import QStashWebhook
27
11
 
28
12
 
29
13
  @csrf_exempt
30
14
  @require_http_methods(["POST"])
31
15
  def qstash_webhook_view(request: HttpRequest) -> HttpResponse:
32
- """
33
- Webhook handler for QStash callbacks.
34
-
35
- Expects a POST request with:
36
- - Upstash-Signature header for verification
37
- - JSON body containing task information:
38
- {
39
- "function": "full.path.to.function",
40
- "module": "module.path",
41
- "args": [...],
42
- "kwargs": {...},
43
- "task_name": "optional_task_name",
44
- "options": {...}
45
- }
46
- """
47
- try:
48
- # Get the signature from headers
49
- signature = request.headers.get("Upstash-Signature")
50
- if not signature:
51
- return HttpResponseForbidden("Missing Upstash-Signature header")
52
-
53
- # Verify the signature using the QStash SDK
54
- url = request.build_absolute_uri()
55
- if DJANGO_QSTASH_FORCE_HTTPS and not url.startswith("https://"):
56
- url = url.replace("http://", "https://")
57
- try:
58
- receiver.verify(
59
- body=request.body.decode(),
60
- signature=signature,
61
- url=url,
62
- )
63
- except Exception as e:
64
- logger.error(f"Signature verification failed: {e}")
65
- return HttpResponseForbidden("Invalid signature")
66
-
67
- # Parse the payload
68
- try:
69
- payload = json.loads(request.body.decode())
70
- except json.JSONDecodeError as e:
71
- logger.error(f"Failed to parse JSON payload: {e}")
72
- return HttpResponseBadRequest("Invalid JSON payload")
73
-
74
- # Validate payload structure
75
- is_valid, error_message = utils.validate_task_payload(payload)
76
- if not is_valid:
77
- logger.error(f"Invalid payload structure: {error_message}")
78
- return HttpResponseBadRequest(error_message)
79
-
80
- # Import the function
81
- try:
82
- function_path = f"{payload['module']}.{payload['function']}"
83
- task_func = utils.import_string(function_path)
84
- except ImportError as e:
85
- logger.error(f"Failed to import task function: {e}")
86
- return HttpResponseBadRequest(f"Could not import task function: {e}")
87
-
88
- # Execute the task
89
- try:
90
- if hasattr(task_func, "__call__") and hasattr(task_func, "actual_func"):
91
- # If it's a wrapped function, call the actual function directly
92
- result = task_func.actual_func(*payload["args"], **payload["kwargs"])
93
- else:
94
- result = task_func(*payload["args"], **payload["kwargs"])
95
-
96
- # Prepare the response
97
- response_data = {
98
- "status": "success",
99
- "task_name": payload.get("task_name", function_path),
100
- "result": result if result is not None else "null",
101
- }
102
-
103
- return HttpResponse(
104
- json.dumps(response_data), content_type="application/json"
105
- )
106
-
107
- except Exception as e:
108
- logger.exception(f"Task execution failed: {e}")
109
- error_response = {
110
- "status": "error",
111
- "task_name": payload.get("task_name", function_path),
112
- "error": str(e),
113
- }
114
- return HttpResponse(
115
- json.dumps(error_response), status=500, content_type="application/json"
116
- )
117
-
118
- except Exception as e:
119
- logger.exception(f"Unexpected error in webhook handler: {e}")
120
- return HttpResponse(
121
- json.dumps({"status": "error", "error": "Internal server error"}),
122
- status=500,
123
- content_type="application/json",
124
- )
16
+ """Handle QStash webhook requests."""
17
+ webhook = QStashWebhook()
18
+ response_data, status_code = webhook.handle_request(request)
19
+ return HttpResponse(
20
+ json.dumps(response_data), status=status_code, content_type="application/json"
21
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-qstash
3
- Version: 0.0.1
3
+ Version: 0.0.3
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
@@ -27,6 +27,8 @@ Requires-Dist: django>=4.2
27
27
  Requires-Dist: qstash>=2
28
28
  Requires-Dist: requests>=2.30
29
29
 
30
+ > :warning: **BETA Software**: Working on being production-ready soon.
31
+
30
32
  # Django QStash `pip install django-qstash`
31
33
 
32
34
  A drop-in replacement for Celery's shared_task leveraging Upstash QStash for a truly serverless Django application to run background tasks asynchronously from the request/response cycle.
@@ -49,6 +51,7 @@ Depends on:
49
51
  # from celery import shared_task
50
52
  from django_qstash import shared_task
51
53
 
54
+
52
55
  @shared_task
53
56
  def math_add_task(a, b, save_to_file=False):
54
57
  logger.info(f"Adding {a} and {b}")
@@ -73,23 +76,24 @@ math_add_task.delay(12, 454, save_to_file=True)
73
76
 
74
77
 
75
78
  ```python
76
- QSTASH_TOKEN="your_token"
77
- QSTASH_CURRENT_SIGNING_KEY="your_current_signing_key"
78
- QSTASH_NEXT_SIGNING_KEY="your_next_signing_key"
79
+ QSTASH_TOKEN = "your_token"
80
+ QSTASH_CURRENT_SIGNING_KEY = "your_current_signing_key"
81
+ QSTASH_NEXT_SIGNING_KEY = "your_next_signing_key"
79
82
 
80
83
  # required for django-qstash
81
- DJANGO_QSTASH_DOMAIN="https://example.com"
82
- DJANGO_QSTASH_WEBHOOK_PATH="/qstash/webhook/"
84
+ DJANGO_QSTASH_DOMAIN = "https://example.com"
85
+ DJANGO_QSTASH_WEBHOOK_PATH = "/qstash/webhook/"
83
86
  ```
84
87
 
85
88
 
89
+ `DJANGO_QSTASH_DOMAIN`: Must be a valid and publicly accessible domain. For example `https://djangoqstash.com`
86
90
 
87
- `DJANGO_QSTASH_DOMAIN`: Must be a valid and publicly accessible domain. For example `https://djangoqstash.net`
91
+ In development mode, we recommend using a tunnel like [Cloudflare Tunnels](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) with a domain name you control. You can also consider [ngrok](https://ngrok.com/).
88
92
 
89
- In development mode, we recommend using a tunnel like [Cloudflare Tunnels](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) with a domain name you control. You can also consider [ngrok](https://ngrok.com/).
90
93
 
94
+ `DJANGO_QSTASH_WEBHOOK_PATH` (default:`/qstash/webhook/`): The path where QStash will send webhooks to your Django application.
91
95
 
92
- `DJANGO_QSTASH_WEBHOOK_PATH`: The path where QStash will send webhooks to your Django application. Defaults to `/qstash/webhook/`
93
96
 
97
+ `DJANGO_QSTASH_FORCE_HTTPS` (default:`True`): Whether to force HTTPS for the webhook.
94
98
 
95
- `DJANGO_QSTASH_FORCE_HTTPS`: Whether to force HTTPS for the webhook. Defaults to `True`.
99
+ `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).
@@ -0,0 +1,19 @@
1
+ django_qstash/__init__.py,sha256=KLUoDDQqN2L9ikWuv1pbu1hY2VEXh93Kzn09C6TbmLI,117
2
+ django_qstash/exceptions.py,sha256=pH6kKRJFIVFkDHUJQ9yRWmtGdBBSXpNAwMSFuNzMgPw,392
3
+ django_qstash/handlers.py,sha256=mmm8TJOqV3j1rQXooNOa128gtmALXFNCAaDZ5xwIcuw,4950
4
+ django_qstash/tasks.py,sha256=X2gFILRvUF2GFuwyAUT43Zvw7OsdFci7870VhRNQ5-M,4929
5
+ django_qstash/utils.py,sha256=wrTU30cobO2di18BNEFtKD4ih2euf7eQNpg6p6TkQ1Y,1185
6
+ django_qstash/views.py,sha256=H32f_jGnlwOTO0YG9znNo2b-GRYZ8TM-Wt0T62SGdXM,639
7
+ django_qstash/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ django_qstash/management/commands/clear_stale_results.py,sha256=stHCnvC1ER_rZXKUK-YVUgazC2q04eGIHjo_BrPSDEI,2096
9
+ django_qstash/results/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ django_qstash/results/admin.py,sha256=q9fn3lfn0gviMfiimYij0wBCYww7FxyrOfGPr1NvntA,434
11
+ django_qstash/results/apps.py,sha256=4me4cg5yeoeSJTphkHYzGMJUfGucT47FNIUMYu5gmIo,275
12
+ django_qstash/results/models.py,sha256=aEiAhGJOuLRtjibUw6xdQqUt3eYKLqY2as4I4QSrF5U,1047
13
+ django_qstash/results/services.py,sha256=HvNp5D1tQ__nz4LVUTAGxuyLl_dnlBps4pJ6E9HD2kA,991
14
+ django_qstash/results/migrations/0001_initial.py,sha256=A90SKgWmBf4SIJYG1Jh6-b_81Ia1zIzGj3Bfl1O4-kg,1902
15
+ django_qstash/results/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ django_qstash-0.0.3.dist-info/METADATA,sha256=t_nf7LAsZI-R5PEyKI2yPxxsRobQ6QoI33e_9div-kE,3332
17
+ django_qstash-0.0.3.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
18
+ django_qstash-0.0.3.dist-info/top_level.txt,sha256=AlV3WSK1A0ZvKuCLsINtIJhJW8zo7SEB-D3_RAjZ0hI,14
19
+ django_qstash-0.0.3.dist-info/RECORD,,
@@ -1,8 +0,0 @@
1
- django_qstash/__init__.py,sha256=HvBJshBllZs_V6B2p1DtRYkcU1CiDglPlk43V8MYleI,81
2
- django_qstash/tasks.py,sha256=X2gFILRvUF2GFuwyAUT43Zvw7OsdFci7870VhRNQ5-M,4929
3
- django_qstash/utils.py,sha256=wrTU30cobO2di18BNEFtKD4ih2euf7eQNpg6p6TkQ1Y,1185
4
- django_qstash/views.py,sha256=ucxKQdBoz9Db_1wpuE3U0BBC4nPqtFv05Kb0wggrpOg,4270
5
- django_qstash-0.0.1.dist-info/METADATA,sha256=QZayHLC5YDuYHMFtMbmBdx8ZTllFhnXk8F2xAxGWcpI,3079
6
- django_qstash-0.0.1.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
7
- django_qstash-0.0.1.dist-info/top_level.txt,sha256=AlV3WSK1A0ZvKuCLsINtIJhJW8zo7SEB-D3_RAjZ0hI,14
8
- django_qstash-0.0.1.dist-info/RECORD,,