karrio-server-core 2025.5rc1__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.

Potentially problematic release.


This version of karrio-server-core might be problematic. Click here for more details.

Files changed (241) 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 +313 -0
  6. karrio/server/core/context_processors.py +12 -0
  7. karrio/server/core/datatypes.py +369 -0
  8. karrio/server/core/dataunits.py +156 -0
  9. karrio/server/core/exceptions.py +200 -0
  10. karrio/server/core/fields.py +12 -0
  11. karrio/server/core/filters.py +823 -0
  12. karrio/server/core/gateway.py +720 -0
  13. karrio/server/core/management/commands/cli.py +19 -0
  14. karrio/server/core/management/commands/create_oauth_client.py +41 -0
  15. karrio/server/core/middleware.py +95 -0
  16. karrio/server/core/migrations/0001_initial.py +28 -0
  17. karrio/server/core/migrations/0002_apilogindex.py +69 -0
  18. karrio/server/core/migrations/0003_apilogindex_test_mode.py +62 -0
  19. karrio/server/core/migrations/0004_metafield.py +74 -0
  20. karrio/server/core/migrations/0005_alter_metafield_type_alter_metafield_value.py +23 -0
  21. karrio/server/core/migrations/__init__.py +0 -0
  22. karrio/server/core/models/__init__.py +48 -0
  23. karrio/server/core/models/base.py +70 -0
  24. karrio/server/core/models/entity.py +22 -0
  25. karrio/server/core/models/metafield.py +144 -0
  26. karrio/server/core/models/third_party.py +21 -0
  27. karrio/server/core/oauth_validators.py +171 -0
  28. karrio/server/core/permissions.py +37 -0
  29. karrio/server/core/renderers.py +11 -0
  30. karrio/server/core/router.py +3 -0
  31. karrio/server/core/serializers.py +1898 -0
  32. karrio/server/core/signals.py +57 -0
  33. karrio/server/core/tests.py +98 -0
  34. karrio/server/core/urls.py +12 -0
  35. karrio/server/core/utils.py +479 -0
  36. karrio/server/core/validators.py +416 -0
  37. karrio/server/core/views/__init__.py +2 -0
  38. karrio/server/core/views/api.py +133 -0
  39. karrio/server/core/views/metadata.py +44 -0
  40. karrio/server/core/views/oauth.py +74 -0
  41. karrio/server/core/views/references.py +82 -0
  42. karrio/server/core/views/schema.py +310 -0
  43. karrio/server/filters/__init__.py +2 -0
  44. karrio/server/filters/abstract.py +26 -0
  45. karrio/server/iam/__init__.py +0 -0
  46. karrio/server/iam/admin.py +3 -0
  47. karrio/server/iam/apps.py +21 -0
  48. karrio/server/iam/migrations/0001_initial.py +33 -0
  49. karrio/server/iam/migrations/__init__.py +0 -0
  50. karrio/server/iam/models.py +48 -0
  51. karrio/server/iam/permissions.py +134 -0
  52. karrio/server/iam/serializers.py +39 -0
  53. karrio/server/iam/signals.py +20 -0
  54. karrio/server/iam/tests.py +3 -0
  55. karrio/server/iam/views.py +3 -0
  56. karrio/server/openapi.py +75 -0
  57. karrio/server/providers/__init__.py +1 -0
  58. karrio/server/providers/admin.py +364 -0
  59. karrio/server/providers/apps.py +10 -0
  60. karrio/server/providers/extension/__init__.py +1 -0
  61. karrio/server/providers/extension/models/__init__.py +1 -0
  62. karrio/server/providers/extension/models/allied_express.py +22 -0
  63. karrio/server/providers/extension/models/allied_express_local.py +22 -0
  64. karrio/server/providers/extension/models/amazon_shipping.py +27 -0
  65. karrio/server/providers/extension/models/aramex.py +25 -0
  66. karrio/server/providers/extension/models/asendia_us.py +21 -0
  67. karrio/server/providers/extension/models/australiapost.py +20 -0
  68. karrio/server/providers/extension/models/boxknight.py +19 -0
  69. karrio/server/providers/extension/models/bpost.py +21 -0
  70. karrio/server/providers/extension/models/canadapost.py +21 -0
  71. karrio/server/providers/extension/models/canpar.py +19 -0
  72. karrio/server/providers/extension/models/chronopost.py +22 -0
  73. karrio/server/providers/extension/models/colissimo.py +22 -0
  74. karrio/server/providers/extension/models/dhl_express.py +23 -0
  75. karrio/server/providers/extension/models/dhl_parcel_de.py +25 -0
  76. karrio/server/providers/extension/models/dhl_poland.py +22 -0
  77. karrio/server/providers/extension/models/dhl_universal.py +19 -0
  78. karrio/server/providers/extension/models/dicom.py +20 -0
  79. karrio/server/providers/extension/models/dpd.py +37 -0
  80. karrio/server/providers/extension/models/dpdhl.py +26 -0
  81. karrio/server/providers/extension/models/easypost.py +20 -0
  82. karrio/server/providers/extension/models/eshipper.py +21 -0
  83. karrio/server/providers/extension/models/fedex.py +25 -0
  84. karrio/server/providers/extension/models/fedex_ws.py +24 -0
  85. karrio/server/providers/extension/models/freightcom.py +21 -0
  86. karrio/server/providers/extension/models/generic.py +35 -0
  87. karrio/server/providers/extension/models/geodis.py +22 -0
  88. karrio/server/providers/extension/models/hay_post.py +22 -0
  89. karrio/server/providers/extension/models/laposte.py +19 -0
  90. karrio/server/providers/extension/models/locate2u.py +22 -0
  91. karrio/server/providers/extension/models/nationex.py +22 -0
  92. karrio/server/providers/extension/models/purolator.py +21 -0
  93. karrio/server/providers/extension/models/roadie.py +18 -0
  94. karrio/server/providers/extension/models/royalmail.py +19 -0
  95. karrio/server/providers/extension/models/sendle.py +22 -0
  96. karrio/server/providers/extension/models/tge.py +63 -0
  97. karrio/server/providers/extension/models/tnt.py +23 -0
  98. karrio/server/providers/extension/models/ups.py +23 -0
  99. karrio/server/providers/extension/models/usps.py +23 -0
  100. karrio/server/providers/extension/models/usps_international.py +23 -0
  101. karrio/server/providers/extension/models/usps_wt.py +24 -0
  102. karrio/server/providers/extension/models/usps_wt_international.py +24 -0
  103. karrio/server/providers/extension/models/zoom2u.py +23 -0
  104. karrio/server/providers/migrations/0001_initial.py +140 -0
  105. karrio/server/providers/migrations/0002_carrier_active.py +18 -0
  106. karrio/server/providers/migrations/0003_auto_20201230_0820.py +24 -0
  107. karrio/server/providers/migrations/0004_auto_20210212_0554.py +178 -0
  108. karrio/server/providers/migrations/0005_auto_20210212_0555.py +18 -0
  109. karrio/server/providers/migrations/0006_australiapostsettings.py +29 -0
  110. karrio/server/providers/migrations/0007_auto_20210213_0206.py +21 -0
  111. karrio/server/providers/migrations/0008_auto_20210214_0409.py +30 -0
  112. karrio/server/providers/migrations/0009_auto_20210308_0302.py +18 -0
  113. karrio/server/providers/migrations/0010_auto_20210409_0852.py +32 -0
  114. karrio/server/providers/migrations/0011_auto_20210409_0853.py +21 -0
  115. karrio/server/providers/migrations/0012_alter_carrier_options.py +17 -0
  116. karrio/server/providers/migrations/0013_tntsettings.py +30 -0
  117. karrio/server/providers/migrations/0014_auto_20210612_1608.py +46 -0
  118. karrio/server/providers/migrations/0015_auto_20210615_1601.py +28 -0
  119. karrio/server/providers/migrations/0016_alter_purolatorsettings_user_token.py +18 -0
  120. karrio/server/providers/migrations/0017_auto_20210805_0359.py +1293 -0
  121. karrio/server/providers/migrations/0018_alter_fedexsettings_user_key.py +18 -0
  122. karrio/server/providers/migrations/0019_dhlpolandsettings_servicelevel.py +65 -0
  123. karrio/server/providers/migrations/0020_genericsettings_labeltemplate.py +52 -0
  124. karrio/server/providers/migrations/0021_auto_20211231_2353.py +40 -0
  125. karrio/server/providers/migrations/0022_carrier_metadata.py +18 -0
  126. karrio/server/providers/migrations/0023_auto_20220124_1916.py +27 -0
  127. karrio/server/providers/migrations/0024_alter_genericsettings_custom_carrier_name.py +19 -0
  128. karrio/server/providers/migrations/0025_alter_servicelevel_service_code.py +19 -0
  129. karrio/server/providers/migrations/0026_auto_20220208_0132.py +59 -0
  130. karrio/server/providers/migrations/0027_auto_20220304_1340.py +29 -0
  131. karrio/server/providers/migrations/0028_auto_20220323_1500.py +33 -0
  132. karrio/server/providers/migrations/0029_easypostsettings.py +27 -0
  133. karrio/server/providers/migrations/0030_amazonmwssettings.py +29 -0
  134. karrio/server/providers/migrations/0031_delete_amazonmwssettings.py +18 -0
  135. karrio/server/providers/migrations/0032_alter_carrier_test.py +18 -0
  136. karrio/server/providers/migrations/0033_auto_20220708_1350.py +22 -0
  137. karrio/server/providers/migrations/0034_amazonmwssettings_dpdhlsettings.py +47 -0
  138. karrio/server/providers/migrations/0035_alter_carrier_capabilities.py +43 -0
  139. karrio/server/providers/migrations/0036_upsfreightsettings.py +31 -0
  140. karrio/server/providers/migrations/0037_chronopostsettings.py +29 -0
  141. karrio/server/providers/migrations/0038_alter_genericsettings_label_template.py +19 -0
  142. karrio/server/providers/migrations/0039_auto_20220906_0612.py +23 -0
  143. karrio/server/providers/migrations/0040_dpdhlsettings_services.py +18 -0
  144. karrio/server/providers/migrations/0041_auto_20221105_0705.py +38 -0
  145. karrio/server/providers/migrations/0042_auto_20221215_1642.py +23 -0
  146. karrio/server/providers/migrations/0043_alter_genericsettings_account_number_and_more.py +39 -0
  147. karrio/server/providers/migrations/0044_carrier_carrier_capabilities.py +64 -0
  148. karrio/server/providers/migrations/0045_alter_carrier_active_alter_carrier_carrier_id.py +31 -0
  149. karrio/server/providers/migrations/0046_remove_dpdhlsettings_signature_and_more.py +41 -0
  150. karrio/server/providers/migrations/0047_dpdsettings.py +286 -0
  151. karrio/server/providers/migrations/0048_servicelevel_min_weight_servicelevel_transit_days_and_more.py +64 -0
  152. karrio/server/providers/migrations/0049_boxknightsettings_geodissettings_lapostesettings_and_more.py +156 -0
  153. karrio/server/providers/migrations/0050_carrier_is_system_alter_carrier_metadata_and_more.py +106 -0
  154. karrio/server/providers/migrations/0051_rename_username_upssettings_client_id_and_more.py +31 -0
  155. karrio/server/providers/migrations/0052_alter_upssettings_account_number_and_more.py +20 -0
  156. karrio/server/providers/migrations/0053_locate2usettings.py +281 -0
  157. karrio/server/providers/migrations/0054_zoom2usettings.py +280 -0
  158. karrio/server/providers/migrations/0055_rename_amazonmwssettings_amazonshippingsettings_and_more.py +44 -0
  159. karrio/server/providers/migrations/0056_asendiaussettings_geodissettings_code_client_and_more.py +75 -0
  160. karrio/server/providers/migrations/0057_alter_servicelevel_weight_unit_belgianpostsettings.py +51 -0
  161. karrio/server/providers/migrations/0058_alliedexpresssettings.py +38 -0
  162. karrio/server/providers/migrations/0059_ratesheet.py +81 -0
  163. karrio/server/providers/migrations/0060_belgianpostsettings_rate_sheet_and_more.py +73 -0
  164. karrio/server/providers/migrations/0061_alliedexpresssettings_service_type.py +17 -0
  165. karrio/server/providers/migrations/0062_sendlesettings_account_country_code.py +257 -0
  166. karrio/server/providers/migrations/0063_servicelevel_metadata.py +25 -0
  167. karrio/server/providers/migrations/0064_alliedexpresslocalsettings.py +43 -0
  168. karrio/server/providers/migrations/0065_servicelevel_carrier_service_code_and_more.py +66 -0
  169. karrio/server/providers/migrations/0066_rename_fedexsettings_fedexwssettings_and_more.py +28 -0
  170. karrio/server/providers/migrations/0067_fedexsettings.py +283 -0
  171. karrio/server/providers/migrations/0068_fedexsettings_track_api_key_and_more.py +38 -0
  172. karrio/server/providers/migrations/0069_alter_canadapostsettings_contract_id_and_more.py +23 -0
  173. karrio/server/providers/migrations/0070_tgesettings_alter_carrier_capabilities.py +65 -0
  174. karrio/server/providers/migrations/0071_alter_tgesettings_my_toll_token.py +18 -0
  175. karrio/server/providers/migrations/0072_rename_eshippersettings_eshipperxmlsettings_and_more.py +28 -0
  176. karrio/server/providers/migrations/0073_delete_eshipperxmlsettings.py +41 -0
  177. karrio/server/providers/migrations/0074_eshippersettings.py +38 -0
  178. karrio/server/providers/migrations/0075_haypostsettings.py +40 -0
  179. karrio/server/providers/migrations/0076_rename_customer_registration_id_uspsinternationalsettings_account_number_and_more.py +125 -0
  180. karrio/server/providers/migrations/0077_uspswtinternationalsettings_uspswtsettings_and_more.py +165 -0
  181. karrio/server/providers/migrations/0078_auto_20240813_1552.py +120 -0
  182. karrio/server/providers/migrations/0079_alter_carrier_options_alter_ratesheet_created_by.py +31 -0
  183. karrio/server/providers/migrations/0080_alter_aramexsettings_account_country_code_and_more.py +3025 -0
  184. karrio/server/providers/migrations/0081_remove_alliedexpresssettings_carrier_ptr_and_more.py +338 -0
  185. karrio/server/providers/migrations/__init__.py +0 -0
  186. karrio/server/providers/models/__init__.py +17 -0
  187. karrio/server/providers/models/carrier.py +309 -0
  188. karrio/server/providers/models/config.py +30 -0
  189. karrio/server/providers/models/service.py +62 -0
  190. karrio/server/providers/models/sheet.py +60 -0
  191. karrio/server/providers/models/template.py +39 -0
  192. karrio/server/providers/models/utils.py +58 -0
  193. karrio/server/providers/router.py +3 -0
  194. karrio/server/providers/serializers/__init__.py +3 -0
  195. karrio/server/providers/serializers/base.py +277 -0
  196. karrio/server/providers/signals.py +27 -0
  197. karrio/server/providers/tests.py +3 -0
  198. karrio/server/providers/urls.py +11 -0
  199. karrio/server/providers/views/__init__.py +0 -0
  200. karrio/server/providers/views/carriers.py +269 -0
  201. karrio/server/providers/views/connections.py +176 -0
  202. karrio/server/samples.py +352 -0
  203. karrio/server/serializers/__init__.py +2 -0
  204. karrio/server/serializers/abstract.py +506 -0
  205. karrio/server/tracing/__init__.py +0 -0
  206. karrio/server/tracing/admin.py +63 -0
  207. karrio/server/tracing/apps.py +8 -0
  208. karrio/server/tracing/migrations/0001_initial.py +41 -0
  209. karrio/server/tracing/migrations/0002_auto_20220710_1307.py +22 -0
  210. karrio/server/tracing/migrations/0003_auto_20221105_0317.py +43 -0
  211. karrio/server/tracing/migrations/0004_tracingrecord_carrier_account_idx.py +24 -0
  212. karrio/server/tracing/migrations/0005_optimise_tracingrecord_request_log_idx.py +25 -0
  213. karrio/server/tracing/migrations/0006_alter_tracingrecord_options_and_more.py +49 -0
  214. karrio/server/tracing/migrations/__init__.py +0 -0
  215. karrio/server/tracing/models.py +80 -0
  216. karrio/server/tracing/tests.py +3 -0
  217. karrio/server/tracing/utils.py +112 -0
  218. karrio/server/user/__init__.py +0 -0
  219. karrio/server/user/admin.py +96 -0
  220. karrio/server/user/apps.py +7 -0
  221. karrio/server/user/forms.py +35 -0
  222. karrio/server/user/migrations/0001_initial.py +41 -0
  223. karrio/server/user/migrations/0002_token.py +29 -0
  224. karrio/server/user/migrations/0003_token_test_mode.py +20 -0
  225. karrio/server/user/migrations/0004_group.py +26 -0
  226. karrio/server/user/migrations/0005_token_label.py +21 -0
  227. karrio/server/user/migrations/0006_workspaceconfig.py +63 -0
  228. karrio/server/user/migrations/__init__.py +0 -0
  229. karrio/server/user/models.py +203 -0
  230. karrio/server/user/serializers.py +46 -0
  231. karrio/server/user/templates/registration/login.html +108 -0
  232. karrio/server/user/templates/registration/registration_confirm_email.html +10 -0
  233. karrio/server/user/templates/registration/registration_confirm_email.txt +3 -0
  234. karrio/server/user/tests.py +3 -0
  235. karrio/server/user/urls.py +10 -0
  236. karrio/server/user/utils.py +60 -0
  237. karrio/server/user/views.py +9 -0
  238. karrio_server_core-2025.5rc1.dist-info/METADATA +32 -0
  239. karrio_server_core-2025.5rc1.dist-info/RECORD +241 -0
  240. karrio_server_core-2025.5rc1.dist-info/WHEEL +5 -0
  241. karrio_server_core-2025.5rc1.dist-info/top_level.txt +2 -0
