karrio-server-core 2025.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. karrio/server/conf.py +54 -0
  2. karrio/server/core/__init__.py +3 -0
  3. karrio/server/core/admin.py +1 -0
  4. karrio/server/core/apps.py +10 -0
  5. karrio/server/core/authentication.py +347 -0
  6. karrio/server/core/config.py +31 -0
  7. karrio/server/core/context_processors.py +12 -0
  8. karrio/server/core/datatypes.py +394 -0
  9. karrio/server/core/dataunits.py +187 -0
  10. karrio/server/core/exceptions.py +404 -0
  11. karrio/server/core/fields.py +12 -0
  12. karrio/server/core/filters.py +837 -0
  13. karrio/server/core/gateway.py +1011 -0
  14. karrio/server/core/logging.py +403 -0
  15. karrio/server/core/management/commands/cli.py +19 -0
  16. karrio/server/core/management/commands/create_oauth_client.py +41 -0
  17. karrio/server/core/management/commands/runserver.py +5 -0
  18. karrio/server/core/middleware.py +197 -0
  19. karrio/server/core/migrations/0001_initial.py +28 -0
  20. karrio/server/core/migrations/0002_apilogindex.py +69 -0
  21. karrio/server/core/migrations/0003_apilogindex_test_mode.py +62 -0
  22. karrio/server/core/migrations/0004_metafield.py +74 -0
  23. karrio/server/core/migrations/0005_alter_metafield_type_alter_metafield_value.py +23 -0
  24. karrio/server/core/migrations/0006_add_api_log_requested_at_index.py +22 -0
  25. karrio/server/core/migrations/__init__.py +0 -0
  26. karrio/server/core/models/__init__.py +48 -0
  27. karrio/server/core/models/base.py +103 -0
  28. karrio/server/core/models/entity.py +24 -0
  29. karrio/server/core/models/metafield.py +144 -0
  30. karrio/server/core/models/third_party.py +21 -0
  31. karrio/server/core/oauth_validators.py +170 -0
  32. karrio/server/core/permissions.py +36 -0
  33. karrio/server/core/renderers.py +11 -0
  34. karrio/server/core/router.py +3 -0
  35. karrio/server/core/serializers.py +1971 -0
  36. karrio/server/core/signals.py +55 -0
  37. karrio/server/core/telemetry.py +573 -0
  38. karrio/server/core/tests.py +99 -0
  39. karrio/server/core/tests_resource_token.py +411 -0
  40. karrio/server/core/urls.py +12 -0
  41. karrio/server/core/utils.py +1025 -0
  42. karrio/server/core/validators.py +264 -0
  43. karrio/server/core/views/__init__.py +2 -0
  44. karrio/server/core/views/api.py +133 -0
  45. karrio/server/core/views/metadata.py +44 -0
  46. karrio/server/core/views/oauth.py +75 -0
  47. karrio/server/core/views/references.py +82 -0
  48. karrio/server/core/views/schema.py +310 -0
  49. karrio/server/filters/__init__.py +2 -0
  50. karrio/server/filters/abstract.py +26 -0
  51. karrio/server/iam/__init__.py +0 -0
  52. karrio/server/iam/admin.py +3 -0
  53. karrio/server/iam/apps.py +21 -0
  54. karrio/server/iam/migrations/0001_initial.py +33 -0
  55. karrio/server/iam/migrations/__init__.py +0 -0
  56. karrio/server/iam/models.py +48 -0
  57. karrio/server/iam/permissions.py +155 -0
  58. karrio/server/iam/serializers.py +54 -0
  59. karrio/server/iam/signals.py +18 -0
  60. karrio/server/iam/tests.py +3 -0
  61. karrio/server/iam/views.py +3 -0
  62. karrio/server/openapi.py +75 -0
  63. karrio/server/providers/__init__.py +1 -0
  64. karrio/server/providers/admin.py +364 -0
  65. karrio/server/providers/apps.py +10 -0
  66. karrio/server/providers/management/commands/migrate_rate_sheets.py +101 -0
  67. karrio/server/providers/migrations/0001_initial.py +140 -0
  68. karrio/server/providers/migrations/0002_carrier_active.py +18 -0
  69. karrio/server/providers/migrations/0003_auto_20201230_0820.py +24 -0
  70. karrio/server/providers/migrations/0004_auto_20210212_0554.py +178 -0
  71. karrio/server/providers/migrations/0005_auto_20210212_0555.py +18 -0
  72. karrio/server/providers/migrations/0006_australiapostsettings.py +29 -0
  73. karrio/server/providers/migrations/0007_auto_20210213_0206.py +21 -0
  74. karrio/server/providers/migrations/0008_auto_20210214_0409.py +30 -0
  75. karrio/server/providers/migrations/0009_auto_20210308_0302.py +18 -0
  76. karrio/server/providers/migrations/0010_auto_20210409_0852.py +32 -0
  77. karrio/server/providers/migrations/0011_auto_20210409_0853.py +21 -0
  78. karrio/server/providers/migrations/0012_alter_carrier_options.py +17 -0
  79. karrio/server/providers/migrations/0013_tntsettings.py +30 -0
  80. karrio/server/providers/migrations/0014_auto_20210612_1608.py +46 -0
  81. karrio/server/providers/migrations/0015_auto_20210615_1601.py +28 -0
  82. karrio/server/providers/migrations/0016_alter_purolatorsettings_user_token.py +18 -0
  83. karrio/server/providers/migrations/0017_auto_20210805_0359.py +1293 -0
  84. karrio/server/providers/migrations/0018_alter_fedexsettings_user_key.py +18 -0
  85. karrio/server/providers/migrations/0019_dhlpolandsettings_servicelevel.py +65 -0
  86. karrio/server/providers/migrations/0020_genericsettings_labeltemplate.py +52 -0
  87. karrio/server/providers/migrations/0021_auto_20211231_2353.py +40 -0
  88. karrio/server/providers/migrations/0022_carrier_metadata.py +18 -0
  89. karrio/server/providers/migrations/0023_auto_20220124_1916.py +27 -0
  90. karrio/server/providers/migrations/0024_alter_genericsettings_custom_carrier_name.py +19 -0
  91. karrio/server/providers/migrations/0025_alter_servicelevel_service_code.py +19 -0
  92. karrio/server/providers/migrations/0026_auto_20220208_0132.py +59 -0
  93. karrio/server/providers/migrations/0027_auto_20220304_1340.py +29 -0
  94. karrio/server/providers/migrations/0028_auto_20220323_1500.py +33 -0
  95. karrio/server/providers/migrations/0029_easypostsettings.py +27 -0
  96. karrio/server/providers/migrations/0030_amazonmwssettings.py +29 -0
  97. karrio/server/providers/migrations/0031_delete_amazonmwssettings.py +18 -0
  98. karrio/server/providers/migrations/0032_alter_carrier_test.py +18 -0
  99. karrio/server/providers/migrations/0033_auto_20220708_1350.py +22 -0
  100. karrio/server/providers/migrations/0034_amazonmwssettings_dpdhlsettings.py +47 -0
  101. karrio/server/providers/migrations/0035_alter_carrier_capabilities.py +43 -0
  102. karrio/server/providers/migrations/0036_upsfreightsettings.py +31 -0
  103. karrio/server/providers/migrations/0037_chronopostsettings.py +29 -0
  104. karrio/server/providers/migrations/0038_alter_genericsettings_label_template.py +19 -0
  105. karrio/server/providers/migrations/0039_auto_20220906_0612.py +23 -0
  106. karrio/server/providers/migrations/0040_dpdhlsettings_services.py +18 -0
  107. karrio/server/providers/migrations/0041_auto_20221105_0705.py +38 -0
  108. karrio/server/providers/migrations/0042_auto_20221215_1642.py +23 -0
  109. karrio/server/providers/migrations/0043_alter_genericsettings_account_number_and_more.py +39 -0
  110. karrio/server/providers/migrations/0044_carrier_carrier_capabilities.py +64 -0
  111. karrio/server/providers/migrations/0045_alter_carrier_active_alter_carrier_carrier_id.py +31 -0
  112. karrio/server/providers/migrations/0046_remove_dpdhlsettings_signature_and_more.py +41 -0
  113. karrio/server/providers/migrations/0047_dpdsettings.py +286 -0
  114. karrio/server/providers/migrations/0048_servicelevel_min_weight_servicelevel_transit_days_and_more.py +64 -0
  115. karrio/server/providers/migrations/0049_boxknightsettings_geodissettings_lapostesettings_and_more.py +156 -0
  116. karrio/server/providers/migrations/0050_carrier_is_system_alter_carrier_metadata_and_more.py +106 -0
  117. karrio/server/providers/migrations/0051_rename_username_upssettings_client_id_and_more.py +31 -0
  118. karrio/server/providers/migrations/0052_alter_upssettings_account_number_and_more.py +20 -0
  119. karrio/server/providers/migrations/0053_locate2usettings.py +281 -0
  120. karrio/server/providers/migrations/0054_zoom2usettings.py +280 -0
  121. karrio/server/providers/migrations/0055_rename_amazonmwssettings_amazonshippingsettings_and_more.py +44 -0
  122. karrio/server/providers/migrations/0056_asendiaussettings_geodissettings_code_client_and_more.py +75 -0
  123. karrio/server/providers/migrations/0057_alter_servicelevel_weight_unit_belgianpostsettings.py +51 -0
  124. karrio/server/providers/migrations/0058_alliedexpresssettings.py +38 -0
  125. karrio/server/providers/migrations/0059_ratesheet.py +81 -0
  126. karrio/server/providers/migrations/0060_belgianpostsettings_rate_sheet_and_more.py +73 -0
  127. karrio/server/providers/migrations/0061_alliedexpresssettings_service_type.py +17 -0
  128. karrio/server/providers/migrations/0062_sendlesettings_account_country_code.py +257 -0
  129. karrio/server/providers/migrations/0063_servicelevel_metadata.py +25 -0
  130. karrio/server/providers/migrations/0064_alliedexpresslocalsettings.py +43 -0
  131. karrio/server/providers/migrations/0065_servicelevel_carrier_service_code_and_more.py +66 -0
  132. karrio/server/providers/migrations/0066_rename_fedexsettings_fedexwssettings_and_more.py +28 -0
  133. karrio/server/providers/migrations/0067_fedexsettings.py +283 -0
  134. karrio/server/providers/migrations/0068_fedexsettings_track_api_key_and_more.py +38 -0
  135. karrio/server/providers/migrations/0069_alter_canadapostsettings_contract_id_and_more.py +23 -0
  136. karrio/server/providers/migrations/0070_tgesettings_alter_carrier_capabilities.py +65 -0
  137. karrio/server/providers/migrations/0071_alter_tgesettings_my_toll_token.py +18 -0
  138. karrio/server/providers/migrations/0072_rename_eshippersettings_eshipperxmlsettings_and_more.py +28 -0
  139. karrio/server/providers/migrations/0073_delete_eshipperxmlsettings.py +41 -0
  140. karrio/server/providers/migrations/0074_eshippersettings.py +38 -0
  141. karrio/server/providers/migrations/0075_haypostsettings.py +40 -0
  142. karrio/server/providers/migrations/0076_rename_customer_registration_id_uspsinternationalsettings_account_number_and_more.py +125 -0
  143. karrio/server/providers/migrations/0077_uspswtinternationalsettings_uspswtsettings_and_more.py +165 -0
  144. karrio/server/providers/migrations/0078_auto_20240813_1552.py +120 -0
  145. karrio/server/providers/migrations/0079_alter_carrier_options_alter_ratesheet_created_by.py +31 -0
  146. karrio/server/providers/migrations/0080_alter_aramexsettings_account_country_code_and_more.py +3025 -0
  147. karrio/server/providers/migrations/0081_remove_alliedexpresssettings_carrier_ptr_and_more.py +338 -0
  148. karrio/server/providers/migrations/0082_add_zone_identifiers.py +50 -0
  149. karrio/server/providers/migrations/0083_add_optimized_rate_sheet_structure.py +33 -0
  150. karrio/server/providers/migrations/0084_alter_servicelevel_currency.py +168 -0
  151. karrio/server/providers/migrations/0085_populate_dhl_parcel_de_oauth_credentials.py +82 -0
  152. karrio/server/providers/migrations/0086_rename_dhl_parcel_de_customer_number_to_billing_number.py +71 -0
  153. karrio/server/providers/migrations/__init__.py +0 -0
  154. karrio/server/providers/models/__init__.py +16 -0
  155. karrio/server/providers/models/carrier.py +387 -0
  156. karrio/server/providers/models/config.py +30 -0
  157. karrio/server/providers/models/service.py +192 -0
  158. karrio/server/providers/models/sheet.py +287 -0
  159. karrio/server/providers/models/template.py +39 -0
  160. karrio/server/providers/models/utils.py +58 -0
  161. karrio/server/providers/router.py +3 -0
  162. karrio/server/providers/serializers/__init__.py +3 -0
  163. karrio/server/providers/serializers/base.py +538 -0
  164. karrio/server/providers/signals.py +25 -0
  165. karrio/server/providers/templates/providers/oauth_callback.html +105 -0
  166. karrio/server/providers/tests/__init__.py +5 -0
  167. karrio/server/providers/tests/test_connections.py +895 -0
  168. karrio/server/providers/urls.py +11 -0
  169. karrio/server/providers/views/__init__.py +0 -0
  170. karrio/server/providers/views/carriers.py +267 -0
  171. karrio/server/providers/views/connections.py +496 -0
  172. karrio/server/samples.py +352 -0
  173. karrio/server/serializers/__init__.py +2 -0
  174. karrio/server/serializers/abstract.py +602 -0
  175. karrio/server/tracing/__init__.py +0 -0
  176. karrio/server/tracing/admin.py +63 -0
  177. karrio/server/tracing/apps.py +8 -0
  178. karrio/server/tracing/migrations/0001_initial.py +41 -0
  179. karrio/server/tracing/migrations/0002_auto_20220710_1307.py +22 -0
  180. karrio/server/tracing/migrations/0003_auto_20221105_0317.py +43 -0
  181. karrio/server/tracing/migrations/0004_tracingrecord_carrier_account_idx.py +24 -0
  182. karrio/server/tracing/migrations/0005_optimise_tracingrecord_request_log_idx.py +25 -0
  183. karrio/server/tracing/migrations/0006_alter_tracingrecord_options_and_more.py +49 -0
  184. karrio/server/tracing/migrations/0007_tracingrecord_tracing_created_at_idx.py +19 -0
  185. karrio/server/tracing/migrations/__init__.py +0 -0
  186. karrio/server/tracing/models.py +82 -0
  187. karrio/server/tracing/tests.py +3 -0
  188. karrio/server/tracing/utils.py +109 -0
  189. karrio/server/user/__init__.py +0 -0
  190. karrio/server/user/admin.py +96 -0
  191. karrio/server/user/apps.py +7 -0
  192. karrio/server/user/forms.py +35 -0
  193. karrio/server/user/migrations/0001_initial.py +41 -0
  194. karrio/server/user/migrations/0002_token.py +29 -0
  195. karrio/server/user/migrations/0003_token_test_mode.py +20 -0
  196. karrio/server/user/migrations/0004_group.py +26 -0
  197. karrio/server/user/migrations/0005_token_label.py +21 -0
  198. karrio/server/user/migrations/0006_workspaceconfig.py +63 -0
  199. karrio/server/user/migrations/0007_user_metadata.py +25 -0
  200. karrio/server/user/migrations/__init__.py +0 -0
  201. karrio/server/user/models.py +218 -0
  202. karrio/server/user/serializers.py +47 -0
  203. karrio/server/user/templates/registration/login.html +108 -0
  204. karrio/server/user/templates/registration/registration_confirm_email.html +10 -0
  205. karrio/server/user/templates/registration/registration_confirm_email.txt +3 -0
  206. karrio/server/user/tests.py +3 -0
  207. karrio/server/user/urls.py +10 -0
  208. karrio/server/user/utils.py +60 -0
  209. karrio/server/user/views.py +9 -0
  210. karrio_server_core-2025.5.dist-info/METADATA +32 -0
  211. karrio_server_core-2025.5.dist-info/RECORD +213 -0
  212. karrio_server_core-2025.5.dist-info/WHEEL +5 -0
  213. karrio_server_core-2025.5.dist-info/top_level.txt +2 -0
