karrio-server-manager 2025.5rc36__py3-none-any.whl → 2025.5.1__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.
@@ -48,6 +48,7 @@ from karrio.server.manager.serializers.customs import CustomsSerializer
48
48
  from karrio.server.manager.serializers.parcel import ParcelSerializer
49
49
  from karrio.server.manager.serializers.rate import RateSerializer
50
50
  import karrio.server.manager.models as models
51
+
51
52
  DEFAULT_CARRIER_FILTER: typing.Any = dict(active=True, capability="shipping")
52
53
 
53
54
 
@@ -288,7 +289,16 @@ class ShipmentSerializer(ShipmentData):
288
289
 
289
290
 
290
291
  class ShipmentPurchaseData(Serializer):
291
- selected_rate_id = CharField(required=True, help_text="The shipment selected rate.")
292
+ selected_rate_id = CharField(
293
+ required=False,
294
+ allow_null=True,
295
+ help_text="The shipment selected rate.",
296
+ )
297
+ service = CharField(
298
+ required=False,
299
+ allow_null=True,
300
+ help_text="The carrier service to use for the shipment (alternative to selected_rate_id).",
301
+ )
292
302
  label_type = ChoiceField(
293
303
  required=False,
294
304
  choices=LABEL_TYPES,
@@ -306,6 +316,15 @@ class ShipmentPurchaseData(Serializer):
306
316
  required=False, help_text="User metadata for the shipment"
307
317
  )
308
318
 
319
+ def validate(self, data):
320
+ if not data.get("selected_rate_id") and not data.get("service"):
321
+ raise exceptions.APIException(
322
+ "Either 'selected_rate_id' or 'service' must be provided.",
323
+ code="validation_error",
324
+ status_code=status.HTTP_400_BAD_REQUEST,
325
+ )
326
+ return data
327
+
309
328
 
310
329
  class ShipmentUpdateData(validators.OptionDefaultSerializer):
311
330
  label_type = ChoiceField(
@@ -792,9 +811,15 @@ def create_shipment_tracker(shipment: typing.Optional[models.Shipment], context)
792
811
  )
793
812
  tracker.save()
794
813
  link_org(tracker, context)
795
- logger.info("Successfully added a tracker to the shipment", shipment_id=shipment.id)
814
+ logger.info(
815
+ "Successfully added a tracker to the shipment", shipment_id=shipment.id
816
+ )
796
817
  except Exception as e:
797
- logger.exception("Failed to create new label tracker", error=str(e), shipment_id=shipment.id)
818
+ logger.exception(
819
+ "Failed to create new label tracker",
820
+ error=str(e),
821
+ shipment_id=shipment.id,
822
+ )
798
823
 
799
824
  # Update shipment tracking url if different from the current one
800
825
  try:
@@ -815,7 +840,12 @@ def create_shipment_tracker(shipment: typing.Optional[models.Shipment], context)
815
840
  shipment.tracking_url = tracking_url
816
841
  shipment.save(update_fields=["tracking_url"])
817
842
  except Exception as e:
818
- logger.exception("Failed to update shipment tracking url", error=str(e), shipment_id=shipment.id, tracking_number=shipment.tracking_number)
843
+ logger.exception(
844
+ "Failed to update shipment tracking url",
845
+ error=str(e),
846
+ shipment_id=shipment.id,
847
+ tracking_number=shipment.tracking_number,
848
+ )
819
849
 
820
850
 
821
851
  def generate_custom_invoice(template: str, shipment: models.Shipment, **kwargs):
@@ -843,7 +873,11 @@ def generate_custom_invoice(template: str, shipment: models.Shipment, **kwargs):
843
873
  shipment.invoice = document["doc_file"]
844
874
  shipment.save(update_fields=["invoice"])
845
875
 
846
- logger.info("Custom document successfully generated", shipment_id=shipment.id, template=template)
876
+ logger.info(
877
+ "Custom document successfully generated",
878
+ shipment_id=shipment.id,
879
+ template=template,
880
+ )
847
881
 
848
882
  return document
849
883
 
