karrio-server-core 2025.5__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 (213) hide show
  1. karrio/server/conf.py +54 -0
  2. karrio/server/core/__init__.py +3 -0
  3. karrio/server/core/admin.py +1 -0
  4. karrio/server/core/apps.py +10 -0
  5. karrio/server/core/authentication.py +347 -0
  6. karrio/server/core/config.py +31 -0
  7. karrio/server/core/context_processors.py +12 -0
  8. karrio/server/core/datatypes.py +394 -0
  9. karrio/server/core/dataunits.py +187 -0
  10. karrio/server/core/exceptions.py +404 -0
  11. karrio/server/core/fields.py +12 -0
  12. karrio/server/core/filters.py +837 -0
  13. karrio/server/core/gateway.py +1011 -0
  14. karrio/server/core/logging.py +403 -0
  15. karrio/server/core/management/commands/cli.py +19 -0
  16. karrio/server/core/management/commands/create_oauth_client.py +41 -0
  17. karrio/server/core/management/commands/runserver.py +5 -0
  18. karrio/server/core/middleware.py +197 -0
  19. karrio/server/core/migrations/0001_initial.py +28 -0
  20. karrio/server/core/migrations/0002_apilogindex.py +69 -0
  21. karrio/server/core/migrations/0003_apilogindex_test_mode.py +62 -0
  22. karrio/server/core/migrations/0004_metafield.py +74 -0
  23. karrio/server/core/migrations/0005_alter_metafield_type_alter_metafield_value.py +23 -0
  24. karrio/server/core/migrations/0006_add_api_log_requested_at_index.py +22 -0
  25. karrio/server/core/migrations/__init__.py +0 -0
  26. karrio/server/core/models/__init__.py +48 -0
  27. karrio/server/core/models/base.py +103 -0
  28. karrio/server/core/models/entity.py +24 -0
  29. karrio/server/core/models/metafield.py +144 -0
  30. karrio/server/core/models/third_party.py +21 -0
  31. karrio/server/core/oauth_validators.py +170 -0
  32. karrio/server/core/permissions.py +36 -0
  33. karrio/server/core/renderers.py +11 -0
  34. karrio/server/core/router.py +3 -0
  35. karrio/server/core/serializers.py +1971 -0
  36. karrio/server/core/signals.py +55 -0
  37. karrio/server/core/telemetry.py +573 -0
  38. karrio/server/core/tests.py +99 -0
  39. karrio/server/core/tests_resource_token.py +411 -0
  40. karrio/server/core/urls.py +12 -0
  41. karrio/server/core/utils.py +1025 -0
  42. karrio/server/core/validators.py +264 -0
  43. karrio/server/core/views/__init__.py +2 -0
  44. karrio/server/core/views/api.py +133 -0
  45. karrio/server/core/views/metadata.py +44 -0
  46. karrio/server/core/views/oauth.py +75 -0
  47. karrio/server/core/views/references.py +82 -0
  48. karrio/server/core/views/schema.py +310 -0
  49. karrio/server/filters/__init__.py +2 -0
  50. karrio/server/filters/abstract.py +26 -0
  51. karrio/server/iam/__init__.py +0 -0
  52. karrio/server/iam/admin.py +3 -0
  53. karrio/server/iam/apps.py +21 -0
  54. karrio/server/iam/migrations/0001_initial.py +33 -0
  55. karrio/server/iam/migrations/__init__.py +0 -0
  56. karrio/server/iam/models.py +48 -0
  57. karrio/server/iam/permissions.py +155 -0
  58. karrio/server/iam/serializers.py +54 -0
  59. karrio/server/iam/signals.py +18 -0
  60. karrio/server/iam/tests.py +3 -0
  61. karrio/server/iam/views.py +3 -0
  62. karrio/server/openapi.py +75 -0
  63. karrio/server/providers/__init__.py +1 -0
  64. karrio/server/providers/admin.py +364 -0
  65. karrio/server/providers/apps.py +10 -0
  66. karrio/server/providers/management/commands/migrate_rate_sheets.py +101 -0
  67. karrio/server/providers/migrations/0001_initial.py +140 -0
  68. karrio/server/providers/migrations/0002_carrier_active.py +18 -0
  69. karrio/server/providers/migrations/0003_auto_20201230_0820.py +24 -0
  70. karrio/server/providers/migrations/0004_auto_20210212_0554.py +178 -0
  71. karrio/server/providers/migrations/0005_auto_20210212_0555.py +18 -0
  72. karrio/server/providers/migrations/0006_australiapostsettings.py +29 -0
  73. karrio/server/providers/migrations/0007_auto_20210213_0206.py +21 -0
  74. karrio/server/providers/migrations/0008_auto_20210214_0409.py +30 -0
  75. karrio/server/providers/migrations/0009_auto_20210308_0302.py +18 -0
  76. karrio/server/providers/migrations/0010_auto_20210409_0852.py +32 -0
  77. karrio/server/providers/migrations/0011_auto_20210409_0853.py +21 -0
  78. karrio/server/providers/migrations/0012_alter_carrier_options.py +17 -0
  79. karrio/server/providers/migrations/0013_tntsettings.py +30 -0
  80. karrio/server/providers/migrations/0014_auto_20210612_1608.py +46 -0
  81. karrio/server/providers/migrations/0015_auto_20210615_1601.py +28 -0
  82. karrio/server/providers/migrations/0016_alter_purolatorsettings_user_token.py +18 -0
  83. karrio/server/providers/migrations/0017_auto_20210805_0359.py +1293 -0
  84. karrio/server/providers/migrations/0018_alter_fedexsettings_user_key.py +18 -0
  85. karrio/server/providers/migrations/0019_dhlpolandsettings_servicelevel.py +65 -0
  86. karrio/server/providers/migrations/0020_genericsettings_labeltemplate.py +52 -0
  87. karrio/server/providers/migrations/0021_auto_20211231_2353.py +40 -0
  88. karrio/server/providers/migrations/0022_carrier_metadata.py +18 -0
  89. karrio/server/providers/migrations/0023_auto_20220124_1916.py +27 -0
  90. karrio/server/providers/migrations/0024_alter_genericsettings_custom_carrier_name.py +19 -0
  91. karrio/server/providers/migrations/0025_alter_servicelevel_service_code.py +19 -0
  92. karrio/server/providers/migrations/0026_auto_20220208_0132.py +59 -0
  93. karrio/server/providers/migrations/0027_auto_20220304_1340.py +29 -0
  94. karrio/server/providers/migrations/0028_auto_20220323_1500.py +33 -0
  95. karrio/server/providers/migrations/0029_easypostsettings.py +27 -0
  96. karrio/server/providers/migrations/0030_amazonmwssettings.py +29 -0
  97. karrio/server/providers/migrations/0031_delete_amazonmwssettings.py +18 -0
  98. karrio/server/providers/migrations/0032_alter_carrier_test.py +18 -0
  99. karrio/server/providers/migrations/0033_auto_20220708_1350.py +22 -0
  100. karrio/server/providers/migrations/0034_amazonmwssettings_dpdhlsettings.py +47 -0
  101. karrio/server/providers/migrations/0035_alter_carrier_capabilities.py +43 -0
  102. karrio/server/providers/migrations/0036_upsfreightsettings.py +31 -0
  103. karrio/server/providers/migrations/0037_chronopostsettings.py +29 -0
  104. karrio/server/providers/migrations/0038_alter_genericsettings_label_template.py +19 -0
  105. karrio/server/providers/migrations/0039_auto_20220906_0612.py +23 -0
  106. karrio/server/providers/migrations/0040_dpdhlsettings_services.py +18 -0
  107. karrio/server/providers/migrations/0041_auto_20221105_0705.py +38 -0
  108. karrio/server/providers/migrations/0042_auto_20221215_1642.py +23 -0
  109. karrio/server/providers/migrations/0043_alter_genericsettings_account_number_and_more.py +39 -0
  110. karrio/server/providers/migrations/0044_carrier_carrier_capabilities.py +64 -0
  111. karrio/server/providers/migrations/0045_alter_carrier_active_alter_carrier_carrier_id.py +31 -0
  112. karrio/server/providers/migrations/0046_remove_dpdhlsettings_signature_and_more.py +41 -0
  113. karrio/server/providers/migrations/0047_dpdsettings.py +286 -0
  114. karrio/server/providers/migrations/0048_servicelevel_min_weight_servicelevel_transit_days_and_more.py +64 -0
  115. karrio/server/providers/migrations/0049_boxknightsettings_geodissettings_lapostesettings_and_more.py +156 -0
  116. karrio/server/providers/migrations/0050_carrier_is_system_alter_carrier_metadata_and_more.py +106 -0
  117. karrio/server/providers/migrations/0051_rename_username_upssettings_client_id_and_more.py +31 -0
  118. karrio/server/providers/migrations/0052_alter_upssettings_account_number_and_more.py +20 -0
  119. karrio/server/providers/migrations/0053_locate2usettings.py +281 -0
  120. karrio/server/providers/migrations/0054_zoom2usettings.py +280 -0
  121. karrio/server/providers/migrations/0055_rename_amazonmwssettings_amazonshippingsettings_and_more.py +44 -0
  122. karrio/server/providers/migrations/0056_asendiaussettings_geodissettings_code_client_and_more.py +75 -0
  123. karrio/server/providers/migrations/0057_alter_servicelevel_weight_unit_belgianpostsettings.py +51 -0
  124. karrio/server/providers/migrations/0058_alliedexpresssettings.py +38 -0
  125. karrio/server/providers/migrations/0059_ratesheet.py +81 -0
  126. karrio/server/providers/migrations/0060_belgianpostsettings_rate_sheet_and_more.py +73 -0
  127. karrio/server/providers/migrations/0061_alliedexpresssettings_service_type.py +17 -0
  128. karrio/server/providers/migrations/0062_sendlesettings_account_country_code.py +257 -0
  129. karrio/server/providers/migrations/0063_servicelevel_metadata.py +25 -0
  130. karrio/server/providers/migrations/0064_alliedexpresslocalsettings.py +43 -0
  131. karrio/server/providers/migrations/0065_servicelevel_carrier_service_code_and_more.py +66 -0
  132. karrio/server/providers/migrations/0066_rename_fedexsettings_fedexwssettings_and_more.py +28 -0
  133. karrio/server/providers/migrations/0067_fedexsettings.py +283 -0
  134. karrio/server/providers/migrations/0068_fedexsettings_track_api_key_and_more.py +38 -0
  135. karrio/server/providers/migrations/0069_alter_canadapostsettings_contract_id_and_more.py +23 -0
  136. karrio/server/providers/migrations/0070_tgesettings_alter_carrier_capabilities.py +65 -0
  137. karrio/server/providers/migrations/0071_alter_tgesettings_my_toll_token.py +18 -0
  138. karrio/server/providers/migrations/0072_rename_eshippersettings_eshipperxmlsettings_and_more.py +28 -0
  139. karrio/server/providers/migrations/0073_delete_eshipperxmlsettings.py +41 -0
  140. karrio/server/providers/migrations/0074_eshippersettings.py +38 -0
  141. karrio/server/providers/migrations/0075_haypostsettings.py +40 -0
  142. karrio/server/providers/migrations/0076_rename_customer_registration_id_uspsinternationalsettings_account_number_and_more.py +125 -0
  143. karrio/server/providers/migrations/0077_uspswtinternationalsettings_uspswtsettings_and_more.py +165 -0
  144. karrio/server/providers/migrations/0078_auto_20240813_1552.py +120 -0
  145. karrio/server/providers/migrations/0079_alter_carrier_options_alter_ratesheet_created_by.py +31 -0
  146. karrio/server/providers/migrations/0080_alter_aramexsettings_account_country_code_and_more.py +3025 -0
  147. karrio/server/providers/migrations/0081_remove_alliedexpresssettings_carrier_ptr_and_more.py +338 -0
  148. karrio/server/providers/migrations/0082_add_zone_identifiers.py +50 -0
  149. karrio/server/providers/migrations/0083_add_optimized_rate_sheet_structure.py +33 -0
  150. karrio/server/providers/migrations/0084_alter_servicelevel_currency.py +168 -0
  151. karrio/server/providers/migrations/0085_populate_dhl_parcel_de_oauth_credentials.py +82 -0
  152. karrio/server/providers/migrations/0086_rename_dhl_parcel_de_customer_number_to_billing_number.py +71 -0
  153. karrio/server/providers/migrations/__init__.py +0 -0
  154. karrio/server/providers/models/__init__.py +16 -0
  155. karrio/server/providers/models/carrier.py +387 -0
  156. karrio/server/providers/models/config.py +30 -0
  157. karrio/server/providers/models/service.py +192 -0
  158. karrio/server/providers/models/sheet.py +287 -0
  159. karrio/server/providers/models/template.py +39 -0
  160. karrio/server/providers/models/utils.py +58 -0
  161. karrio/server/providers/router.py +3 -0
  162. karrio/server/providers/serializers/__init__.py +3 -0
  163. karrio/server/providers/serializers/base.py +538 -0
  164. karrio/server/providers/signals.py +25 -0
  165. karrio/server/providers/templates/providers/oauth_callback.html +105 -0
  166. karrio/server/providers/tests/__init__.py +5 -0
  167. karrio/server/providers/tests/test_connections.py +895 -0
  168. karrio/server/providers/urls.py +11 -0
  169. karrio/server/providers/views/__init__.py +0 -0
  170. karrio/server/providers/views/carriers.py +267 -0
  171. karrio/server/providers/views/connections.py +496 -0
  172. karrio/server/samples.py +352 -0
  173. karrio/server/serializers/__init__.py +2 -0
  174. karrio/server/serializers/abstract.py +602 -0
  175. karrio/server/tracing/__init__.py +0 -0
  176. karrio/server/tracing/admin.py +63 -0
  177. karrio/server/tracing/apps.py +8 -0
  178. karrio/server/tracing/migrations/0001_initial.py +41 -0
  179. karrio/server/tracing/migrations/0002_auto_20220710_1307.py +22 -0
  180. karrio/server/tracing/migrations/0003_auto_20221105_0317.py +43 -0
  181. karrio/server/tracing/migrations/0004_tracingrecord_carrier_account_idx.py +24 -0
  182. karrio/server/tracing/migrations/0005_optimise_tracingrecord_request_log_idx.py +25 -0
  183. karrio/server/tracing/migrations/0006_alter_tracingrecord_options_and_more.py +49 -0
  184. karrio/server/tracing/migrations/0007_tracingrecord_tracing_created_at_idx.py +19 -0
  185. karrio/server/tracing/migrations/__init__.py +0 -0
  186. karrio/server/tracing/models.py +82 -0
  187. karrio/server/tracing/tests.py +3 -0
  188. karrio/server/tracing/utils.py +109 -0
  189. karrio/server/user/__init__.py +0 -0
  190. karrio/server/user/admin.py +96 -0
  191. karrio/server/user/apps.py +7 -0
  192. karrio/server/user/forms.py +35 -0
  193. karrio/server/user/migrations/0001_initial.py +41 -0
  194. karrio/server/user/migrations/0002_token.py +29 -0
  195. karrio/server/user/migrations/0003_token_test_mode.py +20 -0
  196. karrio/server/user/migrations/0004_group.py +26 -0
  197. karrio/server/user/migrations/0005_token_label.py +21 -0
  198. karrio/server/user/migrations/0006_workspaceconfig.py +63 -0
  199. karrio/server/user/migrations/0007_user_metadata.py +25 -0
  200. karrio/server/user/migrations/__init__.py +0 -0
  201. karrio/server/user/models.py +218 -0
  202. karrio/server/user/serializers.py +47 -0
  203. karrio/server/user/templates/registration/login.html +108 -0
  204. karrio/server/user/templates/registration/registration_confirm_email.html +10 -0
  205. karrio/server/user/templates/registration/registration_confirm_email.txt +3 -0
  206. karrio/server/user/tests.py +3 -0
  207. karrio/server/user/urls.py +10 -0
  208. karrio/server/user/utils.py +60 -0
  209. karrio/server/user/views.py +9 -0
  210. karrio_server_core-2025.5.dist-info/METADATA +32 -0
  211. karrio_server_core-2025.5.dist-info/RECORD +213 -0
  212. karrio_server_core-2025.5.dist-info/WHEEL +5 -0
  213. karrio_server_core-2025.5.dist-info/top_level.txt +2 -0