@@ -0,0 +1,506 @@
1
+ import yaml
2
+ import pydoc
3
+ import typing
4
+ import logging
5
+ from django.db import models
6
+ from django.conf import settings
7
+ from django.db import transaction
8
+ from rest_framework import serializers
9
+ from django.forms.models import model_to_dict
10
+ from drf_spectacular.types import OpenApiTypes
11
+
12
+ import karrio.lib as lib
13
+
14
+ logger = logging.getLogger(__name__)
15
+ T = typing.TypeVar("T")
16
+
17
+
18
+ class Context(typing.NamedTuple):
19
+ user: typing.Any
20
+ org: typing.Any = None
21
+ test_mode: bool = None
22
+
23
+ def __getitem__(self, item):
24
+ return getattr(self, item)
25
+
26
+
27
+ class DecoratedSerializer:
28
+ def __init__(
29
+ self,
30
+ instance: models.Model = None,
31
+ serializer: "Serializer" = None,
32
+ ):
33
+ self._instance = instance
34
+ self._serializer = serializer
35
+
36
+ @property
37
+ def data(self) -> typing.Optional[dict]:
38
+ return self._serializer.validated_data if self._serializer is not None else None
39
+
40
+ @property
41
+ def instance(self) -> models.Model:
42
+ return self._instance
43
+
44
+ def save(self, **kwargs) -> "DecoratedSerializer":
45
+ if self._serializer is not None:
46
+ self._instance = self._serializer.save(**kwargs)
47
+
48
+ return self
49
+
50
+
51
+ class AbstractSerializer:
52
+ def create(self, validated_data, **kwargs):
53
+ super().create(validated_data)
54
+
55
+ def update(self, instance, validated_data, **kwargs):
56
+ super().update(instance, validated_data, **kwargs)
57
+
58
+ @classmethod
59
+ def map(
60
+ cls, instance=None, data: typing.Union[str, dict] = None, **kwargs
61
+ ) -> "DecoratedSerializer":
62
+ if data is None and instance is None:
63
+ serializer = None
64
+ else:
65
+ serializer = (
66
+ cls(data=data or {}, **kwargs) # type:ignore
67
+ if instance is None
68
+ else cls(
69
+ instance, data=data or {}, **{**kwargs, "partial": True}
70
+ ) # type:ignore
71
+ )
72
+
73
+ serializer.is_valid(raise_exception=True) # type:ignore
74
+
75
+ return DecoratedSerializer(
76
+ instance=instance,
77
+ serializer=serializer, # type:ignore
78
+ )
79
+
80
+
81
+ class Serializer(serializers.Serializer, AbstractSerializer):
82
+ context: dict = {}
83
+
84
+
85
+ class ModelSerializer(serializers.ModelSerializer, AbstractSerializer):
86
+ def create(self, data: dict, **kwargs): # type: ignore
87
+ return self.Meta.model.objects.create(**data)
88
+
89
+ def update(self, instance, data: dict, **kwargs): # type: ignore
90
+ for name, value in data.items():
91
+ if name != "created_by" and hasattr(instance, name):
92
+ setattr(instance, name, value)
93
+
94
+ instance.save()
95
+ return instance
96
+
97
+
98
+ class StringListField(serializers.ListField):
99
+ child = serializers.CharField()
100
+
101
+
102
+ class PlainDictField(serializers.DictField):
103
+ class Meta:
104
+ swagger_schema_fields = {
105
+ "type": OpenApiTypes.OBJECT,
106
+ "additional_properties": True,
107
+ }
108
+
109
+
110
+ class FlagField(serializers.BooleanField):
111
+ pass
112
+
113
+
114
+ class FlagsSerializer(serializers.Serializer):
115
+ def __init__(self, *args, **kwargs):
116
+ data = kwargs.get("data", {})
117
+ self.flags = [
118
+ (label, label in data)
119
+ for label, field in self.fields.items()
120
+ if isinstance(field, FlagField)
121
+ ]
122
+
123
+ super().__init__(*args, **kwargs)
124
+
125
+ def validate(self, data):
126
+ validated = super().validate(data)
127
+ for flag, specified in self.flags:
128
+ if specified and validated[flag] is None:
129
+ validated.update({flag: True})
130
+
131
+ return validated
132
+
133
+
134
+ class EntitySerializer(serializers.Serializer):
135
+ id = serializers.CharField(required=False, help_text="A unique identifier")
136
+
137
+
138
+ """
139
+ Custom serializer utilities functions
140
+ """
141
+
142
+
143
+ def PaginatedResult(serializer_name: str, content_serializer: typing.Type[Serializer]):
144
+ return type(
145
+ serializer_name,
146
+ (Serializer,),
147
+ dict(
148
+ count=serializers.IntegerField(required=False, allow_null=True),
149
+ next=serializers.URLField(
150
+ required=False, allow_blank=True, allow_null=True
151
+ ),
152
+ previous=serializers.URLField(
153
+ required=False, allow_blank=True, allow_null=True
154
+ ),
155
+ results=content_serializer(many=True),
156
+ ),
157
+ )
158
+
159
+
160
+ def owned_model_serializer(serializer: typing.Type[Serializer]):
161
+ class MetaSerializer(serializer): # type: ignore
162
+ context: dict = {}
163
+
164
+ def __init__(self, *args, **kwargs):
165
+ if "context" in kwargs:
166
+ context = kwargs.get("context") or {}
167
+ user = (
168
+ context.get("user") if isinstance(context, dict) else context.user
169
+ )
170
+ org = context.get("org") if isinstance(context, dict) else context.org
171
+ test_mode = (
172
+ context.get("test_mode")
173
+ if isinstance(context, dict)
174
+ else context.test_mode
175
+ )
176
+
177
+ if settings.MULTI_ORGANIZATIONS and org is None:
178
+ import karrio.server.orgs.models as orgs
179
+
180
+ org = orgs.Organization.objects.filter(
181
+ users__id=getattr(user, "id", None)
182
+ ).first()
183
+
184
+ self.__context: Context = Context(user, org, test_mode)
185
+ else:
186
+ self.__context: Context = getattr(self, "__context", None)
187
+ kwargs.update({"context": self.__context})
188
+
189
+ super().__init__(*args, **kwargs)
190
+
191
+ @transaction.atomic
192
+ def create(self, data: dict, **kwargs):
193
+ payload = {"created_by": self.__context.user, **data}
194
+
195
+ try:
196
+ instance = super().create(payload, context=self.__context)
197
+ link_org(instance, self.__context) # Link to organization if supported
198
+ except Exception as e:
199
+ logger.exception(e)
200
+ raise e
201
+
202
+ return instance
203
+
204
+ def update(self, instance, data: dict, **kwargs):
205
+ payload = {k: v for k, v in data.items()}
206
+
207
+ return super().update(instance, payload, context=self.__context)
208
+
209
+ return type(serializer.__name__, (MetaSerializer,), {})
210
+
211
+
212
+ def link_org(entity: ModelSerializer, context: Context):
213
+ if (
214
+ context.org is not None
215
+ and hasattr(entity, "org")
216
+ and hasattr(entity.org, "exists")
217
+ and not entity.org.exists()
218
+ ):
219
+ entity.link = entity.__class__.link.related.related_model.objects.create(
220
+ org=context.org, item=entity
221
+ )
222
+ entity.save(
223
+ update_fields=(["created_at"] if hasattr(entity, "created_at") else [])
224
+ )
225
+
226
+ def bulk_link_org(entities: typing.List[models.Model], context: Context):
227
+ if len(entities) == 0 or settings.MULTI_ORGANIZATIONS is False:
228
+ return
229
+
230
+ EntityLinkModel = entities[0].__class__.link.related.related_model
231
+ links = []
232
+
233
+ for entity in entities:
234
+ entity.link = EntityLinkModel(org=context.org, item=entity)
235
+ links.append(entity.link)
236
+
237
+ EntityLinkModel.objects.bulk_create(links)
238
+
239
+
240
+ def get_object_context(entity) -> Context:
241
+ org = lib.failsafe(
242
+ lambda: (
243
+ entity.org.first()
244
+ if (hasattr(entity, "org") and entity.org.exists())
245
+ else None
246
+ )
247
+ )
248
+
249
+ return Context(
250
+ org=org,
251
+ user=getattr(entity, "created_by", None),
252
+ test_mode=getattr(entity, "test_mode", None),
253
+ )
254
+
255
+
256
+ def save_many_to_many_data(
257
+ name: str,
258
+ serializer: ModelSerializer,
259
+ parent: models.Model,
260
+ payload: dict = None,
261
+ remove_if_missing: bool = False,
262
+ **kwargs,
263
+ ):
264
+ if not any((key in payload for key in [name])):
265
+ return None
266
+
267
+ collection_data = payload.get(name)
268
+ collection = getattr(parent, name)
269
+
270
+ if collection_data is None and any(collection.all()):
271
+ for item in collection.all():
272
+ item.delete()
273
+
274
+ if remove_if_missing and collection.exists():
275
+ collection.exclude(id__in=[item.get("id") for item in collection_data]).delete()
276
+
277
+ for data in collection_data:
278
+ item_instance = (
279
+ collection.filter(id=data.pop("id")).first() if "id" in data else None
280
+ )
281
+
282
+ if item_instance is None:
283
+ item = serializer.map(data=data, **kwargs).save().instance
284
+ getattr(parent, name).add(item)
285
+ else:
286
+ item = (
287
+ serializer.map(
288
+ data=data,
289
+ instance=item_instance,
290
+ **{**kwargs, "partial": True},
291
+ )
292
+ .save()
293
+ .instance
294
+ )
295
+
296
+
297
+ def save_one_to_one_data(
298
+ name: str,
299
+ serializer: ModelSerializer,
300
+ parent: models.Model = None,
301
+ payload: dict = None,
302
+ **kwargs,
303
+ ):
304
+ if name not in payload:
305
+ return None
306
+
307
+ data = payload.get(name)
308
+ instance = getattr(parent, name, None)
309
+
310
+ if data is None and instance is not None:
311
+ instance.delete()
312
+ setattr(parent, name, None)
313
+
314
+ if instance is None:
315
+ new_instance = serializer.map(data=data, **kwargs).save().instance
316
+ parent and setattr(parent, name, new_instance) # type: ignore
317
+ return new_instance
318
+
319
+ return (
320
+ serializer.map(instance=instance, data=data, **{**kwargs, "partial": True})
321
+ .save()
322
+ .instance
323
+ )
324
+
325
+
326
+ def allow_model_id(model_paths: []): # type: ignore
327
+ def _decorator(serializer: typing.Type[Serializer]):
328
+ class ModelIdSerializer(serializer): # type: ignore
329
+ def __init__(self, *args, **kwargs):
330
+ for param, model_path in model_paths:
331
+ content = kwargs.get("data", {}).get(param)
332
+ values = content if isinstance(content, list) else [content]
333
+ model = pydoc.locate(model_path)
334
+
335
+ if any([isinstance(val, dict) and "id" in val for val in values]):
336
+ new_content = []
337
+ for value in values:
338
+ if (
339
+ isinstance(value, dict)
340
+ and ("id" in value)
341
+ and (model is not None)
342
+ ):
343
+ data = model_to_dict(model.objects.get(pk=value["id"]))
344
+
345
+ for field, field_data in data.items():
346
+ if isinstance(field_data, list):
347
+ data[field] = [
348
+ (
349
+ model_to_dict(item)
350
+ if hasattr(item, "_meta")
351
+ else item
352
+ )
353
+ for item in field_data
354
+ ]
355
+
356
+ if hasattr(field_data, "_meta"):
357
+ data[field] = model_to_dict(field_data)
358
+
359
+ ("id" in data) and data.pop("id")
360
+ new_content.append(data)
361
+
362
+ kwargs.update(
363
+ data={
364
+ **kwargs["data"],
365
+ param: (
366
+ new_content
367
+ if isinstance(content, list)
368
+ else next(iter(new_content))
369
+ ),
370
+ }
371
+ )
372
+
373
+ super().__init__(*args, **kwargs)
374
+
375
+ return type(serializer.__name__, (ModelIdSerializer,), {})
376
+
377
+ return _decorator
378
+
379
+
380
+ def make_fields_optional(serializer: typing.Type[ModelSerializer]):
381
+ _name = f"Partial{serializer.__name__}"
382
+
383
+ class _Meta(serializer.Meta): # type: ignore
384
+ extra_kwargs = {
385
+ **getattr(serializer.Meta, "extra_kwargs", {}),
386
+ **{
387
+ field.name: {"required": False}
388
+ for field in serializer.Meta.model._meta.fields
389
+ },
390
+ }
391
+
392
+ return type(_name, (serializer,), dict(Meta=_Meta))
393
+
394
+
395
+ def exclude_id_field(serializer: typing.Type[ModelSerializer]):
396
+ class _Meta(serializer.Meta): # type: ignore
397
+ exclude = [*getattr(serializer.Meta, "exclude", []), "id"]
398
+
399
+ return type(serializer.__name__, (serializer,), dict(Meta=_Meta))
400
+
401
+
402
+ def is_field_optional(model, field_name: str) -> bool:
403
+ field = getattr(model, field_name)
404
+
405
+ if hasattr(field, "field"):
406
+ return field.field.null
407
+
408
+ return False
409
+
410
+
411
+ def process_dictionaries_mutations(
412
+ keys: typing.List[str], payload: dict, entity
413
+ ) -> dict:
414
+ """This function checks if the payload contains dictionary with the keys and if so, it
415
+ mutate the values content by removing any null values and adding the new one.
416
+ """
417
+ data = payload.copy()
418
+
419
+ for key in [k for k in keys if k in payload]:
420
+ value = lib.to_dict({**getattr(entity, key, {}), **payload.get(key, {})})
421
+ data.update({key: value})
422
+
423
+ return data
424
+
425
+
426
+ def get_query_flag(
427
+ key: str,
428
+ query_params: dict,
429
+ nullable: bool = True,
430
+ ) -> typing.Optional[bool]:
431
+ _value = yaml.safe_load(query_params.get(key) or "")
432
+
433
+ if key in query_params and _value is not False:
434
+ return True
435
+
436
+ if nullable:
437
+ return _value
438
+
439
+ return False
440
+
441
+
442
+ def field_to_serializer(args: dict):
443
+ [type, name, required, default, enum] = [
444
+ args.get("type"),
445
+ args.get("name"),
446
+ args.get("required"),
447
+ args.get("default"),
448
+ args.get("enum"),
449
+ ]
450
+
451
+ if enum:
452
+ return serializers.ChoiceField(
453
+ choices=enum,
454
+ required=required,
455
+ help_text=f"Indicates a {name} {type}",
456
+ )
457
+ if type == "string":
458
+ return serializers.CharField(
459
+ required=required,
460
+ **(dict(default=default) if not required else {}),
461
+ )
462
+ if type == "integer":
463
+ return serializers.IntegerField(
464
+ required=required,
465
+ **(dict(default=default) if not required else {}),
466
+ )
467
+ if type == "boolean":
468
+ return serializers.BooleanField(
469
+ required=required,
470
+ **(dict(default=default) if not required else {}),
471
+ )
472
+ if type == "float":
473
+ return serializers.FloatField(
474
+ required=required,
475
+ **(dict(default=default) if not required else {}),
476
+ )
477
+ if type == "datetime":
478
+ return serializers.DateTimeField(
479
+ required=required,
480
+ **(dict(default=default) if not required else {}),
481
+ )
482
+ if type == "date":
483
+ return serializers.DateField(
484
+ required=required,
485
+ **(dict(default=default) if not required else {}),
486
+ )
487
+ if type == "decimal":
488
+ return serializers.DecimalField(
489
+ required=required,
490
+ **(dict(default=default) if not required else {}),
491
+ )
492
+ if type == "uuid":
493
+ return serializers.UUIDField(
494
+ required=required,
495
+ **(dict(default=default) if not required else {}),
496
+ )
497
+ if type == "email":
498
+ return serializers.EmailField(
499
+ required=required,
500
+ **(dict(default=default) if not required else {}),
501
+ )
502
+ if type == "url":
503
+ return serializers.URLField(
504
+ required=required,
505
+ **(dict(default=default) if not required else {}),
506
+ )
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"
@@ -0,0 +1,41 @@
1
+ # Generated by Django 3.2.13 on 2022-06-28 00:44
2
+
3
+ from django.conf import settings
4
+ from django.db import migrations, models
5
+ import django.db.models.deletion
6
+ import functools
7
+ import karrio.core.utils.helpers
8
+ import karrio.server.core.models.base
9
+
10
+
11
+ class Migration(migrations.Migration):
12
+
13
+ initial = True
14
+
15
+ dependencies = [
16
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
17
+ ]
18
+
19
+ operations = [
20
+ migrations.CreateModel(
21
+ name='TracingRecord',
22
+ fields=[
23
+ ('created_at', models.DateTimeField(auto_now_add=True)),
24
+ ('updated_at', models.DateTimeField(auto_now=True)),
25
+ ('id', models.CharField(default=functools.partial(karrio.server.core.models.base.uuid, *(), **{'prefix': 'trace_'}), editable=False, max_length=50, primary_key=True, serialize=False)),
26
+ ('key', models.CharField(max_length=50)),
27
+ ('record', models.JSONField(default=functools.partial(karrio.core.utils.helpers.identity, *(), **{'value': {}}), help_text='Record data')),
28
+ ('timestamp', models.FloatField()),
29
+ ('meta', models.JSONField(blank=True, default=functools.partial(karrio.core.utils.helpers.identity, *(), **{'value': {}}), help_text='Readonly Context metadata use for filtering and premission check', null=True)),
30
+ ('test_mode', models.BooleanField()),
31
+ ('created_by', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
32
+ ],
33
+ options={
34
+ 'verbose_name': 'Tracing Record',
35
+ 'verbose_name_plural': 'Tracing Records',
36
+ 'db_table': 'tracing-record',
37
+ 'ordering': ['-created_at'],
38
+ },
39
+ bases=(karrio.server.core.models.base.ControlledAccessModel, models.Model),
40
+ ),
41
+ ]
@@ -0,0 +1,22 @@
1
+ # Generated by Django 3.2.13 on 2022-07-10 13:07
2
+
3
+ from django.db import migrations, models
4
+ import django.db.models.fields.json
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ('tracing', '0001_initial'),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.AddIndex(
15
+ model_name='tracingrecord',
16
+ index=models.Index(django.db.models.fields.json.KeyTextTransform('object_id', 'meta'), condition=models.Q(('meta__object_id__isnull', False)), name='trace_object_idx'),
17
+ ),
18
+ migrations.AddIndex(
19
+ model_name='tracingrecord',
20
+ index=models.Index(django.db.models.fields.json.KeyTextTransform('request_log_id', 'meta'), condition=models.Q(('meta__request_log_id__isnull', False)), name='request_log_idx'),
21
+ ),
22
+ ]
@@ -0,0 +1,43 @@
1
+ # Generated by Django 3.2.16 on 2022-11-05 03:17
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ def forwards_func(apps, schema_editor):
7
+ db_alias = schema_editor.connection.alias
8
+ TracingRecord = apps.get_model("tracing", "TracingRecord")
9
+
10
+ duplicates = TracingRecord.objects.using(db_alias)\
11
+ .values('meta__request_log_id')\
12
+ .annotate(count=models.Count('id'))\
13
+ .values('meta__request_log_id')\
14
+ .order_by().filter(count__gt=1)
15
+
16
+ for value in duplicates:
17
+ dups = TracingRecord.objects.using(db_alias)\
18
+ .filter(meta__request_log_id=value['meta__request_log_id'])
19
+
20
+ if dups.filter(key='response').exists():
21
+ dups.filter(key='response').exclude(
22
+ id__in=[dups.filter(key='response').first().id]
23
+ ).delete()
24
+
25
+ if dups.filter(key='request').exists():
26
+ dups.filter(key='request').exclude(
27
+ id__in=[dups.filter(key='request').first().id],
28
+ ).delete()
29
+
30
+
31
+
32
+ def reverse_func(apps, schema_editor):
33
+ pass
34
+
35
+ class Migration(migrations.Migration):
36
+
37
+ dependencies = [
38
+ ('tracing', '0002_auto_20220710_1307'),
39
+ ]
40
+
41
+ operations = [
42
+ migrations.RunPython(forwards_func, reverse_func),
43
+ ]