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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. karrio/server/conf.py +54 -0
  2. karrio/server/core/__init__.py +3 -0
  3. karrio/server/core/admin.py +1 -0
  4. karrio/server/core/apps.py +10 -0
  5. karrio/server/core/authentication.py +347 -0
  6. karrio/server/core/config.py +31 -0
  7. karrio/server/core/context_processors.py +12 -0
  8. karrio/server/core/datatypes.py +394 -0
  9. karrio/server/core/dataunits.py +187 -0
  10. karrio/server/core/exceptions.py +404 -0
  11. karrio/server/core/fields.py +12 -0
  12. karrio/server/core/filters.py +837 -0
  13. karrio/server/core/gateway.py +1011 -0
  14. karrio/server/core/logging.py +403 -0
  15. karrio/server/core/management/commands/cli.py +19 -0
  16. karrio/server/core/management/commands/create_oauth_client.py +41 -0
  17. karrio/server/core/management/commands/runserver.py +5 -0
  18. karrio/server/core/middleware.py +197 -0
  19. karrio/server/core/migrations/0001_initial.py +28 -0
  20. karrio/server/core/migrations/0002_apilogindex.py +69 -0
  21. karrio/server/core/migrations/0003_apilogindex_test_mode.py +62 -0
  22. karrio/server/core/migrations/0004_metafield.py +74 -0
  23. karrio/server/core/migrations/0005_alter_metafield_type_alter_metafield_value.py +23 -0
  24. karrio/server/core/migrations/0006_add_api_log_requested_at_index.py +22 -0
  25. karrio/server/core/migrations/__init__.py +0 -0
  26. karrio/server/core/models/__init__.py +48 -0
  27. karrio/server/core/models/base.py +103 -0
  28. karrio/server/core/models/entity.py +24 -0
  29. karrio/server/core/models/metafield.py +144 -0
  30. karrio/server/core/models/third_party.py +21 -0
  31. karrio/server/core/oauth_validators.py +170 -0
  32. karrio/server/core/permissions.py +36 -0
  33. karrio/server/core/renderers.py +11 -0
  34. karrio/server/core/router.py +3 -0
  35. karrio/server/core/serializers.py +1971 -0
  36. karrio/server/core/signals.py +55 -0
  37. karrio/server/core/telemetry.py +573 -0
  38. karrio/server/core/tests.py +99 -0
  39. karrio/server/core/tests_resource_token.py +411 -0
  40. karrio/server/core/urls.py +12 -0
  41. karrio/server/core/utils.py +1025 -0
  42. karrio/server/core/validators.py +264 -0
  43. karrio/server/core/views/__init__.py +2 -0
  44. karrio/server/core/views/api.py +133 -0
  45. karrio/server/core/views/metadata.py +44 -0
  46. karrio/server/core/views/oauth.py +75 -0
  47. karrio/server/core/views/references.py +82 -0
  48. karrio/server/core/views/schema.py +310 -0
  49. karrio/server/filters/__init__.py +2 -0
  50. karrio/server/filters/abstract.py +26 -0
  51. karrio/server/iam/__init__.py +0 -0
  52. karrio/server/iam/admin.py +3 -0
  53. karrio/server/iam/apps.py +21 -0
  54. karrio/server/iam/migrations/0001_initial.py +33 -0
  55. karrio/server/iam/migrations/__init__.py +0 -0
  56. karrio/server/iam/models.py +48 -0
  57. karrio/server/iam/permissions.py +155 -0
  58. karrio/server/iam/serializers.py +54 -0
  59. karrio/server/iam/signals.py +18 -0
  60. karrio/server/iam/tests.py +3 -0
  61. karrio/server/iam/views.py +3 -0
  62. karrio/server/openapi.py +75 -0
  63. karrio/server/providers/__init__.py +1 -0
  64. karrio/server/providers/admin.py +364 -0
  65. karrio/server/providers/apps.py +10 -0
  66. karrio/server/providers/management/commands/migrate_rate_sheets.py +101 -0
  67. karrio/server/providers/migrations/0001_initial.py +140 -0
  68. karrio/server/providers/migrations/0002_carrier_active.py +18 -0
  69. karrio/server/providers/migrations/0003_auto_20201230_0820.py +24 -0
  70. karrio/server/providers/migrations/0004_auto_20210212_0554.py +178 -0
  71. karrio/server/providers/migrations/0005_auto_20210212_0555.py +18 -0
  72. karrio/server/providers/migrations/0006_australiapostsettings.py +29 -0
  73. karrio/server/providers/migrations/0007_auto_20210213_0206.py +21 -0
  74. karrio/server/providers/migrations/0008_auto_20210214_0409.py +30 -0
  75. karrio/server/providers/migrations/0009_auto_20210308_0302.py +18 -0
  76. karrio/server/providers/migrations/0010_auto_20210409_0852.py +32 -0
  77. karrio/server/providers/migrations/0011_auto_20210409_0853.py +21 -0
  78. karrio/server/providers/migrations/0012_alter_carrier_options.py +17 -0
  79. karrio/server/providers/migrations/0013_tntsettings.py +30 -0
  80. karrio/server/providers/migrations/0014_auto_20210612_1608.py +46 -0
  81. karrio/server/providers/migrations/0015_auto_20210615_1601.py +28 -0
  82. karrio/server/providers/migrations/0016_alter_purolatorsettings_user_token.py +18 -0
  83. karrio/server/providers/migrations/0017_auto_20210805_0359.py +1293 -0
  84. karrio/server/providers/migrations/0018_alter_fedexsettings_user_key.py +18 -0
  85. karrio/server/providers/migrations/0019_dhlpolandsettings_servicelevel.py +65 -0
  86. karrio/server/providers/migrations/0020_genericsettings_labeltemplate.py +52 -0
  87. karrio/server/providers/migrations/0021_auto_20211231_2353.py +40 -0
  88. karrio/server/providers/migrations/0022_carrier_metadata.py +18 -0
  89. karrio/server/providers/migrations/0023_auto_20220124_1916.py +27 -0
  90. karrio/server/providers/migrations/0024_alter_genericsettings_custom_carrier_name.py +19 -0
  91. karrio/server/providers/migrations/0025_alter_servicelevel_service_code.py +19 -0
  92. karrio/server/providers/migrations/0026_auto_20220208_0132.py +59 -0
  93. karrio/server/providers/migrations/0027_auto_20220304_1340.py +29 -0
  94. karrio/server/providers/migrations/0028_auto_20220323_1500.py +33 -0
  95. karrio/server/providers/migrations/0029_easypostsettings.py +27 -0
  96. karrio/server/providers/migrations/0030_amazonmwssettings.py +29 -0
  97. karrio/server/providers/migrations/0031_delete_amazonmwssettings.py +18 -0
  98. karrio/server/providers/migrations/0032_alter_carrier_test.py +18 -0
  99. karrio/server/providers/migrations/0033_auto_20220708_1350.py +22 -0
  100. karrio/server/providers/migrations/0034_amazonmwssettings_dpdhlsettings.py +47 -0
  101. karrio/server/providers/migrations/0035_alter_carrier_capabilities.py +43 -0
  102. karrio/server/providers/migrations/0036_upsfreightsettings.py +31 -0
  103. karrio/server/providers/migrations/0037_chronopostsettings.py +29 -0
  104. karrio/server/providers/migrations/0038_alter_genericsettings_label_template.py +19 -0
  105. karrio/server/providers/migrations/0039_auto_20220906_0612.py +23 -0
  106. karrio/server/providers/migrations/0040_dpdhlsettings_services.py +18 -0
  107. karrio/server/providers/migrations/0041_auto_20221105_0705.py +38 -0
  108. karrio/server/providers/migrations/0042_auto_20221215_1642.py +23 -0
  109. karrio/server/providers/migrations/0043_alter_genericsettings_account_number_and_more.py +39 -0
  110. karrio/server/providers/migrations/0044_carrier_carrier_capabilities.py +64 -0
  111. karrio/server/providers/migrations/0045_alter_carrier_active_alter_carrier_carrier_id.py +31 -0
  112. karrio/server/providers/migrations/0046_remove_dpdhlsettings_signature_and_more.py +41 -0
  113. karrio/server/providers/migrations/0047_dpdsettings.py +286 -0
  114. karrio/server/providers/migrations/0048_servicelevel_min_weight_servicelevel_transit_days_and_more.py +64 -0
  115. karrio/server/providers/migrations/0049_boxknightsettings_geodissettings_lapostesettings_and_more.py +156 -0
  116. karrio/server/providers/migrations/0050_carrier_is_system_alter_carrier_metadata_and_more.py +106 -0
  117. karrio/server/providers/migrations/0051_rename_username_upssettings_client_id_and_more.py +31 -0
  118. karrio/server/providers/migrations/0052_alter_upssettings_account_number_and_more.py +20 -0
  119. karrio/server/providers/migrations/0053_locate2usettings.py +281 -0
  120. karrio/server/providers/migrations/0054_zoom2usettings.py +280 -0
  121. karrio/server/providers/migrations/0055_rename_amazonmwssettings_amazonshippingsettings_and_more.py +44 -0
  122. karrio/server/providers/migrations/0056_asendiaussettings_geodissettings_code_client_and_more.py +75 -0
  123. karrio/server/providers/migrations/0057_alter_servicelevel_weight_unit_belgianpostsettings.py +51 -0
  124. karrio/server/providers/migrations/0058_alliedexpresssettings.py +38 -0
  125. karrio/server/providers/migrations/0059_ratesheet.py +81 -0
  126. karrio/server/providers/migrations/0060_belgianpostsettings_rate_sheet_and_more.py +73 -0
  127. karrio/server/providers/migrations/0061_alliedexpresssettings_service_type.py +17 -0
  128. karrio/server/providers/migrations/0062_sendlesettings_account_country_code.py +257 -0
  129. karrio/server/providers/migrations/0063_servicelevel_metadata.py +25 -0
  130. karrio/server/providers/migrations/0064_alliedexpresslocalsettings.py +43 -0
  131. karrio/server/providers/migrations/0065_servicelevel_carrier_service_code_and_more.py +66 -0
  132. karrio/server/providers/migrations/0066_rename_fedexsettings_fedexwssettings_and_more.py +28 -0
  133. karrio/server/providers/migrations/0067_fedexsettings.py +283 -0
  134. karrio/server/providers/migrations/0068_fedexsettings_track_api_key_and_more.py +38 -0
  135. karrio/server/providers/migrations/0069_alter_canadapostsettings_contract_id_and_more.py +23 -0
  136. karrio/server/providers/migrations/0070_tgesettings_alter_carrier_capabilities.py +65 -0
  137. karrio/server/providers/migrations/0071_alter_tgesettings_my_toll_token.py +18 -0
  138. karrio/server/providers/migrations/0072_rename_eshippersettings_eshipperxmlsettings_and_more.py +28 -0
  139. karrio/server/providers/migrations/0073_delete_eshipperxmlsettings.py +41 -0
  140. karrio/server/providers/migrations/0074_eshippersettings.py +38 -0
  141. karrio/server/providers/migrations/0075_haypostsettings.py +40 -0
  142. karrio/server/providers/migrations/0076_rename_customer_registration_id_uspsinternationalsettings_account_number_and_more.py +125 -0
  143. karrio/server/providers/migrations/0077_uspswtinternationalsettings_uspswtsettings_and_more.py +165 -0
  144. karrio/server/providers/migrations/0078_auto_20240813_1552.py +120 -0
  145. karrio/server/providers/migrations/0079_alter_carrier_options_alter_ratesheet_created_by.py +31 -0
  146. karrio/server/providers/migrations/0080_alter_aramexsettings_account_country_code_and_more.py +3025 -0
  147. karrio/server/providers/migrations/0081_remove_alliedexpresssettings_carrier_ptr_and_more.py +338 -0
  148. karrio/server/providers/migrations/0082_add_zone_identifiers.py +50 -0
  149. karrio/server/providers/migrations/0083_add_optimized_rate_sheet_structure.py +33 -0
  150. karrio/server/providers/migrations/0084_alter_servicelevel_currency.py +168 -0
  151. karrio/server/providers/migrations/0085_populate_dhl_parcel_de_oauth_credentials.py +82 -0
  152. karrio/server/providers/migrations/0086_rename_dhl_parcel_de_customer_number_to_billing_number.py +71 -0
  153. karrio/server/providers/migrations/__init__.py +0 -0
  154. karrio/server/providers/models/__init__.py +16 -0
  155. karrio/server/providers/models/carrier.py +387 -0
  156. karrio/server/providers/models/config.py +30 -0
  157. karrio/server/providers/models/service.py +192 -0
  158. karrio/server/providers/models/sheet.py +287 -0
  159. karrio/server/providers/models/template.py +39 -0
  160. karrio/server/providers/models/utils.py +58 -0
  161. karrio/server/providers/router.py +3 -0
  162. karrio/server/providers/serializers/__init__.py +3 -0
  163. karrio/server/providers/serializers/base.py +538 -0
  164. karrio/server/providers/signals.py +25 -0
  165. karrio/server/providers/templates/providers/oauth_callback.html +105 -0
  166. karrio/server/providers/tests/__init__.py +5 -0
  167. karrio/server/providers/tests/test_connections.py +895 -0
  168. karrio/server/providers/urls.py +11 -0
  169. karrio/server/providers/views/__init__.py +0 -0
  170. karrio/server/providers/views/carriers.py +267 -0
  171. karrio/server/providers/views/connections.py +496 -0
  172. karrio/server/samples.py +352 -0
  173. karrio/server/serializers/__init__.py +2 -0
  174. karrio/server/serializers/abstract.py +602 -0
  175. karrio/server/tracing/__init__.py +0 -0
  176. karrio/server/tracing/admin.py +63 -0
  177. karrio/server/tracing/apps.py +8 -0
  178. karrio/server/tracing/migrations/0001_initial.py +41 -0
  179. karrio/server/tracing/migrations/0002_auto_20220710_1307.py +22 -0
  180. karrio/server/tracing/migrations/0003_auto_20221105_0317.py +43 -0
  181. karrio/server/tracing/migrations/0004_tracingrecord_carrier_account_idx.py +24 -0
  182. karrio/server/tracing/migrations/0005_optimise_tracingrecord_request_log_idx.py +25 -0
  183. karrio/server/tracing/migrations/0006_alter_tracingrecord_options_and_more.py +49 -0
  184. karrio/server/tracing/migrations/0007_tracingrecord_tracing_created_at_idx.py +19 -0
  185. karrio/server/tracing/migrations/__init__.py +0 -0
  186. karrio/server/tracing/models.py +82 -0
  187. karrio/server/tracing/tests.py +3 -0
  188. karrio/server/tracing/utils.py +109 -0
  189. karrio/server/user/__init__.py +0 -0
  190. karrio/server/user/admin.py +96 -0
  191. karrio/server/user/apps.py +7 -0
  192. karrio/server/user/forms.py +35 -0
  193. karrio/server/user/migrations/0001_initial.py +41 -0
  194. karrio/server/user/migrations/0002_token.py +29 -0
  195. karrio/server/user/migrations/0003_token_test_mode.py +20 -0
  196. karrio/server/user/migrations/0004_group.py +26 -0
  197. karrio/server/user/migrations/0005_token_label.py +21 -0
  198. karrio/server/user/migrations/0006_workspaceconfig.py +63 -0
  199. karrio/server/user/migrations/0007_user_metadata.py +25 -0
  200. karrio/server/user/migrations/__init__.py +0 -0
  201. karrio/server/user/models.py +218 -0
  202. karrio/server/user/serializers.py +47 -0
  203. karrio/server/user/templates/registration/login.html +108 -0
  204. karrio/server/user/templates/registration/registration_confirm_email.html +10 -0
  205. karrio/server/user/templates/registration/registration_confirm_email.txt +3 -0
  206. karrio/server/user/tests.py +3 -0
  207. karrio/server/user/urls.py +10 -0
  208. karrio/server/user/utils.py +60 -0
  209. karrio/server/user/views.py +9 -0
  210. karrio_server_core-2025.5.dist-info/METADATA +32 -0
  211. karrio_server_core-2025.5.dist-info/RECORD +213 -0
  212. karrio_server_core-2025.5.dist-info/WHEEL +5 -0
  213. karrio_server_core-2025.5.dist-info/top_level.txt +2 -0
@@ -0,0 +1,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)
@@ -0,0 +1,8 @@
1
+ from django.apps import AppConfig
2
+ from django.utils.translation import gettext_lazy as _
3
+
4
+
5
+ class TracingConfig(AppConfig):
6
+ name = "karrio.server.tracing"
7
+ verbose_name = _("Tracing")
8
+ default_auto_field = "django.db.models.BigAutoField"