karrio-server-core 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.

Potentially problematic release.


This version of karrio-server-core might be problematic. Click here for more details.

Files changed (241) 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 +313 -0
  6. karrio/server/core/context_processors.py +12 -0
  7. karrio/server/core/datatypes.py +369 -0
  8. karrio/server/core/dataunits.py +156 -0
  9. karrio/server/core/exceptions.py +200 -0
  10. karrio/server/core/fields.py +12 -0
  11. karrio/server/core/filters.py +823 -0
  12. karrio/server/core/gateway.py +720 -0
  13. karrio/server/core/management/commands/cli.py +19 -0
  14. karrio/server/core/management/commands/create_oauth_client.py +41 -0
  15. karrio/server/core/middleware.py +95 -0
  16. karrio/server/core/migrations/0001_initial.py +28 -0
  17. karrio/server/core/migrations/0002_apilogindex.py +69 -0
  18. karrio/server/core/migrations/0003_apilogindex_test_mode.py +62 -0
  19. karrio/server/core/migrations/0004_metafield.py +74 -0
  20. karrio/server/core/migrations/0005_alter_metafield_type_alter_metafield_value.py +23 -0
  21. karrio/server/core/migrations/__init__.py +0 -0
  22. karrio/server/core/models/__init__.py +48 -0
  23. karrio/server/core/models/base.py +70 -0
  24. karrio/server/core/models/entity.py +22 -0
  25. karrio/server/core/models/metafield.py +144 -0
  26. karrio/server/core/models/third_party.py +21 -0
  27. karrio/server/core/oauth_validators.py +171 -0
  28. karrio/server/core/permissions.py +37 -0
  29. karrio/server/core/renderers.py +11 -0
  30. karrio/server/core/router.py +3 -0
  31. karrio/server/core/serializers.py +1898 -0
  32. karrio/server/core/signals.py +57 -0
  33. karrio/server/core/tests.py +98 -0
  34. karrio/server/core/urls.py +12 -0
  35. karrio/server/core/utils.py +479 -0
  36. karrio/server/core/validators.py +416 -0
  37. karrio/server/core/views/__init__.py +2 -0
  38. karrio/server/core/views/api.py +133 -0
  39. karrio/server/core/views/metadata.py +44 -0
  40. karrio/server/core/views/oauth.py +74 -0
  41. karrio/server/core/views/references.py +82 -0
  42. karrio/server/core/views/schema.py +310 -0
  43. karrio/server/filters/__init__.py +2 -0
  44. karrio/server/filters/abstract.py +26 -0
  45. karrio/server/iam/__init__.py +0 -0
  46. karrio/server/iam/admin.py +3 -0
  47. karrio/server/iam/apps.py +21 -0
  48. karrio/server/iam/migrations/0001_initial.py +33 -0
  49. karrio/server/iam/migrations/__init__.py +0 -0
  50. karrio/server/iam/models.py +48 -0
  51. karrio/server/iam/permissions.py +134 -0
  52. karrio/server/iam/serializers.py +39 -0
  53. karrio/server/iam/signals.py +20 -0
  54. karrio/server/iam/tests.py +3 -0
  55. karrio/server/iam/views.py +3 -0
  56. karrio/server/openapi.py +75 -0
  57. karrio/server/providers/__init__.py +1 -0
  58. karrio/server/providers/admin.py +364 -0
  59. karrio/server/providers/apps.py +10 -0
  60. karrio/server/providers/extension/__init__.py +1 -0
  61. karrio/server/providers/extension/models/__init__.py +1 -0
  62. karrio/server/providers/extension/models/allied_express.py +22 -0
  63. karrio/server/providers/extension/models/allied_express_local.py +22 -0
  64. karrio/server/providers/extension/models/amazon_shipping.py +27 -0
  65. karrio/server/providers/extension/models/aramex.py +25 -0
  66. karrio/server/providers/extension/models/asendia_us.py +21 -0
  67. karrio/server/providers/extension/models/australiapost.py +20 -0
  68. karrio/server/providers/extension/models/boxknight.py +19 -0
  69. karrio/server/providers/extension/models/bpost.py +21 -0
  70. karrio/server/providers/extension/models/canadapost.py +21 -0
  71. karrio/server/providers/extension/models/canpar.py +19 -0
  72. karrio/server/providers/extension/models/chronopost.py +22 -0
  73. karrio/server/providers/extension/models/colissimo.py +22 -0
  74. karrio/server/providers/extension/models/dhl_express.py +23 -0
  75. karrio/server/providers/extension/models/dhl_parcel_de.py +25 -0
  76. karrio/server/providers/extension/models/dhl_poland.py +22 -0
  77. karrio/server/providers/extension/models/dhl_universal.py +19 -0
  78. karrio/server/providers/extension/models/dicom.py +20 -0
  79. karrio/server/providers/extension/models/dpd.py +37 -0
  80. karrio/server/providers/extension/models/dpdhl.py +26 -0
  81. karrio/server/providers/extension/models/easypost.py +20 -0
  82. karrio/server/providers/extension/models/eshipper.py +21 -0
  83. karrio/server/providers/extension/models/fedex.py +25 -0
  84. karrio/server/providers/extension/models/fedex_ws.py +24 -0
  85. karrio/server/providers/extension/models/freightcom.py +21 -0
  86. karrio/server/providers/extension/models/generic.py +35 -0
  87. karrio/server/providers/extension/models/geodis.py +22 -0
  88. karrio/server/providers/extension/models/hay_post.py +22 -0
  89. karrio/server/providers/extension/models/laposte.py +19 -0
  90. karrio/server/providers/extension/models/locate2u.py +22 -0
  91. karrio/server/providers/extension/models/nationex.py +22 -0
  92. karrio/server/providers/extension/models/purolator.py +21 -0
  93. karrio/server/providers/extension/models/roadie.py +18 -0
  94. karrio/server/providers/extension/models/royalmail.py +19 -0
  95. karrio/server/providers/extension/models/sendle.py +22 -0
  96. karrio/server/providers/extension/models/tge.py +63 -0
  97. karrio/server/providers/extension/models/tnt.py +23 -0
  98. karrio/server/providers/extension/models/ups.py +23 -0
  99. karrio/server/providers/extension/models/usps.py +23 -0
  100. karrio/server/providers/extension/models/usps_international.py +23 -0
  101. karrio/server/providers/extension/models/usps_wt.py +24 -0
  102. karrio/server/providers/extension/models/usps_wt_international.py +24 -0
  103. karrio/server/providers/extension/models/zoom2u.py +23 -0
  104. karrio/server/providers/migrations/0001_initial.py +140 -0
  105. karrio/server/providers/migrations/0002_carrier_active.py +18 -0
  106. karrio/server/providers/migrations/0003_auto_20201230_0820.py +24 -0
  107. karrio/server/providers/migrations/0004_auto_20210212_0554.py +178 -0
  108. karrio/server/providers/migrations/0005_auto_20210212_0555.py +18 -0
  109. karrio/server/providers/migrations/0006_australiapostsettings.py +29 -0
  110. karrio/server/providers/migrations/0007_auto_20210213_0206.py +21 -0
  111. karrio/server/providers/migrations/0008_auto_20210214_0409.py +30 -0
  112. karrio/server/providers/migrations/0009_auto_20210308_0302.py +18 -0
  113. karrio/server/providers/migrations/0010_auto_20210409_0852.py +32 -0
  114. karrio/server/providers/migrations/0011_auto_20210409_0853.py +21 -0
  115. karrio/server/providers/migrations/0012_alter_carrier_options.py +17 -0
  116. karrio/server/providers/migrations/0013_tntsettings.py +30 -0
  117. karrio/server/providers/migrations/0014_auto_20210612_1608.py +46 -0
  118. karrio/server/providers/migrations/0015_auto_20210615_1601.py +28 -0
  119. karrio/server/providers/migrations/0016_alter_purolatorsettings_user_token.py +18 -0
  120. karrio/server/providers/migrations/0017_auto_20210805_0359.py +1293 -0
  121. karrio/server/providers/migrations/0018_alter_fedexsettings_user_key.py +18 -0
  122. karrio/server/providers/migrations/0019_dhlpolandsettings_servicelevel.py +65 -0
  123. karrio/server/providers/migrations/0020_genericsettings_labeltemplate.py +52 -0
  124. karrio/server/providers/migrations/0021_auto_20211231_2353.py +40 -0
  125. karrio/server/providers/migrations/0022_carrier_metadata.py +18 -0
  126. karrio/server/providers/migrations/0023_auto_20220124_1916.py +27 -0
  127. karrio/server/providers/migrations/0024_alter_genericsettings_custom_carrier_name.py +19 -0
  128. karrio/server/providers/migrations/0025_alter_servicelevel_service_code.py +19 -0
  129. karrio/server/providers/migrations/0026_auto_20220208_0132.py +59 -0
  130. karrio/server/providers/migrations/0027_auto_20220304_1340.py +29 -0
  131. karrio/server/providers/migrations/0028_auto_20220323_1500.py +33 -0
  132. karrio/server/providers/migrations/0029_easypostsettings.py +27 -0
  133. karrio/server/providers/migrations/0030_amazonmwssettings.py +29 -0
  134. karrio/server/providers/migrations/0031_delete_amazonmwssettings.py +18 -0
  135. karrio/server/providers/migrations/0032_alter_carrier_test.py +18 -0
  136. karrio/server/providers/migrations/0033_auto_20220708_1350.py +22 -0
  137. karrio/server/providers/migrations/0034_amazonmwssettings_dpdhlsettings.py +47 -0
  138. karrio/server/providers/migrations/0035_alter_carrier_capabilities.py +43 -0
  139. karrio/server/providers/migrations/0036_upsfreightsettings.py +31 -0
  140. karrio/server/providers/migrations/0037_chronopostsettings.py +29 -0
  141. karrio/server/providers/migrations/0038_alter_genericsettings_label_template.py +19 -0
  142. karrio/server/providers/migrations/0039_auto_20220906_0612.py +23 -0
  143. karrio/server/providers/migrations/0040_dpdhlsettings_services.py +18 -0
  144. karrio/server/providers/migrations/0041_auto_20221105_0705.py +38 -0
  145. karrio/server/providers/migrations/0042_auto_20221215_1642.py +23 -0
  146. karrio/server/providers/migrations/0043_alter_genericsettings_account_number_and_more.py +39 -0
  147. karrio/server/providers/migrations/0044_carrier_carrier_capabilities.py +64 -0
  148. karrio/server/providers/migrations/0045_alter_carrier_active_alter_carrier_carrier_id.py +31 -0
  149. karrio/server/providers/migrations/0046_remove_dpdhlsettings_signature_and_more.py +41 -0
  150. karrio/server/providers/migrations/0047_dpdsettings.py +286 -0
  151. karrio/server/providers/migrations/0048_servicelevel_min_weight_servicelevel_transit_days_and_more.py +64 -0
  152. karrio/server/providers/migrations/0049_boxknightsettings_geodissettings_lapostesettings_and_more.py +156 -0
  153. karrio/server/providers/migrations/0050_carrier_is_system_alter_carrier_metadata_and_more.py +106 -0
  154. karrio/server/providers/migrations/0051_rename_username_upssettings_client_id_and_more.py +31 -0
  155. karrio/server/providers/migrations/0052_alter_upssettings_account_number_and_more.py +20 -0
  156. karrio/server/providers/migrations/0053_locate2usettings.py +281 -0
  157. karrio/server/providers/migrations/0054_zoom2usettings.py +280 -0
  158. karrio/server/providers/migrations/0055_rename_amazonmwssettings_amazonshippingsettings_and_more.py +44 -0
  159. karrio/server/providers/migrations/0056_asendiaussettings_geodissettings_code_client_and_more.py +75 -0
  160. karrio/server/providers/migrations/0057_alter_servicelevel_weight_unit_belgianpostsettings.py +51 -0
  161. karrio/server/providers/migrations/0058_alliedexpresssettings.py +38 -0
  162. karrio/server/providers/migrations/0059_ratesheet.py +81 -0
  163. karrio/server/providers/migrations/0060_belgianpostsettings_rate_sheet_and_more.py +73 -0
  164. karrio/server/providers/migrations/0061_alliedexpresssettings_service_type.py +17 -0
  165. karrio/server/providers/migrations/0062_sendlesettings_account_country_code.py +257 -0
  166. karrio/server/providers/migrations/0063_servicelevel_metadata.py +25 -0
  167. karrio/server/providers/migrations/0064_alliedexpresslocalsettings.py +43 -0
  168. karrio/server/providers/migrations/0065_servicelevel_carrier_service_code_and_more.py +66 -0
  169. karrio/server/providers/migrations/0066_rename_fedexsettings_fedexwssettings_and_more.py +28 -0
  170. karrio/server/providers/migrations/0067_fedexsettings.py +283 -0
  171. karrio/server/providers/migrations/0068_fedexsettings_track_api_key_and_more.py +38 -0
  172. karrio/server/providers/migrations/0069_alter_canadapostsettings_contract_id_and_more.py +23 -0
  173. karrio/server/providers/migrations/0070_tgesettings_alter_carrier_capabilities.py +65 -0
  174. karrio/server/providers/migrations/0071_alter_tgesettings_my_toll_token.py +18 -0
  175. karrio/server/providers/migrations/0072_rename_eshippersettings_eshipperxmlsettings_and_more.py +28 -0
  176. karrio/server/providers/migrations/0073_delete_eshipperxmlsettings.py +41 -0
  177. karrio/server/providers/migrations/0074_eshippersettings.py +38 -0
  178. karrio/server/providers/migrations/0075_haypostsettings.py +40 -0
  179. karrio/server/providers/migrations/0076_rename_customer_registration_id_uspsinternationalsettings_account_number_and_more.py +125 -0
  180. karrio/server/providers/migrations/0077_uspswtinternationalsettings_uspswtsettings_and_more.py +165 -0
  181. karrio/server/providers/migrations/0078_auto_20240813_1552.py +120 -0
  182. karrio/server/providers/migrations/0079_alter_carrier_options_alter_ratesheet_created_by.py +31 -0
  183. karrio/server/providers/migrations/0080_alter_aramexsettings_account_country_code_and_more.py +3025 -0
  184. karrio/server/providers/migrations/0081_remove_alliedexpresssettings_carrier_ptr_and_more.py +338 -0
  185. karrio/server/providers/migrations/__init__.py +0 -0
  186. karrio/server/providers/models/__init__.py +17 -0
  187. karrio/server/providers/models/carrier.py +309 -0
  188. karrio/server/providers/models/config.py +30 -0
  189. karrio/server/providers/models/service.py +62 -0
  190. karrio/server/providers/models/sheet.py +60 -0
  191. karrio/server/providers/models/template.py +39 -0
  192. karrio/server/providers/models/utils.py +58 -0
  193. karrio/server/providers/router.py +3 -0
  194. karrio/server/providers/serializers/__init__.py +3 -0
  195. karrio/server/providers/serializers/base.py +277 -0
  196. karrio/server/providers/signals.py +27 -0
  197. karrio/server/providers/tests.py +3 -0
  198. karrio/server/providers/urls.py +11 -0
  199. karrio/server/providers/views/__init__.py +0 -0
  200. karrio/server/providers/views/carriers.py +269 -0
  201. karrio/server/providers/views/connections.py +176 -0
  202. karrio/server/samples.py +352 -0
  203. karrio/server/serializers/__init__.py +2 -0
  204. karrio/server/serializers/abstract.py +506 -0
  205. karrio/server/tracing/__init__.py +0 -0
  206. karrio/server/tracing/admin.py +63 -0
  207. karrio/server/tracing/apps.py +8 -0
  208. karrio/server/tracing/migrations/0001_initial.py +41 -0
  209. karrio/server/tracing/migrations/0002_auto_20220710_1307.py +22 -0
  210. karrio/server/tracing/migrations/0003_auto_20221105_0317.py +43 -0
  211. karrio/server/tracing/migrations/0004_tracingrecord_carrier_account_idx.py +24 -0
  212. karrio/server/tracing/migrations/0005_optimise_tracingrecord_request_log_idx.py +25 -0
  213. karrio/server/tracing/migrations/0006_alter_tracingrecord_options_and_more.py +49 -0
  214. karrio/server/tracing/migrations/__init__.py +0 -0
  215. karrio/server/tracing/models.py +80 -0
  216. karrio/server/tracing/tests.py +3 -0
  217. karrio/server/tracing/utils.py +112 -0
  218. karrio/server/user/__init__.py +0 -0
  219. karrio/server/user/admin.py +96 -0
  220. karrio/server/user/apps.py +7 -0
  221. karrio/server/user/forms.py +35 -0
  222. karrio/server/user/migrations/0001_initial.py +41 -0
  223. karrio/server/user/migrations/0002_token.py +29 -0
  224. karrio/server/user/migrations/0003_token_test_mode.py +20 -0
  225. karrio/server/user/migrations/0004_group.py +26 -0
  226. karrio/server/user/migrations/0005_token_label.py +21 -0
  227. karrio/server/user/migrations/0006_workspaceconfig.py +63 -0
  228. karrio/server/user/migrations/__init__.py +0 -0
  229. karrio/server/user/models.py +203 -0
  230. karrio/server/user/serializers.py +46 -0
  231. karrio/server/user/templates/registration/login.html +108 -0
  232. karrio/server/user/templates/registration/registration_confirm_email.html +10 -0
  233. karrio/server/user/templates/registration/registration_confirm_email.txt +3 -0
  234. karrio/server/user/tests.py +3 -0
  235. karrio/server/user/urls.py +10 -0
  236. karrio/server/user/utils.py +60 -0
  237. karrio/server/user/views.py +9 -0
  238. karrio_server_core-2025.5rc1.dist-info/METADATA +32 -0
  239. karrio_server_core-2025.5rc1.dist-info/RECORD +241 -0
  240. karrio_server_core-2025.5rc1.dist-info/WHEEL +5 -0
  241. karrio_server_core-2025.5rc1.dist-info/top_level.txt +2 -0