@@ -0,0 +1,88 @@
1
+ import json
2
+ from django.urls import reverse
3
+ from rest_framework import status
4
+ from karrio.server.core.tests import APITestCase
5
+
6
+
7
+ class TestNotFoundErrors(APITestCase):
8
+ def test_address_not_found_returns_resource_name(self):
9
+ url = reverse(
10
+ "karrio.server.manager:address-details",
11
+ kwargs=dict(pk="nonexistent_id"),
12
+ )
13
+ response = self.client.get(url)
14
+ response_data = json.loads(response.content)
15
+
16
+ self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
17
+ self.assertDictEqual(
18
+ response_data,
19
+ {"errors": [{"code": "not_found", "message": "Address not found"}]},
20
+ )
21
+
22
+ def test_parcel_not_found_returns_resource_name(self):
23
+ url = reverse(
24
+ "karrio.server.manager:parcel-details",
25
+ kwargs=dict(pk="nonexistent_id"),
26
+ )
27
+ response = self.client.get(url)
28
+ response_data = json.loads(response.content)
29
+
30
+ self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
31
+ self.assertDictEqual(
32
+ response_data,
33
+ {"errors": [{"code": "not_found", "message": "Parcel not found"}]},
34
+ )
35
+
36
+ def test_shipment_not_found_returns_resource_name(self):
37
+ url = reverse(
38
+ "karrio.server.manager:shipment-details",
39
+ kwargs=dict(pk="nonexistent_id"),
40
+ )
41
+ response = self.client.get(url)
42
+ response_data = json.loads(response.content)
43
+
44
+ self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
45
+ self.assertDictEqual(
46
+ response_data,
47
+ {"errors": [{"code": "not_found", "message": "Shipment not found"}]},
48
+ )
49
+
50
+ def test_customs_not_found_returns_resource_name(self):
51
+ url = reverse(
52
+ "karrio.server.manager:customs-details",
53
+ kwargs=dict(pk="nonexistent_id"),
54
+ )
55
+ response = self.client.get(url)
56
+ response_data = json.loads(response.content)
57
+
58
+ self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
59
+ self.assertDictEqual(
60
+ response_data,
61
+ {"errors": [{"code": "not_found", "message": "Customs not found"}]},
62
+ )
63
+
64
+
65
+ class TestValidationErrors(APITestCase):
66
+ def test_shipment_validation_error_format(self):
67
+ url = reverse("karrio.server.manager:shipment-list")
68
+ data = {
69
+ "shipper": {},
70
+ "recipient": {},
71
+ "parcels": [],
72
+ }
73
+ response = self.client.post(url, data, format="json")
74
+ response_data = json.loads(response.content)
75
+
76
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
77
+ self.assertIn("errors", response_data)
78
+ self.assertTrue(len(response_data["errors"]) > 0)
79
+
80
+ def test_address_validation_error_format(self):
81
+ url = reverse("karrio.server.manager:address-list")
82
+ data = {"country_code": "INVALID"}
83
+ response = self.client.post(url, data, format="json")
84
+ response_data = json.loads(response.content)
85
+
86
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
87
+ self.assertIn("errors", response_data)
88
+ self.assertTrue(len(response_data["errors"]) > 0)
@@ -831,3 +831,195 @@ CANCEL_PURCHASED_RESPONSE = {
831
831
  "label_url": None,
832
832
  "invoice_url": None,
833
833
  }
