karrio-server-events 2025.5rc1__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 (44) hide show
  1. karrio/server/events/__init__.py +3 -0
  2. karrio/server/events/admin.py +3 -0
  3. karrio/server/events/apps.py +11 -0
  4. karrio/server/events/filters.py +63 -0
  5. karrio/server/events/migrations/0001_initial.py +65 -0
  6. karrio/server/events/migrations/0002_event.py +39 -0
  7. karrio/server/events/migrations/0003_auto_20220303_1210.py +25 -0
  8. karrio/server/events/migrations/0004_custom_migration_2022_4.py +41 -0
  9. karrio/server/events/migrations/0005_event_event_object_idx.py +18 -0
  10. karrio/server/events/migrations/0006_webhook_events_alter_event_data.py +85 -0
  11. karrio/server/events/migrations/0007_auto_20221130_0255.py +30 -0
  12. karrio/server/events/migrations/0008_alter_event_type.py +18 -0
  13. karrio/server/events/migrations/0009_alter_webhook_enabled_events.py +45 -0
  14. karrio/server/events/migrations/__init__.py +0 -0
  15. karrio/server/events/models.py +83 -0
  16. karrio/server/events/router.py +3 -0
  17. karrio/server/events/serializers/__init__.py +1 -0
  18. karrio/server/events/serializers/base.py +63 -0
  19. karrio/server/events/serializers/event.py +9 -0
  20. karrio/server/events/serializers/webhook.py +27 -0
  21. karrio/server/events/signals.py +138 -0
  22. karrio/server/events/task_definitions/__init__.py +1 -0
  23. karrio/server/events/task_definitions/base/__init__.py +64 -0
  24. karrio/server/events/task_definitions/base/archiving.py +58 -0
  25. karrio/server/events/task_definitions/base/tracking.py +227 -0
  26. karrio/server/events/task_definitions/base/webhook.py +116 -0
  27. karrio/server/events/tasks.py +20 -0
  28. karrio/server/events/tests/__init__.py +7 -0
  29. karrio/server/events/tests/test_events.py +138 -0
  30. karrio/server/events/tests/test_tracking_tasks.py +345 -0
  31. karrio/server/events/tests/test_webhooks.py +132 -0
  32. karrio/server/events/tests.py +3 -0
  33. karrio/server/events/urls.py +10 -0
  34. karrio/server/events/views/__init__.py +2 -0
  35. karrio/server/events/views/webhooks.py +173 -0
  36. karrio/server/graph/schemas/__init__.py +1 -0
  37. karrio/server/graph/schemas/events/__init__.py +47 -0
  38. karrio/server/graph/schemas/events/inputs.py +43 -0
  39. karrio/server/graph/schemas/events/mutations.py +56 -0
  40. karrio/server/graph/schemas/events/types.py +79 -0
  41. karrio_server_events-2025.5rc1.dist-info/METADATA +28 -0
  42. karrio_server_events-2025.5rc1.dist-info/RECORD +44 -0
  43. karrio_server_events-2025.5rc1.dist-info/WHEEL +5 -0
  44. karrio_server_events-2025.5rc1.dist-info/top_level.txt +2 -0
