karrio-locate2u 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.
- karrio/mappers/locate2u/__init__.py +3 -0
- karrio/mappers/locate2u/mapper.py +51 -0
- karrio/mappers/locate2u/proxy.py +69 -0
- karrio/mappers/locate2u/settings.py +32 -0
- karrio/plugins/locate2u/__init__.py +19 -0
- karrio/providers/locate2u/__init__.py +11 -0
- karrio/providers/locate2u/error.py +31 -0
- karrio/providers/locate2u/shipment/__init__.py +9 -0
- karrio/providers/locate2u/shipment/cancel.py +39 -0
- karrio/providers/locate2u/shipment/create.py +116 -0
- karrio/providers/locate2u/tracking.py +73 -0
- karrio/providers/locate2u/units.py +79 -0
- karrio/providers/locate2u/utils.py +88 -0
- karrio/schemas/locate2u/__init__.py +0 -0
- karrio/schemas/locate2u/shipping_request.py +69 -0
- karrio/schemas/locate2u/shipping_response.py +88 -0
- karrio/schemas/locate2u/tracking_response.py +88 -0
- karrio_locate2u-2025.5rc1.dist-info/METADATA +45 -0
- karrio_locate2u-2025.5rc1.dist-info/RECORD +22 -0
- karrio_locate2u-2025.5rc1.dist-info/WHEEL +5 -0
- karrio_locate2u-2025.5rc1.dist-info/entry_points.txt +2 -0
- karrio_locate2u-2025.5rc1.dist-info/top_level.txt +3 -0
@@ -0,0 +1,51 @@
|
|
1
|
+
"""Karrio Locate2u client mapper."""
|
2
|
+
|
3
|
+
import typing
|
4
|
+
import karrio.lib as lib
|
5
|
+
import karrio.api.mapper as mapper
|
6
|
+
import karrio.core.models as models
|
7
|
+
import karrio.providers.locate2u as provider
|
8
|
+
import karrio.mappers.locate2u.settings as provider_settings
|
9
|
+
import karrio.universal.providers.rating as universal_provider
|
10
|
+
|
11
|
+
|
12
|
+
class Mapper(mapper.Mapper):
|
13
|
+
settings: provider_settings.Settings
|
14
|
+
|
15
|
+
def create_rate_request(self, payload: models.RateRequest) -> lib.Serializable:
|
16
|
+
return universal_provider.rate_request(payload, self.settings)
|
17
|
+
|
18
|
+
def create_tracking_request(
|
19
|
+
self, payload: models.TrackingRequest
|
20
|
+
) -> lib.Serializable:
|
21
|
+
return provider.tracking_request(payload, self.settings)
|
22
|
+
|
23
|
+
def create_shipment_request(
|
24
|
+
self, payload: models.ShipmentRequest
|
25
|
+
) -> lib.Serializable:
|
26
|
+
return provider.shipment_request(payload, self.settings)
|
27
|
+
|
28
|
+
def create_cancel_shipment_request(
|
29
|
+
self, payload: models.ShipmentCancelRequest
|
30
|
+
) -> lib.Serializable[str]:
|
31
|
+
return provider.shipment_cancel_request(payload, self.settings)
|
32
|
+
|
33
|
+
def parse_cancel_shipment_response(
|
34
|
+
self, response: lib.Deserializable
|
35
|
+
) -> typing.Tuple[models.ConfirmationDetails, typing.List[models.Message]]:
|
36
|
+
return provider.parse_shipment_cancel_response(response, self.settings)
|
37
|
+
|
38
|
+
def parse_rate_response(
|
39
|
+
self, response: lib.Deserializable
|
40
|
+
) -> typing.Tuple[typing.List[models.RateDetails], typing.List[models.Message]]:
|
41
|
+
return universal_provider.parse_rate_response(response, self.settings)
|
42
|
+
|
43
|
+
def parse_shipment_response(
|
44
|
+
self, response: lib.Deserializable[str]
|
45
|
+
) -> typing.Tuple[models.ShipmentDetails, typing.List[models.Message]]:
|
46
|
+
return provider.parse_shipment_response(response, self.settings)
|
47
|
+
|
48
|
+
def parse_tracking_response(
|
49
|
+
self, response: lib.Deserializable[str]
|
50
|
+
) -> typing.Tuple[typing.List[models.TrackingDetails], typing.List[models.Message]]:
|
51
|
+
return provider.parse_tracking_response(response, self.settings)
|
@@ -0,0 +1,69 @@
|
|
1
|
+
"""Karrio Locate2u client proxy."""
|
2
|
+
|
3
|
+
import typing
|
4
|
+
import karrio.lib as lib
|
5
|
+
import karrio.api.proxy as proxy
|
6
|
+
import karrio.providers.locate2u.error as provider_error
|
7
|
+
import karrio.mappers.locate2u.settings as provider_settings
|
8
|
+
import karrio.universal.mappers.rating_proxy as rating_proxy
|
9
|
+
|
10
|
+
|
11
|
+
class Proxy(rating_proxy.RatingMixinProxy, proxy.Proxy):
|
12
|
+
settings: provider_settings.Settings
|
13
|
+
|
14
|
+
def get_rates(self, request: lib.Serializable) -> lib.Deserializable:
|
15
|
+
return super().get_rates(request)
|
16
|
+
|
17
|
+
def create_shipment(self, request: lib.Serializable) -> lib.Deserializable[str]:
|
18
|
+
response = lib.request(
|
19
|
+
url=f"{self.settings.server_url}/api/v1/stops",
|
20
|
+
data=lib.to_json(request.serialize()),
|
21
|
+
trace=self.trace_as("json"),
|
22
|
+
method="POST",
|
23
|
+
headers={
|
24
|
+
"Content-Type": "application/json",
|
25
|
+
"Authorization": f"Bearer {self.settings.access_token}",
|
26
|
+
},
|
27
|
+
on_error=provider_error.parse_http_response,
|
28
|
+
)
|
29
|
+
|
30
|
+
return lib.Deserializable(response, lib.to_dict)
|
31
|
+
|
32
|
+
def cancel_shipment(self, request: lib.Serializable) -> lib.Deserializable[str]:
|
33
|
+
payload = request.serialize()
|
34
|
+
response = lib.request(
|
35
|
+
url=f"{self.settings.server_url}/api/v1/stops/{payload['stopId']}",
|
36
|
+
trace=self.trace_as("json"),
|
37
|
+
method="DELETE",
|
38
|
+
headers={
|
39
|
+
"Content-Type": "application/json",
|
40
|
+
"Authorization": f"Bearer {self.settings.access_token}",
|
41
|
+
},
|
42
|
+
on_error=provider_error.parse_http_response,
|
43
|
+
decoder=lambda _: dict(ok=True),
|
44
|
+
)
|
45
|
+
|
46
|
+
return lib.Deserializable(response, lib.to_dict)
|
47
|
+
|
48
|
+
def get_tracking(self, request: lib.Serializable) -> lib.Deserializable[str]:
|
49
|
+
def _get_tracking(stop_id: str):
|
50
|
+
return stop_id, lib.request(
|
51
|
+
url=f"{self.settings.server_url}/api/v1/stops/{stop_id}?includeItems=false&includeLines=false",
|
52
|
+
trace=self.trace_as("json"),
|
53
|
+
method="GET",
|
54
|
+
headers={
|
55
|
+
"Content-Type": "application/json",
|
56
|
+
"Authorization": f"Bearer {self.settings.access_token}",
|
57
|
+
},
|
58
|
+
on_error=provider_error.parse_http_response,
|
59
|
+
)
|
60
|
+
|
61
|
+
responses: typing.List[typing.Tuple[str, str]] = lib.run_concurently(
|
62
|
+
_get_tracking, request.serialize()
|
63
|
+
)
|
64
|
+
return lib.Deserializable(
|
65
|
+
responses,
|
66
|
+
lambda res: [
|
67
|
+
(num, lib.to_dict(track)) for num, track in res if any(track.strip())
|
68
|
+
],
|
69
|
+
)
|
@@ -0,0 +1,32 @@
|
|
1
|
+
"""Karrio Locate2u client settings."""
|
2
|
+
|
3
|
+
import attr
|
4
|
+
import typing
|
5
|
+
import jstruct
|
6
|
+
import karrio.lib as lib
|
7
|
+
import karrio.core.models as models
|
8
|
+
import karrio.providers.locate2u.utils as provider_utils
|
9
|
+
import karrio.providers.locate2u.units as provider_units
|
10
|
+
|
11
|
+
|
12
|
+
@attr.s(auto_attribs=True)
|
13
|
+
class Settings(provider_utils.Settings):
|
14
|
+
"""Locate2u connection settings."""
|
15
|
+
|
16
|
+
# required carrier specific properties
|
17
|
+
client_id: str = None
|
18
|
+
client_secret: str = None
|
19
|
+
|
20
|
+
id: str = None
|
21
|
+
test_mode: bool = False
|
22
|
+
carrier_id: str = "locate2u"
|
23
|
+
account_country_code: str = "AU"
|
24
|
+
services: typing.List[models.ServiceLevel] = jstruct.JList[models.ServiceLevel, False, dict(default=provider_units.DEFAULT_SERVICES)] # type: ignore
|
25
|
+
metadata: dict = {}
|
26
|
+
|
27
|
+
@property
|
28
|
+
def shipping_services(self) -> typing.List[models.ServiceLevel]:
|
29
|
+
if any(self.services or []):
|
30
|
+
return self.services
|
31
|
+
|
32
|
+
return provider_units.DEFAULT_SERVICES
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import karrio.core.metadata as metadata
|
2
|
+
import karrio.mappers.locate2u as mappers
|
3
|
+
import karrio.providers.locate2u.units as units
|
4
|
+
|
5
|
+
|
6
|
+
METADATA = metadata.PluginMetadata(
|
7
|
+
status="beta",
|
8
|
+
id="locate2u",
|
9
|
+
label="Locate2u",
|
10
|
+
# Integrations
|
11
|
+
Mapper=mappers.Mapper,
|
12
|
+
Proxy=mappers.Proxy,
|
13
|
+
Settings=mappers.Settings,
|
14
|
+
# Data Units
|
15
|
+
is_hub=False,
|
16
|
+
services=units.ShippingService,
|
17
|
+
options=units.ShippingOption,
|
18
|
+
service_levels=units.DEFAULT_SERVICES,
|
19
|
+
)
|
@@ -0,0 +1,11 @@
|
|
1
|
+
from karrio.providers.locate2u.utils import Settings
|
2
|
+
from karrio.providers.locate2u.shipment import (
|
3
|
+
parse_shipment_cancel_response,
|
4
|
+
parse_shipment_response,
|
5
|
+
shipment_cancel_request,
|
6
|
+
shipment_request,
|
7
|
+
)
|
8
|
+
from karrio.providers.locate2u.tracking import (
|
9
|
+
parse_tracking_response,
|
10
|
+
tracking_request,
|
11
|
+
)
|
@@ -0,0 +1,31 @@
|
|
1
|
+
import typing
|
2
|
+
import urllib.error
|
3
|
+
import karrio.lib as lib
|
4
|
+
import karrio.core.models as models
|
5
|
+
import karrio.providers.locate2u.utils as provider_utils
|
6
|
+
|
7
|
+
|
8
|
+
def parse_error_response(
|
9
|
+
responses: typing.Union[typing.List[dict], dict],
|
10
|
+
settings: provider_utils.Settings,
|
11
|
+
**kwargs,
|
12
|
+
) -> typing.List[models.Message]:
|
13
|
+
results = responses if isinstance(responses, list) else [responses]
|
14
|
+
errors: typing.List[dict] = [
|
15
|
+
error for error in results if error.get("error") is not None
|
16
|
+
]
|
17
|
+
|
18
|
+
return [
|
19
|
+
models.Message(
|
20
|
+
carrier_id=settings.carrier_id,
|
21
|
+
carrier_name=settings.carrier_name,
|
22
|
+
code=error.get("code"),
|
23
|
+
message=error.get("error"),
|
24
|
+
details={**kwargs},
|
25
|
+
)
|
26
|
+
for error in errors
|
27
|
+
]
|
28
|
+
|
29
|
+
|
30
|
+
def parse_http_response(response: urllib.error.HTTPError) -> dict:
|
31
|
+
return lib.to_json(dict(code=str(response.code), error=response.reason))
|
@@ -0,0 +1,39 @@
|
|
1
|
+
import typing
|
2
|
+
import karrio.lib as lib
|
3
|
+
import karrio.core.models as models
|
4
|
+
import karrio.providers.locate2u.error as error
|
5
|
+
import karrio.providers.locate2u.utils as provider_utils
|
6
|
+
import karrio.providers.locate2u.units as provider_units
|
7
|
+
|
8
|
+
|
9
|
+
def parse_shipment_cancel_response(
|
10
|
+
_response: lib.Deserializable[dict],
|
11
|
+
settings: provider_utils.Settings,
|
12
|
+
) -> typing.Tuple[models.ConfirmationDetails, typing.List[models.Message]]:
|
13
|
+
response = _response.deserialize()
|
14
|
+
messages = error.parse_error_response(response, settings)
|
15
|
+
success = response.get("error") is None
|
16
|
+
|
17
|
+
confirmation = (
|
18
|
+
models.ConfirmationDetails(
|
19
|
+
carrier_id=settings.carrier_id,
|
20
|
+
carrier_name=settings.carrier_name,
|
21
|
+
operation="Cancel Shipment",
|
22
|
+
success=success,
|
23
|
+
)
|
24
|
+
if success
|
25
|
+
else None
|
26
|
+
)
|
27
|
+
|
28
|
+
return confirmation, messages
|
29
|
+
|
30
|
+
|
31
|
+
def shipment_cancel_request(
|
32
|
+
payload: models.ShipmentCancelRequest,
|
33
|
+
settings: provider_utils.Settings,
|
34
|
+
) -> lib.Serializable:
|
35
|
+
request = dict(
|
36
|
+
stopId=payload.shipment_identifier,
|
37
|
+
)
|
38
|
+
|
39
|
+
return lib.Serializable(request)
|
@@ -0,0 +1,116 @@
|
|
1
|
+
import karrio.schemas.locate2u.shipping_request as locate2u
|
2
|
+
import karrio.schemas.locate2u.shipping_response as shipping
|
3
|
+
import typing
|
4
|
+
import karrio.lib as lib
|
5
|
+
import karrio.core.units as units
|
6
|
+
import karrio.core.models as models
|
7
|
+
import karrio.providers.locate2u.error as error
|
8
|
+
import karrio.providers.locate2u.utils as provider_utils
|
9
|
+
import karrio.providers.locate2u.units as provider_units
|
10
|
+
|
11
|
+
|
12
|
+
def parse_shipment_response(
|
13
|
+
_response: lib.Deserializable[dict],
|
14
|
+
settings: provider_utils.Settings,
|
15
|
+
) -> typing.Tuple[typing.List[models.RateDetails], typing.List[models.Message]]:
|
16
|
+
response = _response.deserialize()
|
17
|
+
messages = error.parse_error_response(response, settings)
|
18
|
+
shipment = _extract_details(response, settings) if len(messages) == 0 else None
|
19
|
+
|
20
|
+
return shipment, messages
|
21
|
+
|
22
|
+
|
23
|
+
def _extract_details(
|
24
|
+
data: dict,
|
25
|
+
settings: provider_utils.Settings,
|
26
|
+
) -> models.ShipmentDetails:
|
27
|
+
shipment = lib.to_object(shipping.ShippingResponse, data)
|
28
|
+
|
29
|
+
return models.ShipmentDetails(
|
30
|
+
carrier_id=settings.carrier_id,
|
31
|
+
carrier_name=settings.carrier_name,
|
32
|
+
tracking_number=str(shipment.stopId),
|
33
|
+
shipment_identifier=str(shipment.stopId),
|
34
|
+
label_type="PDF",
|
35
|
+
docs=models.Documents(label="No label..."),
|
36
|
+
meta=dict(
|
37
|
+
shipmentId=shipment.shipmentId,
|
38
|
+
durationMinutes=shipment.durationMinutes,
|
39
|
+
),
|
40
|
+
)
|
41
|
+
|
42
|
+
|
43
|
+
def shipment_request(
|
44
|
+
payload: models.ShipmentRequest,
|
45
|
+
settings: provider_utils.Settings,
|
46
|
+
) -> lib.Serializable:
|
47
|
+
recipient = lib.to_address(payload.recipient)
|
48
|
+
package = lib.to_packages(payload.parcels).single
|
49
|
+
options = lib.to_shipping_options(
|
50
|
+
payload.options,
|
51
|
+
package_options=package.options,
|
52
|
+
option_type=provider_units.ShippingOption,
|
53
|
+
)
|
54
|
+
|
55
|
+
request = locate2u.ShippingRequest(
|
56
|
+
contact=locate2u.Contact(
|
57
|
+
name=recipient.contact,
|
58
|
+
phone=recipient.phone_number,
|
59
|
+
email=recipient.email,
|
60
|
+
),
|
61
|
+
name=recipient.company_name or recipient.person_name,
|
62
|
+
address=lib.text(
|
63
|
+
recipient.address_line,
|
64
|
+
lib.text(recipient.city, recipient.postal_code, recipient.state_code),
|
65
|
+
recipient.country_name,
|
66
|
+
separator=", ",
|
67
|
+
),
|
68
|
+
location=(
|
69
|
+
locate2u.Location(
|
70
|
+
latitude=options.latitude.state,
|
71
|
+
longitude=options.longitude.state,
|
72
|
+
)
|
73
|
+
if any([options.latitude.state, options.longitude.state])
|
74
|
+
else None
|
75
|
+
),
|
76
|
+
appointmentTime=options.appointment_time.state,
|
77
|
+
timeWindowStart=options.time_window_start.state,
|
78
|
+
timeWindowEnd=options.time_window_end.state,
|
79
|
+
brandId=options.brand_id.state,
|
80
|
+
durationMinutes=options.duration_minutes.state,
|
81
|
+
tripDate=lib.fdatetime(
|
82
|
+
options.shipment_date.state,
|
83
|
+
current_format="%Y-%m-%d",
|
84
|
+
output_format="%Y-%m-%dT%H:%M:%S.%fZ",
|
85
|
+
),
|
86
|
+
customFields=None,
|
87
|
+
assignedTeamMemberId=options.assigned_team_member_id.state,
|
88
|
+
source=options.source.state,
|
89
|
+
sourceReference=payload.reference,
|
90
|
+
load=locate2u.Load(
|
91
|
+
quantity=package.items.quantity,
|
92
|
+
volume=package.volume.m3,
|
93
|
+
weight=package.weight.KG,
|
94
|
+
length=package.length.CM,
|
95
|
+
width=package.width.CM,
|
96
|
+
height=package.height.CM,
|
97
|
+
),
|
98
|
+
customerId=options.customer_id.state,
|
99
|
+
runNumber=options.run_number.state,
|
100
|
+
teamRegionId=options.team_region_id.state,
|
101
|
+
driverInstructions=options.driver_instructions.state,
|
102
|
+
notes=options.notes.state,
|
103
|
+
lines=[
|
104
|
+
locate2u.Line(
|
105
|
+
barcode=item.hs_code or item.sku,
|
106
|
+
description=item.title or item.description,
|
107
|
+
currentLocation=item.metadata.get("currentLocation"),
|
108
|
+
serviceId=None,
|
109
|
+
productVariantId=None,
|
110
|
+
quantity=item.quantity,
|
111
|
+
)
|
112
|
+
for item in package.items
|
113
|
+
],
|
114
|
+
)
|
115
|
+
|
116
|
+
return lib.Serializable(request, lib.to_dict)
|
@@ -0,0 +1,73 @@
|
|
1
|
+
import karrio.schemas.locate2u.tracking_response as locate2u
|
2
|
+
import typing
|
3
|
+
import karrio.lib as lib
|
4
|
+
import karrio.core.units as units
|
5
|
+
import karrio.core.models as models
|
6
|
+
import karrio.providers.locate2u.error as error
|
7
|
+
import karrio.providers.locate2u.utils as provider_utils
|
8
|
+
import karrio.providers.locate2u.units as provider_units
|
9
|
+
|
10
|
+
|
11
|
+
def parse_tracking_response(
|
12
|
+
_responses: lib.Deserializable[typing.List[typing.Tuple[str, dict]]],
|
13
|
+
settings: provider_utils.Settings,
|
14
|
+
) -> typing.Tuple[typing.List[models.TrackingDetails], typing.List[models.Message]]:
|
15
|
+
responses = _responses.deserialize()
|
16
|
+
messages: typing.List[models.Message] = sum(
|
17
|
+
[
|
18
|
+
error.parse_error_response(response, settings, tracking_number=_)
|
19
|
+
for _, response in responses
|
20
|
+
if response.get("error") is not None
|
21
|
+
],
|
22
|
+
[],
|
23
|
+
)
|
24
|
+
|
25
|
+
tracking_details = [
|
26
|
+
_extract_details(response, settings)
|
27
|
+
for _, response in responses
|
28
|
+
if response.get("error") is None
|
29
|
+
]
|
30
|
+
|
31
|
+
return tracking_details, messages
|
32
|
+
|
33
|
+
|
34
|
+
def _extract_details(
|
35
|
+
data: dict,
|
36
|
+
settings: provider_utils.Settings,
|
37
|
+
) -> models.TrackingDetails:
|
38
|
+
tracking = lib.to_object(locate2u.TrackingResponse, data)
|
39
|
+
status = next(
|
40
|
+
(
|
41
|
+
status.name
|
42
|
+
for status in list(provider_units.TrackingStatus)
|
43
|
+
if tracking.status in status.value
|
44
|
+
),
|
45
|
+
provider_units.TrackingStatus.in_transit.name,
|
46
|
+
)
|
47
|
+
|
48
|
+
return models.TrackingDetails(
|
49
|
+
carrier_id=settings.carrier_id,
|
50
|
+
carrier_name=settings.carrier_name,
|
51
|
+
tracking_number=str(tracking.stopId),
|
52
|
+
events=[
|
53
|
+
models.TrackingEvent(
|
54
|
+
date=lib.fdate(tracking.lastModifiedDate, "%Y-%m-%dT%H:%M:%S.%fZ"),
|
55
|
+
description=tracking.status,
|
56
|
+
code=tracking.status,
|
57
|
+
time=lib.flocaltime(tracking.lastModifiedDate, "%Y-%m-%dT%H:%M:%S.%fZ"),
|
58
|
+
latitude=tracking.location.latitude,
|
59
|
+
longitude=tracking.location.longitude,
|
60
|
+
)
|
61
|
+
],
|
62
|
+
estimated_delivery=lib.fdate(tracking.arrivalDate, "%Y-%m-%dT%H:%M:%S%z"),
|
63
|
+
delivered=(status == "delivered"),
|
64
|
+
)
|
65
|
+
|
66
|
+
|
67
|
+
def tracking_request(
|
68
|
+
payload: models.TrackingRequest,
|
69
|
+
settings: provider_utils.Settings,
|
70
|
+
) -> lib.Serializable:
|
71
|
+
request = payload.tracking_numbers
|
72
|
+
|
73
|
+
return lib.Serializable(request)
|
@@ -0,0 +1,79 @@
|
|
1
|
+
import karrio.lib as lib
|
2
|
+
import karrio.core.units as units
|
3
|
+
import karrio.core.models as models
|
4
|
+
|
5
|
+
|
6
|
+
class PackagingType(lib.StrEnum):
|
7
|
+
"""Carrier specific packaging type"""
|
8
|
+
|
9
|
+
PACKAGE = "PACKAGE"
|
10
|
+
|
11
|
+
""" Unified Packaging type mapping """
|
12
|
+
envelope = PACKAGE
|
13
|
+
pak = PACKAGE
|
14
|
+
tube = PACKAGE
|
15
|
+
pallet = PACKAGE
|
16
|
+
small_box = PACKAGE
|
17
|
+
medium_box = PACKAGE
|
18
|
+
your_packaging = PACKAGE
|
19
|
+
|
20
|
+
|
21
|
+
class ShippingService(lib.StrEnum):
|
22
|
+
"""Carrier specific services"""
|
23
|
+
|
24
|
+
locate2u_local_delivery = "Locate2u Local Delivery"
|
25
|
+
|
26
|
+
|
27
|
+
class ShippingOption(lib.Enum):
|
28
|
+
"""Carrier specific options"""
|
29
|
+
|
30
|
+
appointment_time = lib.OptionEnum("appointment_time")
|
31
|
+
time_window_start = lib.OptionEnum("time_window_start")
|
32
|
+
time_window_end = lib.OptionEnum("time_window_end")
|
33
|
+
brand_id = lib.OptionEnum("brand_id")
|
34
|
+
duration_minutes = lib.OptionEnum("duration_minutes", lib.to_int)
|
35
|
+
assigned_team_member_id = lib.OptionEnum("assigned_team_member_id")
|
36
|
+
source = lib.OptionEnum("source")
|
37
|
+
customer_id = lib.OptionEnum("customer_id")
|
38
|
+
run_number = lib.OptionEnum("run_number")
|
39
|
+
team_region_id = lib.OptionEnum("team_region_id")
|
40
|
+
driver_instructions = lib.OptionEnum("driver_instructions")
|
41
|
+
notes = lib.OptionEnum("notes")
|
42
|
+
latitude = lib.OptionEnum("latitude", float)
|
43
|
+
longitude = lib.OptionEnum("longitude", float)
|
44
|
+
|
45
|
+
|
46
|
+
def shipping_options_initializer(
|
47
|
+
options: dict,
|
48
|
+
package_options: units.ShippingOptions = None,
|
49
|
+
) -> units.ShippingOptions:
|
50
|
+
"""
|
51
|
+
Apply default values to the given options.
|
52
|
+
"""
|
53
|
+
|
54
|
+
if package_options is not None:
|
55
|
+
options.update(package_options.content)
|
56
|
+
|
57
|
+
def items_filter(key: str) -> bool:
|
58
|
+
return key in ShippingOption # type: ignore
|
59
|
+
|
60
|
+
return units.ShippingOptions(options, ShippingOption, items_filter=items_filter)
|
61
|
+
|
62
|
+
|
63
|
+
class TrackingStatus(lib.Enum):
|
64
|
+
on_hold = ["On Hold"]
|
65
|
+
delivered = ["Complete"]
|
66
|
+
in_transit = ["Pending", "Enroute"]
|
67
|
+
delivery_failed = ["Failed", "Cancelled"]
|
68
|
+
delivery_delayed = ["Delayed"]
|
69
|
+
out_for_delivery = ["Arrived"]
|
70
|
+
|
71
|
+
|
72
|
+
DEFAULT_SERVICES = [
|
73
|
+
models.ServiceLevel(
|
74
|
+
service_name="Locate2u Local Delivery",
|
75
|
+
service_code="locate2u_local_delivery",
|
76
|
+
currency="AUD",
|
77
|
+
zones=[models.ServiceZone(label="Zone 1", rate=0.0)],
|
78
|
+
),
|
79
|
+
]
|
@@ -0,0 +1,88 @@
|
|
1
|
+
import base64
|
2
|
+
import jstruct
|
3
|
+
import datetime
|
4
|
+
import urllib.parse
|
5
|
+
import karrio.lib as lib
|
6
|
+
import karrio.core as core
|
7
|
+
import karrio.core.errors as errors
|
8
|
+
|
9
|
+
|
10
|
+
class Settings(core.Settings):
|
11
|
+
"""Locate2u connection settings."""
|
12
|
+
|
13
|
+
client_id: str = None
|
14
|
+
client_secret: str = None
|
15
|
+
|
16
|
+
id: str = None
|
17
|
+
test_mode: bool = False
|
18
|
+
carrier_id: str = "locate2u"
|
19
|
+
account_country_code: str = "AU"
|
20
|
+
metadata: dict = {}
|
21
|
+
|
22
|
+
@property
|
23
|
+
def carrier_name(self):
|
24
|
+
return "locate2u"
|
25
|
+
|
26
|
+
@property
|
27
|
+
def server_url(self):
|
28
|
+
return "https://api.locate2u.com"
|
29
|
+
|
30
|
+
@property
|
31
|
+
def auth_server_url(self):
|
32
|
+
return "https://id.locate2u.com"
|
33
|
+
|
34
|
+
@property
|
35
|
+
def authorization(self):
|
36
|
+
pair = "%s:%s" % (self.client_id, self.client_secret)
|
37
|
+
return base64.b64encode(pair.encode("utf-8")).decode("ascii")
|
38
|
+
|
39
|
+
@property
|
40
|
+
def access_token(self):
|
41
|
+
"""Retrieve the access_token using the client_id|client_secret pair
|
42
|
+
or collect it from the cache if an unexpired access_token exist.
|
43
|
+
"""
|
44
|
+
cache_key = f"{self.carrier_name}|{self.client_id}|{self.client_secret}"
|
45
|
+
now = datetime.datetime.now() + datetime.timedelta(minutes=30)
|
46
|
+
|
47
|
+
auth = self.connection_cache.get(cache_key) or {}
|
48
|
+
token = auth.get("access_token")
|
49
|
+
expiry = lib.to_date(auth.get("expiry"), current_format="%Y-%m-%d %H:%M:%S")
|
50
|
+
|
51
|
+
if token is not None and expiry is not None and expiry > now:
|
52
|
+
return token
|
53
|
+
|
54
|
+
self.connection_cache.set(cache_key, lambda: login(self))
|
55
|
+
new_auth = self.connection_cache.get(cache_key)
|
56
|
+
|
57
|
+
return new_auth["access_token"]
|
58
|
+
|
59
|
+
|
60
|
+
def login(settings: Settings):
|
61
|
+
import karrio.providers.locate2u.error as error
|
62
|
+
|
63
|
+
result = lib.request(
|
64
|
+
url=f"{settings.auth_server_url}/connect/token",
|
65
|
+
method="POST",
|
66
|
+
headers={
|
67
|
+
"content-Type": "application/x-www-form-urlencoded",
|
68
|
+
"Authorization": f"Basic {settings.authorization}",
|
69
|
+
},
|
70
|
+
data=urllib.parse.urlencode(
|
71
|
+
dict(
|
72
|
+
scope="locate2u.api",
|
73
|
+
grant_type="client_credentials",
|
74
|
+
)
|
75
|
+
),
|
76
|
+
)
|
77
|
+
|
78
|
+
response = lib.to_dict(result)
|
79
|
+
messages = error.parse_error_response(response, settings)
|
80
|
+
|
81
|
+
if any(messages):
|
82
|
+
raise errors.ParsedMessagesError(messages)
|
83
|
+
|
84
|
+
expiry = datetime.datetime.now() + datetime.timedelta(
|
85
|
+
seconds=float(response.get("expires_in", 0))
|
86
|
+
)
|
87
|
+
|
88
|
+
return {**response, "expiry": lib.fdatetime(expiry)}
|
File without changes
|
@@ -0,0 +1,69 @@
|
|
1
|
+
import attr
|
2
|
+
import jstruct
|
3
|
+
import typing
|
4
|
+
|
5
|
+
|
6
|
+
@attr.s(auto_attribs=True)
|
7
|
+
class Contact:
|
8
|
+
name: typing.Optional[str] = None
|
9
|
+
phone: typing.Optional[str] = None
|
10
|
+
email: typing.Optional[str] = None
|
11
|
+
|
12
|
+
|
13
|
+
@attr.s(auto_attribs=True)
|
14
|
+
class CustomFields:
|
15
|
+
custom1: typing.Optional[str] = None
|
16
|
+
custom2: typing.Optional[str] = None
|
17
|
+
custom3: typing.Optional[str] = None
|
18
|
+
|
19
|
+
|
20
|
+
@attr.s(auto_attribs=True)
|
21
|
+
class Line:
|
22
|
+
barcode: typing.Optional[int] = None
|
23
|
+
description: typing.Optional[str] = None
|
24
|
+
currentLocation: typing.Optional[str] = None
|
25
|
+
serviceId: typing.Optional[int] = None
|
26
|
+
productVariantId: typing.Optional[int] = None
|
27
|
+
quantity: typing.Optional[int] = None
|
28
|
+
|
29
|
+
|
30
|
+
@attr.s(auto_attribs=True)
|
31
|
+
class Load:
|
32
|
+
quantity: typing.Optional[int] = None
|
33
|
+
volume: typing.Optional[int] = None
|
34
|
+
weight: typing.Optional[int] = None
|
35
|
+
length: typing.Optional[int] = None
|
36
|
+
width: typing.Optional[int] = None
|
37
|
+
height: typing.Optional[int] = None
|
38
|
+
|
39
|
+
|
40
|
+
@attr.s(auto_attribs=True)
|
41
|
+
class Location:
|
42
|
+
latitude: typing.Optional[float] = None
|
43
|
+
longitude: typing.Optional[float] = None
|
44
|
+
|
45
|
+
|
46
|
+
@attr.s(auto_attribs=True)
|
47
|
+
class ShippingRequest:
|
48
|
+
contact: typing.Optional[Contact] = jstruct.JStruct[Contact]
|
49
|
+
name: typing.Optional[str] = None
|
50
|
+
address: typing.Optional[str] = None
|
51
|
+
location: typing.Optional[Location] = jstruct.JStruct[Location]
|
52
|
+
type: typing.Any = None
|
53
|
+
appointmentTime: typing.Optional[str] = None
|
54
|
+
timeWindowStart: typing.Any = None
|
55
|
+
timeWindowEnd: typing.Any = None
|
56
|
+
brandId: typing.Any = None
|
57
|
+
durationMinutes: typing.Optional[int] = None
|
58
|
+
notes: typing.Optional[str] = None
|
59
|
+
tripDate: typing.Optional[str] = None
|
60
|
+
customFields: typing.Optional[CustomFields] = jstruct.JStruct[CustomFields]
|
61
|
+
assignedTeamMemberId: typing.Optional[str] = None
|
62
|
+
source: typing.Any = None
|
63
|
+
sourceReference: typing.Any = None
|
64
|
+
load: typing.Optional[Load] = jstruct.JStruct[Load]
|
65
|
+
customerId: typing.Optional[int] = None
|
66
|
+
runNumber: typing.Optional[int] = None
|
67
|
+
teamRegionId: typing.Optional[int] = None
|
68
|
+
driverInstructions: typing.Any = None
|
69
|
+
lines: typing.Optional[typing.List[Line]] = jstruct.JList[Line]
|
@@ -0,0 +1,88 @@
|
|
1
|
+
import attr
|
2
|
+
import jstruct
|
3
|
+
import typing
|
4
|
+
|
5
|
+
|
6
|
+
@attr.s(auto_attribs=True)
|
7
|
+
class AssignedTo:
|
8
|
+
id: typing.Optional[str] = None
|
9
|
+
name: typing.Optional[str] = None
|
10
|
+
|
11
|
+
|
12
|
+
@attr.s(auto_attribs=True)
|
13
|
+
class Contact:
|
14
|
+
name: typing.Optional[str] = None
|
15
|
+
phone: typing.Optional[str] = None
|
16
|
+
email: typing.Optional[str] = None
|
17
|
+
|
18
|
+
|
19
|
+
@attr.s(auto_attribs=True)
|
20
|
+
class CustomFields:
|
21
|
+
custom1: typing.Optional[str] = None
|
22
|
+
custom2: typing.Optional[str] = None
|
23
|
+
custom3: typing.Optional[str] = None
|
24
|
+
|
25
|
+
|
26
|
+
@attr.s(auto_attribs=True)
|
27
|
+
class Line:
|
28
|
+
lineId: typing.Optional[int] = None
|
29
|
+
itemId: typing.Optional[int] = None
|
30
|
+
serviceId: typing.Any = None
|
31
|
+
productVariantId: typing.Any = None
|
32
|
+
barcode: typing.Optional[int] = None
|
33
|
+
description: typing.Optional[str] = None
|
34
|
+
quantity: typing.Optional[int] = None
|
35
|
+
status: typing.Optional[str] = None
|
36
|
+
itemStatus: typing.Optional[str] = None
|
37
|
+
unitPriceExTax: typing.Optional[int] = None
|
38
|
+
priceCurrency: typing.Optional[str] = None
|
39
|
+
|
40
|
+
|
41
|
+
@attr.s(auto_attribs=True)
|
42
|
+
class Load:
|
43
|
+
quantity: typing.Optional[int] = None
|
44
|
+
volume: typing.Optional[int] = None
|
45
|
+
weight: typing.Optional[int] = None
|
46
|
+
length: typing.Optional[int] = None
|
47
|
+
width: typing.Optional[int] = None
|
48
|
+
height: typing.Optional[int] = None
|
49
|
+
|
50
|
+
|
51
|
+
@attr.s(auto_attribs=True)
|
52
|
+
class Location:
|
53
|
+
latitude: typing.Optional[float] = None
|
54
|
+
longitude: typing.Optional[float] = None
|
55
|
+
|
56
|
+
|
57
|
+
@attr.s(auto_attribs=True)
|
58
|
+
class ShippingResponse:
|
59
|
+
assignedTo: typing.Optional[AssignedTo] = jstruct.JStruct[AssignedTo]
|
60
|
+
stopId: typing.Optional[int] = None
|
61
|
+
status: typing.Optional[str] = None
|
62
|
+
brandId: typing.Any = None
|
63
|
+
contact: typing.Optional[Contact] = jstruct.JStruct[Contact]
|
64
|
+
name: typing.Optional[str] = None
|
65
|
+
address: typing.Optional[str] = None
|
66
|
+
location: typing.Optional[Location] = jstruct.JStruct[Location]
|
67
|
+
tripDate: typing.Optional[str] = None
|
68
|
+
appointmentTime: typing.Optional[str] = None
|
69
|
+
timeWindowStart: typing.Any = None
|
70
|
+
timeWindowEnd: typing.Any = None
|
71
|
+
durationMinutes: typing.Optional[int] = None
|
72
|
+
notes: typing.Optional[str] = None
|
73
|
+
lastModifiedDate: typing.Optional[str] = None
|
74
|
+
customFields: typing.Optional[CustomFields] = jstruct.JStruct[CustomFields]
|
75
|
+
type: typing.Any = None
|
76
|
+
shipmentId: typing.Optional[int] = None
|
77
|
+
load: typing.Optional[Load] = jstruct.JStruct[Load]
|
78
|
+
source: typing.Any = None
|
79
|
+
sourceReference: typing.Any = None
|
80
|
+
customerId: typing.Optional[int] = None
|
81
|
+
runNumber: typing.Optional[int] = None
|
82
|
+
teamRegionId: typing.Optional[int] = None
|
83
|
+
teamMemberInvoiceId: typing.Optional[int] = None
|
84
|
+
customerInvoiceId: typing.Optional[int] = None
|
85
|
+
arrivalDate: typing.Optional[str] = None
|
86
|
+
lines: typing.Optional[typing.List[Line]] = jstruct.JList[Line]
|
87
|
+
driverInstructions: typing.Any = None
|
88
|
+
oneTimePin: typing.Optional[str] = None
|
@@ -0,0 +1,88 @@
|
|
1
|
+
import attr
|
2
|
+
import jstruct
|
3
|
+
import typing
|
4
|
+
|
5
|
+
|
6
|
+
@attr.s(auto_attribs=True)
|
7
|
+
class AssignedTo:
|
8
|
+
id: typing.Optional[str] = None
|
9
|
+
name: typing.Optional[str] = None
|
10
|
+
|
11
|
+
|
12
|
+
@attr.s(auto_attribs=True)
|
13
|
+
class Contact:
|
14
|
+
name: typing.Optional[str] = None
|
15
|
+
phone: typing.Optional[str] = None
|
16
|
+
email: typing.Optional[str] = None
|
17
|
+
|
18
|
+
|
19
|
+
@attr.s(auto_attribs=True)
|
20
|
+
class CustomFields:
|
21
|
+
custom1: typing.Optional[str] = None
|
22
|
+
custom2: typing.Optional[str] = None
|
23
|
+
custom3: typing.Optional[str] = None
|
24
|
+
|
25
|
+
|
26
|
+
@attr.s(auto_attribs=True)
|
27
|
+
class Line:
|
28
|
+
lineId: typing.Optional[int] = None
|
29
|
+
itemId: typing.Optional[int] = None
|
30
|
+
serviceId: typing.Any = None
|
31
|
+
productVariantId: typing.Any = None
|
32
|
+
barcode: typing.Optional[int] = None
|
33
|
+
description: typing.Optional[str] = None
|
34
|
+
quantity: typing.Optional[int] = None
|
35
|
+
status: typing.Optional[str] = None
|
36
|
+
itemStatus: typing.Optional[str] = None
|
37
|
+
unitPriceExTax: typing.Optional[int] = None
|
38
|
+
priceCurrency: typing.Optional[str] = None
|
39
|
+
|
40
|
+
|
41
|
+
@attr.s(auto_attribs=True)
|
42
|
+
class Load:
|
43
|
+
quantity: typing.Optional[int] = None
|
44
|
+
volume: typing.Optional[int] = None
|
45
|
+
weight: typing.Optional[int] = None
|
46
|
+
length: typing.Optional[int] = None
|
47
|
+
width: typing.Optional[int] = None
|
48
|
+
height: typing.Optional[int] = None
|
49
|
+
|
50
|
+
|
51
|
+
@attr.s(auto_attribs=True)
|
52
|
+
class Location:
|
53
|
+
latitude: typing.Optional[float] = None
|
54
|
+
longitude: typing.Optional[float] = None
|
55
|
+
|
56
|
+
|
57
|
+
@attr.s(auto_attribs=True)
|
58
|
+
class TrackingResponse:
|
59
|
+
assignedTo: typing.Optional[AssignedTo] = jstruct.JStruct[AssignedTo]
|
60
|
+
stopId: typing.Optional[int] = None
|
61
|
+
status: typing.Optional[str] = None
|
62
|
+
brandId: typing.Any = None
|
63
|
+
contact: typing.Optional[Contact] = jstruct.JStruct[Contact]
|
64
|
+
name: typing.Optional[str] = None
|
65
|
+
address: typing.Optional[str] = None
|
66
|
+
location: typing.Optional[Location] = jstruct.JStruct[Location]
|
67
|
+
tripDate: typing.Optional[str] = None
|
68
|
+
appointmentTime: typing.Optional[str] = None
|
69
|
+
timeWindowStart: typing.Any = None
|
70
|
+
timeWindowEnd: typing.Any = None
|
71
|
+
durationMinutes: typing.Optional[int] = None
|
72
|
+
notes: typing.Optional[str] = None
|
73
|
+
lastModifiedDate: typing.Optional[str] = None
|
74
|
+
customFields: typing.Optional[CustomFields] = jstruct.JStruct[CustomFields]
|
75
|
+
type: typing.Any = None
|
76
|
+
shipmentId: typing.Optional[int] = None
|
77
|
+
load: typing.Optional[Load] = jstruct.JStruct[Load]
|
78
|
+
source: typing.Any = None
|
79
|
+
sourceReference: typing.Any = None
|
80
|
+
customerId: typing.Optional[int] = None
|
81
|
+
runNumber: typing.Optional[int] = None
|
82
|
+
teamRegionId: typing.Optional[int] = None
|
83
|
+
teamMemberInvoiceId: typing.Optional[int] = None
|
84
|
+
customerInvoiceId: typing.Optional[int] = None
|
85
|
+
arrivalDate: typing.Optional[str] = None
|
86
|
+
lines: typing.Optional[typing.List[Line]] = jstruct.JList[Line]
|
87
|
+
driverInstructions: typing.Any = None
|
88
|
+
oneTimePin: typing.Optional[str] = None
|
@@ -0,0 +1,45 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: karrio_locate2u
|
3
|
+
Version: 2025.5rc1
|
4
|
+
Summary: Karrio - Locate2u Shipping Extension
|
5
|
+
Author-email: karrio <hello@karrio.io>
|
6
|
+
License-Expression: Apache-2.0
|
7
|
+
Project-URL: Homepage, https://github.com/karrioapi/karrio
|
8
|
+
Classifier: Intended Audience :: Developers
|
9
|
+
Classifier: Operating System :: OS Independent
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
11
|
+
Requires-Python: >=3.7
|
12
|
+
Description-Content-Type: text/markdown
|
13
|
+
Requires-Dist: karrio
|
14
|
+
|
15
|
+
|
16
|
+
# karrio.locate2u
|
17
|
+
|
18
|
+
This package is a Locate2u extension of the [karrio](https://pypi.org/project/karrio) multi carrier shipping SDK.
|
19
|
+
|
20
|
+
## Requirements
|
21
|
+
|
22
|
+
`Python 3.7+`
|
23
|
+
|
24
|
+
## Installation
|
25
|
+
|
26
|
+
```bash
|
27
|
+
pip install karrio.locate2u
|
28
|
+
```
|
29
|
+
|
30
|
+
## Usage
|
31
|
+
|
32
|
+
```python
|
33
|
+
import karrio.sdk as karrio
|
34
|
+
from karrio.mappers.locate2u.settings import Settings
|
35
|
+
|
36
|
+
|
37
|
+
# Initialize a carrier gateway
|
38
|
+
locate2u = karrio.gateway["locate2u"].create(
|
39
|
+
Settings(
|
40
|
+
...
|
41
|
+
)
|
42
|
+
)
|
43
|
+
```
|
44
|
+
|
45
|
+
Check the [Karrio Mutli-carrier SDK docs](https://docs.karrio.io) for Shipping API requests
|
@@ -0,0 +1,22 @@
|
|
1
|
+
karrio/mappers/locate2u/__init__.py,sha256=GIqA5pU1RiPY4Pmo5rSFxMq-ge57gt5Y70MYMOcRt0Q,152
|
2
|
+
karrio/mappers/locate2u/mapper.py,sha256=1fG0YzxaCbXiMvykeoyzkGEPiducxEF3SeFQH5Mj7EY,2042
|
3
|
+
karrio/mappers/locate2u/proxy.py,sha256=TkCB2hTN4oa8a3EP3CIWL9g6Nv6hz_Ql0qXCf6vg6rw,2663
|
4
|
+
karrio/mappers/locate2u/settings.py,sha256=YCPAfEv9DfyNcCET23qdQJ6KKxgoMxTLYcm-woN35B0,964
|
5
|
+
karrio/plugins/locate2u/__init__.py,sha256=PWzbROuRfVuUeSiA_GFY_CsZKuPh8w7j5ViTVOPdc7k,480
|
6
|
+
karrio/providers/locate2u/__init__.py,sha256=DD0GxkUX-CBVUApoRkl0Ue2j8Nx3mAottAxa-jej8bE,322
|
7
|
+
karrio/providers/locate2u/error.py,sha256=4daLpSeCZKVfZhad7MSNV8JR3u2GIH4m5D-fEIaPY_I,930
|
8
|
+
karrio/providers/locate2u/tracking.py,sha256=atAmnoeJIR9QJg3LKcyf_Quxnr9sI8q7X9TprcTXIdo,2394
|
9
|
+
karrio/providers/locate2u/units.py,sha256=GdiA_95_8HfSW-r6UoMGPgW9_WmD4vo2ESAYvizITBw,2292
|
10
|
+
karrio/providers/locate2u/utils.py,sha256=81Cl5K0wEq5CD9NbsA_uV7jMsVzWODBjrtIVYpOJGkA,2513
|
11
|
+
karrio/providers/locate2u/shipment/__init__.py,sha256=8UpJ7BqZawO7zi4ziPDjeJw3QJZBYtp_u7nhRVkdAYE,233
|
12
|
+
karrio/providers/locate2u/shipment/cancel.py,sha256=f0ZE78hSGG8cin0s4tmD5piAhrYYSmXwOwVMJszvPYQ,1128
|
13
|
+
karrio/providers/locate2u/shipment/create.py,sha256=tel33d5Ue0sdN_4P5Kg8EgK-8X0IBwSex_qJoOPdNjk,4110
|
14
|
+
karrio/schemas/locate2u/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
15
|
+
karrio/schemas/locate2u/shipping_request.py,sha256=WAQ5SZFnqI8rkAxZR3fIdEegk_sX2JMZvyJCs0Dyleg,2180
|
16
|
+
karrio/schemas/locate2u/shipping_response.py,sha256=5WB5Zc36rMUT72BfDva4U73zG2gCc_v5f3ut01IE9iU,2879
|
17
|
+
karrio/schemas/locate2u/tracking_response.py,sha256=WcZQWV952GnSijBw1XuK5xX_Q-cXTDo5kx2oRuFVlx8,2879
|
18
|
+
karrio_locate2u-2025.5rc1.dist-info/METADATA,sha256=Awgk0xKsLkXn6dKYXydNEMR6aLjeP9FFdCCEe9iuejI,1000
|
19
|
+
karrio_locate2u-2025.5rc1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
20
|
+
karrio_locate2u-2025.5rc1.dist-info/entry_points.txt,sha256=sxqKOEWh7elHsX05SNXUrJaJoqVFCIB0Wit6KsazyiA,61
|
21
|
+
karrio_locate2u-2025.5rc1.dist-info/top_level.txt,sha256=FZCY8Nwft8oEGHdl--xku8P3TrnOxu5dETEU_fWpRSM,20
|
22
|
+
karrio_locate2u-2025.5rc1.dist-info/RECORD,,
|