karrio-server-manager 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.
Files changed (102) hide show
  1. karrio/server/manager/__init__.py +1 -0
  2. karrio/server/manager/admin.py +1 -0
  3. karrio/server/manager/apps.py +13 -0
  4. karrio/server/manager/migrations/0001_initial.py +1358 -0
  5. karrio/server/manager/migrations/0002_auto_20201127_0721.py +61 -0
  6. karrio/server/manager/migrations/0003_auto_20201230_0820.py +34 -0
  7. karrio/server/manager/migrations/0004_auto_20210125_2125.py +18 -0
  8. karrio/server/manager/migrations/0005_auto_20210216_0758.py +27 -0
  9. karrio/server/manager/migrations/0006_auto_20210307_0438.py +24 -0
  10. karrio/server/manager/migrations/0006_auto_20210308_0302.py +53 -0
  11. karrio/server/manager/migrations/0007_merge_20210311_1428.py +14 -0
  12. karrio/server/manager/migrations/0008_remove_shipment_doc_images.py +17 -0
  13. karrio/server/manager/migrations/0009_auto_20210326_1425.py +28 -0
  14. karrio/server/manager/migrations/0010_auto_20210403_1404.py +28 -0
  15. karrio/server/manager/migrations/0011_auto_20210426_1924.py +48 -0
  16. karrio/server/manager/migrations/0012_auto_20210427_1319.py +24 -0
  17. karrio/server/manager/migrations/0013_customs_invoice_date.py +18 -0
  18. karrio/server/manager/migrations/0014_auto_20210515_0928.py +24 -0
  19. karrio/server/manager/migrations/0015_auto_20210601_0340.py +182 -0
  20. karrio/server/manager/migrations/0016_shipment_archived.py +18 -0
  21. karrio/server/manager/migrations/0017_auto_20210629_1650.py +22 -0
  22. karrio/server/manager/migrations/0018_auto_20210705_1049.py +23 -0
  23. karrio/server/manager/migrations/0019_auto_20210722_1131.py +43 -0
  24. karrio/server/manager/migrations/0020_tracking_messages.py +20 -0
  25. karrio/server/manager/migrations/0021_tracking_estimated_delivery.py +18 -0
  26. karrio/server/manager/migrations/0022_auto_20211122_2100.py +53 -0
  27. karrio/server/manager/migrations/0023_auto_20211227_2141.py +118 -0
  28. karrio/server/manager/migrations/0024_alter_parcel_items.py +18 -0
  29. karrio/server/manager/migrations/0025_auto_20220113_1158.py +25 -0
  30. karrio/server/manager/migrations/0026_parcel_reference_number.py +18 -0
  31. karrio/server/manager/migrations/0027_custom_migration_2021_1.py +47 -0
  32. karrio/server/manager/migrations/0028_auto_20220303_1153.py +39 -0
  33. karrio/server/manager/migrations/0029_auto_20220303_1249.py +55 -0
  34. karrio/server/manager/migrations/0030_alter_shipment_status.py +44 -0
  35. karrio/server/manager/migrations/0031_shipment_invoice.py +34 -0
  36. karrio/server/manager/migrations/0032_custom_migration_2022_3.py +26 -0
  37. karrio/server/manager/migrations/0033_auto_20220504_1335.py +57 -0
  38. karrio/server/manager/migrations/0034_commodity_hs_code.py +18 -0
  39. karrio/server/manager/migrations/0035_parcel_options.py +26 -0
  40. karrio/server/manager/migrations/0036_alter_tracking_shipment.py +24 -0
  41. karrio/server/manager/migrations/0037_auto_20220710_1350.py +28 -0
  42. karrio/server/manager/migrations/0038_alter_tracking_status.py +18 -0
  43. karrio/server/manager/migrations/0039_documentuploadrecord.py +43 -0
  44. karrio/server/manager/migrations/0040_parcel_freight_class.py +18 -0
  45. karrio/server/manager/migrations/0041_alter_commodity_options_alter_parcel_options.py +29 -0
  46. karrio/server/manager/migrations/0042_remove_shipment_shipment_tracking_number_idx_and_more.py +658 -0
  47. karrio/server/manager/migrations/0043_customs_duty_billing_address_and_more.py +62 -0
  48. karrio/server/manager/migrations/0044_address_address_line1_temp_and_more.py +326 -0
  49. karrio/server/manager/migrations/0045_alter_customs_duty_billing_address_and_more.py +45 -0
  50. karrio/server/manager/migrations/0046_auto_20230114_0930.py +78 -0
  51. karrio/server/manager/migrations/0047_remove_shipment_shipment_tracking_number_idx_and_more.py +595 -0
  52. karrio/server/manager/migrations/0048_commodity_title_alter_commodity_description_and_more.py +53 -0
  53. karrio/server/manager/migrations/0049_auto_20230318_0708.py +39 -0
  54. karrio/server/manager/migrations/0050_address_street_number_tracking_account_number_and_more.py +60 -0
  55. karrio/server/manager/migrations/0051_auto_20230330_0556.py +56 -0
  56. karrio/server/manager/migrations/0052_auto_20230520_0811.py +35 -0
  57. karrio/server/manager/migrations/0053_alter_commodity_weight_unit_alter_parcel_weight_unit.py +32 -0
  58. karrio/server/manager/migrations/0054_alter_address_company_name_alter_address_person_name.py +22 -0
  59. karrio/server/manager/migrations/0055_alter_tracking_status.py +32 -0
  60. karrio/server/manager/migrations/0056_tracking_delivery_image_tracking_signature_image.py +22 -0
  61. karrio/server/manager/migrations/0057_alter_customs_invoice_date.py +18 -0
  62. karrio/server/manager/migrations/0058_manifest_shipment_manifest.py +124 -0
  63. karrio/server/manager/migrations/0059_shipment_return_address.py +24 -0
  64. karrio/server/manager/migrations/0060_pickup_meta_alter_address_country_code_and_more.py +527 -0
  65. karrio/server/manager/migrations/0061_alter_customs_incoterm.py +37 -0
  66. karrio/server/manager/migrations/0062_alter_tracking_status.py +35 -0
  67. karrio/server/manager/migrations/__init__.py +0 -0
  68. karrio/server/manager/models.py +984 -0
  69. karrio/server/manager/router.py +3 -0
  70. karrio/server/manager/serializers/__init__.py +50 -0
  71. karrio/server/manager/serializers/address.py +82 -0
  72. karrio/server/manager/serializers/commodity.py +51 -0
  73. karrio/server/manager/serializers/customs.py +84 -0
  74. karrio/server/manager/serializers/document.py +113 -0
  75. karrio/server/manager/serializers/manifest.py +85 -0
  76. karrio/server/manager/serializers/parcel.py +84 -0
  77. karrio/server/manager/serializers/pickup.py +285 -0
  78. karrio/server/manager/serializers/rate.py +19 -0
  79. karrio/server/manager/serializers/shipment.py +869 -0
  80. karrio/server/manager/serializers/tracking.py +250 -0
  81. karrio/server/manager/signals.py +70 -0
  82. karrio/server/manager/tests/__init__.py +10 -0
  83. karrio/server/manager/tests/test_addresses.py +110 -0
  84. karrio/server/manager/tests/test_custom_infos.py +97 -0
  85. karrio/server/manager/tests/test_parcels.py +104 -0
  86. karrio/server/manager/tests/test_pickups.py +345 -0
  87. karrio/server/manager/tests/test_shipments.py +833 -0
  88. karrio/server/manager/tests/test_trackers.py +215 -0
  89. karrio/server/manager/urls.py +10 -0
  90. karrio/server/manager/views/__init__.py +9 -0
  91. karrio/server/manager/views/addresses.py +154 -0
  92. karrio/server/manager/views/customs.py +159 -0
  93. karrio/server/manager/views/documents.py +131 -0
  94. karrio/server/manager/views/manifests.py +160 -0
  95. karrio/server/manager/views/parcels.py +155 -0
  96. karrio/server/manager/views/pickups.py +182 -0
  97. karrio/server/manager/views/shipments.py +335 -0
  98. karrio/server/manager/views/trackers.py +364 -0
  99. karrio_server_manager-2025.5rc1.dist-info/METADATA +28 -0
  100. karrio_server_manager-2025.5rc1.dist-info/RECORD +102 -0
  101. karrio_server_manager-2025.5rc1.dist-info/WHEEL +5 -0
  102. karrio_server_manager-2025.5rc1.dist-info/top_level.txt +2 -0
