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,538 @@
1
+ import typing
2
+ import django.db.transaction as transaction
3
+ from rest_framework import status as http_status
4
+
5
+ import karrio.lib as lib
6
+ import karrio.references as references
7
+ import karrio.server.openapi as openapi
8
+ import karrio.server.core.utils as utils
9
+ import karrio.server.serializers as serializers
10
+ import karrio.server.core.dataunits as dataunits
11
+ import karrio.server.core.exceptions as exceptions
12
+ import karrio.server.providers.models as providers
13
+ from karrio.server.core.serializers import CARRIERS, Message
14
+
15
+
16
+ def generate_carrier_serializers() -> typing.Dict[str, serializers.Serializer]:
17
+
18
+ def _create_serializer(carrier_name: str) -> serializers.Serializer:
19
+ fields = dataunits.REFERENCE_MODELS["connection_fields"][carrier_name]
20
+ return type(
21
+ carrier_name,
22
+ (serializers.Serializer,),
23
+ {
24
+ key: serializers.field_to_serializer(field)
25
+ for key, field in fields.items()
26
+ },
27
+ )
28
+
29
+ return {
30
+ carrier_name: _create_serializer(carrier_name)
31
+ for carrier_name in dataunits.REFERENCE_MODELS["carriers"].keys()
32
+ }
33
+
34
+
35
+ CONNECTION_SERIALIZERS = generate_carrier_serializers()
36
+
37
+
38
+ @openapi.extend_schema_field(
39
+ openapi.PolymorphicProxySerializer(
40
+ component_name="ConnectionCredentialsField",
41
+ serializers=CONNECTION_SERIALIZERS.values(),
42
+ resource_type_field_name=None,
43
+ )
44
+ )
45
+ class ConnectionCredentialsField(serializers.DictField):
46
+ pass
47
+
48
+
49
+ @serializers.owned_model_serializer
50
+ class CarrierConfigModelSerializer(serializers.ModelSerializer):
51
+ class Meta:
52
+ model = providers.CarrierConfig
53
+ exclude = ["created_at", "updated_at", "created_by"]
54
+ extra_kwargs = {field: {"read_only": True} for field in ["id"]}
55
+
56
+
57
+ class CarrierConnectionData(serializers.Serializer):
58
+
59
+ carrier_name = serializers.ChoiceField(
60
+ choices=CARRIERS,
61
+ required=True,
62
+ help_text="A carrier connection type.",
63
+ )
64
+ carrier_id = serializers.CharField(
65
+ required=True,
66
+ help_text="A carrier connection friendly name.",
67
+ )
68
+ credentials = ConnectionCredentialsField(
69
+ required=True,
70
+ help_text="Carrier connection credentials.",
71
+ )
72
+ capabilities = serializers.StringListField(
73
+ required=False,
74
+ allow_null=True,
75
+ help_text="""The carrier enabled capabilities.""",
76
+ )
77
+ config = serializers.PlainDictField(
78
+ required=False,
79
+ default={},
80
+ help_text="Carrier connection custom config.",
81
+ )
82
+ metadata = serializers.PlainDictField(
83
+ required=False,
84
+ default={},
85
+ help_text="User metadata for the carrier.",
86
+ )
87
+ active = serializers.BooleanField(
88
+ required=False,
89
+ default=True,
90
+ help_text="The active flag indicates whether the carrier account is active or not.",
91
+ )
92
+
93
+
94
+ class CarrierConnectionUpdateData(serializers.Serializer):
95
+ carrier_id = serializers.CharField(
96
+ required=False, help_text="A carrier connection friendly name."
97
+ )
98
+ credentials = serializers.PlainDictField(
99
+ required=False,
100
+ help_text="Carrier connection credentials.",
101
+ )
102
+ capabilities = serializers.StringListField(
103
+ required=False,
104
+ allow_null=True,
105
+ help_text="""The carrier enabled capabilities.""",
106
+ )
107
+ config = serializers.PlainDictField(
108
+ required=False,
109
+ default={},
110
+ help_text="Carrier connection custom config.",
111
+ )
112
+ metadata = serializers.PlainDictField(
113
+ required=False,
114
+ default={},
115
+ help_text="User metadata for the carrier.",
116
+ )
117
+ active = serializers.BooleanField(
118
+ required=False,
119
+ help_text="The active flag indicates whether the carrier account is active or not.",
120
+ )
121
+
122
+
123
+ class CarrierConnection(serializers.Serializer):
124
+ id = serializers.CharField(
125
+ required=True,
126
+ help_text="A unique carrier connection identifier",
127
+ )
128
+ object_type = serializers.CharField(
129
+ default="carrier-connection",
130
+ help_text="Specifies the object type",
131
+ )
132
+ carrier_name = serializers.ChoiceField(
133
+ choices=CARRIERS,
134
+ required=True,
135
+ help_text="A carrier connection type.",
136
+ )
137
+ display_name = serializers.CharField(
138
+ required=False,
139
+ help_text="The carrier connection type verbose name.",
140
+ )
141
+ carrier_id = serializers.CharField(
142
+ required=True,
143
+ help_text="A carrier connection friendly name.",
144
+ )
145
+ credentials = ConnectionCredentialsField(
146
+ required=False,
147
+ help_text="Carrier connection credentials.",
148
+ )
149
+ capabilities = serializers.StringListField(
150
+ required=False,
151
+ allow_null=True,
152
+ help_text="""The carrier enabled capabilities.""",
153
+ )
154
+ config = serializers.PlainDictField(
155
+ required=False,
156
+ default={},
157
+ help_text="Carrier connection custom config.",
158
+ )
159
+ metadata = serializers.PlainDictField(
160
+ required=False,
161
+ default={},
162
+ help_text="User metadata for the carrier.",
163
+ )
164
+ is_system = serializers.BooleanField(
165
+ required=True,
166
+ help_text="The carrier connection is provided by the system admin.",
167
+ )
168
+ active = serializers.BooleanField(
169
+ required=True,
170
+ help_text="The active flag indicates whether the carrier account is active or not.",
171
+ )
172
+ test_mode = serializers.BooleanField(
173
+ required=True,
174
+ help_text="The test flag indicates whether to use a carrier configured for test.",
175
+ )
176
+
177
+
178
+ @serializers.owned_model_serializer
179
+ class CarrierConnectionModelSerializer(serializers.ModelSerializer):
180
+ class Meta:
181
+ model = providers.Carrier
182
+ exclude = ["created_at", "updated_at", "created_by"]
183
+
184
+ carrier_name = serializers.ChoiceField(
185
+ required=True, choices=CARRIERS, help_text="Indicates a carrier (type)"
186
+ )
187
+ capabilities = serializers.StringListField(
188
+ required=False,
189
+ allow_null=True,
190
+ help_text="""The carrier enabled capabilities.""",
191
+ )
192
+ config = serializers.PlainDictField(
193
+ required=False,
194
+ allow_null=True,
195
+ help_text="Carrier connection custom config.",
196
+ )
197
+
198
+ @transaction.atomic
199
+ @utils.error_wrapper
200
+ def create(
201
+ self,
202
+ validated_data: dict,
203
+ context: serializers.Context,
204
+ **kwargs,
205
+ ) -> providers.Carrier:
206
+ config = validated_data.pop("config", None)
207
+ carrier_name = validated_data.pop("carrier_name")
208
+ default_capabilities = references.get_carrier_capabilities(carrier_name)
209
+ capabilities = lib.identity(
210
+ validated_data.get("capabilities")
211
+ if any(validated_data.get("capabilities") or [])
212
+ else default_capabilities
213
+ )
214
+
215
+ validated_data.update(test_mode=context.test_mode)
216
+ validated_data.update(carrier_code=carrier_name)
217
+ validated_data.update(
218
+ capabilities=[_ for _ in capabilities if _ in default_capabilities]
219
+ )
220
+ validated_data.update(
221
+ credentials=CONNECTION_SERIALIZERS[carrier_name]
222
+ .map(data=validated_data["credentials"])
223
+ .data
224
+ )
225
+
226
+ instance = super().create(validated_data, context=context, **kwargs)
227
+
228
+ if config is not None:
229
+ CarrierConfigModelSerializer.map(
230
+ context=context,
231
+ data={"carrier": instance.pk, "config": config},
232
+ ).save()
233
+
234
+ return providers.Carrier.objects.get(pk=instance.pk)
235
+
236
+ @transaction.atomic
237
+ @utils.error_wrapper
238
+ def update(
239
+ self,
240
+ instance: providers.Carrier,
241
+ validated_data: dict,
242
+ **kwargs,
243
+ ) -> providers.Carrier:
244
+ if any(validated_data.get("capabilities") or []):
245
+ default_capabilities = references.get_carrier_capabilities(instance.ext)
246
+ capabilities = validated_data.get("capabilities")
247
+ instance.capabilities = [
248
+ _ for _ in capabilities if _ in default_capabilities
249
+ ]
250
+
251
+ if "credentials" in validated_data:
252
+ data = serializers.process_dictionaries_mutations(
253
+ ["credentials"],
254
+ validated_data,
255
+ instance,
256
+ )
257
+ validated_data.update(
258
+ credentials=CONNECTION_SERIALIZERS[instance.ext]
259
+ .map(data=data["credentials"])
260
+ .data
261
+ )
262
+
263
+ if "config" in validated_data:
264
+ data = serializers.process_dictionaries_mutations(
265
+ ["config"],
266
+ dict(config=validated_data.pop("config")),
267
+ instance,
268
+ )
269
+ lib.identity(
270
+ CarrierConfigModelSerializer.map(
271
+ instance=instance.carrier_config,
272
+ context=kwargs.get("context"),
273
+ data={"carrier": instance.pk, **data},
274
+ )
275
+ .save()
276
+ .instance
277
+ )
278
+
279
+ return super().update(instance, validated_data, **kwargs)
280
+
281
+
282
+ # =============================================================================
283
+ # Webhook Management Serializers
284
+ # =============================================================================
285
+
286
+
287
+ class WebhookOperationResponse(serializers.Serializer):
288
+ """Response serializer for webhook operations."""
289
+
290
+ operation = serializers.CharField(help_text="The operation performed")
291
+ success = serializers.BooleanField(help_text="Whether the operation was successful")
292
+ carrier_name = serializers.CharField(help_text="The carrier name")
293
+ carrier_id = serializers.CharField(help_text="The carrier connection ID")
294
+ messages = Message(
295
+ required=False,
296
+ many=True,
297
+ help_text="Operation messages or errors",
298
+ )
299
+
300
+
301
+ class WebhookRegisterData(serializers.Serializer):
302
+ """Request serializer for webhook registration."""
303
+
304
+ enabled_events = serializers.StringListField(
305
+ required=False,
306
+ default=["*"],
307
+ help_text="Events to subscribe to. Defaults to all events.",
308
+ )
309
+ description = serializers.CharField(
310
+ required=False,
311
+ allow_blank=True,
312
+ help_text="Description for the webhook registration.",
313
+ )
314
+
315
+
316
+ class WebhookRegisterSerializer(serializers.Serializer):
317
+ """Handles webhook registration with carriers. Returns webhook details on success."""
318
+
319
+ webhook_url = serializers.URLField(
320
+ required=True,
321
+ help_text="The URL to receive webhook events.",
322
+ )
323
+ description = serializers.CharField(
324
+ required=False,
325
+ allow_blank=True,
326
+ help_text="Description for the webhook registration.",
327
+ )
328
+
329
+ @utils.error_wrapper
330
+ def update(self, connection: providers.Carrier, validated_data: dict, **kwargs):
331
+ import karrio.server.core.gateway as gateway
332
+
333
+ webhook_url = validated_data["webhook_url"]
334
+ description = validated_data.get(
335
+ "description", f"Karrio webhook for {connection.carrier_id}"
336
+ )
337
+
338
+ webhook_details, messages = gateway.Webhooks.register(
339
+ dict(url=webhook_url, description=description),
340
+ carrier=connection,
341
+ **kwargs,
342
+ )
343
+
344
+ if webhook_details is None:
345
+ raise exceptions.APIException(
346
+ detail=messages,
347
+ status_code=http_status.HTTP_424_FAILED_DEPENDENCY,
348
+ )
349
+
350
+ return webhook_details
351
+
352
+
353
+ class WebhookDeregisterSerializer(serializers.Serializer):
354
+ """Handles webhook deregistration from carriers. Returns confirmation on success."""
355
+
356
+ webhook_id = serializers.CharField(
357
+ required=True,
358
+ help_text="The webhook ID to deregister.",
359
+ )
360
+
361
+ @utils.error_wrapper
362
+ def update(self, connection: providers.Carrier, validated_data: dict, **kwargs):
363
+ import karrio.server.core.gateway as gateway
364
+
365
+ confirmation, messages = gateway.Webhooks.unregister(
366
+ payload=dict(webhook_id=validated_data["webhook_id"]),
367
+ carrier=connection,
368
+ )
369
+
370
+ if not (confirmation and confirmation.success):
371
+ raise exceptions.APIException(
372
+ detail=messages,
373
+ status_code=http_status.HTTP_424_FAILED_DEPENDENCY,
374
+ )
375
+
376
+ return confirmation
377
+
378
+
379
+ # =============================================================================
380
+ # OAuth Callback Serializers
381
+ # =============================================================================
382
+
383
+
384
+ class OAuthAuthorizeData(serializers.Serializer):
385
+ """Request serializer for OAuth authorization."""
386
+
387
+ frontend_url = serializers.CharField(
388
+ required=False,
389
+ allow_blank=True,
390
+ help_text="Frontend URL to redirect to after OAuth callback.",
391
+ )
392
+
393
+
394
+ class OAuthCallbackData(serializers.Serializer):
395
+ """Request serializer for OAuth callback data."""
396
+
397
+ query = serializers.PlainDictField(
398
+ required=False,
399
+ default={},
400
+ help_text="Query parameters from the OAuth callback.",
401
+ )
402
+ body = serializers.PlainDictField(
403
+ required=False,
404
+ default={},
405
+ help_text="Body data from the OAuth callback.",
406
+ )
407
+ headers = serializers.PlainDictField(
408
+ required=False,
409
+ default={},
410
+ help_text="Headers from the OAuth callback.",
411
+ )
412
+ url = serializers.CharField(
413
+ required=False,
414
+ help_text="The full callback URL.",
415
+ )
416
+
417
+
418
+ class OAuthCallbackSerializer(serializers.Serializer):
419
+ """Handles OAuth callback processing logic."""
420
+
421
+ @staticmethod
422
+ def process_callback(
423
+ request,
424
+ carrier_name: str,
425
+ ) -> dict:
426
+ """Process OAuth callback and return result dict."""
427
+ import json
428
+ import base64
429
+ import karrio.lib as lib
430
+ import karrio.server.core.gateway as gateway
431
+
432
+ payload = OAuthCallbackData.map(
433
+ data=dict(
434
+ query=request.query_params.dict(),
435
+ body=(
436
+ request.data.dict()
437
+ if hasattr(request.data, "dict")
438
+ else dict(request.data or {})
439
+ ),
440
+ headers=dict(request.headers),
441
+ url=request.build_absolute_uri(),
442
+ )
443
+ ).data
444
+
445
+ [output, messages] = gateway.Hooks.on_oauth_callback(
446
+ payload=payload,
447
+ carrier_name=carrier_name,
448
+ test_mode=request.test_mode,
449
+ context=request,
450
+ )
451
+
452
+ result = dict(
453
+ type="oauth_callback",
454
+ success=output is not None,
455
+ carrier_name=carrier_name,
456
+ credentials=lib.to_dict(output) if output else None,
457
+ messages=lib.to_dict(messages),
458
+ state=request.query_params.get("state"),
459
+ )
460
+
461
+ frontend_url = None
462
+ state = request.query_params.get("state")
463
+ if state:
464
+ try:
465
+ state_data = json.loads(base64.b64decode(state).decode("utf-8"))
466
+ frontend_url = state_data.get("frontend_url")
467
+ except Exception:
468
+ pass
469
+
470
+ return result, frontend_url
471
+
472
+
473
+ # =============================================================================
474
+ # Webhook Event Serializers
475
+ # =============================================================================
476
+
477
+
478
+ class WebhookEventSerializer(serializers.Serializer):
479
+ """Handles webhook event processing logic."""
480
+
481
+ @staticmethod
482
+ def process_event(request, pk: str) -> tuple:
483
+ """
484
+ Process webhook event and return response data and status code.
485
+
486
+ Returns:
487
+ tuple: (response_data, http_status_code)
488
+ """
489
+ import django.db.models as django
490
+ import karrio.lib as lib
491
+ import karrio.server.core.gateway as gateway
492
+
493
+ try:
494
+ connection = providers.Carrier.objects.get(pk=pk)
495
+ except providers.Carrier.DoesNotExist:
496
+ return (
497
+ dict(
498
+ operation="Webhook event",
499
+ success=False,
500
+ messages=[{"message": f"Connection not found: {pk}"}],
501
+ ),
502
+ http_status.HTTP_404_NOT_FOUND,
503
+ )
504
+
505
+ event, messages = gateway.Hooks.on_webhook_event(
506
+ payload=dict(
507
+ url=request.build_absolute_uri(),
508
+ body=request.data,
509
+ query=dict(request.query_params),
510
+ headers=dict(request.headers),
511
+ ),
512
+ carrier=connection,
513
+ )
514
+
515
+ if event and event.tracking:
516
+ import karrio.server.manager.models as manager_models
517
+ import karrio.server.manager.serializers.tracking as tracking_serializers
518
+
519
+ tracker = manager_models.Tracking.objects.filter(
520
+ django.Q(tracking_number=event.tracking.tracking_number)
521
+ | django.Q(tracking_carrier=connection)
522
+ ).first()
523
+
524
+ if tracker:
525
+ tracking_serializers.update_tracker(
526
+ tracker, lib.to_dict(event.tracking)
527
+ )
528
+
529
+ return (
530
+ dict(
531
+ operation="Webhook event",
532
+ success=len(messages) == 0,
533
+ carrier_name=connection.carrier_name,
534
+ carrier_id=connection.carrier_id,
535
+ messages=lib.to_dict(messages),
536
+ ),
537
+ http_status.HTTP_200_OK,
538
+ )
@@ -0,0 +1,25 @@
1
+ from django.db.models import signals
2
+
3
+ import karrio.references as ref
4
+ import karrio.server.core.utils as utils
5
+ from karrio.server.core.logging import logger
6
+ import karrio.server.providers.models as models
7
+
8
+
9
+ def register_signals():
10
+ signals.post_save.connect(carrier_changed, sender=models.Carrier)
11
+
12
+ logger.info("Karrio providers signals registered")
13
+
14
+
15
+ @utils.disable_for_loaddata
16
+ def carrier_changed(
17
+ sender, instance, created, raw, using, update_fields, *args, **kwargs
18
+ ):
19
+ """Setup default capabilities when carrier are created."""
20
+ if not created:
21
+ return
22
+
23
+ if len(instance.capabilities or []) == 0:
24
+ instance.capabilities = ref.get_carrier_capabilities(instance.carrier_code)
25
+ instance.save()
@@ -0,0 +1,105 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>OAuth Callback</title>
5
+ <style>
6
+ body {
7
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
8
+ display: flex;
9
+ justify-content: center;
10
+ align-items: center;
11
+ height: 100vh;
12
+ margin: 0;
13
+ background: #f5f5f5;
14
+ }
15
+ .container {
16
+ text-align: center;
17
+ padding: 40px;
18
+ background: white;
19
+ border-radius: 8px;
20
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
21
+ max-width: 400px;
22
+ }
23
+ .success { color: #16a34a; }
24
+ .error { color: #dc2626; }
25
+ .icon {
26
+ font-size: 48px;
27
+ margin-bottom: 16px;
28
+ }
29
+ h2 { margin-bottom: 10px; }
30
+ p { color: #666; margin-bottom: 20px; }
31
+ .close-hint {
32
+ font-size: 12px;
33
+ color: #999;
34
+ }
35
+ </style>
36
+ </head>
37
+ <body>
38
+ <div class="container">
39
+ <div class="icon">{% if success %}✓{% else %}✗{% endif %}</div>
40
+ <h2 class="{% if success %}success{% else %}error{% endif %}">
41
+ {% if success %}Authorization Successful{% else %}Authorization Failed{% endif %}
42
+ </h2>
43
+ <p>{% if success %}You can close this window and return to the application.{% else %}{{ error_message }}{% endif %}</p>
44
+ <p class="close-hint">This window will close automatically...</p>
45
+ </div>
46
+ <script>
47
+ (async function() {
48
+ var result = {{ result_json|safe }};
49
+
50
+ /**
51
+ * Encrypts data using AES-GCM with Web Crypto API.
52
+ * Returns an object containing the encrypted data, IV, and key (all base64 encoded).
53
+ */
54
+ async function encryptData(data) {
55
+ // Generate a random 256-bit key
56
+ var key = await crypto.subtle.generateKey(
57
+ { name: "AES-GCM", length: 256 },
58
+ true, // extractable - needed to export the key
59
+ ["encrypt", "decrypt"]
60
+ );
61
+
62
+ // Generate a random 96-bit IV (recommended for AES-GCM)
63
+ var iv = crypto.getRandomValues(new Uint8Array(12));
64
+
65
+ // Encode the data as UTF-8
66
+ var encoder = new TextEncoder();
67
+ var encodedData = encoder.encode(data);
68
+
69
+ // Encrypt the data
70
+ var encryptedBuffer = await crypto.subtle.encrypt(
71
+ { name: "AES-GCM", iv: iv },
72
+ key,
73
+ encodedData
74
+ );
75
+
76
+ // Export the key for storage
77
+ var exportedKey = await crypto.subtle.exportKey("raw", key);
78
+
79
+ // Convert to base64 for storage
80
+ var ciphertext = btoa(String.fromCharCode.apply(null, new Uint8Array(encryptedBuffer)));
81
+ var ivBase64 = btoa(String.fromCharCode.apply(null, iv));
82
+ var keyBase64 = btoa(String.fromCharCode.apply(null, new Uint8Array(exportedKey)));
83
+
84
+ return { ciphertext: ciphertext, iv: ivBase64, key: keyBase64 };
85
+ }
86
+
87
+ // Store encrypted result in localStorage for the opener to read
88
+ // Using localStorage (not sessionStorage) because popup windows have
89
+ // separate sessionStorage contexts from their opener window.
90
+ // Data is encrypted with AES-GCM for security.
91
+ try {
92
+ var encrypted = await encryptData(JSON.stringify(result));
93
+ localStorage.setItem('karrio_oauth_result', JSON.stringify(encrypted));
94
+ } catch (e) {
95
+ console.error('Failed to encrypt/store OAuth result:', e);
96
+ }
97
+
98
+ // Try to close the window after a short delay
99
+ setTimeout(function() {
100
+ window.close();
101
+ }, 2000);
102
+ })();
103
+ </script>
104
+ </body>
105
+ </html>
@@ -0,0 +1,5 @@
1
+ import logging
2
+
3
+ logging.disable(logging.CRITICAL)
4
+
5
+ from karrio.server.providers.tests.test_connections import *