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.
- karrio/server/conf.py +54 -0
- karrio/server/core/__init__.py +3 -0
- karrio/server/core/admin.py +1 -0
- karrio/server/core/apps.py +10 -0
- karrio/server/core/authentication.py +347 -0
- karrio/server/core/config.py +31 -0
- karrio/server/core/context_processors.py +12 -0
- karrio/server/core/datatypes.py +394 -0
- karrio/server/core/dataunits.py +187 -0
- karrio/server/core/exceptions.py +404 -0
- karrio/server/core/fields.py +12 -0
- karrio/server/core/filters.py +837 -0
- karrio/server/core/gateway.py +1011 -0
- karrio/server/core/logging.py +403 -0
- karrio/server/core/management/commands/cli.py +19 -0
- karrio/server/core/management/commands/create_oauth_client.py +41 -0
- karrio/server/core/management/commands/runserver.py +5 -0
- karrio/server/core/middleware.py +197 -0
- karrio/server/core/migrations/0001_initial.py +28 -0
- karrio/server/core/migrations/0002_apilogindex.py +69 -0
- karrio/server/core/migrations/0003_apilogindex_test_mode.py +62 -0
- karrio/server/core/migrations/0004_metafield.py +74 -0
- karrio/server/core/migrations/0005_alter_metafield_type_alter_metafield_value.py +23 -0
- karrio/server/core/migrations/0006_add_api_log_requested_at_index.py +22 -0
- karrio/server/core/migrations/__init__.py +0 -0
- karrio/server/core/models/__init__.py +48 -0
- karrio/server/core/models/base.py +103 -0
- karrio/server/core/models/entity.py +24 -0
- karrio/server/core/models/metafield.py +144 -0
- karrio/server/core/models/third_party.py +21 -0
- karrio/server/core/oauth_validators.py +170 -0
- karrio/server/core/permissions.py +36 -0
- karrio/server/core/renderers.py +11 -0
- karrio/server/core/router.py +3 -0
- karrio/server/core/serializers.py +1971 -0
- karrio/server/core/signals.py +55 -0
- karrio/server/core/telemetry.py +573 -0
- karrio/server/core/tests.py +99 -0
- karrio/server/core/tests_resource_token.py +411 -0
- karrio/server/core/urls.py +12 -0
- karrio/server/core/utils.py +1025 -0
- karrio/server/core/validators.py +264 -0
- karrio/server/core/views/__init__.py +2 -0
- karrio/server/core/views/api.py +133 -0
- karrio/server/core/views/metadata.py +44 -0
- karrio/server/core/views/oauth.py +75 -0
- karrio/server/core/views/references.py +82 -0
- karrio/server/core/views/schema.py +310 -0
- karrio/server/filters/__init__.py +2 -0
- karrio/server/filters/abstract.py +26 -0
- karrio/server/iam/__init__.py +0 -0
- karrio/server/iam/admin.py +3 -0
- karrio/server/iam/apps.py +21 -0
- karrio/server/iam/migrations/0001_initial.py +33 -0
- karrio/server/iam/migrations/__init__.py +0 -0
- karrio/server/iam/models.py +48 -0
- karrio/server/iam/permissions.py +155 -0
- karrio/server/iam/serializers.py +54 -0
- karrio/server/iam/signals.py +18 -0
- karrio/server/iam/tests.py +3 -0
- karrio/server/iam/views.py +3 -0
- karrio/server/openapi.py +75 -0
- karrio/server/providers/__init__.py +1 -0
- karrio/server/providers/admin.py +364 -0
- karrio/server/providers/apps.py +10 -0
- karrio/server/providers/management/commands/migrate_rate_sheets.py +101 -0
- karrio/server/providers/migrations/0001_initial.py +140 -0
- karrio/server/providers/migrations/0002_carrier_active.py +18 -0
- karrio/server/providers/migrations/0003_auto_20201230_0820.py +24 -0
- karrio/server/providers/migrations/0004_auto_20210212_0554.py +178 -0
- karrio/server/providers/migrations/0005_auto_20210212_0555.py +18 -0
- karrio/server/providers/migrations/0006_australiapostsettings.py +29 -0
- karrio/server/providers/migrations/0007_auto_20210213_0206.py +21 -0
- karrio/server/providers/migrations/0008_auto_20210214_0409.py +30 -0
- karrio/server/providers/migrations/0009_auto_20210308_0302.py +18 -0
- karrio/server/providers/migrations/0010_auto_20210409_0852.py +32 -0
- karrio/server/providers/migrations/0011_auto_20210409_0853.py +21 -0
- karrio/server/providers/migrations/0012_alter_carrier_options.py +17 -0
- karrio/server/providers/migrations/0013_tntsettings.py +30 -0
- karrio/server/providers/migrations/0014_auto_20210612_1608.py +46 -0
- karrio/server/providers/migrations/0015_auto_20210615_1601.py +28 -0
- karrio/server/providers/migrations/0016_alter_purolatorsettings_user_token.py +18 -0
- karrio/server/providers/migrations/0017_auto_20210805_0359.py +1293 -0
- karrio/server/providers/migrations/0018_alter_fedexsettings_user_key.py +18 -0
- karrio/server/providers/migrations/0019_dhlpolandsettings_servicelevel.py +65 -0
- karrio/server/providers/migrations/0020_genericsettings_labeltemplate.py +52 -0
- karrio/server/providers/migrations/0021_auto_20211231_2353.py +40 -0
- karrio/server/providers/migrations/0022_carrier_metadata.py +18 -0
- karrio/server/providers/migrations/0023_auto_20220124_1916.py +27 -0
- karrio/server/providers/migrations/0024_alter_genericsettings_custom_carrier_name.py +19 -0
- karrio/server/providers/migrations/0025_alter_servicelevel_service_code.py +19 -0
- karrio/server/providers/migrations/0026_auto_20220208_0132.py +59 -0
- karrio/server/providers/migrations/0027_auto_20220304_1340.py +29 -0
- karrio/server/providers/migrations/0028_auto_20220323_1500.py +33 -0
- karrio/server/providers/migrations/0029_easypostsettings.py +27 -0
- karrio/server/providers/migrations/0030_amazonmwssettings.py +29 -0
- karrio/server/providers/migrations/0031_delete_amazonmwssettings.py +18 -0
- karrio/server/providers/migrations/0032_alter_carrier_test.py +18 -0
- karrio/server/providers/migrations/0033_auto_20220708_1350.py +22 -0
- karrio/server/providers/migrations/0034_amazonmwssettings_dpdhlsettings.py +47 -0
- karrio/server/providers/migrations/0035_alter_carrier_capabilities.py +43 -0
- karrio/server/providers/migrations/0036_upsfreightsettings.py +31 -0
- karrio/server/providers/migrations/0037_chronopostsettings.py +29 -0
- karrio/server/providers/migrations/0038_alter_genericsettings_label_template.py +19 -0
- karrio/server/providers/migrations/0039_auto_20220906_0612.py +23 -0
- karrio/server/providers/migrations/0040_dpdhlsettings_services.py +18 -0
- karrio/server/providers/migrations/0041_auto_20221105_0705.py +38 -0
- karrio/server/providers/migrations/0042_auto_20221215_1642.py +23 -0
- karrio/server/providers/migrations/0043_alter_genericsettings_account_number_and_more.py +39 -0
- karrio/server/providers/migrations/0044_carrier_carrier_capabilities.py +64 -0
- karrio/server/providers/migrations/0045_alter_carrier_active_alter_carrier_carrier_id.py +31 -0
- karrio/server/providers/migrations/0046_remove_dpdhlsettings_signature_and_more.py +41 -0
- karrio/server/providers/migrations/0047_dpdsettings.py +286 -0
- karrio/server/providers/migrations/0048_servicelevel_min_weight_servicelevel_transit_days_and_more.py +64 -0
- karrio/server/providers/migrations/0049_boxknightsettings_geodissettings_lapostesettings_and_more.py +156 -0
- karrio/server/providers/migrations/0050_carrier_is_system_alter_carrier_metadata_and_more.py +106 -0
- karrio/server/providers/migrations/0051_rename_username_upssettings_client_id_and_more.py +31 -0
- karrio/server/providers/migrations/0052_alter_upssettings_account_number_and_more.py +20 -0
- karrio/server/providers/migrations/0053_locate2usettings.py +281 -0
- karrio/server/providers/migrations/0054_zoom2usettings.py +280 -0
- karrio/server/providers/migrations/0055_rename_amazonmwssettings_amazonshippingsettings_and_more.py +44 -0
- karrio/server/providers/migrations/0056_asendiaussettings_geodissettings_code_client_and_more.py +75 -0
- karrio/server/providers/migrations/0057_alter_servicelevel_weight_unit_belgianpostsettings.py +51 -0
- karrio/server/providers/migrations/0058_alliedexpresssettings.py +38 -0
- karrio/server/providers/migrations/0059_ratesheet.py +81 -0
- karrio/server/providers/migrations/0060_belgianpostsettings_rate_sheet_and_more.py +73 -0
- karrio/server/providers/migrations/0061_alliedexpresssettings_service_type.py +17 -0
- karrio/server/providers/migrations/0062_sendlesettings_account_country_code.py +257 -0
- karrio/server/providers/migrations/0063_servicelevel_metadata.py +25 -0
- karrio/server/providers/migrations/0064_alliedexpresslocalsettings.py +43 -0
- karrio/server/providers/migrations/0065_servicelevel_carrier_service_code_and_more.py +66 -0
- karrio/server/providers/migrations/0066_rename_fedexsettings_fedexwssettings_and_more.py +28 -0
- karrio/server/providers/migrations/0067_fedexsettings.py +283 -0
- karrio/server/providers/migrations/0068_fedexsettings_track_api_key_and_more.py +38 -0
- karrio/server/providers/migrations/0069_alter_canadapostsettings_contract_id_and_more.py +23 -0
- karrio/server/providers/migrations/0070_tgesettings_alter_carrier_capabilities.py +65 -0
- karrio/server/providers/migrations/0071_alter_tgesettings_my_toll_token.py +18 -0
- karrio/server/providers/migrations/0072_rename_eshippersettings_eshipperxmlsettings_and_more.py +28 -0
- karrio/server/providers/migrations/0073_delete_eshipperxmlsettings.py +41 -0
- karrio/server/providers/migrations/0074_eshippersettings.py +38 -0
- karrio/server/providers/migrations/0075_haypostsettings.py +40 -0
- karrio/server/providers/migrations/0076_rename_customer_registration_id_uspsinternationalsettings_account_number_and_more.py +125 -0
- karrio/server/providers/migrations/0077_uspswtinternationalsettings_uspswtsettings_and_more.py +165 -0
- karrio/server/providers/migrations/0078_auto_20240813_1552.py +120 -0
- karrio/server/providers/migrations/0079_alter_carrier_options_alter_ratesheet_created_by.py +31 -0
- karrio/server/providers/migrations/0080_alter_aramexsettings_account_country_code_and_more.py +3025 -0
- karrio/server/providers/migrations/0081_remove_alliedexpresssettings_carrier_ptr_and_more.py +338 -0
- karrio/server/providers/migrations/0082_add_zone_identifiers.py +50 -0
- karrio/server/providers/migrations/0083_add_optimized_rate_sheet_structure.py +33 -0
- karrio/server/providers/migrations/0084_alter_servicelevel_currency.py +168 -0
- karrio/server/providers/migrations/0085_populate_dhl_parcel_de_oauth_credentials.py +82 -0
- karrio/server/providers/migrations/0086_rename_dhl_parcel_de_customer_number_to_billing_number.py +71 -0
- karrio/server/providers/migrations/__init__.py +0 -0
- karrio/server/providers/models/__init__.py +16 -0
- karrio/server/providers/models/carrier.py +387 -0
- karrio/server/providers/models/config.py +30 -0
- karrio/server/providers/models/service.py +192 -0
- karrio/server/providers/models/sheet.py +287 -0
- karrio/server/providers/models/template.py +39 -0
- karrio/server/providers/models/utils.py +58 -0
- karrio/server/providers/router.py +3 -0
- karrio/server/providers/serializers/__init__.py +3 -0
- karrio/server/providers/serializers/base.py +538 -0
- karrio/server/providers/signals.py +25 -0
- karrio/server/providers/templates/providers/oauth_callback.html +105 -0
- karrio/server/providers/tests/__init__.py +5 -0
- karrio/server/providers/tests/test_connections.py +895 -0
- karrio/server/providers/urls.py +11 -0
- karrio/server/providers/views/__init__.py +0 -0
- karrio/server/providers/views/carriers.py +267 -0
- karrio/server/providers/views/connections.py +496 -0
- karrio/server/samples.py +352 -0
- karrio/server/serializers/__init__.py +2 -0
- karrio/server/serializers/abstract.py +602 -0
- karrio/server/tracing/__init__.py +0 -0
- karrio/server/tracing/admin.py +63 -0
- karrio/server/tracing/apps.py +8 -0
- karrio/server/tracing/migrations/0001_initial.py +41 -0
- karrio/server/tracing/migrations/0002_auto_20220710_1307.py +22 -0
- karrio/server/tracing/migrations/0003_auto_20221105_0317.py +43 -0
- karrio/server/tracing/migrations/0004_tracingrecord_carrier_account_idx.py +24 -0
- karrio/server/tracing/migrations/0005_optimise_tracingrecord_request_log_idx.py +25 -0
- karrio/server/tracing/migrations/0006_alter_tracingrecord_options_and_more.py +49 -0
- karrio/server/tracing/migrations/0007_tracingrecord_tracing_created_at_idx.py +19 -0
- karrio/server/tracing/migrations/__init__.py +0 -0
- karrio/server/tracing/models.py +82 -0
- karrio/server/tracing/tests.py +3 -0
- karrio/server/tracing/utils.py +109 -0
- karrio/server/user/__init__.py +0 -0
- karrio/server/user/admin.py +96 -0
- karrio/server/user/apps.py +7 -0
- karrio/server/user/forms.py +35 -0
- karrio/server/user/migrations/0001_initial.py +41 -0
- karrio/server/user/migrations/0002_token.py +29 -0
- karrio/server/user/migrations/0003_token_test_mode.py +20 -0
- karrio/server/user/migrations/0004_group.py +26 -0
- karrio/server/user/migrations/0005_token_label.py +21 -0
- karrio/server/user/migrations/0006_workspaceconfig.py +63 -0
- karrio/server/user/migrations/0007_user_metadata.py +25 -0
- karrio/server/user/migrations/__init__.py +0 -0
- karrio/server/user/models.py +218 -0
- karrio/server/user/serializers.py +47 -0
- karrio/server/user/templates/registration/login.html +108 -0
- karrio/server/user/templates/registration/registration_confirm_email.html +10 -0
- karrio/server/user/templates/registration/registration_confirm_email.txt +3 -0
- karrio/server/user/tests.py +3 -0
- karrio/server/user/urls.py +10 -0
- karrio/server/user/utils.py +60 -0
- karrio/server/user/views.py +9 -0
- karrio_server_core-2025.5.dist-info/METADATA +32 -0
- karrio_server_core-2025.5.dist-info/RECORD +213 -0
- karrio_server_core-2025.5.dist-info/WHEEL +5 -0
- 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,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))
|