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.
Files changed (41) hide show
  1. django_webhook_subscriber/__init__.py +7 -1
  2. django_webhook_subscriber/admin.py +831 -182
  3. django_webhook_subscriber/apps.py +3 -20
  4. django_webhook_subscriber/conf.py +11 -24
  5. django_webhook_subscriber/delivery.py +414 -159
  6. django_webhook_subscriber/http.py +51 -0
  7. django_webhook_subscriber/management/commands/webhook.py +169 -0
  8. django_webhook_subscriber/management/commands/webhook_cache.py +173 -0
  9. django_webhook_subscriber/management/commands/webhook_logs.py +226 -0
  10. django_webhook_subscriber/management/commands/webhook_performance_test.py +469 -0
  11. django_webhook_subscriber/management/commands/webhook_send.py +96 -0
  12. django_webhook_subscriber/management/commands/webhook_status.py +139 -0
  13. django_webhook_subscriber/managers.py +36 -14
  14. django_webhook_subscriber/migrations/0002_remove_webhookregistry_content_type_and_more.py +192 -0
  15. django_webhook_subscriber/models.py +291 -114
  16. django_webhook_subscriber/serializers.py +16 -50
  17. django_webhook_subscriber/tasks.py +434 -56
  18. django_webhook_subscriber/tests/factories.py +40 -0
  19. django_webhook_subscriber/tests/settings.py +27 -8
  20. django_webhook_subscriber/tests/test_delivery.py +453 -190
  21. django_webhook_subscriber/tests/test_http.py +32 -0
  22. django_webhook_subscriber/tests/test_managers.py +26 -37
  23. django_webhook_subscriber/tests/test_models.py +341 -251
  24. django_webhook_subscriber/tests/test_serializers.py +22 -56
  25. django_webhook_subscriber/tests/test_tasks.py +477 -189
  26. django_webhook_subscriber/tests/test_utils.py +98 -94
  27. django_webhook_subscriber/utils.py +87 -69
  28. django_webhook_subscriber/validators.py +53 -0
  29. django_webhook_subscriber-2.0.0.dist-info/METADATA +774 -0
  30. django_webhook_subscriber-2.0.0.dist-info/RECORD +38 -0
  31. django_webhook_subscriber/management/commands/check_webhook_tasks.py +0 -113
  32. django_webhook_subscriber/management/commands/clean_webhook_logs.py +0 -65
  33. django_webhook_subscriber/management/commands/test_webhook.py +0 -96
  34. django_webhook_subscriber/signals.py +0 -152
  35. django_webhook_subscriber/testing.py +0 -14
  36. django_webhook_subscriber/tests/test_signals.py +0 -268
  37. django_webhook_subscriber-1.0.0.dist-info/METADATA +0 -448
  38. django_webhook_subscriber-1.0.0.dist-info/RECORD +0 -33
  39. {django_webhook_subscriber-1.0.0.dist-info → django_webhook_subscriber-2.0.0.dist-info}/WHEEL +0 -0
  40. {django_webhook_subscriber-1.0.0.dist-info → django_webhook_subscriber-2.0.0.dist-info}/licenses/LICENSE +0 -0
  41. {django_webhook_subscriber-1.0.0.dist-info → django_webhook_subscriber-2.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,469 @@
1
+ """webhook_send command for Django Webhook Subscriber."""
2
+
3
+ import statistics
4
+ import time
5
+ from collections import defaultdict
6
+ from concurrent.futures import ThreadPoolExecutor, as_completed
7
+
8
+ from django.apps import apps
9
+ from django.contrib.contenttypes.models import ContentType
10
+ from django.core.management.base import BaseCommand, CommandError
11
+ from django.utils import timezone
12
+ from django_webhook_subscriber.delivery import send_webhooks
13
+ from django_webhook_subscriber.models import (
14
+ WebhookDeliveryLog,
15
+ WebhookSubscription,
16
+ )
17
+
18
+
19
+ class Command(BaseCommand):
20
+ help = "Performance test webhook system with concurrent deliveries"
21
+
22
+ def add_arguments(self, parser):
23
+ # Test configuration
24
+ parser.add_argument(
25
+ "--model",
26
+ type=str,
27
+ required=True,
28
+ help="Model in format app_label.ModelName",
29
+ )
30
+ parser.add_argument(
31
+ "--event",
32
+ type=str,
33
+ required=True,
34
+ help="Event name to test",
35
+ )
36
+ parser.add_argument(
37
+ "--object-ids",
38
+ type=str,
39
+ help="Comma-separated list of object IDs to test "
40
+ "(e.g., '1,2,3,4,5')",
41
+ )
42
+ parser.add_argument(
43
+ "--object-count",
44
+ type=int,
45
+ help="Number of random objects to test (alternative to "
46
+ "--object-ids)",
47
+ )
48
+
49
+ # Concurrency settings
50
+ parser.add_argument(
51
+ "--concurrent-webhooks",
52
+ type=int,
53
+ default=10,
54
+ help="Number of concurrent webhook sends (default: 10)",
55
+ )
56
+ parser.add_argument(
57
+ "--batches",
58
+ type=int,
59
+ default=1,
60
+ help="Number of batches to run (default: 1)",
61
+ )
62
+
63
+ # Test options
64
+ parser.add_argument(
65
+ "--dry-run",
66
+ action="store_true",
67
+ help="Show what would be tested without actually sending webhooks",
68
+ )
69
+ parser.add_argument(
70
+ "--measure-delivery",
71
+ action="store_true",
72
+ help="Wait for and measure actual webhook delivery times.",
73
+ )
74
+ parser.add_argument(
75
+ "--timeout",
76
+ type=int,
77
+ default=60,
78
+ help="Timeout in seconds to wait for delivery when measuring "
79
+ "(default: 60)",
80
+ )
81
+ parser.add_argument(
82
+ "--warmup",
83
+ action="store_true",
84
+ help="Run a warmup round before the actual test",
85
+ )
86
+
87
+ def handle(self, *args, **options):
88
+ # Validate and setup
89
+ model_class, test_objects = self.setup_test(options)
90
+
91
+ # Show test plan
92
+ self.show_test_plan(model_class, test_objects, options)
93
+
94
+ # Warmup if requested
95
+ if options["warmup"]:
96
+ self.run_warmup(test_objects, options)
97
+
98
+ # Run the actual performance tests
99
+ results = self.run_performance_test(test_objects, options)
100
+
101
+ # Show results
102
+ self.show_results(results, options)
103
+
104
+ def setup_test(self, options):
105
+ # Parse and validate model
106
+ try:
107
+ app_label, model_name = options["model"].split(".")
108
+ model_class = apps.get_model(app_label, model_name)
109
+ except (ValueError, LookupError) as e:
110
+ raise CommandError(f"Invalid model: {options['model']}: {e}")
111
+
112
+ # Get test objects
113
+ if options.get("object_ids"):
114
+ object_ids = [
115
+ int(x.strip()) for x in options["object_ids"].split(",")
116
+ ]
117
+ test_objects = list(model_class.objects.filter(id__in=object_ids))
118
+
119
+ if len(test_objects) != len(object_ids):
120
+ found_ids = [obj.id for obj in test_objects]
121
+ missing_ids = set(object_ids) - set(found_ids)
122
+ raise CommandError(f"Objects not found: {missing_ids}")
123
+
124
+ elif options.get("object_count"):
125
+ count = options["object_count"]
126
+ test_objects = list(model_class.objects.all()[:count])
127
+
128
+ if len(test_objects) < count:
129
+ raise CommandError(
130
+ f"Only {len(test_objects)} objects available, {count} "
131
+ "requested"
132
+ )
133
+ else:
134
+ raise CommandError("Specify either --object-ids or --object-count")
135
+
136
+ # Verify subscriptions exist
137
+ content_type = ContentType.objects.get_for_model(model_class)
138
+ subscription_count = WebhookSubscription.objects.filter(
139
+ subscriber__content_type=content_type,
140
+ event_name=options["event"],
141
+ is_active=True,
142
+ subscriber__is_active=True,
143
+ ).count()
144
+
145
+ if subscription_count == 0:
146
+ raise CommandError(
147
+ f"No active subscriptions found for {options['model']} - "
148
+ f"{options['event']}"
149
+ )
150
+
151
+ return model_class, test_objects
152
+
153
+ def show_test_plan(self, model_class, test_objects, options):
154
+
155
+ total_webhooks = len(test_objects) * options["batches"]
156
+ concurrent = options["concurrent_webhooks"]
157
+
158
+ self.stdout.write(self.style.SUCCESS("Performance Test Plan"))
159
+ self.stdout.write("-" * 40)
160
+ self.stdout.write(f"Model: {model_class.__name__}")
161
+ self.stdout.write(f"Event: {options['event']}")
162
+ self.stdout.write(f"Objects to test: {len(test_objects)}")
163
+ self.stdout.write(f"Batches: {options['batches']}")
164
+ self.stdout.write(f"Total webhooks: {total_webhooks}")
165
+ self.stdout.write(f"Concurrent sends: {concurrent}")
166
+
167
+ if options["measure_delivery"]:
168
+ self.stdout.write(f"Delivery timeout: {options['timeout']}s")
169
+
170
+ if options["dry_run"]:
171
+ self.stdout.write(
172
+ self.style.WARNING("DRY RUN - No webhooks will be sent")
173
+ )
174
+
175
+ def run_warmup(self, test_objects, options):
176
+ self.stdout.write("\nRunning warmup...")
177
+
178
+ # Use first object for warmup
179
+ warmup_object = test_objects[0]
180
+
181
+ start_time = time.time()
182
+ result = send_webhooks(warmup_object, options["event"])
183
+ self.stdout.write(
184
+ f"Warmup completed in {time.time() - start_time:.2f}s: {result}"
185
+ )
186
+
187
+ # Give systems time to settle
188
+ time.sleep(2)
189
+
190
+ def run_performance_test(self, test_objects, options):
191
+ self.stdout.write("\nStarting performance test...")
192
+
193
+ all_results = []
194
+
195
+ for batch_num in range(options["batches"]):
196
+ if options["batches"] > 1:
197
+ self.stdout.write(
198
+ f"\nBatch {batch_num + 1}/{options['batches']}"
199
+ )
200
+
201
+ batch_results = self.run_single_batch(test_objects, options)
202
+ all_results.extend(batch_results)
203
+
204
+ return all_results
205
+
206
+ def run_single_batch(self, test_objects, options):
207
+ concurrent = options["concurrent_webhooks"]
208
+ batch_results = []
209
+
210
+ # Prepare tasks
211
+ tasks = []
212
+ for obj in test_objects:
213
+ tasks.append(
214
+ {
215
+ "object": obj,
216
+ "event": options["event"],
217
+ "start_logs": None, # will be set before sending
218
+ }
219
+ )
220
+
221
+ # Record initial log state if measuring delivery
222
+ if options["measure_delivery"]:
223
+ for task in tasks:
224
+ task["start_logs"] = WebhookDeliveryLog.objects.filter(
225
+ subscription__subscriber__content_type__model=obj._meta.model_name.lower(), # noqa: E501
226
+ created_at__gte=timezone.now(),
227
+ ).count()
228
+
229
+ # Execute concurrent webhook sends
230
+ start_time = time.time()
231
+
232
+ with ThreadPoolExecutor(max_workers=concurrent) as executor:
233
+ # Submit all tasks
234
+ future_to_task = {
235
+ executor.submit(self.send_single_webhook, task): task
236
+ for task in tasks
237
+ }
238
+
239
+ # Collect results
240
+ for future in as_completed(future_to_task):
241
+ task = future_to_task[future]
242
+ try:
243
+ result = future.result()
244
+ task["send_result"] = result
245
+ task["send_duration"] = result.get("duration", 0)
246
+
247
+ except Exception as e:
248
+ task["send_result"] = {"error": f"Exception: {e=}"}
249
+ task["send_duration"] = 0
250
+
251
+ batch_send_duration = time.time() - start_time
252
+
253
+ # Measure delivery times if requested
254
+ if options["measure_delivery"]:
255
+ self.stdout.write("Measuring delivery times...")
256
+ self.measure_delivery_times(tasks, options["timeout"])
257
+
258
+ # Compile batch results
259
+ batch_summary = {
260
+ "send_duration": batch_send_duration,
261
+ "tasks": tasks,
262
+ "concurrent_level": concurrent,
263
+ "timestamp": timezone.now(),
264
+ }
265
+
266
+ batch_results.append(batch_summary)
267
+
268
+ # Show batch summary
269
+ successful_sends = sum(
270
+ 1 for task in tasks if not task["send_result"].get("error")
271
+ )
272
+ self.stdout.write(
273
+ f"Batch completed: {successful_sends}/{len(tasks)} successful "
274
+ f"in {batch_send_duration:.2f}s"
275
+ )
276
+
277
+ return batch_results
278
+
279
+ def send_single_webhook(self, task):
280
+ """Send a single webhook and measure timing."""
281
+ obj = task["object"]
282
+ event = task["event"]
283
+
284
+ start_time = time.time()
285
+
286
+ try:
287
+ result = send_webhooks(obj, event)
288
+ duration = time.time() - start_time
289
+
290
+ return {
291
+ "success": True,
292
+ "result": result,
293
+ "duration": duration,
294
+ "object_id": obj.id,
295
+ }
296
+ except Exception as e:
297
+ duration = time.time() - start_time
298
+
299
+ return {
300
+ "success": False,
301
+ "error": str(e),
302
+ "duration": duration,
303
+ "object_id": obj.id,
304
+ }
305
+
306
+ def measure_delivery_times(self, tasks, timeout):
307
+ """Measure actual webhook delivery times."""
308
+ start_time = time.time()
309
+ measured_count = 0
310
+
311
+ while (time.time() - start_time) < timeout:
312
+ for task in tasks:
313
+ if task.get("delivery_measured"):
314
+ continue
315
+
316
+ # Check for new delivery logs
317
+ obj = task["object"]
318
+ new_logs = WebhookDeliveryLog.objects.filter(
319
+ subscription__subscriber__content_type__model=obj._meta.model_name.lower(), # noqa: E501
320
+ created_at__gte=task.get(
321
+ "send_start_time", timezone.now()
322
+ ),
323
+ payload__pk=obj.id,
324
+ ).order_by("-created_at")
325
+
326
+ if new_logs.exists():
327
+ latest_log = new_logs.first()
328
+ task["delivery_duration"] = latest_log.delivery_duration_ms
329
+ task["delivery_status"] = latest_log.response_status
330
+ task["delivery_measured"] = True
331
+ measured_count += 1
332
+
333
+ # Check if all deliveries measured
334
+ if measured_count >= len(tasks):
335
+ break
336
+
337
+ time.sleep(0.5) # Check every 500ms
338
+
339
+ measured_percentage = (measured_count / len(tasks)) * 100
340
+ self.stdout.write(
341
+ f"Measured {measured_count}/{len(tasks)} deliveries "
342
+ f"({measured_percentage:.1f}%)"
343
+ )
344
+
345
+ def show_results(self, all_results, options):
346
+ """Display comprehensive test results."""
347
+ if not all_results:
348
+ return
349
+
350
+ self.stdout.write("\n" + "=" * 60)
351
+ self.stdout.write("PERFORMANCE TEST RESULTS")
352
+ self.stdout.write("=" * 60)
353
+
354
+ # Aggregate statistics
355
+ all_tasks = []
356
+ total_send_time = 0
357
+
358
+ for batch in all_results:
359
+ all_tasks.extend(batch["tasks"])
360
+ total_send_time += batch["send_duration"]
361
+
362
+ successful_sends = sum(
363
+ 1 for task in all_tasks if not task["send_result"].get("error")
364
+ )
365
+ send_durations = [
366
+ task["send_duration"]
367
+ for task in all_tasks
368
+ if task.get("send_duration")
369
+ ]
370
+
371
+ # Send performance metrics
372
+ self.stdout.write("\nSend Performance:")
373
+ self.stdout.write(f" Total webhooks: {len(all_tasks)}")
374
+ self.stdout.write(f" Successful: {successful_sends}")
375
+ self.stdout.write(f" Failed: {len(all_tasks) - successful_sends}")
376
+ self.stdout.write(
377
+ f" Success rate: {(successful_sends/len(all_tasks)*100):.1f}%"
378
+ )
379
+ self.stdout.write(f" Total time: {total_send_time:.2f}s")
380
+ self.stdout.write(
381
+ f" Throughput: {len(all_tasks)/total_send_time:.1f} webhooks/sec"
382
+ )
383
+
384
+ if send_durations:
385
+ self.stdout.write(
386
+ f" Avg send time: {statistics.mean(send_durations):.3f}s"
387
+ )
388
+ self.stdout.write(f" Min send time: {min(send_durations):.3f}s")
389
+ self.stdout.write(f" Max send time: {max(send_durations):.3f}s")
390
+
391
+ if len(send_durations) > 1:
392
+ self.stdout.write(
393
+ " Send time std dev: "
394
+ f"{statistics.stdev(send_durations):.3f}s"
395
+ )
396
+
397
+ # Delivery performance metrics (if measured)
398
+ delivery_tasks = [
399
+ task for task in all_tasks if task.get("delivery_measured")
400
+ ]
401
+
402
+ if delivery_tasks:
403
+ self.stdout.write("\nDelivery Performance:")
404
+ delivery_times = [
405
+ task["delivery_duration"]
406
+ for task in delivery_tasks
407
+ if task.get("delivery_duration")
408
+ ]
409
+ successful_deliveries = sum(
410
+ 1
411
+ for task in delivery_tasks
412
+ if task.get("delivery_status", 0) in range(200, 300)
413
+ )
414
+
415
+ self.stdout.write(f" Measured deliveries: {len(delivery_tasks)}")
416
+ self.stdout.write(
417
+ f" Successful deliveries: {successful_deliveries}"
418
+ )
419
+ self.stdout.write(
420
+ " Delivery success rate: "
421
+ f"{(successful_deliveries/len(delivery_tasks)*100):.1f}%"
422
+ )
423
+
424
+ if delivery_times:
425
+ self.stdout.write(
426
+ " Avg delivery time: "
427
+ f"{statistics.mean(delivery_times):.0f}ms"
428
+ )
429
+ self.stdout.write(
430
+ f" Min delivery time: {min(delivery_times)}ms"
431
+ )
432
+ self.stdout.write(
433
+ f" Max delivery time: {max(delivery_times)}ms"
434
+ )
435
+
436
+ if len(delivery_times) > 1:
437
+ self.stdout.write(
438
+ " Delivery time std dev: "
439
+ f"{statistics.stdev(delivery_times):.0f}ms"
440
+ )
441
+
442
+ # Error analysis
443
+ errors = defaultdict(int)
444
+ for task in all_tasks:
445
+ error = task["send_result"].get("error")
446
+ if error:
447
+ errors[error] += 1
448
+
449
+ if errors:
450
+ self.stdout.write("\nError Analysis:")
451
+ for error, count in sorted(
452
+ errors.items(), key=lambda x: x[1], reverse=True
453
+ ):
454
+ self.stdout.write(f" {error}: {count} occurrences")
455
+
456
+ # Concurrency analysis
457
+ if len(all_results) > 1:
458
+ self.stdout.write("\nBatch Analysis:")
459
+ for i, batch in enumerate(all_results):
460
+ batch_successful = sum(
461
+ 1
462
+ for task in batch["tasks"]
463
+ if not task["send_result"].get("error")
464
+ )
465
+ self.stdout.write(
466
+ f" Batch {i+1}: {batch_successful}/{len(batch['tasks'])} "
467
+ "successful "
468
+ f"in {batch['send_duration']:.2f}s"
469
+ )
@@ -0,0 +1,96 @@
1
+ """webhook_send command for Django Webhook Subscriber."""
2
+
3
+ import json
4
+
5
+ from django.apps import apps
6
+ from django.core.management.base import BaseCommand, CommandError
7
+ from django_webhook_subscriber.delivery import send_webhooks
8
+
9
+
10
+ class Command(BaseCommand):
11
+ help = "Manually send webhooks for testing"
12
+
13
+ def add_arguments(self, parser):
14
+ parser.add_argument(
15
+ "model",
16
+ type=str,
17
+ help="Model in format app_label.ModelName",
18
+ )
19
+ parser.add_argument(
20
+ "object_id",
21
+ type=int,
22
+ help="ID of the object to send webhook for",
23
+ )
24
+ parser.add_argument(
25
+ "event_name",
26
+ type=str,
27
+ help="Event name (e.g., created, updated, deleted)",
28
+ )
29
+ parser.add_argument(
30
+ "--context",
31
+ type=str,
32
+ help="Additional context as JSON string",
33
+ )
34
+ parser.add_argument(
35
+ "--async",
36
+ action="store_true",
37
+ help="Send webhooks asynchronously (default behavior)",
38
+ )
39
+
40
+ def handle(self, *args, **options):
41
+ # Parse model
42
+ try:
43
+ app_label, model_name = options["model"].split(".")
44
+ model_class = apps.get_model(app_label, model_name)
45
+ except (ValueError, LookupError) as e:
46
+ raise CommandError(f"Invalid model: {options['model']}: {e}")
47
+
48
+ # Get object
49
+ try:
50
+ instance = model_class.objects.get(id=options["object_id"])
51
+ except model_class.DoesNotExist:
52
+ raise CommandError(
53
+ f"{model_class.__name__} with ID {options['object_id']} not "
54
+ "found"
55
+ )
56
+
57
+ # Parse context
58
+ context = None
59
+ if options.get("context"):
60
+ try:
61
+ context = json.loads(options["context"])
62
+ except json.JSONDecodeError:
63
+ raise CommandError("Invalid JSON in context")
64
+
65
+ self.stdout.write(
66
+ f"Sending webhook for {model_class.__name__} "
67
+ f"(ID: {instance.id}) - Event: {options['event_name']}"
68
+ )
69
+
70
+ try:
71
+ result = send_webhooks(
72
+ instance=instance,
73
+ event_name=options["event_name"],
74
+ context=context,
75
+ )
76
+
77
+ if "error" in result:
78
+ self.stdout.write(
79
+ self.style.ERROR(f"Error: {result['error']}")
80
+ )
81
+ elif "skipped" in result:
82
+ self.stdout.write(
83
+ self.style.WARNING(f"Skipped: {result['skipped']}")
84
+ )
85
+ else:
86
+ processed = result.get("processed", 0)
87
+ batches = result.get("batches", 1)
88
+ self.stdout.write(
89
+ self.style.SUCCESS(
90
+ f"Queued {processed} webhook deliveries in {batches} "
91
+ "batch(es)"
92
+ )
93
+ )
94
+
95
+ except Exception as e:
96
+ raise CommandError(f"Error sending webhook: {e}")
@@ -0,0 +1,139 @@
1
+ """webhook_status command for Django Webhook Subscriber."""
2
+
3
+ from django.core.management.base import BaseCommand
4
+ from django.db import models
5
+ from django.utils import timezone
6
+ from datetime import timedelta
7
+
8
+ from django_webhook_subscriber.models import (
9
+ WebhookSubscriber,
10
+ WebhookSubscription,
11
+ WebhookDeliveryLog,
12
+ )
13
+
14
+
15
+ class Command(BaseCommand):
16
+ help = "Show overall webhook system status"
17
+
18
+ def add_arguments(self, parser):
19
+ parser.add_argument(
20
+ "--detailed",
21
+ action="store_true",
22
+ help="Show detailed status for each subscriber",
23
+ )
24
+
25
+ def handle(self, *args, **options):
26
+ self.show_overview()
27
+
28
+ if options["detailed"]:
29
+ self.show_detailed_status()
30
+
31
+ def show_overview(self):
32
+ """Show system overview."""
33
+ # Basic counts
34
+ total_subscribers = WebhookSubscriber.objects.count()
35
+ active_subscribers = WebhookSubscriber.objects.filter(
36
+ is_active=True
37
+ ).count()
38
+ total_subscriptions = WebhookSubscription.objects.count()
39
+ active_subscriptions = WebhookSubscription.objects.filter(
40
+ is_active=True
41
+ ).count()
42
+
43
+ # Health metrics
44
+ failing_subscribers = WebhookSubscriber.objects.filter(
45
+ consecutive_failures__gt=0
46
+ ).count()
47
+
48
+ critical_subscribers = WebhookSubscriber.objects.filter(
49
+ consecutive_failures__gte=5
50
+ ).count()
51
+
52
+ # Recent activity (last 24 hours)
53
+ last_24h = timezone.now() - timedelta(hours=24)
54
+ recent_deliveries = WebhookDeliveryLog.objects.filter(
55
+ created_at__gte=last_24h
56
+ ).count()
57
+
58
+ recent_successes = WebhookDeliveryLog.objects.filter(
59
+ created_at__gte=last_24h,
60
+ response_status__gte=200,
61
+ response_status__lt=300,
62
+ ).count()
63
+
64
+ success_rate_24h = (
65
+ (recent_successes / recent_deliveries * 100)
66
+ if recent_deliveries > 0
67
+ else 0
68
+ )
69
+
70
+ self.stdout.write(self.style.SUCCESS("Webhook System Status"))
71
+ self.stdout.write("=" * 50)
72
+
73
+ self.stdout.write(
74
+ f"Subscribers: {active_subscribers}/{total_subscribers} active"
75
+ )
76
+ self.stdout.write(
77
+ f"Subscriptions: {active_subscriptions}/{total_subscriptions} "
78
+ "active"
79
+ )
80
+ self.stdout.write(
81
+ f"Health: {failing_subscribers} failing, {critical_subscribers} "
82
+ "critical"
83
+ )
84
+
85
+ self.stdout.write("\nLast 24 Hours:")
86
+ self.stdout.write(f"Deliveries: {recent_deliveries}")
87
+ self.stdout.write(f"Success rate: {success_rate_24h:.1f}%")
88
+
89
+ def show_detailed_status(self):
90
+ """Show detailed status for each subscriber."""
91
+ subscribers = WebhookSubscriber.objects.prefetch_related(
92
+ "subscriptions"
93
+ ).annotate(
94
+ total_subscriptions=models.Count("subscriptions"),
95
+ active_subscriptions=models.Count(
96
+ "subscriptions", filter=models.Q(subscriptions__is_active=True)
97
+ ),
98
+ )
99
+
100
+ self.stdout.write("\n" + "=" * 80)
101
+ self.stdout.write("DETAILED SUBSCRIBER STATUS")
102
+ self.stdout.write("=" * 80)
103
+
104
+ for subscriber in subscribers:
105
+ # Health indicator
106
+ if not subscriber.is_active:
107
+ health = "DISABLED"
108
+ style = self.style.WARNING
109
+ elif subscriber.consecutive_failures == 0:
110
+ health = "HEALTHY"
111
+ style = self.style.SUCCESS
112
+ elif subscriber.consecutive_failures < 5:
113
+ health = (
114
+ f"WARNING ({subscriber.consecutive_failures} failures)"
115
+ )
116
+ style = self.style.WARNING
117
+ else:
118
+ health = (
119
+ f"CRITICAL ({subscriber.consecutive_failures} failures)"
120
+ )
121
+ style = self.style.ERROR
122
+
123
+ self.stdout.write(style(f"\n{subscriber.name} - {health}"))
124
+ self.stdout.write(f" URL: {subscriber.target_url}")
125
+ self.stdout.write(
126
+ f" Subscriptions: {subscriber.active_subscriptions}/"
127
+ f"{subscriber.total_subscriptions} active"
128
+ )
129
+
130
+ if subscriber.last_success:
131
+ self.stdout.write(
132
+ " Last success: "
133
+ f"{subscriber.last_success.strftime('%Y-%m-%d %H:%M')}"
134
+ )
135
+ if subscriber.last_failure:
136
+ self.stdout.write(
137
+ " Last failure: "
138
+ f"{subscriber.last_failure.strftime('%Y-%m-%d %H:%M')}"
139
+ )