834
+
835
+
836
+ class TestShipmentPurchaseWithAlternativeServices(TestShipmentFixture):
837
+ def setUp(self) -> None:
838
+ super().setUp()
839
+ carrier = providers.Carrier.objects.get(carrier_id="canadapost")
840
+ # Rates have "canadapost_regular_parcel" but we'll request "canadapost_priority"
841
+ self.shipment.rates = [
842
+ {
843
+ "id": "rat_alt_service_test",
844
+ "carrier_id": "canadapost",
845
+ "carrier_name": "canadapost",
846
+ "currency": "CAD",
847
+ "estimated_delivery": None,
848
+ "extra_charges": [
849
+ {"amount": 50.00, "currency": "CAD", "name": "Base charge"},
850
+ ],
851
+ "service": "canadapost_regular_parcel",
852
+ "total_charge": 50.00,
853
+ "transit_days": 5,
854
+ "test_mode": True,
855
+ "meta": {
856
+ "rate_provider": "canadapost",
857
+ "service_name": "CANADAPOST REGULAR PARCEL",
858
+ "carrier_connection_id": carrier.pk,
859
+ },
860
+ }
861
+ ]
862
+ self.shipment.options = {"has_alternative_services": True}
863
+ self.shipment.save()
864
+
865
+ def test_purchase_with_alternative_service(self):
866
+ """
867
+ Test that when canadapost_priority is requested but only canadapost_regular_parcel
868
+ is in rates, the purchase proceeds with has_alternative_services=True,
869
+ delegating service resolution to the carrier.
870
+ """
871
+ url = reverse(
872
+ "karrio.server.manager:shipment-purchase",
873
+ kwargs=dict(pk=self.shipment.pk),
874
+ )
875
+ data = {"service": "canadapost_priority"}
876
+
877
+ with patch("karrio.server.core.gateway.utils.identity") as mock:
878
+ mock.return_value = CREATED_SHIPMENT_RESPONSE
879
+ response = self.client.post(url, data)
880
+ response_data = json.loads(response.content)
881
+
882
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
883
+ self.assertDictEqual(response_data, ALTERNATIVE_SERVICE_PURCHASED_SHIPMENT)
884
+
885
+
886
+ ALTERNATIVE_SERVICE_PURCHASED_SHIPMENT = {
887
+ "id": ANY,
888
+ "object_type": "shipment",
889
+ "tracking_url": "/v1/trackers/canadapost/123456789012",
890
+ "shipper": {
891
+ "id": ANY,
892
+ "postal_code": "E1C4Z8",
893
+ "city": "Moncton",
894
+ "federal_tax_id": None,
895
+ "state_tax_id": None,
896
+ "person_name": "John Poop",
897
+ "company_name": "A corp.",
898
+ "country_code": "CA",
899
+ "email": None,
900
+ "phone_number": "514 000 0000",
901
+ "state_code": "NB",
902
+ "street_number": None,
903
+ "residential": False,
904
+ "address_line1": "125 Church St",
905
+ "address_line2": None,
906
+ "validate_location": False,
907
+ "object_type": "address",
908
+ "validation": None,
909
+ },
910
+ "recipient": {
911
+ "id": ANY,
912
+ "postal_code": "V6M2V9",
913
+ "city": "Vancouver",
914
+ "federal_tax_id": None,
915
+ "state_tax_id": None,
916
+ "person_name": "Jane Doe",
917
+ "company_name": "B corp.",
918
+ "country_code": "CA",
919
+ "email": None,
920
+ "phone_number": "514 000 9999",
921
+ "state_code": "BC",
922
+ "street_number": None,
923
+ "residential": False,
924
+ "address_line1": "5840 Oak St",
925
+ "address_line2": None,
926
+ "validate_location": False,
927
+ "object_type": "address",
928
+ "validation": None,
929
+ },
930
+ "parcels": [
931
+ {
932
+ "id": ANY,
933
+ "weight": 1.0,
934
+ "width": None,
935
+ "height": None,
936
+ "length": None,
937
+ "packaging_type": None,
938
+ "package_preset": "canadapost_corrugated_small_box",
939
+ "description": None,
940
+ "content": None,
941
+ "is_document": False,
942
+ "weight_unit": "KG",
943
+ "dimension_unit": None,
944
+ "items": [],
945
+ "freight_class": None,
946
+ "reference_number": ANY,
947
+ "object_type": "parcel",
948
+ "options": {},
949
+ }
950
+ ],
951
+ "services": [],
952
+ "options": {"has_alternative_services": True, "shipping_date": ANY, "shipment_date": ANY},
953
+ "payment": {"paid_by": "sender", "currency": "CAD", "account_number": None},
954
+ "return_address": None,
955
+ "billing_address": None,
956
+ "customs": None,
957
+ "rates": [
958
+ {
959
+ "id": ANY,
960
+ "object_type": "rate",
961
+ "carrier_name": "canadapost",
962
+ "carrier_id": "canadapost",
963
+ "currency": "CAD",
964
+ "estimated_delivery": ANY,
965
+ "service": "canadapost_regular_parcel",
966
+ "total_charge": 50.00,
967
+ "transit_days": 5,
968
+ "extra_charges": [
969
+ {"name": "Base charge", "amount": 50.00, "currency": "CAD", "id": None},
970
+ ],
971
+ "meta": {
972
+ "service_name": "CANADAPOST REGULAR PARCEL",
973
+ "rate_provider": "canadapost",
974
+ "carrier_connection_id": ANY,
975
+ },
976
+ "test_mode": True,
977
+ }
978
+ ],
979
+ "reference": None,
980
+ "label_type": "PDF",
981
+ "carrier_ids": [],
982
+ "tracker_id": ANY,
983
+ "created_at": ANY,
984
+ "metadata": {},
985
+ "messages": [],
986
+ "status": "purchased",
987
+ "carrier_name": "canadapost",
988
+ "carrier_id": "canadapost",
989
+ "tracking_number": "123456789012",
990
+ "shipment_identifier": "123456789012",
991
+ "selected_rate": {
992
+ "id": ANY,
993
+ "object_type": "rate",
994
+ "carrier_name": "canadapost",
995
+ "carrier_id": "canadapost",
996
+ "currency": "CAD",
997
+ "estimated_delivery": ANY,
998
+ "service": "canadapost_priority",
999
+ "total_charge": 50.00,
1000
+ "transit_days": 5,
1001
+ "extra_charges": [
1002
+ {"name": "Base charge", "amount": 50.00, "currency": "CAD", "id": None},
1003
+ ],
1004
+ "meta": {
1005
+ "ext": "canadapost",
1006
+ "carrier": "canadapost",
1007
+ "service_name": "CANADAPOST REGULAR PARCEL",
1008
+ "rate_provider": "canadapost",
1009
+ "carrier_connection_id": ANY,
1010
+ "has_alternative_services": True,
1011
+ },
1012
+ "test_mode": True,
1013
+ },
1014
+ "meta": {
1015
+ "ext": "canadapost",
1016
+ "carrier": "canadapost",
1017
+ "rate_provider": "canadapost",
1018
+ "service_name": "CANADAPOST PRIORITY",
1019
+ },
1020
+ "service": "canadapost_priority",
1021
+ "selected_rate_id": ANY,
1022
+ "test_mode": True,
1023
+ "label_url": ANY,
1024
+ "invoice_url": None,
1025
+ }
@@ -10,7 +10,7 @@ import rest_framework.pagination as pagination
10
10
  import rest_framework.throttling as throttling