@@ -0,0 +1,335 @@
1
+ import io
2
+ import base64
3
+ import logging
4
+
5
+ from rest_framework import status
6
+ from rest_framework.request import Request
7
+ from rest_framework.response import Response
8
+ from rest_framework.pagination import LimitOffsetPagination
9
+ from django_filters.rest_framework import DjangoFilterBackend
10
+ from django_downloadview import VirtualDownloadView
11
+ from django.core.files.base import ContentFile
12
+ from django.urls import path, re_path
13
+
14
+ import karrio.lib as lib
15
+ import karrio.server.openapi as openapi
16
+ import karrio.server.core.filters as filters
17
+ import karrio.server.manager.models as models
18
+ from karrio.server.core.views.api import GenericAPIView, APIView
19
+ from karrio.server.core.filters import ShipmentFilters
20
+ from karrio.server.manager.router import router
21
+ from karrio.server.manager.serializers import (
22
+ process_dictionaries_mutations,
23
+ fetch_shipment_rates,
24
+ PaginatedResult,
25
+ ErrorResponse,
26
+ ErrorMessages,
27
+ Shipment,
28
+ ShipmentData,
29
+ buy_shipment_label,
30
+ can_mutate_shipment,
31
+ ShipmentSerializer,
32
+ ShipmentRateData,
33
+ ShipmentUpdateData,
34
+ ShipmentPurchaseData,
35
+ ShipmentCancelSerializer,
36
+ )
37
+
38
+ ENDPOINT_ID = "$$$$$" # This endpoint id is used to make operation ids unique make sure not to duplicate
39
+ logger = logging.getLogger(__name__)
40
+ Shipments = PaginatedResult("ShipmentList", Shipment)
41
+
42
+
43
+ class ShipmentList(GenericAPIView):
44
+ throttle_scope = "carrier_request"
45
+ pagination_class = type(
46
+ "CustomPagination", (LimitOffsetPagination,), dict(default_limit=20)
47
+ )
48
+ filter_backends = (DjangoFilterBackend,)
49
+ filterset_class = ShipmentFilters
50
+ serializer_class = Shipments
51
+ model = models.Shipment
52
+
53
+ @openapi.extend_schema(
54
+ tags=["Shipments"],
55
+ operation_id=f"{ENDPOINT_ID}list",
56
+ extensions={"x-operationId": "listShipments"},
57
+ summary="List all shipments",
58
+ parameters=filters.ShipmentFilters.parameters,
59
+ responses={
60
+ 200: Shipment(),
61
+ 404: ErrorResponse(),
62
+ 500: ErrorResponse(),
63
+ },
64
+ )
65
+ def get(self, _: Request):
66
+ """
67
+ Retrieve all shipments.
68
+ """
69
+ shipments = self.filter_queryset(self.get_queryset())
70
+ response = self.paginate_queryset(Shipment(shipments, many=True).data)
71
+
72
+ return self.get_paginated_response(response)
73
+
74
+ @openapi.extend_schema(
75
+ tags=["Shipments"],
76
+ operation_id=f"{ENDPOINT_ID}create",
77
+ extensions={"x-operationId": "createShipment"},
78
+ summary="Create a shipment",
79
+ responses={
80
+ 201: Shipment(),
81
+ 400: ErrorResponse(),
82
+ 424: ErrorMessages(),
83
+ 500: ErrorResponse(),
84
+ },
85
+ request=ShipmentData(),
86
+ )
87
+ def post(self, request: Request):
88
+ """
89
+ Create a new shipment instance.
90
+ """
91
+ shipment = (
92
+ ShipmentSerializer.map(data=request.data, context=request).save().instance
93
+ )
94
+
95
+ return Response(Shipment(shipment).data, status=status.HTTP_201_CREATED)
96
+
97
+
98
+ class ShipmentDetails(APIView):
99
+ throttle_scope = "carrier_request"
100
+
101
+ @openapi.extend_schema(
102
+ tags=["Shipments"],
103
+ operation_id=f"{ENDPOINT_ID}retrieve",
104
+ extensions={"x-operationId": "retrieveShipment"},
105
+ summary="Retrieve a shipment",
106
+ responses={
107
+ 200: Shipment(),
108
+ 404: ErrorResponse(),
109
+ 500: ErrorResponse(),
110
+ },
111
+ )
112
+ def get(self, request: Request, pk: str):
113
+ """
114
+ Retrieve a shipment.
115
+ """
116
+ shipment = models.Shipment.access_by(request).get(pk=pk)
117
+
118
+ return Response(Shipment(shipment).data)
119
+
120
+ @openapi.extend_schema(
121
+ tags=["Shipments"],
122
+ operation_id=f"{ENDPOINT_ID}update",
123
+ extensions={"x-operationId": "updateShipment"},
124
+ summary="Update a shipment",
125
+ responses={
126
+ 200: Shipment(),
127
+ 404: ErrorResponse(),
128
+ 400: ErrorResponse(),
129
+ 409: ErrorResponse(),
130
+ 424: ErrorMessages(),
131
+ 500: ErrorResponse(),
132
+ },
133
+ request=ShipmentUpdateData(),
134
+ )
135
+ def put(self, request: Request, pk: str):
136
+ """
137
+ This operation allows for updating properties of a shipment including `label_type`, `reference`, `payment`, `options` and `metadata`.
138
+ It is not for editing the parcels of a shipment.
139
+ """
140
+ shipment = models.Shipment.access_by(request).get(pk=pk)
141
+ payload = ShipmentUpdateData.map(data=request.data).data
142
+ can_mutate_shipment(shipment, update=True, payload=request.data)
143
+
144
+ update = (
145
+ ShipmentSerializer.map(
146
+ shipment,
147
+ context=request,
148
+ data=process_dictionaries_mutations(
149
+ ["metadata", "options"], payload, shipment
150
+ ),
151
+ )
152
+ .save()
153
+ .instance
154
+ )
155
+
156
+ return Response(Shipment(update).data)
157
+
158
+
159
+ class ShipmentCancel(APIView):
160
+ throttle_scope = "carrier_request"
161
+
162
+ @openapi.extend_schema(
163
+ tags=["Shipments"],
164
+ operation_id=f"{ENDPOINT_ID}cancel",
165
+ extensions={"x-operationId": "cancelShipment"},
166
+ summary="Cancel a shipment",
167
+ request=None,
168
+ responses={
169
+ 200: Shipment(),
170
+ 404: ErrorResponse(),
171
+ 400: ErrorResponse(),
172
+ 409: ErrorResponse(),
173
+ 424: ErrorMessages(),
174
+ 500: ErrorResponse(),
175
+ },
176
+ )
177
+ def post(self, request: Request, pk: str):
178
+ """
179
+ Void a shipment with the associated label.
180
+ """
181
+ shipment = models.Shipment.access_by(request).get(pk=pk)
182
+ can_mutate_shipment(shipment, delete=True)
183
+
184
+ update = ShipmentCancelSerializer.map(shipment, context=request).save().instance
185
+
186
+ return Response(Shipment(update).data)
187
+
188
+
189
+ class ShipmentRates(APIView):
190
+ throttle_scope = "carrier_request"
191
+
192
+ @openapi.extend_schema(
193
+ tags=["Shipments"],
194
+ operation_id=f"{ENDPOINT_ID}rates",
195
+ summary="Fetch new shipment rates",
196
+ responses={
197
+ 200: Shipment(),
198
+ 404: ErrorResponse(),
199
+ 400: ErrorResponse(),
200
+ 409: ErrorResponse(),
201
+ 424: ErrorMessages(),
202
+ 500: ErrorResponse(),
203
+ },
204
+ request=ShipmentRateData(),
205
+ )
206
+ def post(self, request: Request, pk: str):
207
+ """
208
+ Refresh the list of the shipment rates
209
+ """
210
+ shipment = models.Shipment.access_by(request).get(pk=pk)
211
+ can_mutate_shipment(shipment, update=True)
212
+
213
+ payload = ShipmentRateData.map(data=request.data).data
214
+
215
+ update = fetch_shipment_rates(
216
+ shipment,
217
+ context=request,
218
+ data=process_dictionaries_mutations(
219
+ ["metadata", "options"], payload, shipment
220
+ ),
221
+ )
222
+
223
+ return Response(Shipment(update).data)
224
+
225
+
226
+ class ShipmentPurchase(APIView):
227
+ throttle_scope = "carrier_request"
228
+
229
+ @openapi.extend_schema(
230
+ tags=["Shipments"],
231
+ operation_id=f"{ENDPOINT_ID}purchase",
232
+ summary="Buy a shipment label",
233
+ responses={
234
+ 200: Shipment(),
235
+ 404: ErrorResponse(),
236
+ 400: ErrorResponse(),
237
+ 409: ErrorResponse(),
238
+ 424: ErrorMessages(),
239
+ 500: ErrorResponse(),
240
+ },
241
+ request=ShipmentPurchaseData(),
242
+ )
243
+ def post(self, request: Request, pk: str):
244
+ """
245
+ Select your preferred rates to buy a shipment label.
246
+ """
247
+ shipment = models.Shipment.access_by(request).get(pk=pk)
248
+ can_mutate_shipment(shipment, purchase=True, update=True)
249
+
250
+ payload = ShipmentPurchaseData.map(data=request.data).data
251
+
252
+ update = buy_shipment_label(
253
+ shipment,
254
+ context=request,
255
+ data=process_dictionaries_mutations(["metadata"], payload, shipment),
256
+ )
257
+
258
+ return Response(Shipment(update).data)
259
+
260
+
261
+ class ShipmentDocs(VirtualDownloadView):
262
+ @openapi.extend_schema(exclude=True)
263
+ def get(
264
+ self,
265
+ request: Request,
266
+ pk: str,
267
+ doc: str = "label",
268
+ format: str = "pdf",
269
+ **kwargs,
270
+ ):
271
+ """Retrieve a shipment label."""
272
+ self.shipment = models.Shipment.objects.get(pk=pk, label__isnull=False)
273
+ self.document = getattr(self.shipment, doc, None)
274
+ self.name = f"{doc}_{self.shipment.tracking_number}.{format}"
275
+
276
+ query_params = request.GET.dict()
277
+ self.preview = "preview" in query_params
278
+ self.attachment = "download" in query_params
279
+
280
+ response = super(ShipmentDocs, self).get(request, pk, doc, format, **kwargs)
281
+ response["X-Frame-Options"] = "ALLOWALL"
282
+ return response
283
+
284
+ def get_file(self):
285
+ content = base64.b64decode(self.document or "")
286
+ buffer = io.BytesIO()
287
+
288
+ if self.preview and "ZPL" in self.shipment.label_type or "":
289
+ width, height, dpmm = (4, 6, 12)
290
+
291
+ if "8" in self.shipment.label_type:
292
+ width, height, dpmm = (8, 4, 12)
293
+
294
+ _label = lib.failsafe(
295
+ lambda: lib.zpl_to_pdf(
296
+ self.document,
297
+ width,
298
+ height,
299
+ dpmm=dpmm,
300
+ )
301
+ )
302
+
303
+ if _label is not None:
304
+ content = base64.b64decode(_label)
305
+ self.name = self.name.replace("zpl", "pdf")
306
+
307
+ buffer.write(content)
308
+
309
+ return ContentFile(buffer.getvalue(), name=self.name)
310
+
311
+
312
+ router.urls.append(path("shipments", ShipmentList.as_view(), name="shipment-list"))
313
+ router.urls.append(
314
+ path("shipments/<str:pk>", ShipmentDetails.as_view(), name="shipment-details")
315
+ )
316
+ router.urls.append(
317
+ path("shipments/<str:pk>/cancel", ShipmentCancel.as_view(), name="shipment-cancel")
318
+ )
319
+ router.urls.append(
320
+ path("shipments/<str:pk>/rates", ShipmentRates.as_view(), name="shipment-rates")
321
+ )
322
+ router.urls.append(
323
+ path(
324
+ "shipments/<str:pk>/purchase",
325
+ ShipmentPurchase.as_view(),
326
+ name="shipment-purchase",
327
+ )
328
+ )
329
+ router.urls.append(
330
+ re_path(
331
+ r"^shipments/(?P<pk>\w+)/(?P<doc>[a-z0-9]+).(?P<format>[a-z0-9]+)",
332
+ ShipmentDocs.as_view(),
333
+ name="shipment-docs",
334
+ )
335
+ )
@@ -0,0 +1,364 @@
1
+ import io
2
+ import base64
3
+ import logging
4
+ import django_downloadview
5
+
6
+ from django.db.models import Q
7
+ from django.urls import path, re_path
8
+ from django_filters.rest_framework import DjangoFilterBackend
9
+ from rest_framework.permissions import IsAuthenticatedOrReadOnly
10
+ from rest_framework.pagination import LimitOffsetPagination
11
+ from django.core.files.base import ContentFile
12
+ from rest_framework.response import Response
13
+ from rest_framework.request import Request
14
+ from rest_framework import status
15
+
16
+ from karrio.server.core.views.api import GenericAPIView, APIView
17
+ from karrio.server.manager.router import router
18
+ import karrio.server.manager.serializers as serializers
19
+ import karrio.server.core.dataunits as dataunits
20
+ import karrio.server.manager.models as models
21
+ import karrio.server.core.filters as filters
22
+ import karrio.server.openapi as openapi
23
+
24
+ ENDPOINT_ID = "$$$$$$" # This endpoint id is used to make operation ids unique make sure not to duplicate
25
+ logger = logging.getLogger(__name__)
26
+ Trackers = serializers.PaginatedResult("TrackerList", serializers.TrackingStatus)
27
+
28
+
29
+ class TrackerList(GenericAPIView):
30
+ throttle_scope = "carrier_request"
31
+ pagination_class = type(
32
+ "CustomPagination", (LimitOffsetPagination,), dict(default_limit=20)
33
+ )
34
+ filter_backends = (DjangoFilterBackend,)
35
+ filterset_class = filters.TrackerFilters
36
+ serializer_class = Trackers
37
+ model = models.Tracking
38
+
39
+ def get_queryset(self):
40
+ queryset = super().get_queryset()
41
+ _filters = tuple()
42
+ query_params = getattr(self.request, "query_params", {})
43
+ carrier_name = query_params.get("carrier_name")
44
+
45
+ if carrier_name is not None:
46
+ _filters += (Q(tracking_carrier__carrier_code=carrier_name),)
47
+
48
+ return queryset.filter(*_filters)
49
+
50
+ @openapi.extend_schema(
51
+ tags=["Trackers"],
52
+ operation_id=f"{ENDPOINT_ID}list",
53
+ extensions={"x-operationId": "listTrackers"},
54
+ summary="List all package trackers",
55
+ parameters=filters.TrackerFilters.parameters,
56
+ responses={
57
+ 200: Trackers(),
58
+ 404: serializers.ErrorResponse(),
59
+ 500: serializers.ErrorResponse(),
60
+ },
61
+ )
62
+ def get(self, request: Request):
63
+ """
64
+ Retrieve all shipment trackers.
65
+ """
66
+ trackers = self.filter_queryset(self.get_queryset())
67
+ response = self.paginate_queryset(
68
+ serializers.TrackingStatus(trackers, many=True).data
69
+ )
70
+ return self.get_paginated_response(response)
71
+
72
+ @openapi.extend_schema(
73
+ tags=["Trackers"],
74
+ operation_id=f"{ENDPOINT_ID}add",
75
+ extensions={"x-operationId": "addTracker"},
76
+ summary="Add a package tracker",
77
+ request=serializers.TrackingData(),
78
+ responses={
79
+ 200: serializers.TrackingStatus(),
80
+ 400: serializers.ErrorResponse(),
81
+ 424: serializers.ErrorMessages(),
82
+ 500: serializers.ErrorResponse(),
83
+ },
84
+ parameters=[
85
+ openapi.OpenApiParameter(
86
+ "hub",
87
+ location=openapi.OpenApiParameter.QUERY,
88
+ type=openapi.OpenApiTypes.STR,
89
+ required=False,
90
+ ),
91
+ openapi.OpenApiParameter(
92
+ "pending_pickup",
93
+ location=openapi.OpenApiParameter.QUERY,
94
+ type=openapi.OpenApiTypes.BOOL,
95
+ required=False,
96
+ description=(
97
+ "Add this flag to add the tracker whether the tracking info exist or not."
98
+ "When the package is eventually picked up, the tracker with capture real time updates."
99
+ ),
100
+ ),
101
+ ],
102
+ )
103
+ def post(self, request: Request):
104
+ """
105
+ This API creates or retrieves (if existent) a tracking status object containing the
106
+ details and events of a shipping in progress.
107
+ """
108
+ query = request.query_params
109
+ serializer = serializers.TrackingData(data=request.data)
110
+ serializer.is_valid(raise_exception=True)
111
+ data = serializer.validated_data
112
+
113
+ carrier_name = query.get("hub") if "hub" in query else data["carrier_name"]
114
+ pending_pickup = serializers.get_query_flag(
115
+ "pending_pickup", query, nullable=False
116
+ )
117
+
118
+ instance = (
119
+ models.Tracking.access_by(request)
120
+ .filter(tracking_number=data["tracking_number"])
121
+ .first()
122
+ )
123
+
124
+ carrier_filter = {
125
+ **{k: v for k, v in query.items() if k != "hub"},
126
+ # If a hub is specified, use the hub as carrier to track the package
127
+ "carrier_name": carrier_name,
128
+ }
129
+ data = {
130
+ **data,
131
+ "tracking_number": data["tracking_number"],
132
+ "options": (
133
+ {data["tracking_number"]: {"carrier": data["carrier_name"]}}
134
+ if "hub" in query
135
+ else {}
136
+ ),
137
+ }
138
+
139
+ tracker = (
140
+ serializers.TrackingSerializer.map(instance, data=data, context=request)
141
+ .save(carrier_filter=carrier_filter, pending_pickup=pending_pickup)
142
+ .instance
143
+ )
144
+
145
+ return Response(
146
+ serializers.TrackingStatus(tracker).data,
147
+ status=status.HTTP_202_ACCEPTED,
148
+ )
149
+
150
+
151
+ class TrackersCreate(APIView):
152
+ throttle_scope = "carrier_request"
153
+
154
+ @openapi.extend_schema(
155
+ tags=["Trackers"],
156
+ operation_id=f"{ENDPOINT_ID}create",
157
+ extensions={"x-operationId": "createTracker"},
158
+ summary="Create a package tracker",
159
+ deprecated=True,
160
+ responses={
161
+ 200: serializers.TrackingStatus(),
162
+ 400: serializers.ErrorResponse(),
163
+ 424: serializers.ErrorMessages(),
164
+ 500: serializers.ErrorResponse(),
165
+ },
166
+ parameters=[
167
+ openapi.OpenApiParameter(
168
+ "tracking_number",
169
+ location=openapi.OpenApiParameter.PATH,
170
+ type=openapi.OpenApiTypes.STR,
171
+ required=True,
172
+ ),
173
+ openapi.OpenApiParameter(
174
+ "carrier_name",
175
+ location=openapi.OpenApiParameter.QUERY,
176
+ type=openapi.OpenApiTypes.STR,
177
+ enum=dataunits.NON_HUBS_CARRIERS,
178
+ required=True,
179
+ ),
180
+ openapi.OpenApiParameter(
181
+ "hub",
182
+ location=openapi.OpenApiParameter.QUERY,
183
+ type=openapi.OpenApiTypes.STR,
184
+ required=False,
185
+ ),
186
+ ],
187
+ request=None,
188
+ )
189
+ def get(self, request: Request, carrier_name: str, tracking_number: str):
190
+ """
191
+ This API creates or retrieves (if existent) a tracking status object containing the
192
+ details and events of a shipping in progress.
193
+ """
194
+ instance = (
195
+ models.Tracking.access_by(request)
196
+ .filter(tracking_number=tracking_number)
197
+ .first()
198
+ )
199
+
200
+ query = request.query_params
201
+ carrier_filter = {
202
+ **{k: v for k, v in query.items() if k != "hub"},
203
+ # If a hub is specified, use the hub as carrier to track the package
204
+ "carrier_name": (query.get("hub") if "hub" in query else carrier_name),
205
+ }
206
+ data = {
207
+ "tracking_number": tracking_number,
208
+ "options": (
209
+ {tracking_number: {"carrier": carrier_name}} if "hub" in query else {}
210
+ ),
211
+ }
212
+
213
+ tracker = (
214
+ serializers.TrackingSerializer.map(instance, data=data, context=request)
215
+ .save(carrier_filter=carrier_filter, pending_pickup=False)
216
+ .instance
217
+ )
218
+
219
+ return Response(
220
+ serializers.TrackingStatus(tracker).data,
221
+ status=status.HTTP_202_ACCEPTED,
222
+ )
223
+
224
+
225
+ class TrackersDetails(APIView):
226
+ throttle_scope = "carrier_request"
227
+ permission_classes = [IsAuthenticatedOrReadOnly]
228
+
229
+ @openapi.extend_schema(
230
+ tags=["Trackers"],
231
+ operation_id=f"{ENDPOINT_ID}retrieve",
232
+ extensions={"x-operationId": "retrieveTracker"},
233
+ summary="Retrieves a package tracker",
234
+ responses={
235
+ 200: serializers.TrackingStatus(),
236
+ 404: serializers.ErrorMessages(),
237
+ 500: serializers.ErrorResponse(),
238
+ },
239
+ )
240
+ def get(self, request: Request, id_or_tracking_number: str):
241
+ """
242
+ Retrieve a package tracker
243
+ """
244
+ __filter = Q(pk=id_or_tracking_number) | Q(
245
+ tracking_number=id_or_tracking_number
246
+ )
247
+ trackers = models.Tracking.objects.filter(__filter)
248
+
249
+ if len(trackers) == 0:
250
+ models.Tracking.objects.get(__filter)
251
+
252
+ return Response(serializers.TrackingStatus(trackers.first()).data)
253
+
254
+ @openapi.extend_schema(
255
+ tags=["Trackers"],
256
+ operation_id=f"{ENDPOINT_ID}update",
257
+ extensions={"x-operationId": "updateTracker"},
258
+ summary="Update tracker data",
259
+ responses={
260
+ 200: serializers.TrackingStatus(),
261
+ 404: serializers.ErrorResponse(),
262
+ 400: serializers.ErrorResponse(),
263
+ 409: serializers.ErrorResponse(),
264
+ 500: serializers.ErrorResponse(),
265
+ },
266
+ request=serializers.TrackerUpdateData(),
267
+ )
268
+ def put(self, request: Request, id_or_tracking_number: str):
269
+ tracker = models.Tracking.access_by(request).get(
270
+ Q(pk=id_or_tracking_number) | Q(tracking_number=id_or_tracking_number)
271
+ )
272
+ serializers.can_mutate_tracker(tracker, update=True, payload=request.data)
273
+
274
+ payload = serializers.TrackerUpdateData.map(data=request.data).data
275
+ update = (
276
+ serializers.TrackerUpdateData.map(
277
+ tracker,
278
+ context=request,
279
+ data=serializers.process_dictionaries_mutations(
280
+ ["metadata", "options", "info"], payload, tracker
281
+ ),
282
+ )
283
+ .save()
284
+ .instance
285
+ )
286
+
287
+ return Response(serializers.TrackingStatus(update).data)
288
+
289
+ @openapi.extend_schema(
290
+ tags=["Trackers"],
291
+ operation_id=f"{ENDPOINT_ID}remove",
292
+ extensions={"x-operationId": "removeTracker"},
293
+ summary="Discard a package tracker",
294
+ responses={
295
+ 200: serializers.TrackingStatus(),
296
+ 404: serializers.ErrorResponse(),
297
+ 500: serializers.ErrorResponse(),
298
+ },
299
+ )
300
+ def delete(self, request: Request, id_or_tracking_number: str):
301
+ """
302
+ Discard a package tracker.
303
+ """
304
+ tracker = models.Tracking.access_by(request).get(
305
+ Q(pk=id_or_tracking_number) | Q(tracking_number=id_or_tracking_number)
306
+ )
307
+
308
+ tracker.delete(keep_parents=True)
309
+
310
+ return Response(serializers.TrackingStatus(tracker).data)
311
+
312
+
313
+ class TrackerDocs(django_downloadview.VirtualDownloadView):
314
+ @openapi.extend_schema(exclude=True)
315
+ def get(
316
+ self,
317
+ request: Request,
318
+ pk: str,
319
+ doc: str = "delivery_image",
320
+ **kwargs,
321
+ ):
322
+ """Retrieve a tracker image."""
323
+ self.tracker = models.Tracker.objects.get(pk=pk)
324
+ self.image = getattr(self.tracker, doc)
325
+ self.name = f"{doc}_{self.tracker.tracking_number}"
326
+
327
+ query_params = request.GET.dict()
328
+ self.preview = "preview" in query_params
329
+ self.attachment = "download" in query_params
330
+
331
+ response = super(TrackerDocs, self).get(request, pk, doc, format, **kwargs)
332
+ response["X-Frame-Options"] = "ALLOWALL"
333
+ return response
334
+
335
+ def get_file(self):
336
+ content = base64.b64decode(self.image or "")
337
+ buffer = io.BytesIO()
338
+ buffer.write(content)
339
+
340
+ return ContentFile(buffer.getvalue(), name=self.name)
341
+
342
+
343
+ router.urls.append(path("trackers", TrackerList.as_view(), name="trackers-list"))
344
+ router.urls.append(
345
+ path(
346
+ "trackers/<str:id_or_tracking_number>",
347
+ TrackersDetails.as_view(),
348
+ name="tracker-details",
349
+ )
350
+ )
351
+ router.urls.append(
352
+ path(
353
+ "trackers/<str:carrier_name>/<str:tracking_number>",
354
+ TrackersCreate.as_view(),
355
+ name="shipment-tracker",
356
+ )
357
+ )
358
+ router.urls.append(
359
+ re_path(
360
+ r"^trackers/(?P<pk>\w+)/(?P<doc>[a-z0-9]+)",
361
+ TrackerDocs.as_view(),
362
+ name="tracker-docs",
363
+ )
364
+ )