@@ -0,0 +1,720 @@
1
+ import uuid
2
+ import typing
3
+ import logging
4
+ import datetime
5
+
6
+ from django.db.models import Q
7
+ from django.conf import settings
8
+ from rest_framework import status
9
+ from rest_framework.exceptions import NotFound
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.validators as validators
18
+ import karrio.server.core.exceptions as exceptions
19
+ import karrio.server.providers.models as providers
20
+ import karrio.server.serializers as base_serializers
21
+ import karrio.server.core.serializers as serializers
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class Carriers:
27
+ @staticmethod
28
+ def list(context=None, **kwargs) -> typing.List[providers.Carrier]:
29
+ list_filter = kwargs.copy()
30
+ user_filter = core.get_access_filter(context) if context is not None else []
31
+
32
+ test_mode = list_filter.get("test_mode") or getattr(context, "test_mode", None)
33
+ system_only = list_filter.get("system_only") is True
34
+ active_key = lib.identity(
35
+ "active_orgs__id" if settings.MULTI_ORGANIZATIONS else "active_users__id"
36
+ )
37
+ access_id = getattr(
38
+ getattr(context, "org" if settings.MULTI_ORGANIZATIONS else "user", None),
39
+ "id",
40
+ None,
41
+ )
42
+ creator_filter = lib.identity(
43
+ Q(
44
+ created_by__id=context.user.id,
45
+ **(dict(org=None) if settings.MULTI_ORGANIZATIONS else {}),
46
+ )
47
+ if getattr(context, "user", None) is not None
48
+ else Q()
49
+ )
50
+
51
+ _user_carriers = providers.Carrier.user_carriers.filter(
52
+ user_filter if len(user_filter) > 0 else Q() | creator_filter
53
+ )
54
+ _system_carriers = providers.Carrier.system_carriers.filter(
55
+ Q(
56
+ **{
57
+ "active": True,
58
+ **({active_key: access_id} if access_id is not None else {}),
59
+ }
60
+ )
61
+ )
62
+ _queryset = lib.identity(
63
+ _system_carriers if system_only else _user_carriers | _system_carriers
64
+ )
65
+
66
+ # Check if the test filter is specified then set it otherwise return all carriers live and test mode
67
+ if test_mode is not None:
68
+ _queryset = _queryset.filter(test_mode=test_mode)
69
+
70
+ # Check if the active flag is specified and return all active carrier is active is not set to false
71
+ if list_filter.get("active") is not None:
72
+ active = False if list_filter["active"] is False else True
73
+ _queryset = _queryset.filter(Q(active=active))
74
+
75
+ # Check if a specific carrier_id is provided, to add it to the query
76
+ if "carrier_id" in list_filter:
77
+ _queryset = _queryset.filter(carrier_id=list_filter["carrier_id"])
78
+
79
+ # Check if a specific carrier_id is provided, to add it to the query
80
+ if "capability" in list_filter:
81
+ _queryset = _queryset.filter(
82
+ capabilities__icontains=list_filter["capability"]
83
+ )
84
+
85
+ # Check if a metadata key is provided, to add it to the query
86
+ if "metadata_key" in list_filter:
87
+ _queryset = _queryset.filter(metadata__has_key=list_filter["metadata_key"])
88
+
89
+ # Check if a metadata value is provided, to add it to the query
90
+ if "metadata_value" in list_filter:
91
+ _value = list_filter["metadata_value"]
92
+ _queryset = _queryset.filter(
93
+ id__in=[
94
+ _["id"]
95
+ for _ in _queryset.values("id", "metadata")
96
+ if _value in (_.get("metadata") or {}).values()
97
+ ]
98
+ )
99
+
100
+ # Check if a list of carrier_ids are provided, to add the list to the query
101
+ if any(list_filter.get("carrier_ids", [])):
102
+ _queryset = _queryset.filter(carrier_id__in=list_filter["carrier_ids"])
103
+
104
+ if any(list_filter.get("services", [])):
105
+ carrier_names = [
106
+ name
107
+ for name, services in dataunits.contextual_reference(context)[
108
+ "services"
109
+ ].items()
110
+ if any(
111
+ service in list_filter["services"] for service in services.keys()
112
+ )
113
+ ]
114
+
115
+ if len(carrier_names) > 0:
116
+ _queryset = _queryset.filter(carrier_code__in=carrier_names)
117
+ if "carrier_name" in list_filter:
118
+ carrier_name = list_filter["carrier_name"]
119
+ _queryset = _queryset.filter(carrier_code=carrier_name)
120
+
121
+ carriers = _queryset.distinct()
122
+
123
+ # Raise an error if no carrier is found
124
+ if list_filter.get("raise_not_found") and len(carriers) == 0:
125
+ raise NotFound("No active carrier connection found to process the request")
126
+
127
+ return carriers
128
+
129
+ @staticmethod
130
+ def first(**kwargs) -> providers.Carrier:
131
+ return next(iter(Carriers.list(**kwargs)), None)
132
+
133
+
134
+ class Address:
135
+ @staticmethod
136
+ def validate(payload: dict) -> datatypes.AddressValidation:
137
+ validation = validators.Address.validate(datatypes.Address(**payload))
138
+
139
+ if validation.success is False:
140
+ raise exceptions.APIException(detail=validation, code="invalid_address")
141
+
142
+ return validation
143
+
144
+
145
+ class Shipments:
146
+ @staticmethod
147
+ @utils.require_selected_rate
148
+ def create(
149
+ payload: dict,
150
+ carrier: providers.Carrier = None,
151
+ selected_rate: typing.Union[datatypes.Rate, dict] = None,
152
+ resolve_tracking_url: typing.Callable[[str, str], str] = None,
153
+ context: base_serializers.Context = None,
154
+ **kwargs,
155
+ ) -> datatypes.Shipment:
156
+ selected_rate = lib.to_object(
157
+ datatypes.Rate,
158
+ lib.to_dict(selected_rate),
159
+ )
160
+ carrier = carrier or Carriers.first(
161
+ carrier_id=selected_rate.carrier_id,
162
+ test_mode=selected_rate.test_mode,
163
+ services=[selected_rate.service],
164
+ context=context,
165
+ )
166
+
167
+ if carrier is None:
168
+ raise NotFound("No active carrier connection found to process the request")
169
+
170
+ request = lib.to_object(
171
+ datatypes.ShipmentRequest,
172
+ {**lib.to_dict(payload), "service": selected_rate.service},
173
+ )
174
+
175
+ # The request is wrapped in utils.identity to simplify mocking in tests.
176
+ shipment, messages = utils.identity(
177
+ lambda: karrio.Shipment.create(request)
178
+ .from_(carrier.gateway)
179
+ .parse()
180
+ )
181
+
182
+ if shipment is None:
183
+ raise exceptions.APIException(
184
+ detail=messages,
185
+ status_code=status.HTTP_424_FAILED_DEPENDENCY,
186
+ )
187
+
188
+ def process_meta(parent) -> dict:
189
+ service_name = utils.upper(
190
+ (parent.meta or {}).get("service_name") or selected_rate.service
191
+ )
192
+ rate_provider = (
193
+ (parent.meta or {}).get("rate_provider") or carrier.carrier_name
194
+ ).lower()
195
+
196
+ return {
197
+ **(parent.meta or {}),
198
+ "ext": carrier.ext,
199
+ "carrier": rate_provider,
200
+ "service_name": service_name,
201
+ "rate_provider": rate_provider, # TODO: deprecate 'rate_provider' in favor of 'carrier'
202
+ }
203
+
204
+ def process_selected_rate() -> dict:
205
+ rate = lib.identity(
206
+ {
207
+ **lib.to_dict(shipment.selected_rate),
208
+ "id": f"rat_{uuid.uuid4().hex}",
209
+ "test_mode": carrier.test_mode,
210
+ }
211
+ if shipment.selected_rate is not None
212
+ else lib.to_dict(selected_rate)
213
+ )
214
+ return lib.to_dict({
215
+ **rate,
216
+ "meta": process_meta(shipment.selected_rate or selected_rate),
217
+ })
218
+
219
+ def process_tracking_url(rate: datatypes.Rate) -> str:
220
+ rate_provider = (rate.get("meta") or {}).get("rate_provider")
221
+ if (rate_provider not in dataunits.CARRIER_NAMES) and (
222
+ (shipment.meta or {}).get("tracking_url") is not None
223
+ ):
224
+ return shipment.meta["tracking_url"]
225
+
226
+ if resolve_tracking_url is not None:
227
+ url = resolve_tracking_url(
228
+ shipment.tracking_number, rate_provider or rate.carrier_name
229
+ )
230
+ return utils.app_tracking_query_params(url, carrier)
231
+
232
+ return ""
233
+
234
+ def process_parcel_refs(parcels: typing.List[dict]) -> list:
235
+ references = (shipment.meta or {}).get("tracking_numbers") or [
236
+ shipment.tracking_number
237
+ ]
238
+
239
+ return [
240
+ {
241
+ **lib.to_dict(parcel),
242
+ "reference_number": (
243
+ references[index]
244
+ if len(references) > index
245
+ else parcel.get("reference_number")
246
+ ),
247
+ }
248
+ for index, parcel in enumerate(parcels)
249
+ ]
250
+
251
+ shipment_rate = process_selected_rate()
252
+
253
+ return lib.to_object(
254
+ datatypes.Shipment,
255
+ {
256
+ "id": f"shp_{uuid.uuid4().hex}",
257
+ **payload,
258
+ **lib.to_dict(shipment),
259
+ "test_mode": carrier.test_mode,
260
+ "selected_rate": shipment_rate,
261
+ "service": shipment_rate["service"],
262
+ "selected_rate_id": shipment_rate["id"],
263
+ "parcels": process_parcel_refs(payload["parcels"]),
264
+ "tracking_url": process_tracking_url(shipment_rate),
265
+ "status": serializers.ShipmentStatus.purchased.value,
266
+ "created_at": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f%z"),
267
+ "meta": process_meta(shipment),
268
+ "messages": messages,
269
+ },
270
+ )
271
+
272
+ @staticmethod
273
+ def cancel(
274
+ payload: dict, carrier: providers.Carrier = None, **carrier_filters
275
+ ) -> datatypes.ConfirmationResponse:
276
+ carrier_id = lib.identity(
277
+ dict(carrier_id=payload.pop("carrier_id"))
278
+ if any(payload.get("carrier_id") or "")
279
+ else {}
280
+ )
281
+ carrier = carrier or Carriers.first(
282
+ **{
283
+ **dict(active=True, capability="shipping", raise_not_found=True),
284
+ **carrier_id,
285
+ **carrier_filters,
286
+ }
287
+ )
288
+
289
+ if carrier is None:
290
+ raise NotFound("No active carrier connection found to process the request")
291
+
292
+ request = karrio.Shipment.cancel(
293
+ lib.to_object(datatypes.ShipmentCancelRequest, payload)
294
+ )
295
+
296
+ # The request call is wrapped in utils.identity to simplify mocking in tests
297
+ confirmation, messages = lib.identity(
298
+ utils.identity(lambda: request.from_(carrier.gateway).parse())
299
+ if "cancel_shipment" in carrier.gateway.proxy_methods
300
+ else (
301
+ datatypes.Confirmation(
302
+ carrier_name=carrier.gateway.settings.carrier_name,
303
+ carrier_id=carrier.gateway.settings.carrier_id,
304
+ success=True,
305
+ operation="Safe cancellation allowed",
306
+ ),
307
+ [],
308
+ )
309
+ )
310
+
311
+ if confirmation is None:
312
+ raise exceptions.APIException(
313
+ detail=messages,
314
+ status_code=status.HTTP_424_FAILED_DEPENDENCY,
315
+ )
316
+
317
+ return datatypes.ConfirmationResponse(
318
+ confirmation=confirmation,
319
+ messages=messages,
320
+ )
321
+
322
+ @staticmethod
323
+ def track(
324
+ payload: dict,
325
+ carrier: providers.Carrier = None,
326
+ raise_on_error: bool = True,
327
+ **carrier_filters,
328
+ ) -> datatypes.TrackingResponse:
329
+ carrier = carrier or Carriers.first(
330
+ **{
331
+ **dict(active=True, capability="tracking", raise_not_found=True),
332
+ **carrier_filters,
333
+ }
334
+ )
335
+
336
+ if carrier is None:
337
+ raise NotFound("No active carrier connection found to process the request")
338
+
339
+ request = karrio.Tracking.fetch(
340
+ lib.to_object(datatypes.TrackingRequest, payload)
341
+ )
342
+
343
+ # The request call is wrapped in utils.identity to simplify mocking in tests
344
+ results, messages = utils.identity(
345
+ lambda: request.from_(carrier.gateway).parse()
346
+ )
347
+
348
+ if not any(results or []) and (
349
+ raise_on_error or utils.is_sdk_message(messages)
350
+ ):
351
+ raise exceptions.APIException(
352
+ detail=messages,
353
+ status_code=status.HTTP_404_NOT_FOUND,
354
+ )
355
+
356
+ result = next(iter(results or []), None)
357
+ tracking_number = payload["tracking_numbers"][0]
358
+ details = result or datatypes.TrackingDetails(
359
+ carrier_id=carrier.carrier_id,
360
+ carrier_name=carrier.carrier_name,
361
+ tracking_number=tracking_number,
362
+ events=[
363
+ datatypes.TrackingEvent(
364
+ date=datetime.datetime.now().strftime("%Y-%m-%d"),
365
+ description="Awaiting update from carrier...",
366
+ code="UNKNOWN",
367
+ time=datetime.datetime.now().strftime("%H:%M"),
368
+ )
369
+ ],
370
+ delivered=False,
371
+ )
372
+ options = {
373
+ **(payload.get("options") or {}),
374
+ tracking_number: {
375
+ **(details.meta or {}),
376
+ **(payload.get("options") or {}).get(tracking_number, {}),
377
+ },
378
+ }
379
+ meta = {
380
+ "ext": carrier.ext,
381
+ "carrier": carrier.carrier_name,
382
+ **(details.meta or {}),
383
+ }
384
+ info = {
385
+ "carrier_tracking_link": utils.get_carrier_tracking_link(
386
+ carrier, tracking_number
387
+ ),
388
+ "source": "api",
389
+ **(lib.to_dict(details.info or {})),
390
+ **(lib.to_dict(payload.get("info") or {})),
391
+ }
392
+
393
+ return datatypes.TrackingResponse(
394
+ tracking=lib.to_object(
395
+ datatypes.Tracking,
396
+ {
397
+ **lib.to_dict(details),
398
+ "id": f"trk_{uuid.uuid4().hex}",
399
+ "test_mode": carrier.test_mode,
400
+ "status": utils.compute_tracking_status(result).value,
401
+ "options": options,
402
+ "meta": meta,
403
+ "info": info,
404
+ },
405
+ ),
406
+ messages=messages,
407
+ )
408
+
409
+
410
+ class Pickups:
411
+ @staticmethod
412
+ def schedule(
413
+ payload: dict, carrier: providers.Carrier = None, **carrier_filters
414
+ ) -> datatypes.PickupResponse:
415
+ carrier = carrier or Carriers.first(
416
+ **{
417
+ **dict(active=True, capability="pickup", raise_not_found=True),
418
+ **carrier_filters,
419
+ }
420
+ )
421
+
422
+ if carrier is None:
423
+ raise NotFound("No active carrier connection found to process the request")
424
+
425
+ request = karrio.Pickup.schedule(
426
+ datatypes.PickupRequest(**lib.to_dict(payload))
427
+ )
428
+
429
+ # The request call is wrapped in utils.identity to simplify mocking in tests
430
+ pickup, messages = utils.identity(
431
+ lambda: request.from_(carrier.gateway).parse()
432
+ )
433
+
434
+ if pickup is None:
435
+ raise exceptions.APIException(
436
+ detail=messages,
437
+ status_code=status.HTTP_424_FAILED_DEPENDENCY,
438
+ )
439
+
440
+ def process_meta(parent) -> dict:
441
+ return {
442
+ **(parent.meta or {}),
443
+ "ext": carrier.ext,
444
+ }
445
+
446
+ return datatypes.PickupResponse(
447
+ pickup=datatypes.Pickup(
448
+ **{
449
+ **payload,
450
+ **lib.to_dict(pickup),
451
+ "id": f"pck_{uuid.uuid4().hex}",
452
+ "test_mode": carrier.test_mode,
453
+ "meta": process_meta(pickup),
454
+ "messages": messages,
455
+ }
456
+ ),
457
+ messages=messages,
458
+ )
459
+
460
+ @staticmethod
461
+ def update(
462
+ payload: dict, carrier: providers.Carrier = None, **carrier_filters
463
+ ) -> datatypes.PickupResponse:
464
+ carrier = carrier or Carriers.first(
465
+ **{
466
+ **dict(active=True, capability="pickup", raise_not_found=True),
467
+ **carrier_filters,
468
+ }
469
+ )
470
+
471
+ if carrier is None:
472
+ raise NotFound("No active carrier connection found to process the request")
473
+
474
+ request = karrio.Pickup.update(
475
+ datatypes.PickupUpdateRequest(**lib.to_dict(payload))
476
+ )
477
+
478
+ # The request call is wrapped in utils.identity to simplify mocking in tests
479
+ pickup, messages = utils.identity(
480
+ lambda: request.from_(carrier.gateway).parse()
481
+ )
482
+
483
+ if pickup is None:
484
+ raise exceptions.APIException(
485
+ detail=messages,
486
+ status_code=status.HTTP_424_FAILED_DEPENDENCY,
487
+ )
488
+
489
+ return datatypes.PickupResponse(
490
+ pickup=datatypes.Pickup(
491
+ **{
492
+ **payload,
493
+ **lib.to_dict(pickup),
494
+ "test_mode": carrier.test_mode,
495
+ }
496
+ ),
497
+ messages=messages,
498
+ )
499
+
500
+ @staticmethod
501
+ def cancel(
502
+ payload: dict, carrier: providers.Carrier = None, **carrier_filters
503
+ ) -> datatypes.ConfirmationResponse:
504
+ carrier = carrier or Carriers.first(
505
+ **{
506
+ **dict(active=True, capability="pickup"),
507
+ **carrier_filters,
508
+ }
509
+ )
510
+
511
+ if carrier is None:
512
+ raise NotFound("No active carrier connection found to process the request")
513
+
514
+ request = karrio.Pickup.cancel(
515
+ datatypes.PickupCancelRequest(**lib.to_dict(payload))
516
+ )
517
+
518
+ # The request call is wrapped in utils.identity to simplify mocking in tests
519
+ confirmation, messages = lib.identity(
520
+ utils.identity(lambda: request.from_(carrier.gateway).parse())
521
+ if "cancel_shipment" in carrier.gateway.proxy_methods
522
+ else (
523
+ datatypes.Confirmation(
524
+ carrier_name=carrier.gateway.settings.carrier_name,
525
+ carrier_id=carrier.gateway.settings.carrier_id,
526
+ success=True,
527
+ operation="Safe cancellation allowed",
528
+ ),
529
+ [],
530
+ )
531
+ )
532
+
533
+ if confirmation is None:
534
+ raise exceptions.APIException(
535
+ detail=messages,
536
+ status_code=status.HTTP_424_FAILED_DEPENDENCY,
537
+ )
538
+
539
+ return datatypes.ConfirmationResponse(
540
+ confirmation=confirmation, messages=messages
541
+ )
542
+
543
+
544
+ @utils.post_processing(methods=["fetch"])
545
+ class Rates:
546
+ post_process_functions: typing.List[typing.Callable] = []
547
+
548
+ @staticmethod
549
+ def fetch(
550
+ payload: dict,
551
+ carriers: typing.List[providers.Carrier] = None,
552
+ raise_on_error: bool = True,
553
+ **carrier_filters,
554
+ ) -> datatypes.RateResponse:
555
+ services = payload.get("services", [])
556
+ carrier_ids = payload.get("carrier_ids", [])
557
+ shipper_country_code = payload["shipper"].get("country_code")
558
+ carriers = carriers or Carriers.list(
559
+ **{
560
+ "active": True,
561
+ "capability": "rating",
562
+ "carrier_ids": carrier_ids,
563
+ "services": services,
564
+ **carrier_filters,
565
+ }
566
+ )
567
+
568
+ gateways = utils.filter_rate_carrier_compatible_gateways(
569
+ carriers, carrier_ids, shipper_country_code
570
+ )
571
+
572
+ if raise_on_error and len(gateways) == 0:
573
+ raise NotFound("No active carrier connection found to process the request")
574
+
575
+ request = karrio.Rating.fetch(lib.to_object(datatypes.RateRequest, payload))
576
+
577
+ # The request call is wrapped in utils.identity to simplify mocking in tests
578
+ rates, messages = utils.identity(lambda: request.from_(*gateways).parse())
579
+
580
+ if raise_on_error and not any(rates) and any(messages):
581
+ raise exceptions.APIException(
582
+ detail=messages,
583
+ status_code=status.HTTP_424_FAILED_DEPENDENCY,
584
+ )
585
+
586
+ def process_rate(rate: datatypes.Rate) -> datatypes.Rate:
587
+ carrier = next((c for c in carriers if c.carrier_id == rate.carrier_id))
588
+ rate_provider = (
589
+ (rate.meta or {}).get("rate_provider")
590
+ or getattr(carrier, "custom_carrier_name", None)
591
+ or rate.carrier_name
592
+ ).lower()
593
+ service_name = utils.upper(
594
+ (rate.meta or {}).get("service_name") or rate.service
595
+ )
596
+
597
+ meta = {
598
+ **(rate.meta or {}),
599
+ "ext": carrier.ext,
600
+ "carrier": rate_provider,
601
+ "service_name": service_name,
602
+ "rate_provider": rate_provider, # TODO: deprecate rate_provider
603
+ "carrier_connection_id": carrier.id,
604
+ }
605
+
606
+ return lib.to_object(
607
+ datatypes.Rate,
608
+ {
609
+ **lib.to_dict(rate),
610
+ "id": f"rat_{uuid.uuid4().hex}",
611
+ "test_mode": carrier.test_mode,
612
+ "meta": meta,
613
+ },
614
+ )
615
+
616
+ formated_rates: typing.List[datatypes.Rate] = sorted(
617
+ map(process_rate, rates), key=lambda rate: rate.total_charge
618
+ )
619
+
620
+ return lib.to_object(
621
+ datatypes.RateResponse, dict(rates=formated_rates, messages=messages)
622
+ )
623
+
624
+
625
+ class Documents:
626
+ @staticmethod
627
+ def upload(
628
+ payload: dict,
629
+ carrier: providers.Carrier = None,
630
+ **carrier_filters,
631
+ ) -> datatypes.DocumentUploadResponse:
632
+ carrier = carrier or Carriers.first(
633
+ **{
634
+ **dict(active=True, raise_not_found=True),
635
+ **carrier_filters,
636
+ }
637
+ )
638
+
639
+ if "upload_document" not in carrier.gateway.proxy_methods:
640
+ raise exceptions.APIException(
641
+ detail=f"trade document upload is not supported by carrier: '{carrier.carrier_id}'",
642
+ status_code=status.HTTP_406_NOT_ACCEPTABLE,
643
+ )
644
+
645
+ request = karrio.Document.upload(
646
+ lib.to_object(datatypes.DocumentUploadRequest, payload)
647
+ )
648
+
649
+ # The request is wrapped in utils.identity to simplify mocking in tests.
650
+ upload, messages = utils.identity(
651
+ lambda: request.from_(carrier.gateway).parse()
652
+ )
653
+
654
+ if upload is None:
655
+ raise exceptions.APIException(
656
+ detail=messages,
657
+ status_code=status.HTTP_424_FAILED_DEPENDENCY,
658
+ )
659
+
660
+ return lib.to_object(
661
+ datatypes.DocumentUploadResponse,
662
+ {
663
+ **payload,
664
+ **lib.to_dict(upload),
665
+ "test_mode": carrier.test_mode,
666
+ "id": f"sdoc_{uuid.uuid4().hex}",
667
+ "messages": lib.to_dict(messages),
668
+ },
669
+ )
670
+
671
+
672
+ class Manifests:
673
+ @staticmethod
674
+ def create(
675
+ payload: dict, carrier: providers.Carrier = None, **carrier_filters
676
+ ) -> datatypes.ManifestResponse:
677
+ carrier = carrier or Carriers.first(
678
+ **{
679
+ **dict(active=True, capability="manifest", raise_not_found=True),
680
+ **carrier_filters,
681
+ }
682
+ )
683
+
684
+ if carrier is None:
685
+ raise NotFound("No active carrier connection found to process the request")
686
+
687
+ request = karrio.Manifest.create(
688
+ lib.to_object(datatypes.ManifestRequest, lib.to_dict(payload))
689
+ )
690
+
691
+ # The request call is wrapped in utils.identity to simplify mocking in tests
692
+ manifest, messages = utils.identity(
693
+ lambda: request.from_(carrier.gateway).parse()
694
+ )
695
+
696
+ if manifest is None:
697
+ raise exceptions.APIException(
698
+ detail=messages,
699
+ status_code=status.HTTP_424_FAILED_DEPENDENCY,
700
+ )
701
+
702
+ def process_meta(parent) -> dict:
703
+ return {
704
+ **(parent.meta or {}),
705
+ "ext": carrier.ext,
706
+ }
707
+
708
+ return datatypes.ManifestResponse(
709
+ manifest=datatypes.Manifest(
710
+ **{
711
+ **payload,
712
+ **lib.to_dict(manifest),
713
+ "id": f"manf_{uuid.uuid4().hex}",
714
+ "test_mode": carrier.test_mode,
715
+ "meta": process_meta(manifest),
716
+ "messages": messages,
717
+ }
718
+ ),
719
+ messages=messages,
720
+ )