11
11
  import django_filters.rest_framework as django_filters
12
12
 
13
- from karrio.server.core.logging import logger
13
+ from karrio.server.core.utils import validate_resource_token
14
14
  import karrio.server.openapi as openapi
15
15
  import karrio.server.core.views.api as api
16
16
  import karrio.server.core.filters as filters
@@ -107,26 +107,24 @@ class ManifestDetails(api.APIView):
107
107
 
108
108
  class ManifestDoc(django_downloadview.VirtualDownloadView):
109
109
  @openapi.extend_schema(exclude=True)
110
- def get(
111
- self,
112
- request: request.Request,
113
- pk: str,
114
- doc: str = "manifest",
115
- format: str = "pdf",
116
- **kwargs,
117
- ):
110
+ def get(self, req: request.Request, pk: str, doc: str = "manifest", format: str = "pdf", **kwargs):
118
111
  """Retrieve a manifest file."""
112
+ error = validate_resource_token(req, "manifest", [pk], "manifest")
113
+ if error:
114
+ return error
115
+
116
+ query_params = req.GET.dict()
117
+
119
118
  self.manifest = models.Manifest.objects.get(pk=pk, manifest__isnull=False)
120
119
  self.document = getattr(self.manifest, doc, None)
121
120
  self.name = f"{doc}_{self.manifest.id}.{format}"
122
121
 
123
- query_params = request.GET.dict()
124
122
  self.preview = "preview" in query_params
125
123
  self.attachment = "download" in query_params
126
124
 