@@ -0,0 +1,1011 @@
1
+ import uuid
2
+ import typing
3
+ import datetime
4
+
5
+ from django.db.models import Q
6
+ from django.conf import settings
7
+ from rest_framework import status
8
+ from rest_framework.exceptions import NotFound
9
+ from karrio.server.core.logging import logger
10
+
11
+ import karrio.lib as lib
12
+ import karrio.sdk as karrio
13
+ import karrio.server.core.utils as utils
14
+ import karrio.server.core.models as core
15
+ import karrio.server.core.datatypes as datatypes
16
+ import karrio.server.core.dataunits as dataunits
17
+ import karrio.server.core.exceptions as exceptions
18
+ import karrio.server.providers.models as providers
19
+ import karrio.server.serializers as base_serializers
20
+ import karrio.server.core.serializers as serializers
21
+
22
+
23
+ class Carriers:
24
+ @staticmethod
25
+ def list(context=None, **kwargs) -> typing.List[providers.Carrier]:
26
+ list_filter = kwargs.copy()
27
+ user_filter = core.get_access_filter(context) if context is not None else []
28
+
29
+ test_mode = list_filter.get("test_mode") or getattr(context, "test_mode", None)
30
+ system_only = list_filter.get("system_only") is True
31
+ active_key = lib.identity(
32
+ "active_orgs__id" if settings.MULTI_ORGANIZATIONS else "active_users__id"
33
+ )
34
+ access_id = getattr(
35
+ getattr(context, "org" if settings.MULTI_ORGANIZATIONS else "user", None),
36
+ "id",
37
+ None,
38
+ )
39
+ creator_filter = lib.identity(
40
+ Q(
41
+ created_by__id=context.user.id,
42
+ **(dict(org=None) if settings.MULTI_ORGANIZATIONS else {}),
43
+ )
44
+ if getattr(context, "user", None) is not None
45
+ else Q()
46
+ )
47
+
48
+ _user_carriers = providers.Carrier.user_carriers.filter(
49
+ user_filter if len(user_filter) > 0 else Q() | creator_filter
50
+ )
51
+ _system_carriers = providers.Carrier.system_carriers.filter(
52
+ Q(
53
+ **{
54
+ "active": True,
55
+ **({active_key: access_id} if access_id is not None else {}),
56
+ }
57
+ )
58
+ )
59
+ _queryset = lib.identity(
60
+ _system_carriers if system_only else _user_carriers | _system_carriers
61
+ )
62
+
63
+ # Check if the test filter is specified then set it otherwise return all carriers live and test mode
64
+ if test_mode is not None:
65
+ _queryset = _queryset.filter(test_mode=test_mode)
66
+
67
+ # Check if the active flag is specified and return all active carrier is active is not set to false
68
+ if list_filter.get("active") is not None:
69
+ active = False if list_filter["active"] is False else True
70
+ _queryset = _queryset.filter(Q(active=active))
71
+
72
+ # Check if a specific carrier_id is provided, to add it to the query
73
+ if "carrier_id" in list_filter:
74
+ _queryset = _queryset.filter(carrier_id=list_filter["carrier_id"])
75
+
76
+ # Check if a specific carrier_id is provided, to add it to the query
77
+ if "capability" in list_filter:
78
+ _queryset = _queryset.filter(
79
+ capabilities__icontains=list_filter["capability"]
80
+ )
81
+
82
+ # Check if a metadata key is provided, to add it to the query
83
+ if "metadata_key" in list_filter:
84
+ _queryset = _queryset.filter(metadata__has_key=list_filter["metadata_key"])
85
+
86
+ # Check if a metadata value is provided, to add it to the query
87
+ if "metadata_value" in list_filter:
88
+ _value = list_filter["metadata_value"]
89
+ _queryset = _queryset.filter(
90
+ id__in=[
91
+ _["id"]
92
+ for _ in _queryset.values("id", "metadata")
93
+ if _value in (_.get("metadata") or {}).values()
94
+ ]
95
+ )
96
+
97
+ # Check if a list of carrier_ids are provided, to add the list to the query
98
+ if any(list_filter.get("carrier_ids", [])):
99
+ _queryset = _queryset.filter(carrier_id__in=list_filter["carrier_ids"])
100
+
101
+ if any(list_filter.get("services", [])):
102
+ carrier_names = [
103
+ name
104
+ for name, services in dataunits.contextual_reference(context)[
105
+ "services"
106
+ ].items()
107
+ if any(
108
+ service in list_filter["services"] for service in services.keys()
109
+ )
110
+ ]
111
+
112
+ if len(carrier_names) > 0:
113
+ _queryset = _queryset.filter(carrier_code__in=carrier_names)
114
+ if "carrier_name" in list_filter:
115
+ carrier_name = list_filter["carrier_name"]
116
+ _queryset = _queryset.filter(carrier_code=carrier_name)
117
+
118
+ carriers = _queryset.distinct()
119
+
120
+ # Raise an error if no carrier is found
121
+ if list_filter.get("raise_not_found") and len(carriers) == 0:
122
+ raise NotFound("No active carrier connection found to process the request")
123
+
124
+ return carriers
125
+
126
+ @staticmethod
127
+ def first(**kwargs) -> providers.Carrier:
128
+ return next(iter(Carriers.list(**kwargs)), None)
129
+
130
+
131
+ class Address:
132
+ @staticmethod
133
+ @utils.with_telemetry("address_validate")
134
+ def validate(
135
+ payload: dict,
136
+ provider: providers.Carrier = None,
137
+ **carrier_filters,
138
+ ) -> datatypes.AddressValidation:
139
+ provider = provider or Carriers.first(
140
+ **{
141
+ **dict(active=True, raise_not_found=True),
142
+ **carrier_filters,
143
+ }
144
+ )
145
+
146
+ if "validate_address" not in provider.gateway.proxy_methods:
147
+ raise exceptions.APIException(
148
+ detail=f"address validation is not supported by carrier: '{provider.carrier_id}'",
149
+ status_code=status.HTTP_406_NOT_ACCEPTABLE,
150
+ )
151
+
152
+ request = karrio.Address.validate(
153
+ lib.to_object(datatypes.AddressValidationRequest, payload)
154
+ )
155
+
156
+ # The request is wrapped in utils.identity to simplify mocking in tests.
157
+ return utils.identity(lambda: request.from_(provider.gateway).parse())
158
+
159
+
160
+ class Shipments:
161
+ @staticmethod
162
+ @utils.with_telemetry("shipment_create")
163
+ @utils.require_selected_rate
164
+ def create(
165
+ payload: dict,
166
+ carrier: providers.Carrier = None,
167
+ selected_rate: typing.Union[datatypes.Rate, dict] = None,
168
+ resolve_tracking_url: typing.Callable[[str, str], str] = None,
169
+ context: base_serializers.Context = None,
170
+ **kwargs,
171
+ ) -> datatypes.Shipment:
172
+ selected_rate = lib.to_object(
173
+ datatypes.Rate,
174
+ lib.to_dict(selected_rate),
175
+ )
176
+ carrier = carrier or Carriers.first(
177
+ carrier_id=selected_rate.carrier_id,
178
+ test_mode=selected_rate.test_mode,
179
+ services=[selected_rate.service],
180
+ context=context,
181
+ )
182
+
183
+ if carrier is None:
184
+ raise NotFound("No active carrier connection found to process the request")
185
+
186
+ payload = {
187
+ **lib.to_dict(payload),
188
+ "options": {
189
+ **(selected_rate.meta or {}),
190
+ **(payload.get("options") or {}),
191
+ },
192
+ }
193
+ request = lib.to_object(
194
+ datatypes.ShipmentRequest,
195
+ {
196
+ **lib.to_dict(payload),
197
+ "service": selected_rate.service,
198
+ },
199
+ )
200
+
201
+ # The request is wrapped in utils.identity to simplify mocking in tests.
202
+ shipment, messages = utils.identity(
203
+ lambda: karrio.Shipment.create(request).from_(carrier.gateway).parse()
204
+ )
205
+
206
+ if shipment is None:
207
+ raise exceptions.APIException(
208
+ detail=messages,
209
+ status_code=status.HTTP_424_FAILED_DEPENDENCY,
210
+ )
211
+
212
+ def process_meta(parent) -> dict:
213
+ service_name = utils.upper(
214
+ (parent.meta or {}).get("service_name") or selected_rate.service
215
+ )
216
+ rate_provider = (
217
+ (parent.meta or {}).get("rate_provider") or carrier.carrier_name
218
+ ).lower()
219
+ custom_carrier_name = carrier.credentials.get("custom_carrier_name")
220
+
221
+ return {
222
+ **(parent.meta or {}),
223
+ "ext": carrier.ext,
224
+ "carrier": rate_provider,
225
+ "service_name": service_name,
226
+ "rate_provider": rate_provider, # TODO: deprecate 'rate_provider' in favor of 'carrier'
227
+ **(
228
+ {"custom_carrier_name": custom_carrier_name}
229
+ if custom_carrier_name
230
+ else {}
231
+ ),
232
+ }
233
+
234
+ def process_selected_rate() -> dict:
235
+ estimated_delivery = lib.failsafe(
236
+ lambda: (
237
+ getattr(shipment.selected_rate, "estimated_delivery", None)
238
+ or getattr(selected_rate, "estimated_delivery", None)
239
+ )
240
+ )
241
+ transit_days = lib.failsafe(
242
+ lambda: (
243
+ getattr(shipment.selected_rate, "transit_days", None)
244
+ or getattr(selected_rate, "transit_days", None)
245
+ )
246
+ )
247
+ rate = lib.identity(
248
+ {
249
+ **lib.to_dict(shipment.selected_rate),
250
+ "id": f"rat_{uuid.uuid4().hex}",
251
+ "test_mode": carrier.test_mode,
252
+ "estimated_delivery": estimated_delivery,
253
+ "transit_days": transit_days,
254
+ }
255
+ if shipment.selected_rate is not None
256
+ else lib.to_dict(selected_rate)
257
+ )
258
+ return lib.to_dict(
259
+ {
260
+ **rate,
261
+ "meta": process_meta(shipment.selected_rate or selected_rate),
262
+ }
263
+ )
264
+
265
+ def process_tracking_url(rate: datatypes.Rate) -> str:
266
+ rate_provider = (rate.get("meta") or {}).get("rate_provider")
267
+ if (rate_provider not in dataunits.CARRIER_NAMES) and (
268
+ (shipment.meta or {}).get("tracking_url") is not None
269
+ ):
270
+ return shipment.meta["tracking_url"]
271
+
272
+ if resolve_tracking_url is not None:
273
+ url = resolve_tracking_url(
274
+ shipment.tracking_number, rate_provider or rate.carrier_name
275
+ )
276
+ return utils.app_tracking_query_params(url, carrier)
277
+
278
+ return ""
279
+
280
+ def process_parcel_refs(parcels: typing.List[dict]) -> list:
281
+ references = (shipment.meta or {}).get("tracking_numbers") or [
282
+ shipment.tracking_number
283
+ ]
284
+
285
+ return [
286
+ {
287
+ **lib.to_dict(parcel),
288
+ "reference_number": (
289
+ references[index]
290
+ if len(references) > index
291
+ else parcel.get("reference_number")
292
+ ),
293
+ }
294
+ for index, parcel in enumerate(parcels)
295
+ ]
296
+
297
+ shipment_rate = process_selected_rate()
298
+
299
+ return lib.to_object(
300
+ datatypes.Shipment,
301
+ {
302
+ "id": f"shp_{uuid.uuid4().hex}",
303
+ **payload,
304
+ **lib.to_dict(shipment),
305
+ "test_mode": carrier.test_mode,
306
+ "selected_rate": shipment_rate,
307
+ "service": shipment_rate["service"],
308
+ "selected_rate_id": shipment_rate["id"],
309
+ "parcels": process_parcel_refs(payload["parcels"]),
310
+ "tracking_url": process_tracking_url(shipment_rate),
311
+ "status": serializers.ShipmentStatus.purchased.value,
312
+ "created_at": datetime.datetime.now().strftime(
313
+ "%Y-%m-%d %H:%M:%S.%f%z"
314
+ ),
315
+ "meta": process_meta(shipment),
316
+ "messages": messages,
317
+ },
318
+ )
319
+
320
+ @staticmethod
321
+ @utils.with_telemetry("shipment_cancel")
322
+ def cancel(
323
+ payload: dict, carrier: providers.Carrier = None, **carrier_filters
324
+ ) -> datatypes.ConfirmationResponse:
325
+ carrier_id = lib.identity(
326
+ dict(carrier_id=payload.pop("carrier_id"))
327
+ if any(payload.get("carrier_id") or "")
328
+ else {}
329
+ )
330
+ carrier = carrier or Carriers.first(
331
+ **{
332
+ **dict(active=True, capability="shipping", raise_not_found=True),
333
+ **carrier_id,
334
+ **carrier_filters,
335
+ }
336
+ )
337
+
338
+ if carrier is None:
339
+ raise NotFound("No active carrier connection found to process the request")
340
+
341
+ request = karrio.Shipment.cancel(
342
+ lib.to_object(datatypes.ShipmentCancelRequest, payload)
343
+ )
344
+
345
+ # The request call is wrapped in utils.identity to simplify mocking in tests
346
+ confirmation, messages = lib.identity(
347
+ utils.identity(lambda: request.from_(carrier.gateway).parse())
348
+ if "cancel_shipment" in carrier.gateway.proxy_methods
349
+ else (
350
+ datatypes.Confirmation(
351
+ carrier_name=carrier.gateway.settings.carrier_name,
352
+ carrier_id=carrier.gateway.settings.carrier_id,
353
+ success=True,
354
+ operation="Safe cancellation allowed",
355
+ ),
356
+ [],
357
+ )
358
+ )
359
+
360
+ if confirmation is None:
361
+ raise exceptions.APIException(
362
+ detail=messages,
363
+ status_code=status.HTTP_424_FAILED_DEPENDENCY,
364
+ )
365
+
366
+ return datatypes.ConfirmationResponse(
367
+ confirmation=confirmation,
368
+ messages=messages,
369
+ )
370
+
371
+ @staticmethod
372
+ @utils.with_telemetry("tracking_fetch")
373
+ def track(
374
+ payload: dict,
375
+ carrier: providers.Carrier = None,
376
+ raise_on_error: bool = True,
377
+ **carrier_filters,
378
+ ) -> datatypes.TrackingResponse:
379
+ carrier = carrier or Carriers.first(
380
+ **{
381
+ **dict(active=True, capability="tracking", raise_not_found=True),
382
+ **carrier_filters,
383
+ }
384
+ )
385
+
386
+ if carrier is None:
387
+ raise NotFound("No active carrier connection found to process the request")
388
+
389
+ request = karrio.Tracking.fetch(
390
+ lib.to_object(datatypes.TrackingRequest, payload)
391
+ )
392
+
393
+ # The request call is wrapped in utils.identity to simplify mocking in tests
394
+ results, messages = utils.identity(
395
+ lambda: request.from_(carrier.gateway).parse()
396
+ )
397
+
398
+ if not any(results or []) and (
399
+ raise_on_error or utils.is_sdk_message(messages)
400
+ ):
401
+ raise exceptions.APIException(
402
+ detail=messages,
403
+ status_code=status.HTTP_404_NOT_FOUND,
404
+ )
405
+
406
+ result = next(iter(results or []), None)
407
+ tracking_number = payload["tracking_numbers"][0]
408
+ details = result or datatypes.TrackingDetails(
409
+ carrier_id=carrier.carrier_id,
410
+ carrier_name=carrier.carrier_name,
411
+ tracking_number=tracking_number,
412
+ events=[
413
+ datatypes.TrackingEvent(
414
+ date=datetime.datetime.now().strftime("%Y-%m-%d"),
415
+ description="Awaiting update from carrier...",
416
+ code="UNKNOWN",
417
+ time=datetime.datetime.now().strftime("%H:%M"),
418
+ )
419
+ ],
420
+ delivered=False,
421
+ )
422
+ options = {
423
+ **(payload.get("options") or {}),
424
+ tracking_number: {
425
+ **(details.meta or {}),
426
+ **(payload.get("options") or {}).get(tracking_number, {}),
427
+ },
428
+ }
429
+ meta = {
430
+ "ext": carrier.ext,
431
+ "carrier": carrier.carrier_name,
432
+ **(details.meta or {}),
433
+ }
434
+ info = {
435
+ "carrier_tracking_link": utils.get_carrier_tracking_link(
436
+ carrier, tracking_number
437
+ ),
438
+ "source": "api",
439
+ **(lib.to_dict(details.info or {})),
440
+ **(lib.to_dict(payload.get("info") or {})),
441
+ }
442
+
443
+ return datatypes.TrackingResponse(
444
+ tracking=lib.to_object(
445
+ datatypes.Tracking,
446
+ {
447
+ **lib.to_dict(details),
448
+ "id": f"trk_{uuid.uuid4().hex}",
449
+ "test_mode": carrier.test_mode,
450
+ "status": utils.compute_tracking_status(result).value,
451
+ "options": options,
452
+ "meta": meta,
453
+ "info": info,
454
+ },
455
+ ),
456
+ messages=messages,
457
+ )
458
+
459
+
460
+ class Pickups:
461
+ @staticmethod
462
+ @utils.with_telemetry("pickup_schedule")
463
+ def schedule(
464
+ payload: dict, carrier: providers.Carrier = None, **carrier_filters
465
+ ) -> datatypes.PickupResponse:
466
+ carrier = carrier or Carriers.first(
467
+ **{
468
+ **dict(active=True, capability="pickup", raise_not_found=True),
469
+ **carrier_filters,
470
+ }
471
+ )
472
+
473
+ if carrier is None:
474
+ raise NotFound("No active carrier connection found to process the request")
475
+
476
+ request = karrio.Pickup.schedule(
477
+ datatypes.PickupRequest(**lib.to_dict(payload))
478
+ )
479
+
480
+ # The request call is wrapped in utils.identity to simplify mocking in tests
481
+ pickup, messages = utils.identity(
482
+ lambda: request.from_(carrier.gateway).parse()
483
+ )
484
+
485
+ if pickup is None:
486
+ raise exceptions.APIException(
487
+ detail=messages,
488
+ status_code=status.HTTP_424_FAILED_DEPENDENCY,
489
+ )
490
+
491
+ def process_meta(parent) -> dict:
492
+ return {
493
+ **(parent.meta or {}),
494
+ "ext": carrier.ext,
495
+ }
496
+
497
+ return datatypes.PickupResponse(
498
+ pickup=datatypes.Pickup(
499
+ **{
500
+ **payload,
501
+ **lib.to_dict(pickup),
502
+ "id": f"pck_{uuid.uuid4().hex}",
503
+ "test_mode": carrier.test_mode,
504
+ "meta": process_meta(pickup),
505
+ "messages": messages,
506
+ }
507
+ ),
508
+ messages=messages,
509
+ )
510
+
511
+ @staticmethod
512
+ @utils.with_telemetry("pickup_update")
513
+ def update(
514
+ payload: dict, carrier: providers.Carrier = None, **carrier_filters
515
+ ) -> datatypes.PickupResponse:
516
+ carrier = carrier or Carriers.first(
517
+ **{
518
+ **dict(active=True, capability="pickup", raise_not_found=True),
519
+ **carrier_filters,
520
+ }
521
+ )
522
+
523
+ if carrier is None:
524
+ raise NotFound("No active carrier connection found to process the request")
525
+
526
+ request = karrio.Pickup.update(
527
+ datatypes.PickupUpdateRequest(**lib.to_dict(payload))
528
+ )
529
+
530
+ # The request call is wrapped in utils.identity to simplify mocking in tests
531
+ pickup, messages = utils.identity(
532
+ lambda: request.from_(carrier.gateway).parse()
533
+ )
534
+
535
+ if pickup is None:
536
+ raise exceptions.APIException(
537
+ detail=messages,
538
+ status_code=status.HTTP_424_FAILED_DEPENDENCY,
539
+ )
540
+
541
+ return datatypes.PickupResponse(
542
+ pickup=datatypes.Pickup(
543
+ **{
544
+ **payload,
545
+ **lib.to_dict(pickup),
546
+ "test_mode": carrier.test_mode,
547
+ }
548
+ ),
549
+ messages=messages,
550
+ )
551
+
552
+ @staticmethod
553
+ @utils.with_telemetry("pickup_cancel")
554
+ def cancel(
555
+ payload: dict, carrier: providers.Carrier = None, **carrier_filters
556
+ ) -> datatypes.ConfirmationResponse:
557
+ carrier = carrier or Carriers.first(
558
+ **{
559
+ **dict(active=True, capability="pickup"),
560
+ **carrier_filters,
561
+ }
562
+ )
563
+
564
+ if carrier is None:
565
+ raise NotFound("No active carrier connection found to process the request")
566
+
567
+ request = karrio.Pickup.cancel(
568
+ datatypes.PickupCancelRequest(**lib.to_dict(payload))
569
+ )
570
+
571
+ # The request call is wrapped in utils.identity to simplify mocking in tests
572
+ confirmation, messages = lib.identity(
573
+ utils.identity(lambda: request.from_(carrier.gateway).parse())
574
+ if "cancel_shipment" in carrier.gateway.proxy_methods
575
+ else (
576
+ datatypes.Confirmation(
577
+ carrier_name=carrier.gateway.settings.carrier_name,
578
+ carrier_id=carrier.gateway.settings.carrier_id,
579
+ success=True,
580
+ operation="Safe cancellation allowed",
581
+ ),
582
+ [],
583
+ )
584
+ )
585
+
586
+ if confirmation is None:
587
+ raise exceptions.APIException(
588
+ detail=messages,
589
+ status_code=status.HTTP_424_FAILED_DEPENDENCY,
590
+ )
591
+
592
+ return datatypes.ConfirmationResponse(
593
+ confirmation=confirmation, messages=messages
594
+ )
595
+
596
+
597
+ @utils.post_processing(methods=["fetch"])
598
+ class Rates:
599
+ post_process_functions: typing.List[typing.Callable] = []
600
+
601
+ @staticmethod
602
+ @utils.with_telemetry("rates_fetch")
603
+ def fetch(
604
+ payload: dict,
605
+ carriers: typing.List[providers.Carrier] = None,
606
+ raise_on_error: bool = True,
607
+ **carrier_filters,
608
+ ) -> datatypes.RateResponse:
609
+ services = payload.get("services", [])
610
+ carrier_ids = payload.get("carrier_ids", [])
611
+ shipper_country_code = payload["shipper"].get("country_code")
612
+ carriers = carriers or Carriers.list(
613
+ **{
614
+ "active": True,
615
+ "capability": "rating",
616
+ "carrier_ids": carrier_ids,
617
+ "services": services,
618
+ **carrier_filters,
619
+ }
620
+ )
621
+
622
+ gateways = utils.filter_rate_carrier_compatible_gateways(
623
+ carriers, carrier_ids, shipper_country_code
624
+ )
625
+
626
+ if raise_on_error and len(gateways) == 0:
627
+ raise NotFound("No active carrier connection found to process the request")
628
+
629
+ request = karrio.Rating.fetch(lib.to_object(datatypes.RateRequest, payload))
630
+
631
+ # The request call is wrapped in utils.identity to simplify mocking in tests
632
+ rates, messages = utils.identity(lambda: request.from_(*gateways).parse())
633
+
634
+ if raise_on_error and not any(rates) and any(messages):
635
+ raise exceptions.APIException(
636
+ detail=messages,
637
+ status_code=status.HTTP_424_FAILED_DEPENDENCY,
638
+ )
639
+
640
+ def process_rate(rate: datatypes.Rate) -> datatypes.Rate:
641
+ carrier = next((c for c in carriers if c.carrier_id == rate.carrier_id))
642
+ rate_provider = (
643
+ (rate.meta or {}).get("rate_provider")
644
+ or getattr(carrier, "custom_carrier_name", None)
645
+ or rate.carrier_name
646
+ ).lower()
647
+ service_name = utils.upper(
648
+ (rate.meta or {}).get("service_name") or rate.service
649
+ )
650
+
651
+ meta = {
652
+ **(rate.meta or {}),
653
+ "ext": carrier.ext,
654
+ "carrier": rate_provider,
655
+ "service_name": service_name,
656
+ "rate_provider": rate_provider, # TODO: deprecate rate_provider
657
+ "carrier_connection_id": carrier.id,
658
+ }
659
+
660
+ return lib.to_object(
661
+ datatypes.Rate,
662
+ {
663
+ **lib.to_dict(rate),
664
+ "id": f"rat_{uuid.uuid4().hex}",
665
+ "test_mode": carrier.test_mode,
666
+ "meta": meta,
667
+ },
668
+ )
669
+
670
+ formated_rates: typing.List[datatypes.Rate] = sorted(
671
+ map(process_rate, rates), key=lambda rate: rate.total_charge
672
+ )
673
+
674
+ return lib.to_object(
675
+ datatypes.RateResponse, dict(rates=formated_rates, messages=messages)
676
+ )
677
+
678
+
679
+ class Documents:
680
+ @staticmethod
681
+ @utils.with_telemetry("document_upload")
682
+ def upload(
683
+ payload: dict,
684
+ carrier: providers.Carrier = None,
685
+ **carrier_filters,
686
+ ) -> datatypes.DocumentUploadResponse:
687
+ carrier = carrier or Carriers.first(
688
+ **{
689
+ **dict(active=True, raise_not_found=True),
690
+ **carrier_filters,
691
+ }
692
+ )
693
+
694
+ if "upload_document" not in carrier.gateway.proxy_methods:
695
+ raise exceptions.APIException(
696
+ detail=f"trade document upload is not supported by carrier: '{carrier.carrier_id}'",
697
+ status_code=status.HTTP_406_NOT_ACCEPTABLE,
698
+ )
699
+
700
+ request = karrio.Document.upload(
701
+ lib.to_object(datatypes.DocumentUploadRequest, payload)
702
+ )
703
+
704
+ # The request is wrapped in utils.identity to simplify mocking in tests.
705
+ upload, messages = utils.identity(
706
+ lambda: request.from_(carrier.gateway).parse()
707
+ )
708
+
709
+ if upload is None:
710
+ raise exceptions.APIException(
711
+ detail=messages,
712
+ status_code=status.HTTP_424_FAILED_DEPENDENCY,
713
+ )
714
+
715
+ return lib.to_object(
716
+ datatypes.DocumentUploadResponse,
717
+ {
718
+ **payload,
719
+ **lib.to_dict(upload),
720
+ "test_mode": carrier.test_mode,
721
+ "id": f"sdoc_{uuid.uuid4().hex}",
722
+ "messages": lib.to_dict(messages),
723
+ },
724
+ )
725
+
726
+
727
+ class Manifests:
728
+ @staticmethod
729
+ @utils.with_telemetry("manifest_create")
730
+ def create(
731
+ payload: dict, carrier: providers.Carrier = None, **carrier_filters
732
+ ) -> datatypes.ManifestResponse:
733
+ carrier = carrier or Carriers.first(
734
+ **{
735
+ **dict(active=True, capability="manifest", raise_not_found=True),
736
+ **carrier_filters,
737
+ }
738
+ )
739
+
740
+ if carrier is None:
741
+ raise NotFound("No active carrier connection found to process the request")
742
+
743
+ request = karrio.Manifest.create(
744
+ lib.to_object(datatypes.ManifestRequest, lib.to_dict(payload))
745
+ )
746
+
747
+ # The request call is wrapped in utils.identity to simplify mocking in tests
748
+ manifest, messages = utils.identity(
749
+ lambda: request.from_(carrier.gateway).parse()
750
+ )
751
+
752
+ if manifest is None:
753
+ raise exceptions.APIException(
754
+ detail=messages,
755
+ status_code=status.HTTP_424_FAILED_DEPENDENCY,
756
+ )
757
+
758
+ def process_meta(parent) -> dict:
759
+ return {
760
+ **(parent.meta or {}),
761
+ "ext": carrier.ext,
762
+ }
763
+
764
+ return datatypes.ManifestResponse(
765
+ manifest=datatypes.Manifest(
766
+ **{
767
+ **payload,
768
+ **lib.to_dict(manifest),
769
+ "id": f"manf_{uuid.uuid4().hex}",
770
+ "test_mode": carrier.test_mode,
771
+ "meta": process_meta(manifest),
772
+ "messages": messages,
773
+ }
774
+ ),
775
+ messages=messages,
776
+ )
777
+
778
+
779
+ class Insurance:
780
+ @staticmethod
781
+ @utils.with_telemetry("insurance_apply")
782
+ def apply(
783
+ payload: dict, provider: providers.Carrier = None, **provider_filters
784
+ ) -> datatypes.InsuranceDetails:
785
+ provider = provider or Carriers.first(
786
+ **{
787
+ **dict(active=True, raise_not_found=True),
788
+ **provider_filters,
789
+ }
790
+ )
791
+
792
+ if "apply_insurance" not in provider.gateway.proxy_methods:
793
+ raise exceptions.APIException(
794
+ detail=f"insurance application is not supported by carrier: '{provider.carrier_id}'",
795
+ status_code=status.HTTP_406_NOT_ACCEPTABLE,
796
+ )
797
+
798
+ request = karrio.Insurance.apply(
799
+ lib.to_object(datatypes.InsuranceRequest, payload)
800
+ )
801
+
802
+ # The request call is wrapped in utils.identity to simplify mocking in tests
803
+ insurance, messages = utils.identity(
804
+ lambda: request.from_(provider.gateway).parse()
805
+ )
806
+
807
+ if insurance is None:
808
+ raise exceptions.APIException(
809
+ detail=messages,
810
+ status_code=status.HTTP_424_FAILED_DEPENDENCY,
811
+ )
812
+
813
+ def process_meta(parent) -> dict:
814
+ return {
815
+ **(parent.meta or {}),
816
+ "ext": provider.ext,
817
+ }
818
+
819
+ return datatypes.InsuranceResponse(
820
+ insurance=datatypes.Insurance(
821
+ **{
822
+ **payload,
823
+ **lib.to_dict(insurance),
824
+ "id": f"ins_{uuid.uuid4().hex}",
825
+ "test_mode": provider.test_mode,
826
+ "meta": process_meta(insurance),
827
+ "messages": messages,
828
+ }
829
+ ),
830
+ messages=messages,
831
+ )
832
+
833
+
834
+ class Duties:
835
+ @staticmethod
836
+ @utils.with_telemetry("duties_calculate")
837
+ def calculate(
838
+ payload: dict,
839
+ provider: providers.Carrier = None,
840
+ **provider_filters,
841
+ ) -> datatypes.DutiesResponse:
842
+ provider = provider or Carriers.first(
843
+ **{
844
+ **dict(active=True, raise_not_found=True),
845
+ **provider_filters,
846
+ }
847
+ )
848
+
849
+ if "calculate_duties" not in provider.gateway.proxy_methods:
850
+ raise exceptions.APIException(
851
+ detail=f"duties calculation is not supported by carrier: '{provider.carrier_id}'",
852
+ status_code=status.HTTP_406_NOT_ACCEPTABLE,
853
+ )
854
+
855
+ request = karrio.Duty.calculate(
856
+ lib.to_object(datatypes.DutiesCalculationRequest, payload)
857
+ )
858
+
859
+ # The request is wrapped in utils.identity to simplify mocking in tests.
860
+ duties, messages = utils.identity(
861
+ lambda: request.from_(provider.gateway).parse()
862
+ )
863
+
864
+ if duties is None:
865
+ raise exceptions.APIException(
866
+ detail=messages,
867
+ status_code=status.HTTP_424_FAILED_DEPENDENCY,
868
+ )
869
+
870
+ return datatypes.DutiesResponse(duties=duties, messages=messages)
871
+
872
+
873
+ class Webhooks:
874
+ @staticmethod
875
+ @utils.with_telemetry("webhook_register")
876
+ def register(
877
+ payload: dict,
878
+ carrier: providers.Carrier = None,
879
+ **carrier_filters,
880
+ ) -> datatypes.DocumentUploadResponse:
881
+ carrier = carrier or Carriers.first(
882
+ **{
883
+ **dict(active=True, raise_not_found=True),
884
+ **carrier_filters,
885
+ }
886
+ )
887
+
888
+ if "register_webhook" not in carrier.gateway.proxy_methods:
889
+ raise exceptions.APIException(
890
+ detail=f"webhook registration is not supported by carrier: '{carrier.carrier_id}'",
891
+ status_code=status.HTTP_406_NOT_ACCEPTABLE,
892
+ )
893
+
894
+ request = karrio.Webhook.register(
895
+ lib.to_object(datatypes.WebhookRegistrationRequest, payload)
896
+ )
897
+
898
+ # The request is wrapped in utils.identity to simplify mocking in tests.
899
+ return utils.identity(lambda: request.from_(carrier.gateway).parse())
900
+
901
+ @staticmethod
902
+ @utils.with_telemetry("webhook_unregister")
903
+ def unregister(
904
+ payload: dict,
905
+ carrier: providers.Carrier = None,
906
+ **carrier_filters,
907
+ ) -> datatypes.ConfirmationResponse:
908
+ carrier = carrier or Carriers.first(
909
+ **{
910
+ **dict(active=True, raise_not_found=True),
911
+ **carrier_filters,
912
+ }
913
+ )
914
+
915
+ if "deregister_webhook" not in carrier.gateway.proxy_methods:
916
+ raise exceptions.APIException(
917
+ detail=f"webhook deregistration is not supported by carrier: '{carrier.carrier_id}'",
918
+ status_code=status.HTTP_406_NOT_ACCEPTABLE,
919
+ )
920
+
921
+ request = karrio.Webhook.deregister(
922
+ lib.to_object(datatypes.WebhookDeregistrationRequest, payload)
923
+ )
924
+
925
+ # The request is wrapped in utils.identity to simplify mocking in tests.
926
+ return utils.identity(lambda: request.from_(carrier.gateway).parse())
927
+
928
+
929
+ class Hooks:
930
+
931
+ @staticmethod
932
+ def create_stub_gateway(
933
+ carrier_name: str, test_mode: bool = False
934
+ ) -> karrio.Gateway:
935
+ import karrio.server.core.middleware as middleware
936
+ import karrio.server.core.config as system_config
937
+ import django.core.cache as caching
938
+
939
+ _context = middleware.SessionContext.get_current_request()
940
+ _tracer = getattr(_context, "tracer", lib.Tracer())
941
+ _cache = lib.Cache(caching.cache)
942
+ _config = lib.SystemConfig(system_config.config)
943
+
944
+ return karrio.gateway[carrier_name].create(
945
+ dict(
946
+ carrier_id=carrier_name,
947
+ test_mode=test_mode,
948
+ ),
949
+ _tracer,
950
+ _cache,
951
+ _config,
952
+ is_stub=True,
953
+ )
954
+
955
+ @staticmethod
956
+ @utils.with_telemetry("hook_webhook_event")
957
+ def on_webhook_event(
958
+ payload: dict, carrier: providers.Carrier = None, **carrier_filters
959
+ ) -> typing.Tuple[datatypes.WebhookEventDetails, typing.List[datatypes.Message]]:
960
+ carrier = carrier or Carriers.first(
961
+ **{
962
+ **dict(active=True, raise_not_found=True),
963
+ **carrier_filters,
964
+ }
965
+ )
966
+
967
+ if carrier is None:
968
+ raise NotFound("No active carrier connection found to process the request")
969
+
970
+ request = karrio.Hooks.on_webhook_event(
971
+ lib.to_object(datatypes.RequestPayload, lib.to_dict(payload))
972
+ )
973
+
974
+ # The request call is wrapped in utils.identity to simplify mocking in tests
975
+ return utils.identity(lambda: request.from_(carrier.gateway).parse())
976
+
977
+ @staticmethod
978
+ @utils.with_telemetry("hook_oauth_authorize")
979
+ def on_oauth_authorize(
980
+ payload: dict,
981
+ carrier: providers.Carrier = None,
982
+ carrier_name: str = None,
983
+ test_mode: bool = False,
984
+ **kwargs,
985
+ ) -> typing.Tuple[datatypes.OAuthAuthorizeRequest, typing.List[datatypes.Message]]:
986
+ gateway = lib.identity(
987
+ getattr(carrier, "gateway", None)
988
+ or Hooks.create_stub_gateway(carrier_name, test_mode)
989
+ )
990
+
991
+ return utils.identity(
992
+ lambda: karrio.Hooks.on_oauth_authorize(payload).from_(gateway).parse()
993
+ )
994
+
995
+ @staticmethod
996
+ @utils.with_telemetry("hook_oauth_callback")
997
+ def on_oauth_callback(
998
+ payload: dict,
999
+ carrier_name: str = None,
1000
+ test_mode: bool = False,
1001
+ carrier: providers.Carrier = None,
1002
+ **kwargs,
1003
+ ) -> typing.Tuple[typing.List[typing.Dict], typing.List[datatypes.Message]]:
1004
+ gateway = lib.identity(
1005
+ getattr(carrier, "gateway", None)
1006
+ or Hooks.create_stub_gateway(carrier_name, test_mode)
1007
+ )
1008
+
1009
+ return utils.identity(
1010
+ lambda: karrio.Hooks.on_oauth_callback(payload).from_(gateway).parse()
1011
+ )