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
@@ -1,208 +1,463 @@
1
- """Delivery functions for Django Webhook Subscriber."""
1
+ """Deliveries for Django Webhook Subscriber."""
2
2
 
3
- import requests
3
+ import logging
4
+ from collections import defaultdict
4
5
 
6
+ from django.core.cache import cache
5
7
  from django.utils import timezone
6
-
7
- from django_webhook_subscriber.conf import rest_webhook_settings
8
- from django_webhook_subscriber.tasks import (
9
- async_deliver_webhook,
10
- process_webhook_batch,
8
+ from django.utils.module_loading import import_string
9
+
10
+ from .conf import rest_webhook_settings as settings
11
+ from .serializers import serialize_webhook_instance
12
+ from .tasks import process_webhook_delivery_batch
13
+ from .utils import (
14
+ clear_content_type_cache,
15
+ get_content_type_id,
16
+ webhooks_disabled,
11
17
  )
12
18
 
19
+ logger = logging.getLogger(__name__)
13
20
 
14
- def prepare_headers(webhook):
15
- """Prepare headers for the webhook request."""
16
21
 
17
- headers = webhook.headers.copy() if webhook.headers else {}
22
+ class WebhookDeliveryProcessor:
23
+ def __init__(self):
24
+ self.cache_ttl = getattr(settings, "WEBHOOK_CACHE_TTL", 300)
18
25
 
19
- # Add content type header if not present
20
- if 'Content-Type' not in headers:
21
- headers['Content-Type'] = 'application/json'
26
+ def send_webhook(self, instance, event_name, context=None, **kwargs):
27
+ """Send webhooks for a given model instance and event."""
22
28
 
23
- # Add secret key authentication header
24
- headers['X-Secret'] = webhook.secret
29
+ try:
30
+ # Check if webhooks are globally disabled
31
+ if webhooks_disabled():
32
+ logger.info(
33
+ "Webhooks are disabled, skipping delivery.",
34
+ extra={"instance": instance, "event_name": event_name},
35
+ )
36
+ return {"skipped": "Webhooks disabled"}
25
37
 
26
- return headers
38
+ # Get active subscriptions for this model and event
39
+ subscriptions = self._get_subscriptions_cached(
40
+ instance, event_name
41
+ )
27
42
 
43
+ if not subscriptions:
44
+ logger.info(
45
+ "No subscriptions found, skipping delivery.",
46
+ extra={"instance": instance, "event_name": event_name},
47
+ )
48
+ return {"skipped": "No subscriptions"}
28
49
 
29
- def deliver_webhook(webhook, payload, event_signal):
30
- """Deliver the webhook to the specified endpoint."""
50
+ # Optimize payload generation by grouping serializer
51
+ subscription_groups = self._group_subscriptions_by_serializer(
52
+ subscriptions
53
+ )
31
54
 
32
- from django_webhook_subscriber.models import WebhookDeliveryLog
55
+ # Generate payloads for each serializer group
56
+ for serializer_class, subs in subscription_groups.items():
57
+ payload = self._generate_payload(
58
+ instance, event_name, serializer_class
59
+ )
33
60
 
34
- # Create delivery log
35
- delivery_log = WebhookDeliveryLog.objects.create(
36
- webhook=webhook,
37
- event_signal=event_signal,
38
- payload=payload,
39
- )
61
+ # Assign payload to all subscriptions in this group
62
+ for sub in subs:
63
+ sub["payload"] = payload
64
+
65
+ # Deliver the webhooks
66
+ result = self._deliver_webhooks(subscriptions)
67
+
68
+ logger.info(
69
+ f"Queued {len(subscriptions)} webhooks for {event_name}",
70
+ extra={"instance": instance, "event_name": event_name},
71
+ )
72
+
73
+ return result
40
74
 
41
- try:
42
- # Prepare request parameters
43
- headers = prepare_headers(webhook)
44
- timeout = getattr(rest_webhook_settings, 'REQUEST_TIMEOUT')
45
-
46
- # Send the request
47
- response = requests.post(
48
- webhook.endpoint,
49
- json=payload,
50
- headers=headers,
51
- timeout=timeout,
75
+ except Exception as e:
76
+ logger.error(
77
+ f"Error sending webhook: {e=}",
78
+ extra={"instance": instance, "event_name": event_name},
79
+ exc_info=True,
80
+ )
81
+ return {"error": f"Error sending webhook: {e=}"}
82
+
83
+ def _group_subscriptions_by_serializer(self, subscriptions):
84
+ """Group subscriptions by their serializer class."""
85
+
86
+ # Using defaultdict to avoid key errors
87
+ groups = defaultdict(list)
88
+ for sub in subscriptions:
89
+ serializer_key = sub.get("serializer_class") or None
90
+ groups[serializer_key].append(sub)
91
+
92
+ return groups
93
+
94
+ def _get_subscriptions_cached(self, instance, event_name):
95
+ """Retrieve active subscriptions with optimized caching."""
96
+
97
+ content_type_id = get_content_type_id(
98
+ instance._meta.app_label,
99
+ instance._meta.model_name,
52
100
  )
53
101
 
54
- # Update delivery log with response
55
- delivery_log.response_status = response.status_code
56
- delivery_log.response_body = response.content
57
- delivery_log.save()
102
+ if content_type_id is None:
103
+ logger.error(
104
+ f"Content type not found for {instance._meta.label}",
105
+ extra={"instance": instance},
106
+ )
107
+ return []
108
+
109
+ cache_key = f"webhook_subscriptions:{content_type_id}:{event_name}"
110
+
111
+ # Try to get from cache
112
+ subscriptions = cache.get(cache_key)
113
+
114
+ if subscriptions is None:
115
+ # Cache miss - fetch from database
116
+ subscriptions = self._fetch_subscriptions_from_db(
117
+ content_type_id, event_name
118
+ )
58
119
 
59
- # Update webhook with response info
60
- if webhook.keep_last_response:
61
- webhook.last_response = response.content
120
+ # Cache the result
121
+ cache.set(cache_key, subscriptions, self.cache_ttl)
62
122
 
63
- # Update success/failure timestamps
64
- now = timezone.now()
65
- if 200 <= response.status_code < 300:
66
- # if response.ok:
67
- webhook.last_success = now
123
+ logger.debug(
124
+ f"Cached {len(subscriptions)} subscriptions for {cache_key} "
125
+ "key"
126
+ )
68
127
  else:
69
- webhook.last_failure = now
128
+ logger.debug(
129
+ f"Cache hit for {cache_key}: {len(subscriptions)} "
130
+ "subscriptions"
131
+ )
132
+
133
+ return subscriptions
134
+
135
+ def _fetch_subscriptions_from_db(self, content_type_id, event_name):
136
+ """Fetch subscriptions from database with optimizations."""
137
+
138
+ from .models import WebhookSubscription
139
+
140
+ qs = (
141
+ WebhookSubscription.objects.select_related("subscriber")
142
+ .filter(
143
+ is_active=True,
144
+ event_name=event_name,
145
+ subscriber__is_active=True,
146
+ subscriber__content_type_id=content_type_id,
147
+ )
148
+ .only(
149
+ # Only load fields we need
150
+ "id",
151
+ "custom_endpoint",
152
+ "subscriber_id",
153
+ "subscriber__target_url",
154
+ "subscriber__serializer_class",
155
+ )
156
+ )
70
157
 
71
- webhook.save()
158
+ # Converting to lightweight dict format for caching
159
+ subscriptions = []
160
+ for sub in qs:
161
+ subscription_data = {
162
+ "id": sub.id,
163
+ "subscriber_id": sub.subscriber_id,
164
+ "url": sub.endpoint, # This property handles URL logic
165
+ "serializer_class": sub.subscriber.serializer_class,
166
+ }
167
+ subscriptions.append(subscription_data)
72
168
 
73
- except Exception as e:
74
- # Handle errors
75
- delivery_log.error_message = str(e)
76
- delivery_log.save()
169
+ return subscriptions
77
170
 
78
- # Update webhook failure timestamp
79
- webhook.last_failure = timezone.now()
80
- webhook.save()
171
+ def _generate_payload(self, instance, event_name, serializer_class_path):
172
+ """Generate webhook payload for a specific serializer."""
81
173
 
82
- return delivery_log
174
+ try:
175
+ serializer_class = None
176
+
177
+ # Import serializer class if specified
178
+ if serializer_class_path:
179
+ serializer_class = import_string(serializer_class_path)
180
+
181
+ # Serialize the instance
182
+ fields_data = serialize_webhook_instance(
183
+ instance, serializer_class
184
+ )
185
+
186
+ # Build standard payload structure
187
+ payload = {
188
+ "pk": instance.pk,
189
+ "event_signal": event_name,
190
+ "source": f"{instance._meta.app_label}."
191
+ f"{instance._meta.model_name}",
192
+ "timestamp": timezone.now().isoformat(),
193
+ "fields": fields_data,
194
+ }
195
+
196
+ return payload
197
+
198
+ except Exception as e:
199
+ logger.error(
200
+ f"Error generating payload: {e=}",
201
+ extra={"instance": instance, "event_name": event_name},
202
+ exc_info=True,
203
+ )
204
+
205
+ return {
206
+ "pk": getattr(instance, "pk", None),
207
+ "event_signal": event_name,
208
+ "source": f"{instance._meta.app_label}."
209
+ f"{instance._meta.model_name}",
210
+ "timestamp": timezone.now().isoformat(),
211
+ "error": f"Serialization failed: {e=}",
212
+ "fields": {},
213
+ }
214
+
215
+ def _deliver_webhooks(self, subscriptions):
216
+ """Deliver webhooks with batch processing and size limits."""
217
+
218
+ if not subscriptions:
219
+ return {"processed": 0}
220
+
221
+ # Splitting large batches to avoid overwhelming Celery
222
+ max_batch_size = getattr(settings, "MAX_BATCH_SIZE")
223
+
224
+ if len(subscriptions) <= max_batch_size:
225
+ # Single batch
226
+ return self._process_single_batch(subscriptions)
83
227
 
228
+ else:
229
+ # Multiple batches
230
+ return self._process_multiple_batches(
231
+ subscriptions, max_batch_size
232
+ )
84
233
 
85
- def get_webhook_for_model(model_instance):
86
- """Get all active webhooks for a model instance."""
234
+ def _process_single_batch(self, subscriptions):
235
+ """Process a single batch of webhook deliveries."""
87
236
 
88
- from django.contrib.contenttypes.models import ContentType
237
+ try:
238
+ result = process_webhook_delivery_batch.delay(subscriptions)
239
+ return {
240
+ "processed": len(subscriptions),
241
+ "batches": 1,
242
+ "task_id": result.id,
243
+ }
244
+
245
+ except Exception as e:
246
+ logger.error(f"Failed to queue webhook batch: {e=}", exc_info=True)
247
+ return {"error": f"{e=}", "processed": 0}
248
+
249
+ def _process_multiple_batches(self, subscriptions, batch_size):
250
+ """Process multiple batches of webhook deliveries."""
251
+
252
+ batches = []
253
+ task_ids = []
254
+ total_processed = 0
255
+
256
+ # Splitting into chunks
257
+ for i in range(0, len(subscriptions), batch_size):
258
+ batch = subscriptions[i:i + batch_size] # fmt: skip
259
+
260
+ try:
261
+ result = process_webhook_delivery_batch.delay(batch)
262
+ batches.append({"size": len(batch), "task_id": result.id})
263
+ task_ids.append(result.id)
264
+ total_processed += len(batch)
265
+
266
+ except Exception as e:
267
+ logger.error(
268
+ f"Failed to queue webhook batch {i//batch_size + 1}: {e=}"
269
+ )
270
+ batches.append({"size": len(batch), "error": f"{e=}"})
271
+ logger.info(
272
+ f"Queued {len(batches)} batches for {total_processed} webhook "
273
+ "deliveries"
274
+ )
89
275
 
90
- model_class = model_instance.__class__
91
- content_type = ContentType.objects.get_for_model(model_class)
276
+ return {
277
+ "processed": total_processed,
278
+ "batches": len(batches),
279
+ "batch_details": batches,
280
+ "task_ids": task_ids,
281
+ }
92
282
 
93
- from django_webhook_subscriber.models import WebhookRegistry
283
+ def clear_webhook_cache(self, content_type=None, event_name=None):
284
+ """Clear cached webhook subscription data."""
94
285
 
95
- # Get al active webhooks for this content type
96
- return WebhookRegistry.objects.filter(
97
- content_type=content_type,
98
- is_active=True,
99
- )
286
+ from .models import WebhookSubscription
100
287
 
288
+ if content_type and event_name:
289
+ # Clear specific cache key
290
+ cache_key = f"webhook_subscriptions:{content_type.id}:{event_name}"
291
+ cache.delete(cache_key)
292
+ logger.debug(f"Cleared cache for {cache_key}")
101
293
 
102
- def get_async_setting(webhook, system_default):
103
- """Determine if the webhook should use async delivery."""
294
+ elif content_type:
295
+ # Clear all cache keys for this content type
296
+ event_names = (
297
+ WebhookSubscription.objects.filter(
298
+ subscriber__content_type=content_type,
299
+ )
300
+ .values_list("event_name", flat=True)
301
+ .distinct()
302
+ )
303
+
304
+ cleared_count = 0
305
+ for event_name in event_names:
306
+ cache_key = (
307
+ f"webhook_subscriptions:{content_type.id}:{event_name}"
308
+ )
309
+ cache.delete(cache_key)
310
+ cleared_count += 1
311
+ logger.debug(
312
+ f"Cleared {cleared_count} cache keys for content type "
313
+ f"{content_type}"
314
+ )
104
315
 
105
- # If webhook has specific setting, use it
106
- if webhook.use_async is not None:
107
- return webhook.use_async
316
+ else:
317
+ # Clear all webhook caches (use pattern-based deletion if
318
+ # available)
319
+ if hasattr(cache, "delete_pattern"):
320
+ # Redis backend supports pattern deletion
321
+ deleted = cache.delete_pattern("webhook_subscriptions:*")
322
+ logger.debug(
323
+ f"Cleared {deleted} webhook cache keys using pattern"
324
+ )
325
+ else:
326
+ # Fallback: clear entire cache
327
+ cache.clear()
328
+ logger.warning(
329
+ "Cleared entire cache (pattern deletion not supported)"
330
+ )
108
331
 
109
- # Otherwise, use system default
110
- return system_default
332
+ def get_cache_stats(self):
333
+ """Get webhook cache statistics."""
111
334
 
335
+ # Get all possible cache keys
336
+ cache_keys = self._get_all_cache_keys()
112
337
 
113
- def process_and_deliver_webhook(
114
- instance,
115
- event_signal,
116
- serialized_payload=None,
117
- async_delivery=False,
118
- serializer=None,
119
- ):
120
- """Process and deliver the webhook for a given instance and event type.
338
+ stats = {
339
+ "total_possible_keys": len(cache_keys),
340
+ "cached_keys": 0,
341
+ "total_cached_subscriptions": 0,
342
+ "cache_hit_ratio": 0.0,
343
+ "key_details": [],
344
+ }
121
345
 
122
- This function checks the event type and the model class of the instance in
123
- the registry. If the event type is registered, it serializes the instance
124
- using the specified serializer (if any) and calls the delivery function to
125
- send the webhook.
126
- If the webhook is configured for async delivery, it will be processed
127
- asynchronously using Celery tasks. If the webhook is configured for
128
- synchronous delivery, it will be processed immediately.
129
- """
346
+ for key in cache_keys:
347
+ cached_data = cache.get(key)
130
348
 
131
- # Determine system default for async delivery
132
- system_default = getattr(rest_webhook_settings, 'DEFAULT_USE_ASYNC')
349
+ key_info = {
350
+ "key": key,
351
+ "is_cached": cached_data is not None,
352
+ "subscription_count": len(cached_data) if cached_data else 0,
353
+ }
133
354
 
134
- # If specific async_delivery is specified, override the system default
135
- if async_delivery is not None:
136
- system_default = async_delivery
355
+ if cached_data is not None:
356
+ stats["cached_keys"] += 1
357
+ stats["total_cached_subscriptions"] += len(cached_data)
137
358
 
138
- # If no payload is provided, serialize the instance
139
- if serialized_payload is None:
140
- from django_webhook_subscriber.serializers import serialize_instance
359
+ stats["key_details"].append(key_info)
141
360
 
142
- serialized_payload = serialize_instance(
143
- instance,
144
- event_signal,
145
- field_serializer=serializer,
146
- )
361
+ # Calculate hit ratio
362
+ if stats["total_possible_keys"] > 0:
363
+ stats["cache_hit_ratio"] = (
364
+ stats["cached_keys"] / stats["total_possible_keys"]
365
+ ) * 100
147
366
 
148
- # Get all active webhooks for this model
149
- webhooks = get_webhook_for_model(instance)
150
- delivery_logs = []
367
+ return stats
151
368
 
152
- # Filter webhooks for this model
153
- sync_webhooks = []
154
- async_webhooks = []
369
+ def _get_all_cache_keys(self):
370
+ """Get all possible webhook cache keys."""
155
371
 
156
- for webhook in webhooks:
157
- # Map event type to registry format
158
- event_mapping = {
159
- 'created': 'CREATE',
160
- 'updated': 'UPDATE',
161
- 'deleted': 'DELETE',
162
- }
163
- registry_event = event_mapping.get(event_signal, event_signal)
372
+ # Query all unique content_type + event_name combinations
373
+ from .models import WebhookSubscription
164
374
 
165
- # Skip if this webhook isn't configured for this event type
166
- if registry_event not in webhook.event_signals:
167
- continue
375
+ combinations = WebhookSubscription.objects.values_list(
376
+ "subscriber__content_type_id", "event_name"
377
+ ).distinct()
168
378
 
169
- # Check if this webhook should use async delivery
170
- if get_async_setting(webhook, system_default):
171
- async_webhooks.append(webhook)
172
- else:
173
- sync_webhooks.append(webhook)
379
+ cache_keys = []
380
+ for content_type_id, event_name in combinations:
381
+ cache_key = f"webhook_subscriptions:{content_type_id}:{event_name}"
382
+ cache_keys.append(cache_key)
174
383
 
175
- # Process async webhooks
176
- tasks_ids = []
177
- if async_webhooks:
178
- try:
179
- # If we only have one webhook, we'll process it directly
180
- if len(async_webhooks) == 1:
181
- webhook = async_webhooks[0]
182
- task_id = async_deliver_webhook.delay(
183
- webhook.id,
184
- serialized_payload,
185
- event_signal,
186
- )
187
- tasks_ids.append(task_id.id)
188
- else:
189
- # Otherwise, we'll process them in a batch
190
- webhook_ids = [w.id for w in async_webhooks]
191
- batch_task = process_webhook_batch.delay(
192
- webhook_ids,
193
- serialized_payload,
194
- event_signal,
195
- )
196
- tasks_ids.append(batch_task.id)
197
- except ImportError:
198
- # Celery is not installed, fall back to synchronous delivery
199
- sync_webhooks.extend(async_webhooks)
200
-
201
- # Process sync webhooks
202
- for webhook in sync_webhooks:
203
- delivery_log = deliver_webhook(
204
- webhook, serialized_payload, event_signal
384
+ return cache_keys
385
+
386
+
387
+ # Global instance of the delivery processor
388
+ webhook_delivery_processor = WebhookDeliveryProcessor()
389
+
390
+
391
+ def send_webhooks(instance, event_name, context=None, **kwargs):
392
+ """Send webhooks for a given model instance and event."""
393
+
394
+ return webhook_delivery_processor.send_webhook(
395
+ instance,
396
+ event_name,
397
+ context=context,
398
+ **kwargs,
399
+ )
400
+
401
+
402
+ def clear_webhook_cache(content_type=None, event_name=None):
403
+ """Clear all cached data from the delivery process."""
404
+
405
+ webhook_delivery_processor.clear_webhook_cache(
406
+ content_type=content_type, event_name=event_name
407
+ )
408
+ # Clear cached data from get_content_type_id function
409
+ clear_content_type_cache()
410
+
411
+
412
+ def get_webhook_cache_stats():
413
+ """Get statistics about the webhook subscription cache."""
414
+
415
+ return webhook_delivery_processor.get_cache_stats()
416
+
417
+
418
+ def warm_webhook_cache():
419
+ """Pre-fill the webhook cache by loading all active subscription
420
+ combinations."""
421
+
422
+ processor = webhook_delivery_processor
423
+ warmed_count = 0
424
+
425
+ from django.contrib.contenttypes.models import ContentType
426
+
427
+ from .models import WebhookSubscription
428
+
429
+ # Get all unique combinations of active subscriptions
430
+ combinations = (
431
+ WebhookSubscription.objects.filter(
432
+ is_active=True,
433
+ subscriber__is_active=True,
205
434
  )
206
- delivery_logs.append(delivery_log)
435
+ .values_list("subscriber__content_type_id", "event_name")
436
+ .distinct()
437
+ )
438
+
439
+ for content_type_id, event_name in combinations:
440
+ try:
441
+
442
+ content_type = ContentType.objects.get(id=content_type_id)
443
+ model_class = content_type.model_class()
207
444
 
208
- return delivery_logs + tasks_ids
445
+ if model_class:
446
+ # Create a minimal dummy instance for cache warming
447
+ dummy_instance = model_class()
448
+ dummy_instance._meta = model_class._meta
449
+
450
+ # This will populate the cache
451
+ processor._get_subscriptions_cached(dummy_instance, event_name)
452
+ warmed_count += 1
453
+
454
+ except Exception as e:
455
+ logger.warning(
456
+ f"Could not warm cache for {content_type_id}:{event_name}: "
457
+ f"{e=}"
458
+ )
459
+
460
+ logger.info(
461
+ f"Warmed webhook cache for {warmed_count} subscription combinations"
462
+ )
463
+ return {"warmed": warmed_count}
@@ -0,0 +1,51 @@
1
+ """HTTP session management for webhook delivery."""
2
+
3
+ from contextlib import contextmanager
4
+
5
+ import requests
6
+ from requests.adapters import HTTPAdapter
7
+
8
+
9
+ def create_webhook_session():
10
+ """Create an optimized session for webhook delivery."""
11
+
12
+ session = requests.Session()
13
+
14
+ # Configure adapter for connection pooling
15
+ adapter = HTTPAdapter(
16
+ pool_connections=2,
17
+ pool_maxsize=5, # Max connections per host
18
+ max_retries=0, # We handle the retries
19
+ )
20
+
21
+ # Mount adapter for both HTTP and HTTPS
22
+ session.mount("http://", adapter)
23
+ session.mount("https://", adapter)
24
+
25
+ # Set default handlers
26
+ session.headers.update(
27
+ {
28
+ "User-Agent": "Django-Webhook-Subscriber/2.0",
29
+ "Content-Type": "application/json",
30
+ }
31
+ )
32
+
33
+ return session
34
+
35
+
36
+ @contextmanager
37
+ def webhook_session():
38
+ """Context manager for webhook session.
39
+
40
+ Usage:
41
+ with webhook_session() as session:
42
+ # Use session to send requests like
43
+ response = session.post(url, json=payload)
44
+ ...
45
+ """
46
+
47
+ session = create_webhook_session()
48
+ try:
49
+ yield session
50
+ finally:
51
+ session.close()