127
- response = super(ManifestDoc, self).get(request, pk, doc, format, **kwargs)
128
- response["X-Frame-Options"] = "ALLOWALL"
129
- return response
125
+ resp = super(ManifestDoc, self).get(req, pk, doc, format, **kwargs)
126
+ resp["X-Frame-Options"] = "ALLOWALL"
127
+ return resp
130
128
 
131
129
  def get_file(self):
132
130
  content = base64.b64decode(self.document or "")
@@ -14,7 +14,6 @@ import karrio.lib as lib
14
14
  import karrio.server.openapi as openapi
15
15
  import karrio.server.core.filters as filters
16
16
  import karrio.server.manager.models as models
17
- from karrio.server.core.logging import logger
18
17
  from karrio.server.core.views.api import GenericAPIView, APIView
19
18
  from karrio.server.core.filters import ShipmentFilters
20
19
  from karrio.server.manager.router import router
@@ -267,12 +266,19 @@ class ShipmentDocs(VirtualDownloadView):
267
266
  format: str = "pdf",
268
267
  **kwargs,
269
268
  ):
270
- """Retrieve a shipment label."""
269
+ """Retrieve a shipment label or invoice."""
270
+ from karrio.server.core.utils import validate_resource_token
271
+
272
+ error = validate_resource_token(request, "shipment", [pk], doc)
273
+ if error:
274
+ return error
275
+
276
+ query_params = request.GET.dict()
277
+
271
278
  self.shipment = models.Shipment.objects.get(pk=pk, label__isnull=False)
272
279
  self.document = getattr(self.shipment, doc, None)
273
280
  self.name = f"{doc}_{self.shipment.tracking_number}.{format}"
274
281
 
275
- query_params = request.GET.dict()
276
282
  self.preview = "preview" in query_params
277
283
  self.attachment = "download" in query_params
278
284
 
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: karrio_server_manager
3
- Version: 2025.5rc36
3
+ Version: 2025.5.1
4
4
  Summary: Multi-carrier shipping API Shipments manager module
5
5
  Author-email: karrio <hello@karrio.io>
6
- License-Expression: Apache-2.0
6
+ License-Expression: LGPL-3.0
7
7
  Project-URL: Homepage, https://github.com/karrioapi/karrio
8
8
  Classifier: Programming Language :: Python :: 3
9
9
  Requires-Python: >=3.11
@@ -82,25 +82,26 @@ karrio/server/manager/serializers/manifest.py,sha256=mSneCk_7HMXpi64_7hggWvkR7Ma
82
82
  karrio/server/manager/serializers/parcel.py,sha256=733Bg26lVbEkoWtAVM5Qt2IRBS2QDuVxhG40Hiqh3bw,2621
83
83
  karrio/server/manager/serializers/pickup.py,sha256=sX0VmcQxGkXn3IEosMuFwdXh4HhdkPcuBOp79O8PoDQ,9233
84
84
  karrio/server/manager/serializers/rate.py,sha256=7vYK_v8iWEDnswqYHG2Lir16_UhHTOxW5rdC6lw3lzA,652
85
- karrio/server/manager/serializers/shipment.py,sha256=G4vA51UFHvBVNNR9z3Vtac-Y7GexjGadPOCRxSUVqRE,30355
85
+ karrio/server/manager/serializers/shipment.py,sha256=gkDFTDaKXA8Iz3yjS6fxRi46AzZPw40RhJEOlnBZy6o,31143
86
86
  karrio/server/manager/serializers/tracking.py,sha256=ixrAjIiZQsvSt4y0qtisGkt6TFOJ3ORNkJAQVt6YQrA,12483
87
87
  karrio/server/manager/tests/__init__.py,sha256=Y1UNteEE60vWdUAkjbldu_r_-h4u0He8-UoiBgTjKcU,391
88
88
  karrio/server/manager/tests/test_addresses.py,sha256=pNkZC_yJyb29ZlEOtOAs4blcEYiOarw0zhZIZC5uj1w,3111
89
89
  karrio/server/manager/tests/test_custom_infos.py,sha256=iv2cLdZVoVWFZK_mDUEnrZssncAnQcn87Rn2sAk8UQI,2731