@@ -0,0 +1,138 @@
1
+ import logging
2
+ from django.db.models import signals
3
+
4
+ from karrio.server.core import utils
5
+ from karrio.server.conf import settings
6
+ from karrio.server.events.serializers import EventTypes
7
+ import karrio.server.core.serializers as serializers
8
+ import karrio.server.manager.models as models
9
+ import karrio.server.events.tasks as tasks
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def register_signals():
15
+ signals.post_save.connect(shipment_updated, sender=models.Shipment)
16
+ signals.post_delete.connect(shipment_cancelled, sender=models.Shipment)
17
+ signals.post_save.connect(tracker_updated, sender=models.Tracking)
18
+
19
+ logger.info("karrio.events signals registered...")
20
+
21
+
22
+ @utils.disable_for_loaddata
23
+ @utils.error_wrapper
24
+ def shipment_updated(
25
+ sender, instance, created, raw, using, update_fields, *args, **kwargs
26
+ ):
27
+ """Shipment related events:
28
+ - shipment purchased (label purchased)
29
+ - shipment fulfilled (shipped)
30
+ """
31
+ is_bound = "created_at" in (update_fields or [])
32
+ status_updated = "status" in (update_fields or [])
33
+
34
+ if created:
35
+ return
36
+ if is_bound and instance.status == serializers.ShipmentStatus.purchased.value:
37
+ event = EventTypes.shipment_purchased.value
38
+ elif (
39
+ status_updated and instance.status == serializers.ShipmentStatus.purchased.value
40
+ ):
41
+ event = EventTypes.shipment_purchased.value
42
+ elif (
43
+ status_updated
44
+ and instance.status == serializers.ShipmentStatus.in_transit.value
45
+ ):
46
+ event = EventTypes.shipment_fulfilled.value
47
+ elif (
48
+ status_updated and instance.status == serializers.ShipmentStatus.cancelled.value
49
+ ):
50
+ event = EventTypes.shipment_cancelled.value
51
+ elif (
52
+ status_updated
53
+ and instance.status == serializers.ShipmentStatus.out_for_delivery.value
54
+ ):
55
+ event = EventTypes.shipment_out_for_delivery.value
56
+ elif (
57
+ status_updated
58
+ and instance.status == serializers.ShipmentStatus.needs_attention.value
59
+ ):
60
+ event = EventTypes.shipment_needs_attention.value
61
+ elif (
62
+ status_updated
63
+ and instance.status == serializers.ShipmentStatus.delivery_failed.value
64
+ ):
65
+ event = EventTypes.shipment_delivery_failed.value
66
+ else:
67
+ return
68
+
69
+ data = serializers.Shipment(instance).data
70
+ event_at = instance.updated_at
71
+ context = dict(
72
+ test_mode=instance.test_mode,
73
+ user_id=utils.failsafe(lambda: instance.created_by.id),
74
+ org_id=utils.failsafe(
75
+ lambda: instance.org.first().id if hasattr(instance, "org") else None
76
+ ),
77
+ )
78
+
79
+ if settings.MULTI_ORGANIZATIONS and context["org_id"] is None:
80
+ return
81
+
82
+ tasks.notify_webhooks(event, data, event_at, context, schema=settings.schema)
83
+
84
+
85
+ @utils.disable_for_loaddata
86
+ def shipment_cancelled(sender, instance, *args, **kwargs):
87
+ """Shipment related events:
88
+ - shipment cancelled/deleted (label voided)
89
+ """
90
+ event = EventTypes.shipment_cancelled.value
91
+ data = serializers.Shipment(instance)
92
+ event_at = instance.updated_at
93
+ context = dict(
94
+ user_id=utils.failsafe(lambda: instance.created_by.id),
95
+ test_mode=instance.test_mode,
96
+ org_id=utils.failsafe(
97
+ lambda: instance.org.first().id if hasattr(instance, "org") else None
98
+ ),
99
+ )
100
+
101
+ if settings.MULTI_ORGANIZATIONS and context["org_id"] is None:
102
+ return
103
+
104
+ tasks.notify_webhooks(event, data, event_at, context, schema=settings.schema)
105
+
106
+
107
+ @utils.disable_for_loaddata
108
+ def tracker_updated(
109
+ sender, instance, created, raw, using, update_fields, *args, **kwargs
110
+ ):
111
+ """Tracking related events:
112
+ - tracker created (pending)
113
+ - tracker status changed (in_transit, delivered or blocked)
114
+ """
115
+ changes = update_fields or []
116
+ post_create = created or "created_at" in changes
117
+
118
+ if post_create:
119
+ event = EventTypes.tracker_created.value
120
+ elif any(field in changes for field in ["status", "events"]):
121
+ event = EventTypes.tracker_updated.value
122
+ else:
123
+ return
124
+
125
+ data = serializers.TrackingStatus(instance).data
126
+ event_at = instance.updated_at
127
+ context = dict(
128
+ user_id=utils.failsafe(lambda: instance.created_by.id),
129
+ test_mode=instance.test_mode,
130
+ org_id=utils.failsafe(
131
+ lambda: instance.org.first().id if hasattr(instance, "org") else None
132
+ ),
133
+ )
134
+
135
+ if settings.MULTI_ORGANIZATIONS and context["org_id"] is None:
136
+ return
137
+
138
+ tasks.notify_webhooks(event, data, event_at, context, schema=settings.schema)
@@ -0,0 +1 @@
1
+ __path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore
@@ -0,0 +1,64 @@
1
+ import time
2
+ import logging
3
+ from django.conf import settings
4
+ from huey import crontab
5
+ from huey.contrib.djhuey import db_task, db_periodic_task
6
+
7
+ import karrio.server.core.utils as utils
8
+
9
+ logger = logging.getLogger(__name__)
10
+ DATA_ARCHIVING_SCHEDULE = int(getattr(settings, "DATA_ARCHIVING_SCHEDULE", 168))
11
+ DEFAULT_TRACKERS_UPDATE_INTERVAL = int(
12
+ getattr(settings, "DEFAULT_TRACKERS_UPDATE_INTERVAL", 7200) / 60
13
+ )
14
+
15
+
16
+ @db_periodic_task(crontab(minute=f"*/{DEFAULT_TRACKERS_UPDATE_INTERVAL}"))
17
+ def background_trackers_update():
18
+ from karrio.server.events.task_definitions.base import tracking
19
+
20
+ @utils.run_on_all_tenants
21
+ def _run(**kwargs):
22
+ try:
23
+ tracking.update_trackers()
24
+ except Exception as e:
25
+ logger.error(f"An error occured during tracking statuses update: {e}")
26
+
27
+ _run()
28
+
29
+
30
+ @db_task(retries=5, retry_delay=60)
31
+ @utils.tenant_aware
32
+ def notify_webhooks(*args, **kwargs):
33
+ try:
34
+ from karrio.server.events.task_definitions.base import webhook
35
+
36
+ webhook.notify_webhook_subscribers(*args, **kwargs)
37
+
38
+ except Exception as e:
39
+ logger.error(f"An error occured during webhook notification: {e}")
40
+ raise e
41
+
42
+
43
+ @db_periodic_task(crontab(hour=f"*/{DATA_ARCHIVING_SCHEDULE}"))
44
+ def periodic_data_archiving(*args, **kwargs):
45
+ from karrio.server.events.task_definitions.base import archiving
46
+
47
+ @utils.run_on_all_tenants
48
+ def _run(**kwargs):
49
+ try:
50
+ utils.failsafe(
51
+ lambda: archiving.run_data_archiving(*args, **kwargs),
52
+ "An error occured during data archiving: $error",
53
+ )
54
+ except Exception as e:
55
+ logger.error(f"An error occured during data archiving: {e}")
56
+
57
+ _run()
58
+
59
+
60
+ TASK_DEFINITIONS = [
61
+ background_trackers_update,
62
+ periodic_data_archiving,
63
+ notify_webhooks,
64
+ ]
@@ -0,0 +1,58 @@
1
+ import logging
2
+ import datetime
3
+ import django.utils.timezone as timezone
4
+
5
+ import karrio.server.conf as conf
6
+ import karrio.server.core.utils as utils
7
+ import karrio.server.core.models as core
8
+ import karrio.server.events.models as events
9
+ import karrio.server.orders.models as orders
10
+ import karrio.server.tracing.models as tracing
11
+ import karrio.server.manager.models as manager
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def run_data_archiving(*args, **kwargs):
17
+ now = timezone.now()
18
+ log_retention = now - datetime.timedelta(days=conf.settings.API_LOGS_DATA_RETENTION)
19
+ order_retention = now - datetime.timedelta(days=conf.settings.ORDER_DATA_RETENTION)
20
+ shipment_retention = now - datetime.timedelta(
21
+ days=conf.settings.SHIPMENT_DATA_RETENTION
22
+ )
23
+ tracker_retention = now - datetime.timedelta(
24
+ days=conf.settings.TRACKER_DATA_RETENTION
25
+ )
26
+
27
+ shipping_data = manager.Shipment.objects.filter(created_at__lt=shipment_retention)
28
+ tracking_Data = manager.Tracking.objects.filter(created_at__lt=tracker_retention)
29
+ tracing_data = tracing.TracingRecord.objects.filter(created_at__lt=log_retention)
30
+ api_log_data = core.APILog.objects.filter(requested_at__lt=log_retention)
31
+ order_data = orders.Order.objects.filter(created_at__lt=order_retention)
32
+ event_data = events.Event.objects.filter(created_at__lt=log_retention)
33
+
34
+ if any(tracing_data):
35
+ logger.info(">> archiving SDK tracing backlog...")
36
+ utils.failsafe(lambda: tracing_data.delete())
37
+
38
+ if any(event_data):
39
+ logger.info(">> archiving events backlog...")
40
+ utils.failsafe(lambda: event_data.delete())
41
+
42
+ if any(api_log_data):
43
+ logger.info(">> archiving API request logs backlog...")
44
+ utils.failsafe(lambda: api_log_data.delete())
45
+
46
+ if any(tracking_Data):
47
+ logger.info(">> archiving tracking data backlog...")
48
+ utils.failsafe(lambda: tracking_Data.delete())
49
+
50
+ if any(shipping_data):
51
+ logger.info(">> archiving shipping data backlog...")
52
+ utils.failsafe(lambda: shipping_data.delete())
53
+
54
+ if any(order_data):
55
+ logger.info(">> archiving order data backlog...")
56
+ utils.failsafe(lambda: order_data.delete())
57
+
58
+ logger.info("> ending scheduled backlog archiving!")
@@ -0,0 +1,227 @@
1
+ import time
2
+ import typing
3
+ import logging
4
+ import datetime
5
+ import functools
6
+ from itertools import groupby
7
+
8
+ from django.conf import settings
9
+ from django.utils import timezone
10
+
11
+ import karrio.sdk as karrio
12
+ import karrio.lib as lib
13
+ from karrio.api.gateway import Gateway
14
+ from karrio.api.interface import IRequestFrom
15
+ from karrio.core.models import TrackingDetails, Message, TrackingEvent
16
+
17
+ import karrio.server.core.utils as utils
18
+ import karrio.server.manager.models as models
19
+ import karrio.server.tracing.utils as tracing
20
+ import karrio.server.core.datatypes as datatypes
21
+ import karrio.server.manager.serializers as serializers
22
+
23
+ logger = logging.getLogger(__name__)
24
+ Delay = int
25
+ RequestBatches = typing.Tuple[
26
+ Gateway, IRequestFrom, Delay, typing.List[models.Tracking]
27
+ ]
28
+ BatchResponse = typing.List[typing.Tuple[TrackingDetails, typing.List[Message]]]
29
+
30
+ DEFAULT_TRACKERS_UPDATE_INTERVAL = getattr(
31
+ settings, "DEFAULT_TRACKERS_UPDATE_INTERVAL", 7200
32
+ )
33
+
34
+
35
+ def update_trackers(
36
+ delta: datetime.timedelta = datetime.timedelta(
37
+ seconds=DEFAULT_TRACKERS_UPDATE_INTERVAL
38
+ ),
39
+ tracker_ids: typing.List[str] = [],
40
+ ):
41
+ logger.info("> starting scheduled trackers update")
42
+
43
+ active_trackers = lib.identity(
44
+ models.Tracking.objects.filter(id__in=tracker_ids)
45
+ if any(tracker_ids)
46
+ else models.Tracking.objects.filter(
47
+ delivered=False,
48
+ updated_at__lt=timezone.now() - delta,
49
+ )
50
+ )
51
+
52
+ if any(active_trackers):
53
+ trackers_grouped_by_carrier = [
54
+ list(g) for _, g in groupby(active_trackers, key=lambda t: t.carrier_id)
55
+ ]
56
+ request_batches: typing.List[RequestBatches] = sum(
57
+ [create_request_batches(group) for group in trackers_grouped_by_carrier], []
58
+ )
59
+
60
+ responses = lib.run_concurently(fetch_tracking_info, request_batches, 2)
61
+ save_tracing_records(request_batches)
62
+ save_updated_trackers(responses, active_trackers)
63
+ else:
64
+ logger.info("no active trackers found needing update")
65
+
66
+ logger.info("> ending scheduled trackers update")
67
+
68
+
69
+ def create_request_batches(
70
+ trackers: typing.List[models.Tracking],
71
+ ) -> typing.List[RequestBatches]:
72
+ start = 0
73
+ end = 10
74
+ batches = []
75
+
76
+ while any(trackers[start:end]):
77
+ try:
78
+ # Add a request delay to avoid sending two request batches to a carrier at the same time
79
+ delay = int(((end / 10) * 10) - 10)
80
+ # Get the common tracking carrier
81
+ carrier = trackers[0].tracking_carrier
82
+ # Collect the 5 trackers between the start and end indexes
83
+ batch_trackers = trackers[start:end]
84
+ tracking_numbers = [t.tracking_number for t in batch_trackers]
85
+ options: dict = functools.reduce(
86
+ lambda acc, t: {**acc, **(t.options or {})}, batch_trackers, {}
87
+ )
88
+
89
+ logger.debug(f"prepare tracking request for {tracking_numbers}")
90
+
91
+ # Prepare and send tracking request(s) using the karrio interface.
92
+ request: IRequestFrom = karrio.Tracking.fetch(
93
+ datatypes.TrackingRequest(
94
+ tracking_numbers=tracking_numbers, options=options
95
+ )
96
+ )
97
+ gateway: Gateway = carrier.gateway
98
+
99
+ batches.append((gateway, request, delay, batch_trackers))
100
+
101
+ except Exception as request_error:
102
+ logger.warning(f"failed to prepare tracking batch ({start}, {end}) request")
103
+ logger.error(request_error, exc_info=True)
104
+
105
+ end += 10
106
+ start += 10
107
+
108
+ return batches
109
+
110
+
111
+ def fetch_tracking_info(request_batch: RequestBatches) -> BatchResponse:
112
+ gateway, request, delay, trackers = request_batch
113
+ logger.debug(f"fetching batch {[t.tracking_number for t in trackers]}")
114
+ time.sleep(delay) # apply delay before request
115
+
116
+ try:
117
+ return utils.identity(lambda: request.from_(gateway).parse())
118
+ except Exception as request_error:
119
+ logger.warning("batch request failed")
120
+ logger.error(request_error, exc_info=True)
121
+
122
+ return []
123
+
124
+
125
+ @utils.error_wrapper
126
+ def save_tracing_records(request_batches: typing.List[RequestBatches]):
127
+ logger.info("> saving tracing records...")
128
+
129
+ try:
130
+ for request_batch in request_batches:
131
+ gateway, _, __, trackers = request_batch
132
+
133
+ if not any(trackers):
134
+ continue
135
+
136
+ context = serializers.get_object_context(trackers[0])
137
+ tracing.bulk_save_tracing_records(gateway.tracer, context=context)
138
+ except Exception as error:
139
+ print(error)
140
+ logger.warning("Failed failed saving tracing record...")
141
+ logger.error(error, exc_info=True)
142
+
143
+
144
+ def save_updated_trackers(
145
+ responses: typing.List[BatchResponse], trackers: typing.List[models.Tracking]
146
+ ):
147
+ logger.info("> saving updated trackers")
148
+
149
+ for tracking_details, _ in responses:
150
+ for details in tracking_details or []:
151
+ try:
152
+ logger.debug(f"update tracking info for {details.tracking_number}")
153
+ related_trackers = [
154
+ t for t in trackers if t.tracking_number == details.tracking_number
155
+ ]
156
+ for tracker in related_trackers:
157
+ # update values only if changed; This is important for webhooks notification
158
+ changes = []
159
+ meta = details.meta or {}
160
+ status = utils.compute_tracking_status(details).value
161
+ events = utils.process_events(
162
+ response_events=details.events,
163
+ current_events=tracker.events,
164
+ )
165
+ options = {
166
+ **(tracker.options or {}),
167
+ tracker.tracking_number: details.meta,
168
+ }
169
+ info = lib.to_dict(details.info or {})
170
+
171
+ if events != tracker.events:
172
+ tracker.events = events
173
+ changes.append("events")
174
+
175
+ if options != tracker.options:
176
+ tracker.options = options
177
+ changes.append("options")
178
+
179
+ if details.meta != tracker.meta:
180
+ tracker.meta = meta
181
+ changes.append("meta")
182
+
183
+ if details.delivered != tracker.delivered:
184
+ tracker.delivered = details.delivered
185
+ changes.append("delivered")
186
+
187
+ if status != tracker.status:
188
+ tracker.status = status
189
+ changes.append("status")
190
+
191
+ if details.estimated_delivery != tracker.estimated_delivery:
192
+ tracker.estimated_delivery = details.estimated_delivery
193
+ changes.append("estimated_delivery")
194
+
195
+ if details.images is not None and (
196
+ details.images.delivery_image != tracker.delivery_image
197
+ or details.images.signature_image != tracker.signature_image
198
+ ):
199
+ changes.append("delivery_image")
200
+ changes.append("signature_image")
201
+ tracker.delivery_image = (
202
+ details.images.delivery_image or tracker.delivery_image
203
+ )
204
+ tracker.signature_image = (
205
+ details.images.signature_image or tracker.signature_image
206
+ )
207
+
208
+ if any(info.keys()) and info != tracker.info:
209
+ tracker.info = serializers.process_dictionaries_mutations(
210
+ ["info"], dict(info=info), tracker
211
+ )["info"]
212
+ changes.append("info")
213
+
214
+ if any(changes):
215
+ tracker.save(update_fields=changes)
216
+ serializers.update_shipment_tracker(tracker)
217
+ logger.debug(
218
+ f"tracking info {details.tracking_number} updated successfully"
219
+ )
220
+ else:
221
+ logger.debug(f"no changes detect")
222
+
223
+ except Exception as update_error:
224
+ logger.warning(
225
+ f"failed to update tracker with tracking number: {details.tracking_number}"
226
+ )
227
+ logger.error(update_error, exc_info=True)
@@ -0,0 +1,116 @@
1
+ import typing
2
+ import requests
3
+ import logging
4
+ from datetime import datetime
5
+ from django.conf import settings
6
+ from django.db.models import Q
7
+ from django.contrib.auth import get_user_model
8
+
9
+ from karrio.core import utils
10
+ from karrio.server.core.utils import identity
11
+ from karrio.server.serializers import Context
12
+ from karrio.server.events import models
13
+ import karrio.server.events.serializers.event as serializers
14
+
15
+ logger = logging.getLogger(__name__)
16
+ NotificationResponse = typing.Tuple[str, requests.Response]
17
+ User = get_user_model()
18
+
19
+
20
+ def notify_webhook_subscribers(
21
+ event: str,
22
+ data: dict,
23
+ event_at: datetime,
24
+ ctx: dict,
25
+ **kwargs,
26
+ ):
27
+ logger.info(f"> starting {event} subscribers notification")
28
+ context = retrieve_context(ctx)
29
+ query = (
30
+ (Q(enabled_events__icontains=event) | Q(enabled_events__icontains="all")),
31
+ (Q(disabled__isnull=True) | Q(disabled=False)),
32
+ )
33
+
34
+ webhooks = models.Webhook.access_by(context).filter(*query)
35
+ serializers.EventSerializer.map(
36
+ data=dict(
37
+ type=event,
38
+ data=data,
39
+ test_mode=context.test_mode,
40
+ pending_webhooks=webhooks.count(),
41
+ ),
42
+ context=context,
43
+ ).save()
44
+
45
+ if any(webhooks):
46
+ payload = dict(event=event, data=data)
47
+ responses: typing.List[NotificationResponse] = notify_subscribers(
48
+ webhooks, payload
49
+ )
50
+ update_notified_webhooks(webhooks, responses, event_at)
51
+ else:
52
+ logger.info("no subscribers found")
53
+
54
+ logger.info(f"> ending {event} subscribers notification")
55
+
56
+
57
+ def notify_subscribers(webhooks: typing.List[models.Webhook], payload: dict):
58
+ def notify_subscriber(webhook: models.Webhook):
59
+ response = identity(
60
+ lambda: requests.post(
61
+ webhook.url,
62
+ json=payload,
63
+ headers={
64
+ "Content-type": "application/json",
65
+ "X-Event-Id": webhook.secret,
66
+ },
67
+ )
68
+ )
69
+
70
+ return webhook.id, response
71
+
72
+ return utils.exec_async(notify_subscriber, webhooks)
73
+
74
+
75
+ def update_notified_webhooks(
76
+ webhooks: typing.List[models.Webhook],
77
+ responses: typing.List[NotificationResponse],
78
+ event_at: datetime,
79
+ ):
80
+ logger.info("> saving updated webhooks")
81
+
82
+ for webhook_id, response in responses:
83
+ try:
84
+ logger.debug(f"update webhook {webhook_id}")
85
+
86
+ webhook = next((w for w in webhooks if w.id == webhook_id))
87
+ if response.ok:
88
+ webhook.last_event_at = event_at
89
+ webhook.failure_streak_count = 0
90
+ else:
91
+ webhook.failure_streak_count += 1
92
+ # Disable the webhook if notification failed more than 5 times
93
+ webhook.disabled = webhook.failure_streak_count > 5
94
+
95
+ webhook.save()
96
+
97
+ logger.debug(f"webhook {webhook_id} updated successfully")
98
+
99
+ except Exception as update_error:
100
+ logger.warning(f"failed to update webhook {webhook_id}")
101
+ logger.error(update_error, exc_info=True)
102
+
103
+
104
+ def retrieve_context(info: dict) -> Context:
105
+ org = None
106
+
107
+ if settings.MULTI_ORGANIZATIONS and "org_id" in info:
108
+ import karrio.server.orgs.models as orgs_models
109
+
110
+ org = orgs_models.Organization.objects.filter(id=info["org_id"]).first()
111
+
112
+ return Context(
113
+ org=org,
114
+ user=User.objects.filter(id=info["user_id"]).first(),
115
+ test_mode=info.get("test_mode"),
116
+ )
@@ -0,0 +1,20 @@
1
+ import typing
2
+ import logging
3
+ import pkgutil
4
+ import karrio.server.events.task_definitions as definitions
5
+
6
+ logger = logging.getLogger(__name__)
7
+ DEFINITIONS: typing.List[typing.Any] = []
8
+
9
+ # Register karrio background tasks
10
+ for _, name, _ in pkgutil.iter_modules(definitions.__path__): # type: ignore
11
+ try:
12
+ definition = __import__(f"{definitions.__name__}.{name}", fromlist=[name]) # type: ignore
13
+ if hasattr(definition, "TASK_DEFINITIONS"):
14
+ DEFINITIONS += definition.TASK_DEFINITIONS
15
+ except Exception as e:
16
+ logger.warning(f'Failed to register "{name}" schema')
17
+ logger.exception(e)
18
+
19
+ for wrapper in DEFINITIONS:
20
+ globals()[wrapper.task_class.__name__] = wrapper
@@ -0,0 +1,7 @@
1
+ import logging
2
+
3
+ logging.disable(logging.CRITICAL)
4
+
5
+ from karrio.server.events.tests.test_tracking_tasks import *
6
+ from karrio.server.events.tests.test_webhooks import *
7
+ from karrio.server.events.tests.test_events import *