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,404 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import typing
|
|
3
|
+
from rest_framework.response import Response
|
|
4
|
+
from rest_framework import status, exceptions
|
|
5
|
+
from rest_framework.views import exception_handler
|
|
6
|
+
from django.core.exceptions import ObjectDoesNotExist
|
|
7
|
+
from django.utils.translation import gettext_lazy as _
|
|
8
|
+
from karrio.server.core.logging import logger
|
|
9
|
+
|
|
10
|
+
import karrio.lib as lib
|
|
11
|
+
import karrio.core.errors as sdk
|
|
12
|
+
from karrio.server.core.datatypes import Error, Message
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ValidationError(exceptions.ValidationError, sdk.ValidationError):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class APIException(exceptions.APIException):
|
|
20
|
+
default_status_code = status.HTTP_400_BAD_REQUEST
|
|
21
|
+
default_detail = _("Invalid input.")
|
|
22
|
+
default_code = "failure"
|
|
23
|
+
|
|
24
|
+
def __init__(self, detail=None, code=None, status_code=None):
|
|
25
|
+
if detail is None:
|
|
26
|
+
detail = self.default_detail
|
|
27
|
+
if code is None:
|
|
28
|
+
code = self.default_code
|
|
29
|
+
if status_code is None:
|
|
30
|
+
status_code = self.default_status_code
|
|
31
|
+
|
|
32
|
+
self.status_code = status_code
|
|
33
|
+
self.code = code
|
|
34
|
+
self.detail = detail
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class IndexedAPIException(APIException):
|
|
38
|
+
def __init__(self, index=None, **kwargs):
|
|
39
|
+
super().__init__(**kwargs)
|
|
40
|
+
self.index = index
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class APIExceptions(APIException):
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def custom_exception_handler(exc, context):
|
|
48
|
+
from django.conf import settings
|
|
49
|
+
|
|
50
|
+
# Extract request details and log exception
|
|
51
|
+
request_details = _get_request_details(context)
|
|
52
|
+
_log_exception(exc, request_details, debug=getattr(settings, "DEBUG", False))
|
|
53
|
+
|
|
54
|
+
# Capture exception to telemetry (Sentry/OTEL/Datadog)
|
|
55
|
+
# This ensures handled exceptions are still tracked in APM
|
|
56
|
+
_capture_exception_to_telemetry(exc, request_details, context)
|
|
57
|
+
|
|
58
|
+
response = exception_handler(exc, context)
|
|
59
|
+
detail = getattr(exc, "detail", None)
|
|
60
|
+
messages = message_handler(exc)
|
|
61
|
+
status_code = getattr(exc, "status_code", status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
62
|
+
code = get_code(exc)
|
|
63
|
+
|
|
64
|
+
if isinstance(exc, exceptions.ValidationError) or isinstance(
|
|
65
|
+
exc, sdk.ValidationError
|
|
66
|
+
):
|
|
67
|
+
formatted_errors = _format_validation_errors(detail) if detail else None
|
|
68
|
+
return Response(
|
|
69
|
+
messages
|
|
70
|
+
or dict(
|
|
71
|
+
errors=lib.to_dict(
|
|
72
|
+
formatted_errors
|
|
73
|
+
or [
|
|
74
|
+
Error(
|
|
75
|
+
code=code or "validation",
|
|
76
|
+
message=detail if isinstance(detail, str) else None,
|
|
77
|
+
details=(detail if not isinstance(detail, str) else None),
|
|
78
|
+
)
|
|
79
|
+
]
|
|
80
|
+
)
|
|
81
|
+
),
|
|
82
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
83
|
+
headers=getattr(response, "headers", None),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if isinstance(exc, ObjectDoesNotExist):
|
|
87
|
+
resource_name = _get_resource_name(exc)
|
|
88
|
+
message = f"{resource_name} not found" if resource_name else (
|
|
89
|
+
detail if isinstance(detail, str) else "Resource not found"
|
|
90
|
+
)
|
|
91
|
+
return Response(
|
|
92
|
+
dict(
|
|
93
|
+
errors=lib.to_dict(
|
|
94
|
+
[
|
|
95
|
+
Error(
|
|
96
|
+
code=code or "not_found",
|
|
97
|
+
message=message,
|
|
98
|
+
details=(detail if not isinstance(detail, str) else None),
|
|
99
|
+
)
|
|
100
|
+
]
|
|
101
|
+
)
|
|
102
|
+
),
|
|
103
|
+
status=status.HTTP_404_NOT_FOUND,
|
|
104
|
+
headers=getattr(response, "headers", None),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if isinstance(exc, APIExceptions):
|
|
108
|
+
errors = error_handler(exc)
|
|
109
|
+
if errors is not None:
|
|
110
|
+
return Response(
|
|
111
|
+
lib.to_dict(errors),
|
|
112
|
+
status=status_code,
|
|
113
|
+
headers=getattr(response, "headers", None),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if isinstance(exc, APIException) or isinstance(exc, exceptions.APIException):
|
|
117
|
+
return Response(
|
|
118
|
+
messages
|
|
119
|
+
or dict(
|
|
120
|
+
errors=lib.to_dict(
|
|
121
|
+
[
|
|
122
|
+
Error(
|
|
123
|
+
code=code,
|
|
124
|
+
message=detail if isinstance(detail, str) else None,
|
|
125
|
+
details=(detail if not isinstance(detail, str) else None),
|
|
126
|
+
)
|
|
127
|
+
]
|
|
128
|
+
)
|
|
129
|
+
),
|
|
130
|
+
status=status_code,
|
|
131
|
+
headers=getattr(response, "headers", None),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
elif isinstance(exc, Exception):
|
|
135
|
+
message, *_ = list(exc.args)
|
|
136
|
+
return Response(
|
|
137
|
+
dict(errors=lib.to_dict([Error(code=code, message=message)])),
|
|
138
|
+
status=status_code,
|
|
139
|
+
headers=getattr(response, "headers", None),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
return response
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def message_handler(exc) -> typing.Optional[dict]:
|
|
146
|
+
if (
|
|
147
|
+
hasattr(exc, "detail")
|
|
148
|
+
and isinstance(exc.detail, list)
|
|
149
|
+
and len(exc.detail) > 0
|
|
150
|
+
and isinstance(exc.detail[0], Message)
|
|
151
|
+
):
|
|
152
|
+
return dict(
|
|
153
|
+
messages=lib.to_dict(
|
|
154
|
+
[
|
|
155
|
+
dict(
|
|
156
|
+
code=msg.code,
|
|
157
|
+
message=msg.message,
|
|
158
|
+
details=msg.details,
|
|
159
|
+
carrier_id=msg.carrier_id,
|
|
160
|
+
carrier_name=msg.carrier_name,
|
|
161
|
+
)
|
|
162
|
+
for msg in typing.cast(typing.List[Message], exc.detail)
|
|
163
|
+
]
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def error_handler(exc) -> typing.Optional[dict]:
|
|
171
|
+
if (
|
|
172
|
+
hasattr(exc, "detail")
|
|
173
|
+
and isinstance(exc.detail, list)
|
|
174
|
+
and len(exc.detail) > 0
|
|
175
|
+
and isinstance(exc.detail[0], Exception)
|
|
176
|
+
):
|
|
177
|
+
errors: typing.List[dict] = []
|
|
178
|
+
|
|
179
|
+
for error in exc.detail:
|
|
180
|
+
message, *_ = list(exc.args)
|
|
181
|
+
detail = getattr(error, "detail", None)
|
|
182
|
+
index = getattr(error, "index", None)
|
|
183
|
+
code = get_code(error) or "error"
|
|
184
|
+
errors.append(
|
|
185
|
+
dict(
|
|
186
|
+
index=index,
|
|
187
|
+
code=code,
|
|
188
|
+
message=(
|
|
189
|
+
(detail if isinstance(detail, str) else None)
|
|
190
|
+
if detail
|
|
191
|
+
else message
|
|
192
|
+
),
|
|
193
|
+
details=(detail if not isinstance(detail, str) else None),
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
return dict(errors=errors)
|
|
198
|
+
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def get_code(exc):
|
|
203
|
+
from karrio.server.core.utils import failsafe
|
|
204
|
+
|
|
205
|
+
if hasattr(exc, "get_codes"):
|
|
206
|
+
return (
|
|
207
|
+
failsafe(lambda: exc.get_codes())
|
|
208
|
+
or getattr(exc, "code", None)
|
|
209
|
+
or getattr(exc, "default_code", None)
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
return getattr(exc, "default_code", None)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _get_request_details(context: dict) -> dict:
|
|
216
|
+
"""Extract request details from context for logging."""
|
|
217
|
+
request = context.get("view", None) and context.get("view").request
|
|
218
|
+
|
|
219
|
+
if not request:
|
|
220
|
+
return {}
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
"method": getattr(request, "method", None),
|
|
224
|
+
"path": getattr(request, "path", None),
|
|
225
|
+
"user": str(getattr(request, "user", None)),
|
|
226
|
+
"user_id": getattr(getattr(request, "user", None), "id", None),
|
|
227
|
+
"query_params": dict(getattr(request, "GET", {})),
|
|
228
|
+
"content_type": getattr(request, "content_type", None),
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _capture_exception_to_telemetry(exc: Exception, request_details: dict, context: dict):
|
|
233
|
+
"""Capture exception to APM telemetry (Sentry/OTEL/Datadog).
|
|
234
|
+
|
|
235
|
+
This ensures that handled exceptions (which return proper HTTP responses)
|
|
236
|
+
are still tracked in external APM tools for visibility and alerting.
|
|
237
|
+
"""
|
|
238
|
+
from karrio.server.core.utils import failsafe
|
|
239
|
+
|
|
240
|
+
def _capture():
|
|
241
|
+
from karrio.server.core.telemetry import get_telemetry_for_request
|
|
242
|
+
|
|
243
|
+
telemetry = get_telemetry_for_request()
|
|
244
|
+
status_code = getattr(exc, "status_code", 500)
|
|
245
|
+
|
|
246
|
+
# Build context for the exception
|
|
247
|
+
exc_context = {
|
|
248
|
+
"exception_type": type(exc).__name__,
|
|
249
|
+
"status_code": status_code,
|
|
250
|
+
**{k: str(v) if isinstance(v, (dict, list)) else v for k, v in request_details.items()},
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
# Add carrier info if available in exception detail
|
|
254
|
+
detail = getattr(exc, "detail", None)
|
|
255
|
+
if isinstance(detail, list) and len(detail) > 0:
|
|
256
|
+
first = detail[0]
|
|
257
|
+
if hasattr(first, "carrier_name"):
|
|
258
|
+
exc_context["carrier_name"] = first.carrier_name
|
|
259
|
+
if hasattr(first, "carrier_id"):
|
|
260
|
+
exc_context["carrier_id"] = first.carrier_id
|
|
261
|
+
|
|
262
|
+
# Build tags
|
|
263
|
+
tags = {
|
|
264
|
+
"error_type": type(exc).__name__,
|
|
265
|
+
"status_code": str(status_code),
|
|
266
|
+
"error_class": "client" if status_code < 500 else "server",
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
# Capture to telemetry
|
|
270
|
+
telemetry.capture_exception(exc, context=exc_context, tags=tags)
|
|
271
|
+
|
|
272
|
+
# Record error metric
|
|
273
|
+
telemetry.record_metric(
|
|
274
|
+
"karrio.api.exception",
|
|
275
|
+
1,
|
|
276
|
+
tags={
|
|
277
|
+
"exception_type": type(exc).__name__,
|
|
278
|
+
"status_code": str(status_code),
|
|
279
|
+
"path": request_details.get("path", "unknown"),
|
|
280
|
+
},
|
|
281
|
+
metric_type="counter",
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
failsafe(_capture)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _log_exception(exc: Exception, request_details: dict, debug: bool = False):
|
|
288
|
+
"""Log exception with appropriate detail level based on environment."""
|
|
289
|
+
exc_type = type(exc).__name__
|
|
290
|
+
exc_message = str(exc)
|
|
291
|
+
|
|
292
|
+
# Build context dict - convert dicts to strings to avoid format string issues
|
|
293
|
+
context = {
|
|
294
|
+
"exception_type": exc_type,
|
|
295
|
+
"exception_message": exc_message,
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
# Add request details, flattening nested structures
|
|
299
|
+
for key, value in request_details.items():
|
|
300
|
+
if isinstance(value, (dict, list)):
|
|
301
|
+
# Convert to string to avoid KeyError when loguru formats the message
|
|
302
|
+
context[key] = str(value)
|
|
303
|
+
else:
|
|
304
|
+
context[key] = value
|
|
305
|
+
|
|
306
|
+
if debug:
|
|
307
|
+
# In development, log with full traceback for better debugging
|
|
308
|
+
# Use positional args to avoid format string issues with curly braces in exception messages
|
|
309
|
+
logger.opt(exception=exc).error(
|
|
310
|
+
"Exception in request: {} - {}",
|
|
311
|
+
exc_type,
|
|
312
|
+
exc_message,
|
|
313
|
+
**context,
|
|
314
|
+
)
|
|
315
|
+
else:
|
|
316
|
+
# In production, log without full traceback but with context
|
|
317
|
+
logger.error(
|
|
318
|
+
"Exception in request: {}",
|
|
319
|
+
exc_type,
|
|
320
|
+
**context,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _get_resource_name(exc: ObjectDoesNotExist) -> typing.Optional[str]:
|
|
325
|
+
"""Extract resource name from ObjectDoesNotExist exception."""
|
|
326
|
+
exc_class_name = type(exc).__name__
|
|
327
|
+
|
|
328
|
+
# Handle Model.DoesNotExist pattern (e.g., Address.DoesNotExist -> Address)
|
|
329
|
+
if exc_class_name == "DoesNotExist" and hasattr(exc, "args") and exc.args:
|
|
330
|
+
match = re.search(r"(\w+) matching query", str(exc.args[0]))
|
|
331
|
+
if match:
|
|
332
|
+
return match.group(1)
|
|
333
|
+
|
|
334
|
+
# Handle ObjectDoesNotExist with model info in class hierarchy
|
|
335
|
+
for cls in type(exc).__mro__:
|
|
336
|
+
if cls.__name__ not in ("DoesNotExist", "ObjectDoesNotExist", "Exception", "BaseException", "object"):
|
|
337
|
+
return cls.__name__
|
|
338
|
+
|
|
339
|
+
return None
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _format_validation_errors(
|
|
343
|
+
detail: typing.Any,
|
|
344
|
+
prefix: str = "",
|
|
345
|
+
) -> typing.Optional[typing.List[Error]]:
|
|
346
|
+
"""Format validation errors with items[index].field pattern for list errors."""
|
|
347
|
+
if detail is None:
|
|
348
|
+
return None
|
|
349
|
+
|
|
350
|
+
if isinstance(detail, str):
|
|
351
|
+
return [Error(code="validation", message=detail)]
|
|
352
|
+
|
|
353
|
+
def _build_path(base: str, key: str) -> str:
|
|
354
|
+
return f"{base}.{key}" if base else key
|
|
355
|
+
|
|
356
|
+
def _build_index_path(base: str, index: int, field: str = None) -> str:
|
|
357
|
+
index_part = f"{base}[{index}]" if base else f"items[{index}]"
|
|
358
|
+
return f"{index_part}.{field}" if field else index_part
|
|
359
|
+
|
|
360
|
+
def _flatten_errors(data: typing.Any, path: str = "") -> typing.List[Error]:
|
|
361
|
+
if data is None:
|
|
362
|
+
return []
|
|
363
|
+
|
|
364
|
+
if isinstance(data, str):
|
|
365
|
+
message = f"{path}: {data}" if path else data
|
|
366
|
+
return [Error(code="validation", message=message)]
|
|
367
|
+
|
|
368
|
+
if isinstance(data, dict):
|
|
369
|
+
return [
|
|
370
|
+
err
|
|
371
|
+
for key, value in data.items()
|
|
372
|
+
for err in _flatten_errors(value, _build_path(path, key))
|
|
373
|
+
]
|
|
374
|
+
|
|
375
|
+
if isinstance(data, list):
|
|
376
|
+
has_indexed_items = any(isinstance(item, dict) for item in data)
|
|
377
|
+
if has_indexed_items:
|
|
378
|
+
return [
|
|
379
|
+
err
|
|
380
|
+
for index, item in enumerate(data)
|
|
381
|
+
if item # skip empty dicts for list rows without errors
|
|
382
|
+
for err in (
|
|
383
|
+
[
|
|
384
|
+
nested_err
|
|
385
|
+
for field, field_errors in item.items()
|
|
386
|
+
for nested_err in _flatten_errors(
|
|
387
|
+
field_errors, _build_index_path(path, index, field)
|
|
388
|
+
)
|
|
389
|
+
]
|
|
390
|
+
if isinstance(item, dict)
|
|
391
|
+
else _flatten_errors(item, _build_index_path(path, index))
|
|
392
|
+
)
|
|
393
|
+
]
|
|
394
|
+
return [
|
|
395
|
+
Error(code="validation", message=f"{path}: {item}" if path else str(item))
|
|
396
|
+
for item in data
|
|
397
|
+
if item
|
|
398
|
+
]
|
|
399
|
+
|
|
400
|
+
message = f"{path}: {data}" if path else str(data)
|
|
401
|
+
return [Error(code="validation", message=message)]
|
|
402
|
+
|
|
403
|
+
errors = _flatten_errors(detail, prefix)
|
|
404
|
+
return errors if errors else None
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from django import forms
|
|
2
|
+
from django.db import models
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class MultiChoiceField(models.JSONField):
|
|
6
|
+
def formfield(self, **kwargs):
|
|
7
|
+
defaults = {"choices_form_class": forms.TypedMultipleChoiceField}
|
|
8
|
+
defaults.update(kwargs)
|
|
9
|
+
return super().formfield(**defaults)
|
|
10
|
+
|
|
11
|
+
def validate(self, value, model_instance):
|
|
12
|
+
pass
|