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,416 @@
1
+ import re
2
+ import typing
3
+ import logging
4
+ import requests # type: ignore
5
+ import phonenumbers
6
+ from constance import config
7
+ from datetime import datetime
8
+
9
+ import karrio.lib as lib
10
+ import karrio.core.units as units
11
+ import karrio.server.serializers as serializers
12
+ import karrio.server.core.datatypes as datatypes
13
+
14
+ # Try to import the references module, which may not be available in all environments
15
+ try:
16
+ import karrio.references as references
17
+ except ImportError:
18
+ references = None
19
+
20
+ logger = logging.getLogger(__name__)
21
+ DIMENSIONS = ["width", "height", "length"]
22
+
23
+
24
+ def dimensions_required_together(value):
25
+ any_dimension_specified = any(value.get(dim) is not None for dim in DIMENSIONS)
26
+ has_any_dimension_undefined = any(value.get(dim) is None for dim in DIMENSIONS)
27
+ dimension_unit_is_undefined = value.get("dimension_unit") is None
28
+
29
+ if any_dimension_specified and has_any_dimension_undefined:
30
+ raise serializers.ValidationError(
31
+ {
32
+ "dimensions": "When one dimension is specified, all must be specified with a dimension_unit"
33
+ }
34
+ )
35
+
36
+ if (
37
+ any_dimension_specified
38
+ and not has_any_dimension_undefined
39
+ and dimension_unit_is_undefined
40
+ ):
41
+ raise serializers.ValidationError(
42
+ {
43
+ "dimension_unit": "dimension_unit is required when dimensions are specified"
44
+ }
45
+ )
46
+
47
+
48
+ def valid_time_format(prop: str):
49
+ def validate(value):
50
+
51
+ try:
52
+ datetime.strptime(value, "%H:%M")
53
+ except Exception:
54
+ raise serializers.ValidationError(
55
+ "The time format must match HH:HM",
56
+ code="invalid",
57
+ )
58
+
59
+ return validate
60
+
61
+
62
+ def valid_date_format(prop: str):
63
+ def validate(value):
64
+
65
+ try:
66
+ datetime.strptime(value, "%Y-%m-%d")
67
+ except Exception:
68
+ raise serializers.ValidationError(
69
+ "The date format must match YYYY-MM-DD",
70
+ code="invalid",
71
+ )
72
+
73
+ return validate
74
+
75
+
76
+ def valid_datetime_format(prop: str):
77
+ def validate(value):
78
+
79
+ try:
80
+ datetime.strptime(value, "%Y-%m-%d %H:%M")
81
+ except Exception:
82
+ raise serializers.ValidationError(
83
+ "The datetime format must match YYYY-MM-DD HH:HM",
84
+ code="invalid",
85
+ )
86
+
87
+ return validate
88
+
89
+
90
+ def valid_base64(prop: str, max_size: int = 5242880):
91
+ def validate(value: str):
92
+ error = None
93
+
94
+ try:
95
+ buffer = lib.to_buffer(value, validate=True)
96
+
97
+ if buffer.getbuffer().nbytes > max_size:
98
+ error = f"Error: file size exceeds {max_size} bytes."
99
+
100
+ except Exception as e:
101
+ logger.exception(e)
102
+ error = "Invalid base64 file content"
103
+ raise serializers.ValidationError(
104
+ error,
105
+ code="invalid",
106
+ )
107
+
108
+ if error is not None:
109
+ raise serializers.ValidationError(error, code="invalid")
110
+
111
+ return validate
112
+
113
+
114
+ class OptionDefaultSerializer(serializers.Serializer):
115
+ def __init__(self, instance=None, **kwargs):
116
+ data = kwargs.get("data", {})
117
+ if data:
118
+ # Get existing options from data and instance
119
+ options = {
120
+ **(
121
+ getattr(instance, "options", None) or {}
122
+ ), # Start with instance options
123
+ **(data.get("options") or {}), # Override with new options
124
+ }
125
+
126
+ # Get shipping_date from options or default to next business day
127
+ shipping_date = options.get("shipping_date")
128
+ shipment_date = options.get("shipment_date")
129
+
130
+ if not shipping_date:
131
+ shipping_date = lib.fdatetime(
132
+ lib.to_next_business_datetime(
133
+ lib.to_date(shipment_date) or datetime.now()
134
+ ),
135
+ output_format="%Y-%m-%dT%H:%M",
136
+ )
137
+
138
+ if not shipment_date:
139
+ shipment_date = lib.fdate(
140
+ shipping_date, current_format="%Y-%m-%dT%H:%M"
141
+ )
142
+
143
+ # Update only the date fields in options
144
+ options.update(
145
+ {"shipping_date": shipping_date, "shipment_date": shipment_date}
146
+ )
147
+
148
+ # Update the data with merged options
149
+ kwargs["data"]["options"] = options
150
+
151
+ super().__init__(instance, **kwargs)
152
+
153
+
154
+ class PresetSerializer(serializers.Serializer):
155
+ def validate(self, data):
156
+ import karrio.server.core.dataunits as dataunits
157
+
158
+ dimensions_required_together(data)
159
+
160
+ if data is not None and "package_preset" in data:
161
+ preset = next(
162
+ (
163
+ presets[data["package_preset"]]
164
+ for _, presets in dataunits.REFERENCE_MODELS[
165
+ "package_presets"
166
+ ].items()
167
+ if data["package_preset"] in presets
168
+ ),
169
+ {},
170
+ )
171
+
172
+ data.update(
173
+ {
174
+ **data,
175
+ "width": data.get("width", preset.get("width")),
176
+ "length": data.get("length", preset.get("length")),
177
+ "height": data.get("height", preset.get("height")),
178
+ "dimension_unit": data.get(
179
+ "dimension_unit", preset.get("dimension_unit")
180
+ ),
181
+ }
182
+ )
183
+
184
+ return data
185
+
186
+
187
+ class AugmentedAddressSerializer(serializers.Serializer):
188
+ def validate(self, data):
189
+ # Format and validate Postal Code
190
+ if all(data.get(key) is not None for key in ["country_code", "postal_code"]):
191
+ postal_code = data["postal_code"]
192
+ country_code = data["country_code"]
193
+
194
+ if country_code == units.Country.CA.name:
195
+ formatted = "".join(
196
+ [c for c in postal_code.split() if c not in ["-", "_"]]
197
+ ).upper()
198
+ if not re.match(r"^([A-Za-z]\d[A-Za-z][-]?\d[A-Za-z]\d)", formatted):
199
+ raise serializers.ValidationError(
200
+ {"postal_code": "The Canadian postal code must match Z9Z9Z9"}
201
+ )
202
+
203
+ elif country_code == units.Country.US.name:
204
+ formatted = "".join(postal_code.split())
205
+ if not re.match(r"^\d{5}(-\d{4})?$", formatted):
206
+ raise serializers.ValidationError(
207
+ {
208
+ "postal_code": "The American postal code must match 12345 or 12345-6789"
209
+ }
210
+ )
211
+
212
+ else:
213
+ formatted = postal_code
214
+
215
+ data.update({**data, "postal_code": formatted})
216
+
217
+ # Format and validate Phone Number
218
+ if all(
219
+ data.get(key) is not None and data.get(key) != ""
220
+ for key in ["country_code", "phone_number"]
221
+ ):
222
+ phone_number = data["phone_number"]
223
+ country_code = data["country_code"]
224
+
225
+ try:
226
+ formatted = phonenumbers.parse(phone_number, country_code)
227
+ data.update(
228
+ {
229
+ **data,
230
+ "phone_number": phonenumbers.format_number(
231
+ formatted, phonenumbers.PhoneNumberFormat.INTERNATIONAL
232
+ ),
233
+ }
234
+ )
235
+ except Exception as e:
236
+ logger.warning(e)
237
+ raise serializers.ValidationError(
238
+ {"phone_number": "Invalid phone number format"}
239
+ )
240
+
241
+ return data
242
+
243
+
244
+ class AddressValidatorAbstract:
245
+ """
246
+ Abstract base class for address validators.
247
+
248
+ This class defines the interface that all address validators must implement.
249
+ Note: This class is kept here for backwards compatibility.
250
+ New validators should use the karrio.validators.abstract.AddressValidatorAbstract class.
251
+ """
252
+
253
+ @staticmethod
254
+ def get_info(is_authenticated: bool = None) -> dict:
255
+ """
256
+ Get information about the validator.
257
+
258
+ Args:
259
+ is_authenticated: Whether authenticated information should be returned
260
+
261
+ Returns:
262
+ Dictionary with information about the validator
263
+
264
+ Raises:
265
+ Exception: If the method is not implemented
266
+ """
267
+ raise Exception("get_info method is not implemented")
268
+
269
+ @staticmethod
270
+ def validate(address: datatypes.Address) -> datatypes.AddressValidation:
271
+ """
272
+ Validate an address using the service.
273
+
274
+ Args:
275
+ address: Address object to validate
276
+
277
+ Returns:
278
+ Address validation result
279
+
280
+ Raises:
281
+ Exception: If the method is not implemented
282
+ """
283
+ raise Exception("validate method is not implemented")
284
+
285
+
286
+ class Address:
287
+ """
288
+ Address validation service provider.
289
+
290
+ This class provides methods to validate addresses using various service providers
291
+ which are loaded as plugins.
292
+ """
293
+
294
+ @staticmethod
295
+ def get_info(is_authenticated: bool = True) -> dict:
296
+ """
297
+ Get information about the available address validation services.
298
+
299
+ Args:
300
+ is_authenticated: Whether to include sensitive information like API keys
301
+
302
+ Returns:
303
+ Dictionary with information about the available validators
304
+ """
305
+ # If references module is available, get validator plugins info
306
+ refs = references.REFERENCES
307
+ if len(refs.get("address_validators", {})) > 0:
308
+ # Return information about the first available validator
309
+ validator_name = next(iter(refs["address_validators"].keys()))
310
+ validator_class = None
311
+
312
+ # Try to get the validator class from the references
313
+ try:
314
+ # Import the validator module dynamically
315
+ import importlib
316
+ module = importlib.import_module(f"karrio.validators.{validator_name}")
317
+ if hasattr(module, "METADATA"):
318
+ validator_class = module.METADATA.Validator
319
+ except (ImportError, AttributeError) as e:
320
+ logger.warning(f"Could not import validator {validator_name}: {e}")
321
+
322
+ if validator_class is not None:
323
+ # Return validator info with is_enabled=True
324
+ validator_info = validator_class.get_info(is_authenticated)
325
+ return {"is_enabled": True, **validator_info}
326
+
327
+ # Check for legacy config-based validation
328
+ is_enabled = any(
329
+ [config.GOOGLE_CLOUD_API_KEY, config.CANADAPOST_ADDRESS_COMPLETE_API_KEY]
330
+ )
331
+
332
+ if is_enabled:
333
+ # Legacy validation is enabled, use the legacy validator
334
+ return {
335
+ "is_enabled": is_enabled,
336
+ **Address._get_legacy_validator().get_info(is_authenticated),
337
+ }
338
+
339
+ # No validation is available
340
+ return dict(is_enabled=False)
341
+
342
+ @staticmethod
343
+ def _get_legacy_validator() -> typing.Type[AddressValidatorAbstract]:
344
+ """
345
+ Get a legacy validator instance based on configuration.
346
+
347
+ This method is used for backwards compatibility with the old validation system.
348
+
349
+ Returns:
350
+ An instance of a validator class
351
+
352
+ Raises:
353
+ Exception: If no validator is configured
354
+ """
355
+ # For backwards compatibility, check if Google or Canada Post is configured
356
+ if any(config.GOOGLE_CLOUD_API_KEY or ""):
357
+ from karrio.validators.googlegeocoding import Validator as GoogleGeocode
358
+ return GoogleGeocode
359
+ elif any(config.CANADAPOST_ADDRESS_COMPLETE_API_KEY or ""):
360
+ from karrio.validators.addresscomplete import Validator as AddressComplete
361
+ return AddressComplete
362
+
363
+ raise Exception("No address validation service provider configured")
364
+
365
+ @staticmethod
366
+ def get_validator() -> typing.Type:
367
+ """
368
+ Get a validator instance based on configuration or plugins.
369
+
370
+ This method first checks for validator plugins, then falls back to
371
+ legacy validators if no plugins are available.
372
+
373
+ Returns:
374
+ An instance of a validator class
375
+
376
+ Raises:
377
+ Exception: If no validator is configured
378
+ """
379
+ # If references module is available, check for validator plugins
380
+ refs = references.REFERENCES
381
+ if len(refs.get("address_validators", {})) > 0:
382
+ # Get the first available validator
383
+ validator_name = next(iter(refs["address_validators"].keys()))
384
+
385
+ # Try to get the validator class from the references
386
+ try:
387
+ # Import the validator module dynamically
388
+ import importlib
389
+ module = importlib.import_module(f"karrio.validators.{validator_name}")
390
+ if hasattr(module, "METADATA"):
391
+ return module.METADATA.Validator
392
+ except (ImportError, AttributeError) as e:
393
+ logger.warning(f"Could not import validator {validator_name}: {e}")
394
+
395
+ # Fall back to legacy validator
396
+ return Address._get_legacy_validator()
397
+
398
+ @staticmethod
399
+ def validate(address: datatypes.Address) -> datatypes.AddressValidation:
400
+ """
401
+ Validate an address using the configured validator.
402
+
403
+ Args:
404
+ address: The address to validate
405
+
406
+ Returns:
407
+ AddressValidation object with validation results
408
+ """
409
+ validator = Address.get_validator()
410
+ result = validator.validate(address)
411
+
412
+ # Handle the case where the validator returns a dict instead of AddressValidation
413
+ if isinstance(result, dict):
414
+ return datatypes.AddressValidation(**result)
415
+
416
+ return result
@@ -0,0 +1,2 @@
1
+ import karrio.server.core.views.references
2
+ from karrio.server.core.router import router
@@ -0,0 +1,133 @@
1
+ import pydoc
2
+ import typing
3
+ from django.conf import settings
4
+ from django.http import JsonResponse
5
+ from rest_framework import generics, views
6
+ from rest_framework.permissions import IsAuthenticated
7
+ from rest_framework.throttling import UserRateThrottle, AnonRateThrottle
8
+ from rest_framework_tracking import mixins
9
+ from rest_framework import status
10
+
11
+ from karrio.core.utils import DP
12
+ from karrio.server.serializers import link_org
13
+ from karrio.server.tracing.utils import set_tracing_context
14
+ from karrio.server.core.utils import failsafe
15
+ from karrio.server.core.authentication import (
16
+ TokenAuthentication,
17
+ JWTAuthentication,
18
+ TokenBasicAuthentication,
19
+ OAuth2Authentication,
20
+ )
21
+ from karrio.server.core.models import APILogIndex
22
+
23
+ AccessMixin: typing.Any = pydoc.locate(
24
+ getattr(settings, "ACCESS_METHOD", "karrio.server.core.authentication.AccessMixin")
25
+ )
26
+
27
+
28
+ class LoggingMixin(mixins.LoggingMixin):
29
+ def handle_log(self):
30
+ data = None if "data" not in self.log else DP.jsonify(self.log["data"])
31
+ query_params = (
32
+ None
33
+ if "query_params" not in self.log
34
+ else DP.jsonify(self.log["query_params"])
35
+ )
36
+ response = (
37
+ dict(response=response)
38
+ if "response" not in self.log
39
+ else (
40
+ DP.jsonify(self.log["response"])
41
+ if isinstance(DP.to_object(self.log["response"]), dict)
42
+ else self.log["response"]
43
+ )
44
+ )
45
+ entity_id = failsafe(lambda: DP.to_dict(response)["id"])
46
+ test_mode = failsafe(lambda: self.request.test_mode)
47
+
48
+ if test_mode is None and '"test_mode": true' in (self.log["response"] or ""):
49
+ test_mode = True
50
+ if test_mode is None and '"test_mode": false' in (self.log["response"] or ""):
51
+ test_mode = False
52
+
53
+ log = APILogIndex(
54
+ **{
55
+ **self.log,
56
+ "data": data,
57
+ "response": response,
58
+ "entity_id": entity_id,
59
+ "test_mode": test_mode,
60
+ "query_params": query_params,
61
+ }
62
+ )
63
+
64
+ log.save()
65
+ link_org(log, self.request)
66
+
67
+ set_tracing_context(
68
+ request_log_id=getattr(log, "id", None),
69
+ object_id=failsafe(lambda: (self.log.get("response") or {}).get("id")),
70
+ )
71
+
72
+
73
+ class BaseView:
74
+ permission_classes = [IsAuthenticated]
75
+ throttle_classes = [UserRateThrottle, AnonRateThrottle]
76
+ authentication_classes = [
77
+ TokenAuthentication,
78
+ JWTAuthentication,
79
+ OAuth2Authentication,
80
+ TokenBasicAuthentication,
81
+ ]
82
+
83
+
84
+ class BaseAPIView(views.APIView, BaseView):
85
+ pass
86
+
87
+
88
+ class BaseGenericAPIView(generics.GenericAPIView, BaseView):
89
+ def get_queryset(self):
90
+ if hasattr(self, "model") and getattr(self, "swagger_fake_view", False):
91
+ # queryset just for schema generation metadata
92
+ return self.model.objects.none()
93
+
94
+ if hasattr(self, "model") and hasattr(self.model, "access_by"):
95
+ return self.model.access_by(self.request)
96
+
97
+ return getattr(self, "queryset", None)
98
+
99
+
100
+ class GenericAPIView(LoggingMixin, BaseGenericAPIView):
101
+ logging_methods = ["POST", "PUT", "PATCH", "DELETE"]
102
+
103
+
104
+ class APIView(LoggingMixin, BaseAPIView):
105
+ logging_methods = ["POST", "PUT", "PATCH", "DELETE"]
106
+
107
+
108
+ class LoginRequiredView(AccessMixin):
109
+ def dispatch(self, request, *args, **kwargs):
110
+ auth = super().dispatch(request, *args, **kwargs)
111
+ if not request.user.is_authenticated:
112
+ return JsonResponse(
113
+ dict(
114
+ errors=[
115
+ {
116
+ "code": "not_authenticated",
117
+ "message": "Authentication credentials were not provided.",
118
+ }
119
+ ]
120
+ ),
121
+ status=status.HTTP_401_UNAUTHORIZED,
122
+ )
123
+
124
+ if not request.user.is_verified():
125
+ return JsonResponse(
126
+ dict(
127
+ errors=[
128
+ {"code": "not_verified", "message": "User is not verified."}
129
+ ]
130
+ ),
131
+ status=status.HTTP_403_FORBIDDEN,
132
+ )
133
+ return auth
@@ -0,0 +1,44 @@
1
+ import rest_framework.request as request
2
+ import rest_framework.response as response
3
+ import rest_framework.renderers as renderers
4
+ import rest_framework.decorators as decorators
5
+ import rest_framework.permissions as permissions
6
+
7
+ import karrio.server.conf as conf
8
+ import karrio.server.openapi as openapi
9
+ import karrio.server.core.dataunits as dataunits
10
+
11
+ ENDPOINT_ID = "&&" # This endpoint id is used to make operation ids unique make sure not to duplicate
12
+ Metadata = openapi.OpenApiResponse(
13
+ openapi.OpenApiTypes.OBJECT,
14
+ examples=[
15
+ openapi.OpenApiExample(
16
+ name="Metadata",
17
+ value={
18
+ "VERSION": "",
19
+ "APP_NAME": "",
20
+ "APP_WEBSITE": "",
21
+ "HOST": "",
22
+ "ADMIN": "",
23
+ "OPENAPI": "",
24
+ "GRAPHQL": "",
25
+ **{flag: True for flag in conf.FEATURE_FLAGS},
26
+ },
27
+ )
28
+ ],
29
+ )
30
+
31
+
32
+ @openapi.extend_schema(
33
+ auth=[],
34
+ methods=["get"],
35
+ tags=["API"],
36
+ operation_id=f"{ENDPOINT_ID}ping",
37
+ summary="Instance Metadata",
38
+ responses={200: Metadata},
39
+ )
40
+ @decorators.api_view(["GET"])
41
+ @decorators.permission_classes([permissions.AllowAny])
42
+ @decorators.renderer_classes([renderers.JSONRenderer])
43
+ def view(request: request.Request) -> response.Response:
44
+ return response.Response(dataunits.contextual_metadata(request))
@@ -0,0 +1,74 @@
1
+ import logging
2
+ from django.views.decorators.csrf import csrf_exempt
3
+ from django.utils.decorators import method_decorator
4
+ from oauth2_provider.views import TokenView as BaseTokenView
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ @method_decorator(csrf_exempt, name='dispatch')
10
+ class CustomTokenView(BaseTokenView):
11
+ """
12
+ Custom OAuth2 token view that handles grant type format conversion.
13
+
14
+ django-oauth-toolkit stores grant types with hyphens (e.g., 'authorization-code')
15
+ but OAuth2 spec uses underscores (e.g., 'authorization_code').
16
+ This view converts the format before processing.
17
+ """
18
+
19
+ def post(self, request, *args, **kwargs):
20
+ """
21
+ Handle token requests with grant type conversion.
22
+ """
23
+ print(f"CustomTokenView called")
24
+ print(f"Request method: {request.method}")
25
+ print(f"Request content type: {request.content_type}")
26
+ print(f"Request POST: {dict(request.POST)}")
27
+ print(f"Request body: {request.body.decode('utf-8') if request.body else 'Empty'}")
28
+
29
+ # Parse the request body if POST data is empty
30
+ if not request.POST and request.body:
31
+ from django.http import QueryDict
32
+ import urllib.parse
33
+
34
+ # Parse the form data from the request body
35
+ body_data = urllib.parse.parse_qs(request.body.decode('utf-8'))
36
+ # Convert to single values (parse_qs returns lists)
37
+ parsed_data = {k: v[0] if v else '' for k, v in body_data.items()}
38
+
39
+ # Create a QueryDict from the parsed data
40
+ post_data = QueryDict('', mutable=True)
41
+ for key, value in parsed_data.items():
42
+ post_data[key] = value
43
+
44
+ # Replace the request POST data
45
+ request.POST = post_data
46
+ request._post = post_data
47
+
48
+ # Convert OAuth2 spec grant types to django-oauth-toolkit format
49
+ grant_type_mapping = {
50
+ 'authorization_code': 'authorization-code',
51
+ 'client_credentials': 'client-credentials',
52
+ 'refresh_token': 'refresh-token',
53
+ 'password': 'password',
54
+ }
55
+
56
+ original_grant_type = request.POST.get('grant_type')
57
+ print(f"Original grant type after parsing: {original_grant_type}")
58
+
59
+ if original_grant_type in grant_type_mapping:
60
+ # Create a mutable copy of the POST data
61
+ post_data = request.POST.copy()
62
+ converted_grant_type = grant_type_mapping[original_grant_type]
63
+ print(f"Converting grant type from '{original_grant_type}' to '{converted_grant_type}'")
64
+ post_data['grant_type'] = converted_grant_type
65
+
66
+ # Replace the request POST data
67
+ request.POST = post_data
68
+ request._post = post_data
69
+ print(f"Updated grant type: {request.POST.get('grant_type')}")
70
+ else:
71
+ print(f"No conversion needed for grant type: {original_grant_type}")
72
+
73
+ # Call the parent token view with converted grant type
74
+ return super().post(request, *args, **kwargs)