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,602 @@
|
|
|
1
|
+
import yaml
|
|
2
|
+
import pydoc
|
|
3
|
+
import typing
|
|
4
|
+
from django.db import models
|
|
5
|
+
from django.conf import settings
|
|
6
|
+
from django.db import transaction
|
|
7
|
+
from django.forms.models import model_to_dict
|
|
8
|
+
from drf_spectacular.types import OpenApiTypes
|
|
9
|
+
from rest_framework import serializers, request
|
|
10
|
+
|
|
11
|
+
import karrio.lib as lib
|
|
12
|
+
from karrio.server.core.logging import logger
|
|
13
|
+
|
|
14
|
+
T = typing.TypeVar("T")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Context(typing.NamedTuple):
|
|
18
|
+
user: typing.Any
|
|
19
|
+
org: typing.Any = None
|
|
20
|
+
test_mode: bool = None
|
|
21
|
+
|
|
22
|
+
def __getitem__(self, item):
|
|
23
|
+
return getattr(self, item)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
RequestContext = typing.Union[Context, dict, request.Request]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class DecoratedSerializer:
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
instance: models.Model = None,
|
|
33
|
+
serializer: "Serializer" = None,
|
|
34
|
+
):
|
|
35
|
+
self._instance = instance
|
|
36
|
+
self._serializer = serializer
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def data(self) -> typing.Optional[dict]:
|
|
40
|
+
return self._serializer.validated_data if self._serializer is not None else None
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def instance(self) -> models.Model:
|
|
44
|
+
return self._instance
|
|
45
|
+
|
|
46
|
+
def save(self, **kwargs) -> "DecoratedSerializer":
|
|
47
|
+
if self._serializer is not None:
|
|
48
|
+
self._instance = self._serializer.save(**kwargs)
|
|
49
|
+
|
|
50
|
+
return self
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class AbstractSerializer:
|
|
54
|
+
def create(self, validated_data, **kwargs):
|
|
55
|
+
super().create(validated_data)
|
|
56
|
+
|
|
57
|
+
def update(self, instance, validated_data, **kwargs):
|
|
58
|
+
super().update(instance, validated_data, **kwargs)
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def map(
|
|
62
|
+
cls, instance=None, data: typing.Union[str, dict] = None, **kwargs
|
|
63
|
+
) -> "DecoratedSerializer":
|
|
64
|
+
if data is None and instance is None:
|
|
65
|
+
serializer = None
|
|
66
|
+
else:
|
|
67
|
+
serializer = (
|
|
68
|
+
cls(data=data or {}, **kwargs) # type:ignore
|
|
69
|
+
if instance is None
|
|
70
|
+
else cls(
|
|
71
|
+
instance, data=data or {}, **{**kwargs, "partial": True}
|
|
72
|
+
) # type:ignore
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
serializer.is_valid(raise_exception=True) # type:ignore
|
|
76
|
+
|
|
77
|
+
return DecoratedSerializer(
|
|
78
|
+
instance=instance,
|
|
79
|
+
serializer=serializer, # type:ignore
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class Serializer(serializers.Serializer, AbstractSerializer):
|
|
84
|
+
context: dict = {}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class ModelSerializer(serializers.ModelSerializer, AbstractSerializer):
|
|
88
|
+
def create(self, data: dict, **kwargs): # type: ignore
|
|
89
|
+
return self.Meta.model.objects.create(**data)
|
|
90
|
+
|
|
91
|
+
def update(self, instance, data: dict, **kwargs): # type: ignore
|
|
92
|
+
for name, value in data.items():
|
|
93
|
+
if name != "created_by" and hasattr(instance, name):
|
|
94
|
+
setattr(instance, name, value)
|
|
95
|
+
|
|
96
|
+
instance.save()
|
|
97
|
+
return instance
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class StringListField(serializers.ListField):
|
|
101
|
+
child = serializers.CharField()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class PlainDictField(serializers.DictField):
|
|
105
|
+
class Meta:
|
|
106
|
+
swagger_schema_fields = {
|
|
107
|
+
"type": OpenApiTypes.OBJECT,
|
|
108
|
+
"additional_properties": True,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class FlagField(serializers.BooleanField):
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class FlagsSerializer(serializers.Serializer):
|
|
117
|
+
def __init__(self, *args, **kwargs):
|
|
118
|
+
data = kwargs.get("data", {})
|
|
119
|
+
self.flags = [
|
|
120
|
+
(label, label in data)
|
|
121
|
+
for label, field in self.fields.items()
|
|
122
|
+
if isinstance(field, FlagField)
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
super().__init__(*args, **kwargs)
|
|
126
|
+
|
|
127
|
+
def validate(self, data):
|
|
128
|
+
validated = super().validate(data)
|
|
129
|
+
for flag, specified in self.flags:
|
|
130
|
+
if specified and validated[flag] is None:
|
|
131
|
+
validated.update({flag: True})
|
|
132
|
+
|
|
133
|
+
return validated
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class EntitySerializer(serializers.Serializer):
|
|
137
|
+
id = serializers.CharField(required=False, help_text="A unique identifier")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
"""
|
|
141
|
+
Custom serializer utilities functions
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def PaginatedResult(serializer_name: str, content_serializer: typing.Type[Serializer]):
|
|
146
|
+
return type(
|
|
147
|
+
serializer_name,
|
|
148
|
+
(Serializer,),
|
|
149
|
+
dict(
|
|
150
|
+
count=serializers.IntegerField(required=False, allow_null=True),
|
|
151
|
+
next=serializers.URLField(
|
|
152
|
+
required=False, allow_blank=True, allow_null=True
|
|
153
|
+
),
|
|
154
|
+
previous=serializers.URLField(
|
|
155
|
+
required=False, allow_blank=True, allow_null=True
|
|
156
|
+
),
|
|
157
|
+
results=content_serializer(many=True),
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def owned_model_serializer(
|
|
163
|
+
serializer: typing.Type[typing.Union[Serializer, ModelSerializer]],
|
|
164
|
+
):
|
|
165
|
+
class MetaSerializer(serializer): # type: ignore
|
|
166
|
+
context: dict = {}
|
|
167
|
+
|
|
168
|
+
def __init__(self, *args, **kwargs):
|
|
169
|
+
if "context" in kwargs:
|
|
170
|
+
context = kwargs.get("context") or {}
|
|
171
|
+
user = (
|
|
172
|
+
context.get("user") if isinstance(context, dict) else context.user
|
|
173
|
+
)
|
|
174
|
+
org = context.get("org") if isinstance(context, dict) else context.org
|
|
175
|
+
test_mode = (
|
|
176
|
+
context.get("test_mode")
|
|
177
|
+
if isinstance(context, dict)
|
|
178
|
+
else context.test_mode
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
if settings.MULTI_ORGANIZATIONS and org is None:
|
|
182
|
+
import karrio.server.orgs.models as orgs
|
|
183
|
+
|
|
184
|
+
org = orgs.Organization.objects.filter(
|
|
185
|
+
users__id=getattr(user, "id", None)
|
|
186
|
+
).first()
|
|
187
|
+
|
|
188
|
+
self.__context: Context = Context(user, org, test_mode)
|
|
189
|
+
else:
|
|
190
|
+
self.__context: Context = getattr(self, "__context", None)
|
|
191
|
+
kwargs.update({"context": self.__context})
|
|
192
|
+
|
|
193
|
+
super().__init__(*args, **kwargs)
|
|
194
|
+
|
|
195
|
+
@transaction.atomic
|
|
196
|
+
def create(self, data: dict, **kwargs):
|
|
197
|
+
payload = {"created_by": self.__context.user, **data}
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
instance = super().create(payload, context=self.__context)
|
|
201
|
+
link_org(instance, self.__context) # Link to organization if supported
|
|
202
|
+
except Exception as e:
|
|
203
|
+
# Log exception with full traceback for debugging
|
|
204
|
+
meta = getattr(self.__class__, "Meta", None)
|
|
205
|
+
model_name = getattr(
|
|
206
|
+
getattr(meta, "model", None), "__name__", "Unknown"
|
|
207
|
+
)
|
|
208
|
+
logger.exception(
|
|
209
|
+
f"Failed to create {model_name} instance using {self.__class__.__name__}: {str(e)}"
|
|
210
|
+
)
|
|
211
|
+
raise
|
|
212
|
+
|
|
213
|
+
return instance
|
|
214
|
+
|
|
215
|
+
def update(self, instance, data: dict, **kwargs):
|
|
216
|
+
payload = {k: v for k, v in data.items()}
|
|
217
|
+
|
|
218
|
+
return super().update(instance, payload, context=self.__context)
|
|
219
|
+
|
|
220
|
+
return type(serializer.__name__, (MetaSerializer,), {})
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def link_org(entity: ModelSerializer, context: Context):
|
|
224
|
+
from django.utils.functional import SimpleLazyObject
|
|
225
|
+
|
|
226
|
+
# Evaluate org from context (handles SimpleLazyObject)
|
|
227
|
+
org = (
|
|
228
|
+
context.org if not isinstance(context.org, SimpleLazyObject)
|
|
229
|
+
else (context.org if context.org else None)
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# Check if entity can be linked to org
|
|
233
|
+
entity_org = getattr(entity, "org", None)
|
|
234
|
+
has_org_relation = entity_org is not None and hasattr(entity_org, "exists")
|
|
235
|
+
should_link = org is not None and has_org_relation and not entity_org.exists()
|
|
236
|
+
|
|
237
|
+
if should_link:
|
|
238
|
+
entity.link = entity.__class__.link.related.related_model.objects.create(org=org, item=entity)
|
|
239
|
+
entity.save(update_fields=(["created_at"] if hasattr(entity, "created_at") else []))
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def bulk_link_org(entities: typing.List[models.Model], context: Context):
|
|
243
|
+
if len(entities) == 0 or settings.MULTI_ORGANIZATIONS is False:
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
EntityLinkModel = entities[0].__class__.link.related.related_model
|
|
247
|
+
links = []
|
|
248
|
+
|
|
249
|
+
for entity in entities:
|
|
250
|
+
entity.link = EntityLinkModel(org=context.org, item=entity)
|
|
251
|
+
links.append(entity.link)
|
|
252
|
+
|
|
253
|
+
EntityLinkModel.objects.bulk_create(links)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def get_object_context(entity) -> Context:
|
|
257
|
+
org = lib.failsafe(
|
|
258
|
+
lambda: (
|
|
259
|
+
entity.org.first()
|
|
260
|
+
if (hasattr(entity, "org") and entity.org.exists())
|
|
261
|
+
else None
|
|
262
|
+
)
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
return Context(
|
|
266
|
+
org=org,
|
|
267
|
+
user=getattr(entity, "created_by", None),
|
|
268
|
+
test_mode=getattr(entity, "test_mode", None),
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def save_many_to_many_data(
|
|
273
|
+
name: str,
|
|
274
|
+
serializer: ModelSerializer,
|
|
275
|
+
parent: models.Model,
|
|
276
|
+
payload: dict = None,
|
|
277
|
+
remove_if_missing: bool = False,
|
|
278
|
+
**kwargs,
|
|
279
|
+
):
|
|
280
|
+
if not any((key in payload for key in [name])):
|
|
281
|
+
return None
|
|
282
|
+
|
|
283
|
+
collection_data = payload.get(name)
|
|
284
|
+
collection = getattr(parent, name)
|
|
285
|
+
|
|
286
|
+
if collection_data is None and any(collection.all()):
|
|
287
|
+
for item in collection.all():
|
|
288
|
+
item.delete()
|
|
289
|
+
|
|
290
|
+
if remove_if_missing and collection.exists():
|
|
291
|
+
collection.exclude(id__in=[item.get("id") for item in collection_data]).delete()
|
|
292
|
+
|
|
293
|
+
for data in collection_data:
|
|
294
|
+
item_instance = (
|
|
295
|
+
collection.filter(id=data.pop("id")).first() if "id" in data else None
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
if item_instance is None:
|
|
299
|
+
item = serializer.map(data=data, **kwargs).save().instance
|
|
300
|
+
getattr(parent, name).add(item)
|
|
301
|
+
else:
|
|
302
|
+
item = (
|
|
303
|
+
serializer.map(
|
|
304
|
+
data=data,
|
|
305
|
+
instance=item_instance,
|
|
306
|
+
**{**kwargs, "partial": True},
|
|
307
|
+
)
|
|
308
|
+
.save()
|
|
309
|
+
.instance
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def save_one_to_one_data(
|
|
314
|
+
name: str,
|
|
315
|
+
serializer: ModelSerializer,
|
|
316
|
+
parent: models.Model = None,
|
|
317
|
+
payload: dict = None,
|
|
318
|
+
**kwargs,
|
|
319
|
+
):
|
|
320
|
+
if name not in payload:
|
|
321
|
+
return None
|
|
322
|
+
|
|
323
|
+
data = payload.get(name)
|
|
324
|
+
instance = getattr(parent, name, None)
|
|
325
|
+
|
|
326
|
+
if data is None and instance is not None:
|
|
327
|
+
instance.delete()
|
|
328
|
+
setattr(parent, name, None)
|
|
329
|
+
|
|
330
|
+
if instance is None:
|
|
331
|
+
new_instance = serializer.map(data=data, **kwargs).save().instance
|
|
332
|
+
parent and setattr(parent, name, new_instance) # type: ignore
|
|
333
|
+
return new_instance
|
|
334
|
+
|
|
335
|
+
return (
|
|
336
|
+
serializer.map(instance=instance, data=data, **{**kwargs, "partial": True})
|
|
337
|
+
.save()
|
|
338
|
+
.instance
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def allow_model_id(model_paths: []): # type: ignore
|
|
343
|
+
def _decorator(serializer: typing.Type[Serializer]):
|
|
344
|
+
class ModelIdSerializer(serializer): # type: ignore
|
|
345
|
+
def __init__(self, *args, **kwargs):
|
|
346
|
+
for param, model_path in model_paths:
|
|
347
|
+
content = kwargs.get("data", {}).get(param)
|
|
348
|
+
values = content if isinstance(content, list) else [content]
|
|
349
|
+
model = pydoc.locate(model_path)
|
|
350
|
+
|
|
351
|
+
if any([isinstance(val, dict) and "id" in val for val in values]):
|
|
352
|
+
new_content = []
|
|
353
|
+
for value in values:
|
|
354
|
+
if (
|
|
355
|
+
isinstance(value, dict)
|
|
356
|
+
and ("id" in value)
|
|
357
|
+
and (model is not None)
|
|
358
|
+
):
|
|
359
|
+
data = model_to_dict(model.objects.get(pk=value["id"]))
|
|
360
|
+
|
|
361
|
+
for field, field_data in data.items():
|
|
362
|
+
if isinstance(field_data, list):
|
|
363
|
+
data[field] = [
|
|
364
|
+
(
|
|
365
|
+
model_to_dict(item)
|
|
366
|
+
if hasattr(item, "_meta")
|
|
367
|
+
else item
|
|
368
|
+
)
|
|
369
|
+
for item in field_data
|
|
370
|
+
]
|
|
371
|
+
|
|
372
|
+
if hasattr(field_data, "_meta"):
|
|
373
|
+
data[field] = model_to_dict(field_data)
|
|
374
|
+
|
|
375
|
+
("id" in data) and data.pop("id")
|
|
376
|
+
new_content.append(data)
|
|
377
|
+
|
|
378
|
+
kwargs.update(
|
|
379
|
+
data={
|
|
380
|
+
**kwargs["data"],
|
|
381
|
+
param: (
|
|
382
|
+
new_content
|
|
383
|
+
if isinstance(content, list)
|
|
384
|
+
else next(iter(new_content))
|
|
385
|
+
),
|
|
386
|
+
}
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
super().__init__(*args, **kwargs)
|
|
390
|
+
|
|
391
|
+
return type(serializer.__name__, (ModelIdSerializer,), {})
|
|
392
|
+
|
|
393
|
+
return _decorator
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def make_fields_optional(serializer: typing.Type[ModelSerializer]):
|
|
397
|
+
_name = f"Partial{serializer.__name__}"
|
|
398
|
+
|
|
399
|
+
class _Meta(serializer.Meta): # type: ignore
|
|
400
|
+
extra_kwargs = {
|
|
401
|
+
**getattr(serializer.Meta, "extra_kwargs", {}),
|
|
402
|
+
**{
|
|
403
|
+
field.name: {"required": False}
|
|
404
|
+
for field in serializer.Meta.model._meta.fields
|
|
405
|
+
},
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return type(_name, (serializer,), dict(Meta=_Meta))
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def exclude_id_field(serializer: typing.Type[ModelSerializer]):
|
|
412
|
+
class _Meta(serializer.Meta): # type: ignore
|
|
413
|
+
exclude = [*getattr(serializer.Meta, "exclude", []), "id"]
|
|
414
|
+
|
|
415
|
+
return type(serializer.__name__, (serializer,), dict(Meta=_Meta))
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def is_field_optional(model, field_name: str) -> bool:
|
|
419
|
+
field = getattr(model, field_name)
|
|
420
|
+
|
|
421
|
+
if hasattr(field, "field"):
|
|
422
|
+
return field.field.null
|
|
423
|
+
|
|
424
|
+
return False
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def deep_merge_remove_nulls(base: dict, updates: dict) -> dict:
|
|
428
|
+
"""Deep merge two dictionaries, removing keys with null values from updates.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
base: The base dictionary (existing data)
|
|
432
|
+
updates: The updates dictionary (new data with potential nulls to remove)
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
Merged dictionary with null values removed
|
|
436
|
+
|
|
437
|
+
Examples:
|
|
438
|
+
>>> base = {"a": 1, "b": {"c": 2, "d": 3}}
|
|
439
|
+
>>> updates = {"b": {"c": null, "e": 4}}
|
|
440
|
+
>>> deep_merge_remove_nulls(base, updates)
|
|
441
|
+
{"a": 1, "b": {"d": 3, "e": 4}} # c removed due to null
|
|
442
|
+
"""
|
|
443
|
+
result = base.copy()
|
|
444
|
+
|
|
445
|
+
for key, value in updates.items():
|
|
446
|
+
if value is None:
|
|
447
|
+
# Explicit null means remove the key
|
|
448
|
+
result.pop(key, None)
|
|
449
|
+
elif isinstance(value, dict) and isinstance(result.get(key), dict):
|
|
450
|
+
# Both are dicts: recursively merge
|
|
451
|
+
result[key] = deep_merge_remove_nulls(result[key], value)
|
|
452
|
+
else:
|
|
453
|
+
# Overwrite with new value
|
|
454
|
+
result[key] = value
|
|
455
|
+
|
|
456
|
+
return result
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def process_nested_dictionaries_mutations(
|
|
460
|
+
keys: typing.List[str], payload: dict, entity
|
|
461
|
+
) -> dict:
|
|
462
|
+
"""Process nested dictionary mutations with deep merge and null removal.
|
|
463
|
+
|
|
464
|
+
This function is designed for complex nested JSON fields where you need:
|
|
465
|
+
- Deep merging of nested objects
|
|
466
|
+
- Removal of keys when explicit null is sent
|
|
467
|
+
- Preservation of unaffected nested keys
|
|
468
|
+
|
|
469
|
+
Use this for fields like shipping rule actions/conditions that have nested extensions.
|
|
470
|
+
For simple flat dictionaries, use process_dictionaries_mutations instead.
|
|
471
|
+
|
|
472
|
+
Args:
|
|
473
|
+
keys: List of field names to process
|
|
474
|
+
payload: Input data from mutation
|
|
475
|
+
entity: Existing entity instance
|
|
476
|
+
|
|
477
|
+
Returns:
|
|
478
|
+
Updated payload with deep merged values
|
|
479
|
+
|
|
480
|
+
Examples:
|
|
481
|
+
Existing: {"actions": {"select_service": {"carrier": "ups"}, "extensions": {"old": "data"}}}
|
|
482
|
+
Update: {"actions": {"extensions": {"new": "data"}}}
|
|
483
|
+
Result: {"actions": {"select_service": {"carrier": "ups"}, "extensions": {"old": "data", "new": "data"}}}
|
|
484
|
+
"""
|
|
485
|
+
data = payload.copy()
|
|
486
|
+
|
|
487
|
+
for key in [k for k in keys if k in payload]:
|
|
488
|
+
existing_value = getattr(entity, key, None) or {}
|
|
489
|
+
new_value = payload.get(key)
|
|
490
|
+
|
|
491
|
+
if new_value is None:
|
|
492
|
+
# Explicit null means clear the entire field
|
|
493
|
+
data[key] = {}
|
|
494
|
+
else:
|
|
495
|
+
# Deep merge with null removal
|
|
496
|
+
data[key] = deep_merge_remove_nulls(existing_value, new_value)
|
|
497
|
+
|
|
498
|
+
return data
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def process_dictionaries_mutations(
|
|
502
|
+
keys: typing.List[str], payload: dict, entity
|
|
503
|
+
) -> dict:
|
|
504
|
+
"""This function checks if the payload contains dictionary with the keys and if so, it
|
|
505
|
+
mutate the values content by removing any null values and adding the new one.
|
|
506
|
+
"""
|
|
507
|
+
data = payload.copy()
|
|
508
|
+
|
|
509
|
+
for key in [k for k in keys if k in payload and payload.get(k) is not None]:
|
|
510
|
+
value = lib.to_dict(
|
|
511
|
+
{**(getattr(entity, key, None) or {}), **(payload.get(key, None) or {})}
|
|
512
|
+
)
|
|
513
|
+
data.update({key: value})
|
|
514
|
+
|
|
515
|
+
return data
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def get_query_flag(
|
|
519
|
+
key: str,
|
|
520
|
+
query_params: dict,
|
|
521
|
+
nullable: bool = True,
|
|
522
|
+
) -> typing.Optional[bool]:
|
|
523
|
+
_value = yaml.safe_load(query_params.get(key) or "")
|
|
524
|
+
|
|
525
|
+
if key in query_params and _value is not False:
|
|
526
|
+
return True
|
|
527
|
+
|
|
528
|
+
if nullable:
|
|
529
|
+
return _value
|
|
530
|
+
|
|
531
|
+
return False
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def field_to_serializer(args: dict):
|
|
535
|
+
[type, name, required, default, enum] = [
|
|
536
|
+
args.get("type"),
|
|
537
|
+
args.get("name"),
|
|
538
|
+
args.get("required"),
|
|
539
|
+
args.get("default"),
|
|
540
|
+
args.get("enum"),
|
|
541
|
+
]
|
|
542
|
+
|
|
543
|
+
if enum:
|
|
544
|
+
return serializers.ChoiceField(
|
|
545
|
+
choices=enum,
|
|
546
|
+
required=required,
|
|
547
|
+
help_text=f"Indicates a {name} {type}",
|
|
548
|
+
)
|
|
549
|
+
if type == "string":
|
|
550
|
+
return serializers.CharField(
|
|
551
|
+
required=required,
|
|
552
|
+
**(
|
|
553
|
+
dict(default=default, allow_blank=True, allow_null=True)
|
|
554
|
+
if not required
|
|
555
|
+
else {}
|
|
556
|
+
),
|
|
557
|
+
)
|
|
558
|
+
if type == "integer":
|
|
559
|
+
return serializers.IntegerField(
|
|
560
|
+
required=required,
|
|
561
|
+
**(dict(default=default) if not required else {}),
|
|
562
|
+
)
|
|
563
|
+
if type == "boolean":
|
|
564
|
+
return serializers.BooleanField(
|
|
565
|
+
required=required,
|
|
566
|
+
**(dict(default=default) if not required else {}),
|
|
567
|
+
)
|
|
568
|
+
if type == "float":
|
|
569
|
+
return serializers.FloatField(
|
|
570
|
+
required=required,
|
|
571
|
+
**(dict(default=default) if not required else {}),
|
|
572
|
+
)
|
|
573
|
+
if type == "datetime":
|
|
574
|
+
return serializers.DateTimeField(
|
|
575
|
+
required=required,
|
|
576
|
+
**(dict(default=default) if not required else {}),
|
|
577
|
+
)
|
|
578
|
+
if type == "date":
|
|
579
|
+
return serializers.DateField(
|
|
580
|
+
required=required,
|
|
581
|
+
**(dict(default=default) if not required else {}),
|
|
582
|
+
)
|
|
583
|
+
if type == "decimal":
|
|
584
|
+
return serializers.DecimalField(
|
|
585
|
+
required=required,
|
|
586
|
+
**(dict(default=default) if not required else {}),
|
|
587
|
+
)
|
|
588
|
+
if type == "uuid":
|
|
589
|
+
return serializers.UUIDField(
|
|
590
|
+
required=required,
|
|
591
|
+
**(dict(default=default) if not required else {}),
|
|
592
|
+
)
|
|
593
|
+
if type == "email":
|
|
594
|
+
return serializers.EmailField(
|
|
595
|
+
required=required,
|
|
596
|
+
**(dict(default=default) if not required else {}),
|
|
597
|
+
)
|
|
598
|
+
if type == "url":
|
|
599
|
+
return serializers.URLField(
|
|
600
|
+
required=required,
|
|
601
|
+
**(dict(default=default) if not required else {}),
|
|
602
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
from django.urls import reverse
|
|
3
|
+
from django.contrib import admin
|
|
4
|
+
from django.conf import settings
|
|
5
|
+
from django.utils.safestring import mark_safe
|
|
6
|
+
from django.utils.translation import gettext_lazy as _
|
|
7
|
+
from rest_framework_tracking.admin import APIRequestLog
|
|
8
|
+
|
|
9
|
+
from karrio.server.tracing import models
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TracingRecordAdmin(admin.ModelAdmin):
|
|
13
|
+
list_display = ("id", "log", "key", "test_mode", "request_timestamp", "created_at")
|
|
14
|
+
search_fields = ("meta__request_log_id", "meta__carrier_name")
|
|
15
|
+
list_filter = ("key", "test_mode")
|
|
16
|
+
readonly_fields = [
|
|
17
|
+
f.name
|
|
18
|
+
for f in models.TracingRecord._meta.get_fields()
|
|
19
|
+
if f.name not in ["org", "link"]
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
def get_queryset(self, request):
|
|
23
|
+
if settings.MULTI_ORGANIZATIONS:
|
|
24
|
+
return (
|
|
25
|
+
models.TracingRecord.objects
|
|
26
|
+
.all()
|
|
27
|
+
.filter(link__org__users__id=request.user.id)
|
|
28
|
+
.order_by("-timestamp")
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
return super().get_queryset(request).order_by("-timestamp")
|
|
32
|
+
|
|
33
|
+
def has_add_permission(self, request) -> bool:
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
def log(self, obj):
|
|
37
|
+
log_id = obj.meta.get("request_log_id")
|
|
38
|
+
|
|
39
|
+
if any(str(log_id)):
|
|
40
|
+
return mark_safe(
|
|
41
|
+
'<a href="{}">{}</a>'.format(
|
|
42
|
+
reverse(
|
|
43
|
+
f"admin:{APIRequestLog._meta.app_label}_{APIRequestLog._meta.model_name}_change",
|
|
44
|
+
args=(log_id,),
|
|
45
|
+
),
|
|
46
|
+
log_id,
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
return ""
|
|
51
|
+
|
|
52
|
+
def request_timestamp(self, obj):
|
|
53
|
+
timestamp = datetime.datetime.fromtimestamp(obj.timestamp)
|
|
54
|
+
|
|
55
|
+
if timestamp:
|
|
56
|
+
return timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
|
57
|
+
|
|
58
|
+
return ""
|
|
59
|
+
|
|
60
|
+
request_timestamp.admin_order_field = "timestamp"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
admin.site.register(models.TracingRecord, TracingRecordAdmin)
|