90
+ karrio/server/manager/tests/test_errors.py,sha256=QYsGLUtwMvrHeX1XSCpdteTKbug7-y1-Xgvbl96aN9g,3220
90
91
  karrio/server/manager/tests/test_parcels.py,sha256=lVLBOsHzXgXQvYjHIUy5oiPvrMfxYpueVvvhtuhstWk,2559
91
92
  karrio/server/manager/tests/test_pickups.py,sha256=8jxddwTnBvBM9FOyWxW9TtZ-GOVYUje7HQ2EZjsbtD8,10681
92
- karrio/server/manager/tests/test_shipments.py,sha256=9QlsvAZK0owvDwK8lVAdVVUYAtFOkr7cIKoU7W-wRls,26946
93
+ karrio/server/manager/tests/test_shipments.py,sha256=N1eVH-n0BTANmzehKL0cp917goskU5X8l_ftXzcVSmw,33349
93
94
  karrio/server/manager/tests/test_trackers.py,sha256=KvmWkplokNDZ0dzB16mFl0WcMJ0OYp_ErZeWJPGW_NA,7151
94
95
  karrio/server/manager/views/__init__.py,sha256=kDFUaORRQ3Xh0ZPm-Jk88Ss8dgGYM57iUFXb9TPMzh0,401
95
96
  karrio/server/manager/views/addresses.py,sha256=7YCAs2ZYgd1icYwMcGGWfX7A7vZEL4BEAbU4eIxhiMY,4620
96
97
  karrio/server/manager/views/customs.py,sha256=-ZreiKyJ1xeLeNVG53nMfRQFeURduWr1QkDItdLPnE8,4875
97
98
  karrio/server/manager/views/documents.py,sha256=znW54qJ_k7WInIut5FBZFDT93CioozXTOYFKRSUTBhA,4005
98
- karrio/server/manager/views/manifests.py,sha256=4VyeiFaeKkWRvKmZZcfw7qjHynsR03t4M_fwwiZklZQ,5171
99
+ karrio/server/manager/views/manifests.py,sha256=_Dd83YxVJOgWhAhD745Kr4tcLWnKaU1dxnT5xB8opvk,5227
99
100
  karrio/server/manager/views/parcels.py,sha256=hZY45rg6SrTWfQqyJ38MGKSor1yqgPUEVHtu16aG37g,4594
100
101
  karrio/server/manager/views/pickups.py,sha256=gmpxz9ot1OR-BP1qh-0MXU3kUJi1ht_74hfaLJzJ42w,5503
101
- karrio/server/manager/views/shipments.py,sha256=_5EJfgxEO6H2EdQQbaSwILgnim7slqxMKDkEk_97F9c,10267
102
+ karrio/server/manager/views/shipments.py,sha256=TqLpBH5Jf-rI3enJwvNptRwGzfo7co9R1VSP_oqhB3o,10419
102
103
  karrio/server/manager/views/trackers.py,sha256=3oGn2qDpHgk8GZvuz-Cb93Fc0j_h_HbXQR692Zhfiok,12363
103
- karrio_server_manager-2025.5rc36.dist-info/METADATA,sha256=gqVdjXCp3F4TIfvQWnxEJ76Js6deZo0n6BRct9pJC9I,734
104
- karrio_server_manager-2025.5rc36.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
105
- karrio_server_manager-2025.5rc36.dist-info/top_level.txt,sha256=D1D7x8R3cTfjF_15mfiO7wCQ5QMtuM4x8GaPr7z5i78,12
106
- karrio_server_manager-2025.5rc36.dist-info/RECORD,,
104
+ karrio_server_manager-2025.5.1.dist-info/METADATA,sha256=7G3zIzLe_6ux6VkjCj1ub1xQc4b9Cz-uiyzwvBpSDDs,730
105
+ karrio_server_manager-2025.5.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
106
+ karrio_server_manager-2025.5.1.dist-info/top_level.txt,sha256=D1D7x8R3cTfjF_15mfiO7wCQ5QMtuM4x8GaPr7z5i78,12
107
+ karrio_server_manager-2025.5.1.dist-info/RECORD,,