@@ -0,0 +1,264 @@
1
+ import re
2
+ import phonenumbers
3
+ from datetime import datetime
4
+ from karrio.server.core.logging import logger
5
+
6
+ import karrio.lib as lib
7
+ import karrio.core.units as units
8
+ import karrio.server.serializers as serializers
9
+
10
+ DIMENSIONS = ["width", "height", "length"]
11
+
12
+
13
+ def dimensions_required_together(value):
14
+ any_dimension_specified = any(value.get(dim) is not None for dim in DIMENSIONS)
15
+ has_any_dimension_undefined = any(value.get(dim) is None for dim in DIMENSIONS)
16
+ dimension_unit_is_undefined = value.get("dimension_unit") is None
17
+
18
+ if any_dimension_specified and has_any_dimension_undefined:
19
+ raise serializers.ValidationError(
20
+ {
21
+ "dimensions": "When one dimension is specified, all must be specified with a dimension_unit"
22
+ }
23
+ )
24
+
25
+ if (
26
+ any_dimension_specified
27
+ and not has_any_dimension_undefined
28
+ and dimension_unit_is_undefined
29
+ ):
30
+ raise serializers.ValidationError(
31
+ {
32
+ "dimension_unit": "dimension_unit is required when dimensions are specified"
33
+ }
34
+ )
35
+
36
+
37
+ class TimeFormatValidator:
38
+ """Validator for HH:MM time format that can be pickled."""
39
+
40
+ def __init__(self, prop: str):
41
+ self.prop = prop
42
+
43
+ def __call__(self, value):
44
+ try:
45
+ datetime.strptime(value, "%H:%M")
46
+ except Exception:
47
+ raise serializers.ValidationError(
48
+ "The time format must match HH:HM",
49
+ code="invalid",
50
+ )
51
+
52
+
53
+ class DateFormatValidator:
54
+ """Validator for YYYY-MM-DD date format that can be pickled."""
55
+
56
+ def __init__(self, prop: str):
57
+ self.prop = prop
58
+
59
+ def __call__(self, value):
60
+ try:
61
+ datetime.strptime(value, "%Y-%m-%d")
62
+ except Exception:
63
+ raise serializers.ValidationError(
64
+ "The date format must match YYYY-MM-DD",
65
+ code="invalid",
66
+ )
67
+
68
+
69
+ class DateTimeFormatValidator:
70
+ """Validator for YYYY-MM-DD HH:MM datetime format that can be pickled."""
71
+
72
+ def __init__(self, prop: str):
73
+ self.prop = prop
74
+
75
+ def __call__(self, value):
76
+ try:
77
+ datetime.strptime(value, "%Y-%m-%d %H:%M")
78
+ except Exception:
79
+ raise serializers.ValidationError(
80
+ "The datetime format must match YYYY-MM-DD HH:HM",
81
+ code="invalid",
82
+ )
83
+
84
+
85
+ def valid_time_format(prop: str):
86
+ """Factory function for time format validator."""
87
+ return TimeFormatValidator(prop)
88
+
89
+
90
+ def valid_date_format(prop: str):
91
+ """Factory function for date format validator."""
92
+ return DateFormatValidator(prop)
93
+
94
+
95
+ def valid_datetime_format(prop: str):
96
+ """Factory function for datetime format validator."""
97
+ return DateTimeFormatValidator(prop)
98
+
99
+
100
+ class Base64Validator:
101
+ """Validator for base64 encoded content that can be pickled."""
102
+
103
+ def __init__(self, prop: str, max_size: int = 5242880):
104
+ self.prop = prop
105
+ self.max_size = max_size
106
+
107
+ def __call__(self, value: str):
108
+ error = None
109
+
110
+ try:
111
+ buffer = lib.to_buffer(value, validate=True)
112
+
113
+ if buffer.getbuffer().nbytes > self.max_size:
114
+ error = f"Error: file size exceeds {self.max_size} bytes."
115
+
116
+ except Exception as e:
117
+ logger.error("Invalid base64 file content", error=str(e))
118
+ error = "Invalid base64 file content"
119
+ raise serializers.ValidationError(
120
+ error,
121
+ code="invalid",
122
+ )
123
+
124
+ if error is not None:
125
+ raise serializers.ValidationError(error, code="invalid")
126
+
127
+
128
+ def valid_base64(prop: str, max_size: int = 5242880):
129
+ """Factory function for base64 validator."""
130
+ return Base64Validator(prop, max_size)
131
+
132
+
133
+ class OptionDefaultSerializer(serializers.Serializer):
134
+ def __init__(self, instance=None, **kwargs):
135
+ data = kwargs.get("data", {})
136
+ if data:
137
+ # Get existing options from data and instance
138
+ options = {
139
+ **(
140
+ getattr(instance, "options", None) or {}
141
+ ), # Start with instance options
142
+ **(data.get("options") or {}), # Override with new options
143
+ }
144
+
145
+ # Get shipping_date from options or default to next business day
146
+ shipping_date = options.get("shipping_date")
147
+ shipment_date = options.get("shipment_date")
148
+
149
+ if not shipping_date:
150
+ shipping_date = lib.fdatetime(
151
+ lib.to_next_business_datetime(
152
+ lib.to_date(shipment_date) or datetime.now()
153
+ ),
154
+ output_format="%Y-%m-%dT%H:%M",
155
+ )
156
+
157
+ if not shipment_date:
158
+ shipment_date = lib.fdate(
159
+ shipping_date, current_format="%Y-%m-%dT%H:%M"
160
+ )
161
+
162
+ # Update only the date fields in options
163
+ options.update(
164
+ {"shipping_date": shipping_date, "shipment_date": shipment_date}
165
+ )
166
+
167
+ # Update the data with merged options
168
+ kwargs["data"]["options"] = options
169
+
170
+ super().__init__(instance, **kwargs)
171
+
172
+
173
+ class PresetSerializer(serializers.Serializer):
174
+ def validate(self, data):
175
+ import karrio.server.core.dataunits as dataunits
176
+
177
+ dimensions_required_together(data)
178
+
179
+ if data is not None and "package_preset" in data:
180
+ package_presets = dataunits.REFERENCE_MODELS.get("package_presets", {})
181
+ preset_name = data["package_preset"]
182
+
183
+ # Find the preset across all carriers
184
+ preset = lib.identity(
185
+ next(
186
+ (
187
+ presets[preset_name]
188
+ for carrier_id, presets in package_presets.items()
189
+ if preset_name in presets
190
+ ),
191
+ None,
192
+ )
193
+ or {}
194
+ )
195
+
196
+ data.update(
197
+ {
198
+ **data,
199
+ "width": data.get("width") or preset.get("width"),
200
+ "length": data.get("length") or preset.get("length"),
201
+ "height": data.get("height") or preset.get("height"),
202
+ "dimension_unit": data.get("dimension_unit")
203
+ or preset.get("dimension_unit"),
204
+ }
205
+ )
206
+
207
+ return data
208
+
209
+
210
+ class AugmentedAddressSerializer(serializers.Serializer):
211
+ def validate(self, data):
212
+ # Format and validate Postal Code
213
+ if all(data.get(key) is not None for key in ["country_code", "postal_code"]):
214
+ postal_code = data["postal_code"]
215
+ country_code = data["country_code"]
216
+
217
+ if country_code == units.Country.CA.name:
218
+ formatted = "".join(
219
+ [c for c in postal_code.split() if c not in ["-", "_"]]
220
+ ).upper()
221
+ if not re.match(r"^([A-Za-z]\d[A-Za-z][-]?\d[A-Za-z]\d)", formatted):
222
+ raise serializers.ValidationError(
223
+ {"postal_code": "The Canadian postal code must match Z9Z9Z9"}
224
+ )
225
+
226
+ elif country_code == units.Country.US.name:
227
+ formatted = "".join(postal_code.split())
228
+ if not re.match(r"^\d{5}(-\d{4})?$", formatted):
229
+ raise serializers.ValidationError(
230
+ {
231
+ "postal_code": "The American postal code must match 12345 or 12345-6789"
232
+ }
233
+ )
234
+
235
+ else:
236
+ formatted = postal_code
237
+
238
+ data.update({**data, "postal_code": formatted})
239
+
240
+ # Format and validate Phone Number
241
+ if all(
242
+ data.get(key) is not None and data.get(key) != ""
243
+ for key in ["country_code", "phone_number"]
244
+ ):
245
+ phone_number = data["phone_number"]
246
+ country_code = data["country_code"]
247
+
248
+ try:
249
+ formatted = phonenumbers.parse(phone_number, country_code)
250
+ data.update(
251
+ {
252
+ **data,
253
+ "phone_number": phonenumbers.format_number(
254
+ formatted, phonenumbers.PhoneNumberFormat.INTERNATIONAL
255
+ ),
256
+ }
257
+ )
258
+ except Exception as e:
259
+ logger.warning("Invalid phone number format", error=str(e))
260
+ raise serializers.ValidationError(
261
+ {"phone_number": "Invalid phone number format"}
262
+ )
263
+
264
+ return data
@@ -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,75 @@
1
+ from django.views.decorators.csrf import csrf_exempt
2
+ from django.utils.decorators import method_decorator
3
+ from oauth2_provider.views import TokenView as BaseTokenView
4
+ from karrio.server.core.logging import logger
5
+
6
+
7
+ @method_decorator(csrf_exempt, name='dispatch')
8
+ class CustomTokenView(BaseTokenView):
9
+ """
10
+ Custom OAuth2 token view that handles grant type format conversion.
11
+
12
+ django-oauth-toolkit stores grant types with hyphens (e.g., 'authorization-code')
13
+ but OAuth2 spec uses underscores (e.g., 'authorization_code').
14
+ This view converts the format before processing.
15
+ """
16
+
17
+ def post(self, request, *args, **kwargs):
18
+ """
19
+ Handle token requests with grant type conversion.
20
+ """
21
+ logger.debug("CustomTokenView called")
22
+ logger.debug("Processing token request",
23
+ method=request.method,
24
+ content_type=request.content_type,
25
+ post_data=dict(request.POST),
26
+ body=request.body.decode('utf-8') if request.body else 'Empty')
27
+
28
+ # Parse the request body if POST data is empty
29
+ if not request.POST and request.body:
30
+ from django.http import QueryDict
31
+ import urllib.parse
32
+
33
+ # Parse the form data from the request body
34
+ body_data = urllib.parse.parse_qs(request.body.decode('utf-8'))
35
+ # Convert to single values (parse_qs returns lists)
36
+ parsed_data = {k: v[0] if v else '' for k, v in body_data.items()}
37
+
38
+ # Create a QueryDict from the parsed data
39
+ post_data = QueryDict('', mutable=True)
40
+ for key, value in parsed_data.items():
41
+ post_data[key] = value
42
+
43
+ # Replace the request POST data
44
+ request.POST = post_data
45
+ request._post = post_data
46
+
47
+ # Convert OAuth2 spec grant types to django-oauth-toolkit format
48
+ grant_type_mapping = {
49
+ 'authorization_code': 'authorization-code',
50
+ 'client_credentials': 'client-credentials',
51
+ 'refresh_token': 'refresh-token',
52
+ 'password': 'password',
53
+ }
54
+
55
+ original_grant_type = request.POST.get('grant_type')
56
+ logger.debug("Grant type parsed", grant_type=original_grant_type)
57
+
58
+ if original_grant_type in grant_type_mapping:
59
+ # Create a mutable copy of the POST data
60
+ post_data = request.POST.copy()
61
+ converted_grant_type = grant_type_mapping[original_grant_type]
62
+ logger.debug("Converting grant type",
63
+ original=original_grant_type,
64
+ converted=converted_grant_type)
65
+ post_data['grant_type'] = converted_grant_type
66
+
67
+ # Replace the request POST data
68
+ request.POST = post_data
69
+ request._post = post_data
70
+ logger.debug("Grant type updated", grant_type=request.POST.get('grant_type'))
71
+ else:
72
+ logger.debug("No grant type conversion needed", grant_type=original_grant_type)
73
+
74
+ # Call the parent token view with converted grant type
75
+ return super().post(request, *args, **kwargs)
@@ -0,0 +1,82 @@
1
+ import yaml # type: ignore
2
+ from rest_framework import status
3
+ from rest_framework.decorators import api_view, renderer_classes, permission_classes
4
+ from rest_framework.permissions import AllowAny
5
+ from rest_framework.response import Response
6
+ from rest_framework.request import Request
7
+ from rest_framework.renderers import JSONRenderer
8
+ from django.urls import path
9
+ from django.conf import settings
10
+
11
+ from karrio.server.conf import FEATURE_FLAGS
12
+ from karrio.server.core.router import router
13
+ import karrio.server.core.dataunits as dataunits
14
+ import karrio.server.openapi as openapi
15
+
16
+ ENDPOINT_ID = "&&" # This endpoint id is used to make operation ids unique make sure not to duplicate
17
+ BASE_PATH = getattr(settings, "BASE_PATH", "")
18
+ References = openapi.OpenApiResponse(
19
+ openapi.OpenApiTypes.OBJECT,
20
+ examples=[
21
+ openapi.OpenApiExample(
22
+ name="References",
23
+ value={
24
+ "VERSION": "",
25
+ "APP_NAME": "",
26
+ "APP_WEBSITE": "",
27
+ "HOST": "",
28
+ "ADMIN": "",
29
+ "OPENAPI": "",
30
+ "GRAPHQL": "",
31
+ **{flag: True for flag in FEATURE_FLAGS},
32
+ "ADDRESS_AUTO_COMPLETE": {},
33
+ "countries": {},
34
+ "currencies": {},
35
+ "carriers": {},
36
+ "customs_content_type": {},
37
+ "incoterms": {},
38
+ "states": {},
39
+ "services": {},
40
+ "connection_configs": {},
41
+ "service_names": {},
42
+ "options": {},
43
+ "option_names": {},
44
+ "package_presets": {},
45
+ "packaging_types": {},
46
+ "payment_types": {},
47
+ "carrier_capabilities": {},
48
+ "service_levels": {},
49
+ "integration_status": {},
50
+ },
51
+ )
52
+ ],
53
+ )
54
+
55
+
56
+ @openapi.extend_schema(
57
+ auth=[],
58
+ methods=["get"],
59
+ tags=["API"],
60
+ operation_id=f"{ENDPOINT_ID}data",
61
+ summary="Data References",
62
+ responses={200: References},
63
+ )
64
+ @api_view(["GET"])
65
+ @permission_classes([AllowAny])
66
+ @renderer_classes([JSONRenderer])
67
+ def references(request: Request):
68
+ try:
69
+ reduced = bool(yaml.safe_load(request.query_params.get("reduced", "true")))
70
+
71
+ return Response(
72
+ dataunits.contextual_reference(reduced=reduced),
73
+ status=status.HTTP_200_OK,
74
+ )
75
+ except Exception as e:
76
+ from karrio.server.core.logging import logger
77
+
78
+ logger.exception("Failed to retrieve references", error=str(e))
79
+ raise e
80
+
81
+
82
+ router.urls.append(path("references", references))