django-webhook-subscriber 1.0.0__py3-none-any.whl → 2.0.0__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.
- django_webhook_subscriber/__init__.py +7 -1
- django_webhook_subscriber/admin.py +831 -182
- django_webhook_subscriber/apps.py +3 -20
- django_webhook_subscriber/conf.py +11 -24
- django_webhook_subscriber/delivery.py +414 -159
- django_webhook_subscriber/http.py +51 -0
- django_webhook_subscriber/management/commands/webhook.py +169 -0
- django_webhook_subscriber/management/commands/webhook_cache.py +173 -0
- django_webhook_subscriber/management/commands/webhook_logs.py +226 -0
- django_webhook_subscriber/management/commands/webhook_performance_test.py +469 -0
- django_webhook_subscriber/management/commands/webhook_send.py +96 -0
- django_webhook_subscriber/management/commands/webhook_status.py +139 -0
- django_webhook_subscriber/managers.py +36 -14
- django_webhook_subscriber/migrations/0002_remove_webhookregistry_content_type_and_more.py +192 -0
- django_webhook_subscriber/models.py +291 -114
- django_webhook_subscriber/serializers.py +16 -50
- django_webhook_subscriber/tasks.py +434 -56
- django_webhook_subscriber/tests/factories.py +40 -0
- django_webhook_subscriber/tests/settings.py +27 -8
- django_webhook_subscriber/tests/test_delivery.py +453 -190
- django_webhook_subscriber/tests/test_http.py +32 -0
- django_webhook_subscriber/tests/test_managers.py +26 -37
- django_webhook_subscriber/tests/test_models.py +341 -251
- django_webhook_subscriber/tests/test_serializers.py +22 -56
- django_webhook_subscriber/tests/test_tasks.py +477 -189
- django_webhook_subscriber/tests/test_utils.py +98 -94
- django_webhook_subscriber/utils.py +87 -69
- django_webhook_subscriber/validators.py +53 -0
- django_webhook_subscriber-2.0.0.dist-info/METADATA +774 -0
- django_webhook_subscriber-2.0.0.dist-info/RECORD +38 -0
- django_webhook_subscriber/management/commands/check_webhook_tasks.py +0 -113
- django_webhook_subscriber/management/commands/clean_webhook_logs.py +0 -65
- django_webhook_subscriber/management/commands/test_webhook.py +0 -96
- django_webhook_subscriber/signals.py +0 -152
- django_webhook_subscriber/testing.py +0 -14
- django_webhook_subscriber/tests/test_signals.py +0 -268
- django_webhook_subscriber-1.0.0.dist-info/METADATA +0 -448
- django_webhook_subscriber-1.0.0.dist-info/RECORD +0 -33
- {django_webhook_subscriber-1.0.0.dist-info → django_webhook_subscriber-2.0.0.dist-info}/WHEEL +0 -0
- {django_webhook_subscriber-1.0.0.dist-info → django_webhook_subscriber-2.0.0.dist-info}/licenses/LICENSE +0 -0
- {django_webhook_subscriber-1.0.0.dist-info → django_webhook_subscriber-2.0.0.dist-info}/top_level.txt +0 -0
|
@@ -1,181 +1,358 @@
|
|
|
1
1
|
"""Models for Django Webhook Subscriber."""
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
|
|
3
|
+
from django.core.exceptions import ValidationError
|
|
5
4
|
from django.db import models
|
|
5
|
+
from django.utils import timezone
|
|
6
6
|
from django.utils.translation import gettext_lazy as _
|
|
7
|
-
from django.contrib.contenttypes.models import ContentType
|
|
8
|
-
|
|
9
|
-
from django_webhook_subscriber import managers
|
|
10
7
|
|
|
8
|
+
from .managers import WebhookDeliveryLogManager
|
|
9
|
+
from .utils import generate_secret
|
|
10
|
+
from .validators import validate_class_path, validate_headers
|
|
11
11
|
|
|
12
|
-
class WebhookRegistry(models.Model):
|
|
13
|
-
"""Webhook model to store webhook configurations.
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
class WebhookSubscriber(models.Model):
|
|
14
|
+
"""
|
|
15
|
+
Represents an external service subscribing to receive webhooks from a
|
|
16
|
+
specific model.
|
|
19
17
|
"""
|
|
20
18
|
|
|
21
19
|
id = models.AutoField(primary_key=True)
|
|
22
20
|
name = models.CharField(
|
|
23
|
-
max_length=255,
|
|
24
|
-
help_text=_('Name of this webhook'),
|
|
21
|
+
max_length=255, help_text=_("Name of this subscriber")
|
|
25
22
|
)
|
|
23
|
+
description = models.TextField(blank=True)
|
|
26
24
|
|
|
27
|
-
#
|
|
25
|
+
# What model they're subscribing to
|
|
28
26
|
content_type = models.ForeignKey(
|
|
29
|
-
ContentType,
|
|
27
|
+
"contenttypes.ContentType",
|
|
30
28
|
on_delete=models.CASCADE,
|
|
31
|
-
help_text=_(
|
|
32
|
-
)
|
|
33
|
-
event_signals = models.JSONField(
|
|
34
|
-
default=list,
|
|
35
|
-
help_text=_(
|
|
36
|
-
'Event types that trigger this webhook (e.g., CREATE, UPDATE,'
|
|
37
|
-
' DELETE)'
|
|
38
|
-
),
|
|
29
|
+
help_text=_("The model this subscriber watches"),
|
|
39
30
|
)
|
|
40
31
|
|
|
41
|
-
#
|
|
42
|
-
|
|
43
|
-
max_length=
|
|
44
|
-
help_text=_(
|
|
32
|
+
# Endpoint configuration
|
|
33
|
+
target_url = models.CharField(
|
|
34
|
+
max_length=500, # Increased length for long URLs
|
|
35
|
+
help_text=_("The base URL for webhook delivery"),
|
|
45
36
|
)
|
|
46
37
|
secret = models.CharField(
|
|
47
38
|
max_length=64,
|
|
48
|
-
default=
|
|
39
|
+
default=generate_secret,
|
|
49
40
|
help_text=_(
|
|
50
|
-
|
|
41
|
+
"Secret key for webhook authentication via X-Secret header"
|
|
51
42
|
),
|
|
52
43
|
)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
44
|
+
|
|
45
|
+
serializer_class = models.CharField(
|
|
46
|
+
max_length=512,
|
|
56
47
|
blank=True,
|
|
48
|
+
validators=[validate_class_path],
|
|
57
49
|
help_text=_(
|
|
58
|
-
|
|
50
|
+
"Dot path to DRF serializer class "
|
|
51
|
+
"(e.g., 'myapp.serializers.MySerializer')"
|
|
59
52
|
),
|
|
60
53
|
)
|
|
61
54
|
|
|
62
|
-
#
|
|
63
|
-
|
|
64
|
-
default=
|
|
65
|
-
help_text=_('Whether to store the last response received'),
|
|
66
|
-
)
|
|
67
|
-
last_response = models.TextField(
|
|
68
|
-
blank=True,
|
|
69
|
-
null=True,
|
|
70
|
-
help_text=_('Last response received from the webhook endpoint'),
|
|
71
|
-
)
|
|
72
|
-
last_success = models.DateTimeField(
|
|
73
|
-
blank=True,
|
|
74
|
-
null=True,
|
|
75
|
-
help_text=_('Timestamp of last successful delivery'),
|
|
76
|
-
)
|
|
77
|
-
last_failure = models.DateTimeField(
|
|
55
|
+
# Headers and configuration
|
|
56
|
+
headers = models.JSONField(
|
|
57
|
+
default=dict,
|
|
78
58
|
blank=True,
|
|
79
|
-
|
|
80
|
-
help_text=_(
|
|
59
|
+
validators=[validate_headers],
|
|
60
|
+
help_text=_("Additional headers to send (JSON format)"),
|
|
81
61
|
)
|
|
82
62
|
|
|
83
|
-
#
|
|
63
|
+
# Delivery settings
|
|
84
64
|
max_retries = models.PositiveIntegerField(
|
|
85
|
-
default=3,
|
|
86
|
-
null=True,
|
|
87
|
-
blank=True,
|
|
88
|
-
help_text=_('Maximum number of delivery attempts'),
|
|
65
|
+
default=3, help_text=_("Max delivery attempts")
|
|
89
66
|
)
|
|
90
67
|
retry_delay = models.PositiveIntegerField(
|
|
91
|
-
default=60,
|
|
92
|
-
null=True,
|
|
93
|
-
blank=True,
|
|
94
|
-
help_text=_('Seconds to wait between retry attempts'),
|
|
68
|
+
default=60, help_text=_("Seconds between retries")
|
|
95
69
|
)
|
|
96
|
-
|
|
97
|
-
default=
|
|
98
|
-
null=True,
|
|
99
|
-
blank=True,
|
|
100
|
-
help_text=_(
|
|
101
|
-
'Whether to use async delivery (None = use system default)'
|
|
102
|
-
),
|
|
70
|
+
timeout = models.PositiveIntegerField(
|
|
71
|
+
default=30, help_text=_("Request timeout in seconds")
|
|
103
72
|
)
|
|
104
73
|
|
|
105
|
-
#
|
|
106
|
-
|
|
107
|
-
default=
|
|
108
|
-
help_text=_(
|
|
74
|
+
# Auto-disable settings
|
|
75
|
+
auto_disable_after_failures = models.PositiveIntegerField(
|
|
76
|
+
default=10,
|
|
77
|
+
help_text=_("Auto-disable after N consecutive failures (0 = never)"),
|
|
109
78
|
)
|
|
79
|
+
|
|
80
|
+
# Status tracking
|
|
81
|
+
is_active = models.BooleanField(default=True)
|
|
82
|
+
consecutive_failures = models.PositiveIntegerField(default=0)
|
|
83
|
+
last_success = models.DateTimeField(null=True, blank=True)
|
|
84
|
+
last_failure = models.DateTimeField(null=True, blank=True)
|
|
85
|
+
|
|
86
|
+
# Metadata
|
|
110
87
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
111
88
|
updated_at = models.DateTimeField(auto_now=True)
|
|
112
89
|
|
|
113
90
|
class Meta:
|
|
114
|
-
ordering = [
|
|
115
|
-
db_table =
|
|
116
|
-
verbose_name = _(
|
|
117
|
-
verbose_name_plural = _(
|
|
91
|
+
ordering = ["-created_at"]
|
|
92
|
+
db_table = "django_webhook_subscriber_webhook_subscriber"
|
|
93
|
+
verbose_name = _("Webhook Subscriber")
|
|
94
|
+
verbose_name_plural = _("Webhook Subscribers")
|
|
95
|
+
unique_together = (("target_url", "content_type"),)
|
|
96
|
+
indexes = [
|
|
97
|
+
models.Index(fields=["content_type", "is_active"]),
|
|
98
|
+
models.Index(fields=["is_active"]),
|
|
99
|
+
models.Index(fields=["content_type"]),
|
|
100
|
+
]
|
|
118
101
|
|
|
119
102
|
def __str__(self):
|
|
120
|
-
return f
|
|
103
|
+
return f"{self.name} ({self.target_url})"
|
|
121
104
|
|
|
105
|
+
def clean(self):
|
|
106
|
+
"""Additional validation."""
|
|
107
|
+
super().clean()
|
|
122
108
|
|
|
123
|
-
|
|
124
|
-
|
|
109
|
+
# Validate timeout is reasonable
|
|
110
|
+
if self.timeout > 300: # 5 minutes max
|
|
111
|
+
raise ValidationError("timeout cannot exceed 300 seconds")
|
|
125
112
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
113
|
+
# Validate retry settings
|
|
114
|
+
if self.retry_delay > 3600: # 1 hour max
|
|
115
|
+
raise ValidationError("retry_delay cannot exceed 3600 seconds")
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def model_class(self):
|
|
119
|
+
return self.content_type.model_class()
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def model_name(self):
|
|
123
|
+
return f"{self.content_type.app_label}.{self.content_type.model}"
|
|
124
|
+
|
|
125
|
+
def record_success(self):
|
|
126
|
+
"""Record a successful delivery."""
|
|
127
|
+
from django.utils import timezone
|
|
128
|
+
|
|
129
|
+
self.consecutive_failures = 0
|
|
130
|
+
self.last_success = timezone.now()
|
|
131
|
+
self.save(update_fields=["consecutive_failures", "last_success"])
|
|
132
|
+
|
|
133
|
+
def record_failure(self):
|
|
134
|
+
"""Record a failed delivery and handle auto-disable."""
|
|
135
|
+
|
|
136
|
+
self.consecutive_failures += 1
|
|
137
|
+
self.last_failure = timezone.now()
|
|
138
|
+
|
|
139
|
+
# Auto-disable if threshold reached
|
|
140
|
+
if (
|
|
141
|
+
self.auto_disable_after_failures > 0
|
|
142
|
+
and self.consecutive_failures >= self.auto_disable_after_failures
|
|
143
|
+
):
|
|
144
|
+
self.is_active = False
|
|
145
|
+
|
|
146
|
+
self.save(
|
|
147
|
+
update_fields=["consecutive_failures", "last_failure", "is_active"]
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
def save(self, *args, **kwargs):
|
|
151
|
+
self.full_clean() # Always validate
|
|
152
|
+
super().save(*args, **kwargs)
|
|
153
|
+
|
|
154
|
+
# Clear cache on save
|
|
155
|
+
from .delivery import clear_webhook_cache
|
|
156
|
+
|
|
157
|
+
clear_webhook_cache(content_type=self.content_type)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class WebhookSubscription(models.Model):
|
|
161
|
+
"""Individual event subscription for a subscriber."""
|
|
131
162
|
|
|
132
163
|
id = models.AutoField(primary_key=True)
|
|
133
|
-
|
|
134
|
-
|
|
164
|
+
subscriber = models.ForeignKey(
|
|
165
|
+
WebhookSubscriber,
|
|
135
166
|
on_delete=models.CASCADE,
|
|
136
|
-
related_name=
|
|
137
|
-
|
|
138
|
-
)
|
|
139
|
-
# Later on this could be a choice field
|
|
140
|
-
event_signal = models.CharField(
|
|
141
|
-
max_length=255,
|
|
142
|
-
help_text=_('The event type that triggered this delivery'),
|
|
167
|
+
related_name="subscriptions",
|
|
168
|
+
limit_choices_to=models.Q(is_active=True),
|
|
143
169
|
)
|
|
144
170
|
|
|
145
|
-
|
|
146
|
-
|
|
171
|
+
event_name = models.CharField(
|
|
172
|
+
max_length=100,
|
|
173
|
+
help_text=_("Event name (e.g., 'created', 'published', 'archived')"),
|
|
174
|
+
)
|
|
147
175
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
null=True,
|
|
176
|
+
custom_endpoint = models.CharField(
|
|
177
|
+
max_length=512,
|
|
151
178
|
blank=True,
|
|
152
|
-
help_text=_(
|
|
179
|
+
help_text=_("Optional custom endpoint path or full URL"),
|
|
153
180
|
)
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
181
|
+
|
|
182
|
+
# Status
|
|
183
|
+
is_active = models.BooleanField(default=True)
|
|
184
|
+
|
|
185
|
+
# Response handling
|
|
186
|
+
keep_last_response = models.BooleanField(
|
|
187
|
+
default=True,
|
|
188
|
+
help_text=_("Whether to store the last response received"),
|
|
158
189
|
)
|
|
159
|
-
|
|
160
|
-
null=True,
|
|
190
|
+
last_response = models.TextField(
|
|
161
191
|
blank=True,
|
|
162
|
-
help_text=_(
|
|
192
|
+
help_text=_("Last response received (truncated if too long)"),
|
|
163
193
|
)
|
|
194
|
+
last_success = models.DateTimeField(blank=True, null=True)
|
|
195
|
+
last_failure = models.DateTimeField(blank=True, null=True)
|
|
196
|
+
|
|
197
|
+
# Added tracking fields
|
|
198
|
+
consecutive_failures = models.PositiveIntegerField(default=0)
|
|
199
|
+
total_deliveries = models.PositiveIntegerField(default=0)
|
|
200
|
+
successful_deliveries = models.PositiveIntegerField(default=0)
|
|
201
|
+
|
|
202
|
+
# Metadata
|
|
203
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
204
|
+
updated_at = models.DateTimeField(auto_now=True)
|
|
205
|
+
|
|
206
|
+
class Meta:
|
|
207
|
+
ordering = ("subscriber__name", "event_name")
|
|
208
|
+
db_table = "django_webhook_subscriber_subscription"
|
|
209
|
+
verbose_name = _("Webhook Subscription")
|
|
210
|
+
verbose_name_plural = _("Webhook Subscriptions")
|
|
211
|
+
unique_together = (("subscriber", "event_name"),)
|
|
212
|
+
indexes = [
|
|
213
|
+
models.Index(fields=["subscriber", "event_name"]),
|
|
214
|
+
models.Index(fields=["is_active"]),
|
|
215
|
+
models.Index(fields=["subscriber", "is_active"]),
|
|
216
|
+
models.Index(fields=["event_name", "is_active"]),
|
|
217
|
+
]
|
|
218
|
+
|
|
219
|
+
def __str__(self):
|
|
220
|
+
return f"{self.subscriber.name} - {self.event_name}"
|
|
221
|
+
|
|
222
|
+
@property
|
|
223
|
+
def endpoint(self):
|
|
224
|
+
"""Full URL endpoint for this subscription."""
|
|
225
|
+
if not self.custom_endpoint:
|
|
226
|
+
return self.subscriber.target_url
|
|
227
|
+
|
|
228
|
+
# Check if custom_endpoint is a full URL
|
|
229
|
+
if self.custom_endpoint.startswith(("http://", "https://")):
|
|
230
|
+
return self.custom_endpoint
|
|
231
|
+
|
|
232
|
+
# Join base URL with endpoint path
|
|
233
|
+
base_url = self.subscriber.target_url.rstrip("/")
|
|
234
|
+
endpoint = self.custom_endpoint.lstrip("/")
|
|
235
|
+
return f"{base_url}/{endpoint}" if endpoint else base_url
|
|
236
|
+
|
|
237
|
+
@property
|
|
238
|
+
def success_rate(self):
|
|
239
|
+
"""Calculate success rate percentage."""
|
|
240
|
+
if self.total_deliveries == 0:
|
|
241
|
+
return None
|
|
242
|
+
return (self.successful_deliveries / self.total_deliveries) * 100
|
|
243
|
+
|
|
244
|
+
def record_delivery_attempt(self, success=False, response_text=None):
|
|
245
|
+
"""Record a delivery attempt."""
|
|
246
|
+
|
|
247
|
+
self.total_deliveries += 1
|
|
248
|
+
|
|
249
|
+
if success:
|
|
250
|
+
self.successful_deliveries += 1
|
|
251
|
+
self.consecutive_failures = 0
|
|
252
|
+
self.last_success = timezone.now()
|
|
253
|
+
else:
|
|
254
|
+
self.consecutive_failures += 1
|
|
255
|
+
self.last_failure = timezone.now()
|
|
256
|
+
|
|
257
|
+
# Store response if requested and not too large
|
|
258
|
+
if self.keep_last_response and response_text:
|
|
259
|
+
self.last_response = response_text[:1024] # Limit size
|
|
260
|
+
|
|
261
|
+
self.save(
|
|
262
|
+
update_fields=[
|
|
263
|
+
"total_deliveries",
|
|
264
|
+
"successful_deliveries",
|
|
265
|
+
"consecutive_failures",
|
|
266
|
+
"last_success",
|
|
267
|
+
"last_failure",
|
|
268
|
+
"last_response",
|
|
269
|
+
]
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# Proxy properties for backward compatibility
|
|
273
|
+
@property
|
|
274
|
+
def model_name(self):
|
|
275
|
+
return self.subscriber.model_name
|
|
276
|
+
|
|
277
|
+
@property
|
|
278
|
+
def content_type(self):
|
|
279
|
+
return self.subscriber.content_type
|
|
280
|
+
|
|
281
|
+
@property
|
|
282
|
+
def serializer_class(self):
|
|
283
|
+
return self.subscriber.serializer_class
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class WebhookDeliveryLog(models.Model):
|
|
287
|
+
"""Log of webhook delivery attempts."""
|
|
288
|
+
|
|
289
|
+
id = models.AutoField(primary_key=True)
|
|
290
|
+
subscription = models.ForeignKey(
|
|
291
|
+
WebhookSubscription,
|
|
292
|
+
on_delete=models.CASCADE,
|
|
293
|
+
related_name="delivery_logs", # Better name
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
# Retry tracking
|
|
297
|
+
attempt_number = models.PositiveSmallIntegerField(default=1)
|
|
298
|
+
is_retry = models.BooleanField(default=False)
|
|
299
|
+
|
|
300
|
+
# Payload and response data
|
|
301
|
+
payload = models.JSONField(default=dict)
|
|
302
|
+
response_status = models.PositiveIntegerField(null=True, blank=True)
|
|
303
|
+
response_body = models.TextField(blank=True)
|
|
304
|
+
response_headers = models.JSONField(default=dict, blank=True)
|
|
305
|
+
|
|
306
|
+
# Error tracking
|
|
307
|
+
error_message = models.TextField(blank=True)
|
|
308
|
+
|
|
309
|
+
# Delivery metadata
|
|
310
|
+
delivery_url = models.CharField(max_length=500)
|
|
311
|
+
delivery_duration_ms = models.PositiveIntegerField(null=True, blank=True)
|
|
164
312
|
|
|
165
313
|
# Metadata
|
|
166
314
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
167
315
|
|
|
168
316
|
# Manager
|
|
169
|
-
objects =
|
|
317
|
+
objects = WebhookDeliveryLogManager()
|
|
170
318
|
|
|
171
319
|
class Meta:
|
|
172
|
-
ordering = [
|
|
173
|
-
db_table =
|
|
174
|
-
verbose_name = _(
|
|
175
|
-
verbose_name_plural = _(
|
|
320
|
+
ordering = ["-created_at"]
|
|
321
|
+
db_table = "django_webhook_subscriber_webhook_delivery_log"
|
|
322
|
+
verbose_name = _("Webhook Delivery Log")
|
|
323
|
+
verbose_name_plural = _("Webhook Delivery Logs")
|
|
324
|
+
indexes = [
|
|
325
|
+
models.Index(fields=["subscription", "-created_at"]),
|
|
326
|
+
models.Index(fields=["response_status"]),
|
|
327
|
+
models.Index(fields=["created_at"]),
|
|
328
|
+
models.Index(fields=["attempt_number"]),
|
|
329
|
+
]
|
|
176
330
|
|
|
177
331
|
def __str__(self):
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
332
|
+
status = f" ({self.response_status})" if self.response_status else ""
|
|
333
|
+
retry_info = f" (retry {self.attempt_number})" if self.is_retry else ""
|
|
334
|
+
return f"{self.subscription}{status}{retry_info}"
|
|
335
|
+
|
|
336
|
+
@property
|
|
337
|
+
def event_name(self):
|
|
338
|
+
return self.subscription.event_name
|
|
339
|
+
|
|
340
|
+
@property
|
|
341
|
+
def is_success(self):
|
|
342
|
+
"""Check if delivery was successful."""
|
|
343
|
+
|
|
344
|
+
# If there's an error message, it means an exception occurred
|
|
345
|
+
if self.error_message:
|
|
346
|
+
return False
|
|
347
|
+
|
|
348
|
+
return self.response_status and 200 <= self.response_status < 300
|
|
349
|
+
|
|
350
|
+
@property
|
|
351
|
+
def is_client_error(self):
|
|
352
|
+
"""Check if error was client-side (4xx)."""
|
|
353
|
+
return self.response_status and 400 <= self.response_status < 500
|
|
354
|
+
|
|
355
|
+
@property
|
|
356
|
+
def is_server_error(self):
|
|
357
|
+
"""Check if error was server-side (5xx)."""
|
|
358
|
+
return self.response_status and 500 <= self.response_status < 600
|
|
@@ -1,14 +1,9 @@
|
|
|
1
|
-
"""Serializers for Django Webhook Subscriber
|
|
1
|
+
"""Serializers for Django Webhook Subscriber"""
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
Framework. It provides functionality to serialize model instances into a
|
|
5
|
-
format suitable for webhook payloads.
|
|
6
|
-
"""
|
|
3
|
+
from rest_framework.serializers import Serializer, ModelSerializer
|
|
7
4
|
|
|
8
|
-
from rest_framework import serializers
|
|
9
5
|
|
|
10
|
-
|
|
11
|
-
def serialize_webhook_instance(instance, field_serializer):
|
|
6
|
+
def serialize_webhook_instance(instance, field_serializer=None):
|
|
12
7
|
"""Default serializer for webhook events.
|
|
13
8
|
|
|
14
9
|
This function receives an instance, and serializes all its fields into a
|
|
@@ -17,30 +12,7 @@ def serialize_webhook_instance(instance, field_serializer):
|
|
|
17
12
|
ValueError.
|
|
18
13
|
"""
|
|
19
14
|
|
|
20
|
-
|
|
21
|
-
if not issubclass(field_serializer, serializers.Serializer):
|
|
22
|
-
raise ValueError(
|
|
23
|
-
'field_serializer must be a subclass of rest_framework.Serializer'
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
# Create an instance of the serializer
|
|
27
|
-
serializer = field_serializer(instance=instance)
|
|
28
|
-
# Serialize the instance
|
|
29
|
-
serialized_data = serializer.data
|
|
30
|
-
# Return the serialized data
|
|
31
|
-
return serialized_data
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def serialize_instance(instance, event_signal, field_serializer=None):
|
|
35
|
-
"""Serialize a model instance for a webhook event.
|
|
36
|
-
|
|
37
|
-
This function takes a model instance, an event type, and an optional
|
|
38
|
-
serializer class. It serializes the instance using the specified
|
|
39
|
-
serializer class. If no serializer class is provided, it falls back to
|
|
40
|
-
a default serializer that serializes all fields of the model instance.
|
|
41
|
-
"""
|
|
42
|
-
|
|
43
|
-
class DefaultWebhookSerializer(serializers.ModelSerializer):
|
|
15
|
+
class DefaultWebhookSerializer(ModelSerializer):
|
|
44
16
|
"""Default serializer class for webhook events.
|
|
45
17
|
|
|
46
18
|
This class is used to serialize all fields of a model instance into a
|
|
@@ -50,26 +22,20 @@ def serialize_instance(instance, event_signal, field_serializer=None):
|
|
|
50
22
|
|
|
51
23
|
class Meta:
|
|
52
24
|
model = instance.__class__
|
|
53
|
-
fields =
|
|
25
|
+
fields = "__all__"
|
|
54
26
|
|
|
55
|
-
# TODO: need to have this integrated with rest_framework serializers
|
|
56
27
|
if field_serializer is None:
|
|
57
28
|
field_serializer = DefaultWebhookSerializer
|
|
58
29
|
|
|
59
|
-
#
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
# Create the payload structure
|
|
65
|
-
payload = {
|
|
66
|
-
'pk': instance.pk,
|
|
67
|
-
'source': f'{app_label}.{model_name}',
|
|
68
|
-
'event_signal': event_signal,
|
|
69
|
-
'fields': serialize_webhook_instance(
|
|
70
|
-
instance,
|
|
71
|
-
field_serializer=field_serializer,
|
|
72
|
-
),
|
|
73
|
-
}
|
|
30
|
+
# Check that the field_serializer is a rest_framework serializer
|
|
31
|
+
if not issubclass(field_serializer, Serializer):
|
|
32
|
+
raise ValueError(
|
|
33
|
+
"field_serializer must be a subclass of rest_framework.Serializer"
|
|
34
|
+
)
|
|
74
35
|
|
|
75
|
-
|
|
36
|
+
# Create an instance of the serializer
|
|
37
|
+
serializer = field_serializer(instance=instance)
|
|
38
|
+
# Serialize the instance
|
|
39
|
+
serialized_data = serializer.data
|
|
40
|
+
# Return the serialized data
|
|
41
|
+
return serialized_data
|