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,57 @@
1
+ import logging
2
+ from django.conf import settings
3
+ from django.dispatch import receiver
4
+ from constance import config
5
+ from constance.signals import config_updated
6
+ from django.core.signals import request_started
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ def register_signals():
12
+ config_updated.connect(constance_updated)
13
+ # Defer config initialization until after Django is fully loaded
14
+ request_started.connect(initialize_settings)
15
+
16
+ logger.info("karrio.core signals registered...")
17
+
18
+
19
+ def initialize_settings(sender=None, **kwargs):
20
+ # Only run once
21
+ if not getattr(initialize_settings, 'has_run', False):
22
+ try:
23
+ update_settings(config)
24
+ initialize_settings.has_run = True
25
+ except Exception as e:
26
+ logger.error(f"Failed to initialize settings: {e}")
27
+
28
+
29
+ @receiver(config_updated)
30
+ def constance_updated(sender, key, old_value, new_value, **kwargs):
31
+ logger.info(f"Updated config {key} to {new_value}")
32
+ update_settings(sender)
33
+
34
+
35
+ def update_settings(current):
36
+ CONSTANCE_CONFIG_KEYS = [
37
+ key for key in settings.CONSTANCE_CONFIG.keys() if hasattr(settings, key)
38
+ ]
39
+
40
+ for key in CONSTANCE_CONFIG_KEYS:
41
+ try:
42
+ setattr(settings, key, getattr(current, key))
43
+ except Exception as e:
44
+ logger.error(f"Failed to update setting {key}: {e}")
45
+
46
+ # Check EMAIL_ENABLED after all settings are updated
47
+ try:
48
+ settings.EMAIL_ENABLED = all(
49
+ cfg is not None and cfg != ""
50
+ for cfg in [
51
+ current.EMAIL_HOST,
52
+ current.EMAIL_HOST_USER,
53
+ ]
54
+ )
55
+ except Exception as e:
56
+ logger.error(f"Failed to set EMAIL_ENABLED: {e}")
57
+ settings.EMAIL_ENABLED = False
@@ -0,0 +1,98 @@
1
+ import logging
2
+ from django.contrib.auth import get_user_model
3
+ from rest_framework.test import APITestCase as BaseAPITestCase, APIClient
4
+
5
+ from karrio.server.user.models import Token
6
+ import karrio.server.iam.permissions as iam
7
+ import karrio.server.providers.models as providers
8
+
9
+ logger = logging.getLogger(__name__)
10
+ iam.setup_groups()
11
+
12
+
13
+ class APITestCase(BaseAPITestCase):
14
+ def setUp(self) -> None:
15
+ self.maxDiff = None
16
+ logging.basicConfig(level=logging.DEBUG)
17
+
18
+ # Setup user and API Token.
19
+ self.user = get_user_model().objects.create_superuser(
20
+ "admin@example.com", "test"
21
+ )
22
+ self.token = Token.objects.create(user=self.user, test_mode=True)
23
+
24
+ # Setup API client.
25
+ self.client = APIClient()
26
+ self.client.credentials(HTTP_AUTHORIZATION="Token " + self.token.key)
27
+
28
+ # Setup test carrier connections.
29
+ self.carrier = providers.Carrier.objects.create(
30
+ carrier_code="canadapost",
31
+ carrier_id="canadapost",
32
+ test_mode=True,
33
+ active=True,
34
+ created_by=self.user,
35
+ credentials=dict(
36
+ username="6e93d53968881714",
37
+ customer_number="2004381",
38
+ contract_id="42708517",
39
+ password="0bfa9fcb9853d1f51ee57a",
40
+ ),
41
+ )
42
+ self.ups_carrier = providers.Carrier.objects.create(
43
+ carrier_code="ups",
44
+ carrier_id="ups_package",
45
+ test_mode=True,
46
+ active=True,
47
+ created_by=self.user,
48
+ credentials=dict(
49
+ client_id="test",
50
+ client_secret="test",
51
+ account_number="000000",
52
+ ),
53
+ )
54
+ self.fedex_carrier = providers.Carrier.objects.create(
55
+ carrier_code="fedex",
56
+ carrier_id="fedex_express",
57
+ test_mode=True,
58
+ active=True,
59
+ created_by=self.user,
60
+ credentials=dict(
61
+ api_key="test",
62
+ secret_key="password",
63
+ account_number="000000",
64
+ track_api_key="test",
65
+ track_secret_key="password",
66
+ ),
67
+ )
68
+ self.dhl_carrier = providers.Carrier.objects.create(
69
+ carrier_code="dhl_express",
70
+ carrier_id="dhl_express",
71
+ test_mode=True,
72
+ active=True,
73
+ created_by=self.user,
74
+ credentials=dict(
75
+ site_id="test",
76
+ password="password",
77
+ account_number="000000",
78
+ )
79
+ )
80
+
81
+ def getJWTToken(self, email: str, password: str) -> str:
82
+ url = reverse("jwt-obtain-pair")
83
+ data = dict(
84
+ email=email,
85
+ password=password,
86
+ )
87
+ response = self.client.post(url, data)
88
+
89
+ return response.data.get("access")
90
+
91
+ def assertResponseNoErrors(self, response):
92
+ is_ok = f"{response.status_code}".startswith("2")
93
+
94
+ if is_ok is False or response.data.get("errors") is not None:
95
+ print(response.data)
96
+
97
+ self.assertTrue(is_ok)
98
+ assert response.data.get("errors") is None
@@ -0,0 +1,12 @@
1
+ """
2
+ karrio server core module urls
3
+ """
4
+ from django.urls import include, path
5
+ from karrio.server.core.views import metadata, router
6
+
7
+ app_name = "karrio.server.core"
8
+ urlpatterns = [
9
+ path("", metadata.view, name="metadata"),
10
+ path("v1/", include(router.urls), name="references"),
11
+ path("status/", include("health_check.urls")),
12
+ ]
@@ -0,0 +1,479 @@
1
+ import sys
2
+ import typing
3
+ import inspect
4
+ import logging
5
+ import functools
6
+ from string import Template
7
+ from concurrent import futures
8
+ from datetime import timedelta, datetime
9
+ from typing import TypeVar, Union, Callable, Any, List, Optional
10
+
11
+ from django.conf import settings
12
+ from django.utils.translation import gettext_lazy as _
13
+ import django_email_verification.confirm as confirm
14
+ import rest_framework_simplejwt.tokens as jwt
15
+ import rest_framework.status as status
16
+
17
+ import karrio.lib as lib
18
+ from karrio.core.utils import DP, DF
19
+ from karrio.server.core import datatypes, serializers, exceptions
20
+
21
+ T = TypeVar("T")
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ def identity(value: Union[Any, Callable]) -> Any:
26
+ """
27
+ :param value: function or value desired to be wrapped
28
+ :return: value or callable return
29
+ """
30
+ return value() if callable(value) else value
31
+
32
+
33
+ def failsafe(callable: Callable[[], T], warning: str = None) -> T:
34
+ """This higher order function wraps a callable in a try..except
35
+ scope to capture any exception raised.
36
+ Only use it when you are running something unstable that you
37
+ don't mind if it fails.
38
+ """
39
+ try:
40
+ return callable()
41
+ except Exception as e:
42
+ if warning:
43
+ logger.warning(Template(warning).substitute(error=e))
44
+ return None
45
+
46
+
47
+ def run_async(callable: Callable[[], Any]) -> futures.Future:
48
+ """This higher order function initiate the execution
49
+ of a callable in a non-blocking thread and return a
50
+ handle for a future response.
51
+ """
52
+ return futures.ThreadPoolExecutor(max_workers=1).submit(callable)
53
+
54
+
55
+ def error_wrapper(func):
56
+ @functools.wraps(func)
57
+ def wrapper(*args, **kwargs):
58
+ try:
59
+ return func(*args, **kwargs)
60
+ except Exception as e:
61
+ logger.exception(e)
62
+ raise e
63
+
64
+ return wrapper
65
+
66
+
67
+ def async_wrapper(func):
68
+ @functools.wraps(func)
69
+ def wrapper(*args, run_synchronous: bool = False, **kwargs):
70
+ def _run():
71
+ return func(*args, **kwargs)
72
+
73
+ if run_synchronous:
74
+ return _run()
75
+
76
+ return run_async(_run)
77
+
78
+ return wrapper
79
+
80
+
81
+ def tenant_aware(func):
82
+ @functools.wraps(func)
83
+ def wrapper(*args, **kwargs):
84
+ if settings.MULTI_TENANTS:
85
+ import django_tenants.utils as tenant_utils
86
+
87
+ schema = kwargs.get("schema") or "public"
88
+
89
+ with tenant_utils.schema_context(schema):
90
+ return func(*args, **kwargs)
91
+ else:
92
+ return func(*args, **kwargs)
93
+
94
+ return wrapper
95
+
96
+
97
+ def run_on_all_tenants(func):
98
+ @functools.wraps(func)
99
+ def wrapper(*args, **kwargs):
100
+ if settings.MULTI_TENANTS:
101
+ import django_tenants.utils as tenant_utils
102
+
103
+ tenants = tenant_utils.get_tenant_model().objects.exclude(
104
+ schema_name="public"
105
+ )
106
+
107
+ for tenant in tenants:
108
+ with tenant_utils.tenant_context(tenant):
109
+ func(*args, **kwargs, schema=tenant.schema_name)
110
+ else:
111
+ func(*args, **kwargs)
112
+
113
+ return wrapper
114
+
115
+
116
+ def disable_for_loaddata(signal_handler):
117
+ @functools.wraps(signal_handler)
118
+ def wrapper(*args, **kwargs):
119
+ if is_system_loading_data():
120
+ return
121
+
122
+ signal_handler(*args, **kwargs)
123
+
124
+ return wrapper
125
+
126
+
127
+ def skip_on_loadata(func):
128
+ @functools.wraps(func)
129
+ def wrapper(*args, **kwargs):
130
+ if "loaddata" in sys.argv:
131
+ return
132
+
133
+ return func(*args, **kwargs)
134
+
135
+ return wrapper
136
+
137
+
138
+ def skip_on_commands(
139
+ commands: typing.List[str] = ["loaddata", "migrate", "makemigrations"]
140
+ ):
141
+ def _decorator(func):
142
+ @functools.wraps(func)
143
+ def wrapper(*args, **kwargs):
144
+ if any(cmd in sys.argv for cmd in commands):
145
+ return
146
+
147
+ return func(*args, **kwargs)
148
+
149
+ return wrapper
150
+
151
+ return _decorator
152
+
153
+
154
+ def email_setup_required(func):
155
+ @functools.wraps(func)
156
+ def wrapper(*args, **kwargs):
157
+ if not settings.EMAIL_ENABLED:
158
+ raise Exception(_("The email service is not configured."))
159
+
160
+ return func(*args, **kwargs)
161
+
162
+ return wrapper
163
+
164
+
165
+ def post_processing(methods: List[str] = None):
166
+ def class_wrapper(klass):
167
+ setattr(
168
+ klass,
169
+ "post_process_functions",
170
+ getattr(klass, "post_process_functions") or [],
171
+ )
172
+
173
+ for name in methods:
174
+ method = getattr(klass, name)
175
+
176
+ def wrapper(*args, **kwargs):
177
+ result = method(*args, **kwargs)
178
+ processes = klass.post_process_functions
179
+ context = kwargs.get("context")
180
+
181
+ return functools.reduce(
182
+ lambda cummulated_result, process: process(
183
+ context, cummulated_result
184
+ ),
185
+ processes,
186
+ result,
187
+ )
188
+
189
+ setattr(klass, name, wrapper)
190
+
191
+ return klass
192
+
193
+ return class_wrapper
194
+
195
+
196
+ def upper(value_str: Optional[str]) -> Optional[str]:
197
+ if value_str is None:
198
+ return None
199
+
200
+ return value_str.upper().replace("_", " ")
201
+
202
+
203
+ def compute_tracking_status(
204
+ details: Optional[datatypes.Tracking] = None,
205
+ ) -> serializers.TrackerStatus:
206
+ if details is None:
207
+ return serializers.TrackerStatus.pending
208
+ elif details.delivered:
209
+ return serializers.TrackerStatus.delivered
210
+ elif (len(details.events) == 0) or (
211
+ len(details.events) == 1 and details.events[0].code == "CREATED"
212
+ ):
213
+ return serializers.TrackerStatus.pending
214
+
215
+ if (
216
+ any(details.status or "")
217
+ and serializers.TrackerStatus.map(details.status).value is not None
218
+ ):
219
+ return serializers.TrackerStatus.map(details.status)
220
+
221
+ return serializers.TrackerStatus.in_transit
222
+
223
+
224
+ def is_sdk_message(
225
+ message: Optional[Union[datatypes.Message, List[datatypes.Message]]]
226
+ ) -> bool:
227
+ msg = next(iter(message), None) if isinstance(message, list) else message
228
+
229
+ return "SHIPPING_SDK_" in str(getattr(msg, "code", ""))
230
+
231
+
232
+ def filter_rate_carrier_compatible_gateways(
233
+ carriers: List, carrier_ids: List[str], shipper_country_code: Optional[str] = None
234
+ ) -> List:
235
+ """
236
+ This function filters the carriers based on the capability to "rating"
237
+ and if no explicit carrier list is provided, it will filter out any
238
+ carrier that does not support the shipper's country code.
239
+ """
240
+ _gateways = [
241
+ carrier.gateway
242
+ for carrier in carriers
243
+ if (
244
+ # If no carrier list is provided, and gateway has "rating" capability.
245
+ ("rating" in carrier.gateway.capabilities and len(carrier_ids) > 0)
246
+ # If a carrier list is provided, and gateway is in the list.
247
+ or (
248
+ # the gateway has "rating" capability.
249
+ "rating" in carrier.gateway.capabilities
250
+ # and no explicit carrier list is provided.
251
+ and len(carrier_ids) == 0
252
+ # and the shipper country code is provided.
253
+ and shipper_country_code is not None
254
+ and (
255
+ carrier.gateway.settings.account_country_code
256
+ == shipper_country_code
257
+ or carrier.gateway.settings.account_country_code is None
258
+ )
259
+ )
260
+ )
261
+ ]
262
+
263
+ return ({_.settings.carrier_id: _ for _ in _gateways}).values()
264
+
265
+
266
+ def is_system_loading_data() -> bool:
267
+ try:
268
+ for fr in inspect.stack():
269
+ if inspect.getmodulename(fr[1]) == "loaddata":
270
+ return True
271
+ except:
272
+ pass
273
+
274
+ return False
275
+
276
+
277
+ @email_setup_required
278
+ def send_email(
279
+ emails: List[str],
280
+ subject: str,
281
+ email_template: str,
282
+ context: dict = {},
283
+ text_template: str = None,
284
+ **kwargs,
285
+ ):
286
+ sender = confirm._get_validated_field("EMAIL_FROM_ADDRESS")
287
+ html = confirm.render_to_string(email_template, context)
288
+ text = confirm.render_to_string(text_template or email_template, context)
289
+
290
+ msg = confirm.EmailMultiAlternatives(subject, text, sender, emails)
291
+ msg.attach_alternative(html, "text/html")
292
+ msg.send()
293
+
294
+
295
+ class ConfirmationToken(jwt.Token):
296
+ token_type = "confirmation"
297
+ lifetime = timedelta(hours=2)
298
+
299
+ @classmethod
300
+ def for_data(cls, user, data: dict) -> str:
301
+ token = super().for_user(user)
302
+
303
+ for k, v in data.items():
304
+ token[k] = v
305
+
306
+ return token
307
+
308
+
309
+ def app_tracking_query_params(url: str, carrier) -> str:
310
+ hub_flag = f"&hub={carrier.carrier_name}" if carrier.gateway.is_hub else ""
311
+
312
+ return f"{url}{hub_flag}"
313
+
314
+
315
+ def default_tracking_event(
316
+ event_at: datetime = None,
317
+ code: str = None,
318
+ description: str = None,
319
+ ):
320
+ return [
321
+ DP.to_dict(
322
+ datatypes.TrackingEvent(
323
+ date=DF.fdate(event_at or datetime.now()),
324
+ description=(description or "Label created and ready for shipment"),
325
+ location="",
326
+ code=(code or "CREATED"),
327
+ time=DF.ftime(event_at or datetime.now()),
328
+ )
329
+ )
330
+ ]
331
+
332
+
333
+ def get_carrier_tracking_link(carrier, tracking_number: str):
334
+ tracking_url = getattr(carrier.gateway.settings, "tracking_url", None)
335
+
336
+ return tracking_url.format(tracking_number) if tracking_url is not None else None
337
+
338
+
339
+ def process_events(
340
+ response_events: typing.List[datatypes.TrackingEvent],
341
+ current_events: typing.List[dict],
342
+ ) -> typing.List[dict]:
343
+ """Merge new tracking events with existing ones, avoiding duplicates by comparing event hashes.
344
+ Latest events are kept at the top of the list."""
345
+ if not any(response_events):
346
+ return current_events
347
+
348
+ new_events = lib.to_dict(response_events)
349
+ if not any(current_events):
350
+ return sorted(
351
+ new_events,
352
+ key=lambda e: f"{e.get('date', '')} {e.get('time', '')}",
353
+ reverse=True,
354
+ )
355
+
356
+ # Create hash for comparison using lib.to_json
357
+ event_hashes = {lib.to_json(event): event for event in current_events}
358
+
359
+ for event in new_events:
360
+ event_hash = lib.to_json(event)
361
+ if event_hash not in event_hashes:
362
+ event_hashes[event_hash] = event
363
+
364
+ # Sort events by date and time in descending order (latest first)
365
+ return sorted(
366
+ event_hashes.values(),
367
+ key=lambda e: f"{e.get('date', '')} {e.get('time', '')}",
368
+ reverse=True,
369
+ )
370
+
371
+
372
+ def apply_rate_selection(payload: typing.Union[dict, typing.Any], **kwargs):
373
+ data = kwargs.get("data") or kwargs
374
+ get = lambda key, default=None: lib.identity(
375
+ payload.get(key, data.get(key, default)) if isinstance(payload, dict)
376
+ else getattr(payload, key, data.get(key, default))
377
+ )
378
+
379
+ ctx = kwargs.get("context")
380
+ rates = get("rates") or data.get("rates", [])
381
+ options = get("options") or data.get("options", {})
382
+ service = get("service") or data.get("service", None)
383
+ rate_id = get("selected_rate_id") or data.get("selected_rate_id", None)
384
+ selected_rate = get("selected_rate") or data.get("selected_rate", None)
385
+ apply_shipping_rules = lib.identity(
386
+ getattr(settings, "SHIPPING_RULES", False)
387
+ and options.get("apply_shipping_rules", False)
388
+ )
389
+
390
+ if selected_rate:
391
+ kwargs.update(selected_rate=selected_rate)
392
+ return kwargs
393
+
394
+ # Select by id or service if provided
395
+ if rate_id or service:
396
+ kwargs.update(selected_rate=next(
397
+ (
398
+ rate for rate in rates
399
+ if (rate_id and rate.get("id") == rate_id)
400
+ or (service and rate.get("service") == service)
401
+ ),
402
+ None,
403
+ ))
404
+ return kwargs
405
+
406
+ # Apply shipping rules if enabled and no selected rate is provided
407
+ if apply_shipping_rules:
408
+ # Import rules engine only when needed
409
+ import karrio.server.automation.models as automation_models
410
+ import karrio.server.automation.services.rules_engine as engine
411
+
412
+ # Get active shipping rules
413
+ active_rules = list(
414
+ automation_models.ShippingRule
415
+ .access_by(ctx)
416
+ .filter(is_active=True)
417
+ )
418
+
419
+ # Always run rule evaluation for activity tracking
420
+ if active_rules:
421
+ _, rule_selected_rate, rule_activity = engine.process_shipping_rules(
422
+ shipment=payload,
423
+ rules=active_rules,
424
+ )
425
+
426
+ kwargs.update(
427
+ selected_rate=rule_selected_rate,
428
+ rule_activity=rule_activity,
429
+ )
430
+
431
+ return kwargs
432
+
433
+
434
+ def require_selected_rate(func):
435
+ """
436
+ Decorator for rate selection process.
437
+ - Checks if shipping rules are enabled
438
+ - Evaluates and applies rules to modify service if needed
439
+ - Augments response metadata with applied rules
440
+ """
441
+
442
+ @functools.wraps(func)
443
+ def wrapper(payload, **kwargs):
444
+
445
+ kwargs = apply_rate_selection(payload, **kwargs)
446
+
447
+ if kwargs.get("selected_rate") is None:
448
+ raise exceptions.APIException(
449
+ "The service you selected is not available for this shipment.",
450
+ code="service_unavailable",
451
+ status_code=status.HTTP_400_BAD_REQUEST,
452
+ )
453
+
454
+ # Execute original function
455
+ result = func(payload, **kwargs)
456
+
457
+ if isinstance(result, datatypes.Shipment) and kwargs.get("rule_activity"):
458
+ return lib.to_object(
459
+ datatypes.Shipment,
460
+ {
461
+ **lib.to_dict(result),
462
+ "meta": {
463
+ **(result.meta or {}),
464
+ **({"rule_activity": kwargs.get("rule_activity")}),
465
+ }
466
+ }
467
+ )
468
+
469
+ if hasattr(result, "save") and kwargs.get("rule_activity"):
470
+ result.meta = {
471
+ **(result.meta or {}),
472
+ **({"rule_activity": kwargs.get("rule_activity")}),
473
+ }
474
+ result.save()
475
+ return result
476
+
477
+ return result
478
+
479
+ return wrapper