karrio-sendcloud 2025.5rc7__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/sendcloud/__init__.py +20 -0
- karrio/mappers/sendcloud/mapper.py +84 -0
- karrio/mappers/sendcloud/proxy.py +114 -0
- karrio/mappers/sendcloud/settings.py +21 -0
- karrio/plugins/sendcloud/__init__.py +2 -0
- karrio/providers/sendcloud/__init__.py +24 -0
- karrio/providers/sendcloud/error.py +74 -0
- karrio/providers/sendcloud/pickup/__init__.py +14 -0
- karrio/providers/sendcloud/pickup/cancel.py +53 -0
- karrio/providers/sendcloud/pickup/create.py +66 -0
- karrio/providers/sendcloud/pickup/update.py +67 -0
- karrio/providers/sendcloud/rate.py +139 -0
- karrio/providers/sendcloud/shipment/__init__.py +9 -0
- karrio/providers/sendcloud/shipment/cancel.py +57 -0
- karrio/providers/sendcloud/shipment/create.py +128 -0
- karrio/providers/sendcloud/tracking.py +115 -0
- karrio/providers/sendcloud/units.py +87 -0
- karrio/providers/sendcloud/utils.py +108 -0
- karrio/schemas/sendcloud/__init__.py +0 -0
- karrio/schemas/sendcloud/auth_request.py +32 -0
- karrio/schemas/sendcloud/auth_response.py +13 -0
- karrio/schemas/sendcloud/error.py +22 -0
- karrio/schemas/sendcloud/rate_request.py +17 -0
- karrio/schemas/sendcloud/rate_response.py +144 -0
- karrio/schemas/sendcloud/shipment_request.py +45 -0
- karrio/schemas/sendcloud/shipment_response.py +207 -0
- karrio/schemas/sendcloud/tracking_request.py +22 -0
- karrio/schemas/sendcloud/tracking_response.py +129 -0
- karrio_sendcloud-2025.5rc7.dist-info/METADATA +44 -0
- karrio_sendcloud-2025.5rc7.dist-info/RECORD +33 -0
- karrio_sendcloud-2025.5rc7.dist-info/WHEEL +5 -0
- karrio_sendcloud-2025.5rc7.dist-info/entry_points.txt +2 -0
- karrio_sendcloud-2025.5rc7.dist-info/top_level.txt +3 -0
@@ -0,0 +1,20 @@
|
|
1
|
+
from karrio.mappers.sendcloud.mapper import Mapper
|
2
|
+
from karrio.mappers.sendcloud.proxy import Proxy
|
3
|
+
from karrio.mappers.sendcloud.settings import Settings
|
4
|
+
|
5
|
+
# Define METADATA here to avoid circular imports
|
6
|
+
from karrio.core.metadata import Metadata as PluginMetadata
|
7
|
+
import karrio.providers.sendcloud.units as units
|
8
|
+
import karrio.providers.sendcloud.utils as utils
|
9
|
+
|
10
|
+
METADATA = PluginMetadata(
|
11
|
+
id="sendcloud",
|
12
|
+
label="SendCloud",
|
13
|
+
Mapper=Mapper,
|
14
|
+
Proxy=Proxy,
|
15
|
+
Settings=Settings,
|
16
|
+
is_hub=True,
|
17
|
+
options=units.ShippingOption,
|
18
|
+
services=units.ShippingService,
|
19
|
+
connection_configs=utils.ConnectionConfig,
|
20
|
+
)
|
@@ -0,0 +1,84 @@
|
|
1
|
+
"""Karrio SendCloud 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.sendcloud as provider
|
8
|
+
import karrio.mappers.sendcloud.settings as provider_settings
|
9
|
+
|
10
|
+
|
11
|
+
class Mapper(mapper.Mapper):
|
12
|
+
settings: provider_settings.Settings
|
13
|
+
|
14
|
+
def create_rate_request(
|
15
|
+
self, payload: models.RateRequest
|
16
|
+
) -> lib.Serializable:
|
17
|
+
return provider.rate_request(payload, self.settings)
|
18
|
+
|
19
|
+
def create_tracking_request(
|
20
|
+
self, payload: models.TrackingRequest
|
21
|
+
) -> lib.Serializable:
|
22
|
+
return provider.tracking_request(payload, self.settings)
|
23
|
+
|
24
|
+
def create_shipment_request(
|
25
|
+
self, payload: models.ShipmentRequest
|
26
|
+
) -> lib.Serializable:
|
27
|
+
return provider.shipment_request(payload, self.settings)
|
28
|
+
|
29
|
+
def create_pickup_request(
|
30
|
+
self, payload: models.PickupRequest
|
31
|
+
) -> lib.Serializable:
|
32
|
+
return provider.pickup_request(payload, self.settings)
|
33
|
+
|
34
|
+
def create_pickup_update_request(
|
35
|
+
self, payload: models.PickupUpdateRequest
|
36
|
+
) -> lib.Serializable:
|
37
|
+
return provider.pickup_update_request(payload, self.settings)
|
38
|
+
|
39
|
+
def create_cancel_pickup_request(
|
40
|
+
self, payload: models.PickupCancelRequest
|
41
|
+
) -> lib.Serializable:
|
42
|
+
return provider.pickup_cancel_request(payload, self.settings)
|
43
|
+
|
44
|
+
def create_cancel_shipment_request(
|
45
|
+
self, payload: models.ShipmentCancelRequest
|
46
|
+
) -> lib.Serializable[str]:
|
47
|
+
return provider.shipment_cancel_request(payload, self.settings)
|
48
|
+
|
49
|
+
|
50
|
+
def parse_cancel_pickup_response(
|
51
|
+
self, response: lib.Deserializable[str]
|
52
|
+
) -> typing.Tuple[models.ConfirmationDetails, typing.List[models.Message]]:
|
53
|
+
return provider.parse_pickup_cancel_response(response, self.settings)
|
54
|
+
|
55
|
+
def parse_cancel_shipment_response(
|
56
|
+
self, response: lib.Deserializable[str]
|
57
|
+
) -> typing.Tuple[models.ConfirmationDetails, typing.List[models.Message]]:
|
58
|
+
return provider.parse_shipment_cancel_response(response, self.settings)
|
59
|
+
|
60
|
+
def parse_pickup_response(
|
61
|
+
self, response: lib.Deserializable[str]
|
62
|
+
) -> typing.Tuple[models.PickupDetails, typing.List[models.Message]]:
|
63
|
+
return provider.parse_pickup_response(response, self.settings)
|
64
|
+
|
65
|
+
def parse_pickup_update_response(
|
66
|
+
self, response: lib.Deserializable[str]
|
67
|
+
) -> typing.Tuple[models.PickupDetails, typing.List[models.Message]]:
|
68
|
+
return provider.parse_pickup_update_response(response, self.settings)
|
69
|
+
|
70
|
+
def parse_rate_response(
|
71
|
+
self, response: lib.Deserializable[str]
|
72
|
+
) -> typing.Tuple[typing.List[models.RateDetails], typing.List[models.Message]]:
|
73
|
+
return provider.parse_rate_response(response, self.settings)
|
74
|
+
|
75
|
+
def parse_shipment_response(
|
76
|
+
self, response: lib.Deserializable[str]
|
77
|
+
) -> typing.Tuple[models.ShipmentDetails, typing.List[models.Message]]:
|
78
|
+
return provider.parse_shipment_response(response, self.settings)
|
79
|
+
|
80
|
+
def parse_tracking_response(
|
81
|
+
self, response: lib.Deserializable[str]
|
82
|
+
) -> typing.Tuple[typing.List[models.TrackingDetails], typing.List[models.Message]]:
|
83
|
+
return provider.parse_tracking_response(response, self.settings)
|
84
|
+
|
@@ -0,0 +1,114 @@
|
|
1
|
+
"""Karrio SendCloud client proxy."""
|
2
|
+
|
3
|
+
import karrio.lib as lib
|
4
|
+
import karrio.api.proxy as proxy
|
5
|
+
import karrio.mappers.sendcloud.settings as provider_settings
|
6
|
+
|
7
|
+
|
8
|
+
class Proxy(proxy.Proxy):
|
9
|
+
settings: provider_settings.Settings
|
10
|
+
|
11
|
+
def get_rates(self, request: lib.Serializable) -> lib.Deserializable[dict]:
|
12
|
+
response = lib.request(
|
13
|
+
url=f"{self.settings.server_url}/fetch-shipping-options",
|
14
|
+
data=request.serialize(),
|
15
|
+
trace=self.trace_as("json"),
|
16
|
+
method="POST",
|
17
|
+
headers={
|
18
|
+
"Content-Type": "application/json",
|
19
|
+
"Authorization": f"Bearer {self.settings.access_token}",
|
20
|
+
},
|
21
|
+
)
|
22
|
+
|
23
|
+
return lib.Deserializable(response, lib.to_dict)
|
24
|
+
|
25
|
+
def create_shipment(self, request: lib.Serializable) -> lib.Deserializable[dict]:
|
26
|
+
response = lib.request(
|
27
|
+
url=f"{self.settings.server_url}/parcels",
|
28
|
+
data=request.serialize(),
|
29
|
+
trace=self.trace_as("json"),
|
30
|
+
method="POST",
|
31
|
+
headers={
|
32
|
+
"Content-Type": "application/json",
|
33
|
+
"Authorization": f"Bearer {self.settings.access_token}",
|
34
|
+
},
|
35
|
+
)
|
36
|
+
|
37
|
+
return lib.Deserializable(response, lib.to_dict)
|
38
|
+
|
39
|
+
def cancel_shipment(self, request: lib.Serializable) -> lib.Deserializable[dict]:
|
40
|
+
shipment_id = request.ctx.get("shipment_id")
|
41
|
+
response = lib.request(
|
42
|
+
url=f"{self.settings.server_url}/parcels/{shipment_id}/cancel",
|
43
|
+
trace=self.trace_as("json"),
|
44
|
+
method="POST",
|
45
|
+
headers={
|
46
|
+
"Content-Type": "application/json",
|
47
|
+
"Authorization": f"Bearer {self.settings.access_token}",
|
48
|
+
},
|
49
|
+
)
|
50
|
+
|
51
|
+
return lib.Deserializable(response, lib.to_dict)
|
52
|
+
|
53
|
+
def get_tracking(self, request: lib.Serializable) -> lib.Deserializable[dict]:
|
54
|
+
tracking_number = request.ctx.get("tracking_number")
|
55
|
+
carrier = request.ctx.get("carrier", "")
|
56
|
+
|
57
|
+
# SendCloud tracking endpoint
|
58
|
+
url = f"{self.settings.server_url}/tracking/{tracking_number}"
|
59
|
+
if carrier:
|
60
|
+
url = f"{url}/{carrier}"
|
61
|
+
|
62
|
+
response = lib.request(
|
63
|
+
url=url,
|
64
|
+
trace=self.trace_as("json"),
|
65
|
+
method="GET",
|
66
|
+
headers={
|
67
|
+
"Authorization": f"Bearer {self.settings.access_token}",
|
68
|
+
},
|
69
|
+
)
|
70
|
+
|
71
|
+
return lib.Deserializable(response, lib.to_dict)
|
72
|
+
|
73
|
+
def schedule_pickup(self, request: lib.Serializable) -> lib.Deserializable[dict]:
|
74
|
+
response = lib.request(
|
75
|
+
url=f"{self.settings.server_url}/pickups",
|
76
|
+
data=request.serialize(),
|
77
|
+
trace=self.trace_as("json"),
|
78
|
+
method="POST",
|
79
|
+
headers={
|
80
|
+
"Content-Type": "application/json",
|
81
|
+
"Authorization": f"Bearer {self.settings.access_token}",
|
82
|
+
},
|
83
|
+
)
|
84
|
+
|
85
|
+
return lib.Deserializable(response, lib.to_dict)
|
86
|
+
|
87
|
+
def modify_pickup(self, request: lib.Serializable) -> lib.Deserializable[dict]:
|
88
|
+
pickup_id = request.ctx.get("pickup_id")
|
89
|
+
response = lib.request(
|
90
|
+
url=f"{self.settings.server_url}/pickups/{pickup_id}",
|
91
|
+
data=request.serialize(),
|
92
|
+
trace=self.trace_as("json"),
|
93
|
+
method="PUT",
|
94
|
+
headers={
|
95
|
+
"Content-Type": "application/json",
|
96
|
+
"Authorization": f"Bearer {self.settings.access_token}",
|
97
|
+
},
|
98
|
+
)
|
99
|
+
|
100
|
+
return lib.Deserializable(response, lib.to_dict)
|
101
|
+
|
102
|
+
def cancel_pickup(self, request: lib.Serializable) -> lib.Deserializable[dict]:
|
103
|
+
pickup_id = request.ctx.get("pickup_id")
|
104
|
+
response = lib.request(
|
105
|
+
url=f"{self.settings.server_url}/pickups/{pickup_id}",
|
106
|
+
trace=self.trace_as("json"),
|
107
|
+
method="DELETE",
|
108
|
+
headers={
|
109
|
+
"Authorization": f"Bearer {self.settings.access_token}",
|
110
|
+
},
|
111
|
+
)
|
112
|
+
|
113
|
+
return lib.Deserializable(response, lib.to_dict)
|
114
|
+
|
@@ -0,0 +1,21 @@
|
|
1
|
+
"""Karrio SendCloud client settings."""
|
2
|
+
|
3
|
+
import attr
|
4
|
+
import karrio.providers.sendcloud.utils as provider_utils
|
5
|
+
|
6
|
+
|
7
|
+
@attr.s(auto_attribs=True)
|
8
|
+
class Settings(provider_utils.Settings):
|
9
|
+
"""SendCloud connection settings."""
|
10
|
+
|
11
|
+
# OAuth2 API connection properties
|
12
|
+
client_id: str
|
13
|
+
client_secret: str
|
14
|
+
|
15
|
+
# generic properties
|
16
|
+
id: str = None
|
17
|
+
test_mode: bool = False
|
18
|
+
carrier_id: str = "sendcloud"
|
19
|
+
account_country_code: str = None
|
20
|
+
metadata: dict = {}
|
21
|
+
config: dict = {}
|
@@ -0,0 +1,24 @@
|
|
1
|
+
"""Karrio SendCloud provider imports."""
|
2
|
+
from karrio.providers.sendcloud.utils import Settings
|
3
|
+
from karrio.providers.sendcloud.rate import (
|
4
|
+
parse_rate_response,
|
5
|
+
rate_request,
|
6
|
+
)
|
7
|
+
from karrio.providers.sendcloud.shipment import (
|
8
|
+
parse_shipment_cancel_response,
|
9
|
+
parse_shipment_response,
|
10
|
+
shipment_cancel_request,
|
11
|
+
shipment_request,
|
12
|
+
)
|
13
|
+
from karrio.providers.sendcloud.pickup import (
|
14
|
+
parse_pickup_cancel_response,
|
15
|
+
parse_pickup_update_response,
|
16
|
+
parse_pickup_response,
|
17
|
+
pickup_update_request,
|
18
|
+
pickup_cancel_request,
|
19
|
+
pickup_request,
|
20
|
+
)
|
21
|
+
from karrio.providers.sendcloud.tracking import (
|
22
|
+
parse_tracking_response,
|
23
|
+
tracking_request,
|
24
|
+
)
|
@@ -0,0 +1,74 @@
|
|
1
|
+
"""Karrio SendCloud error parser."""
|
2
|
+
|
3
|
+
import typing
|
4
|
+
import karrio.lib as lib
|
5
|
+
import karrio.core.models as models
|
6
|
+
import karrio.providers.sendcloud.utils as provider_utils
|
7
|
+
import karrio.schemas.sendcloud.error as error_schemas
|
8
|
+
|
9
|
+
|
10
|
+
def parse_error_response(
|
11
|
+
response: typing.Union[dict, list],
|
12
|
+
settings: provider_utils.Settings,
|
13
|
+
**kwargs,
|
14
|
+
) -> typing.List[models.Message]:
|
15
|
+
errors: typing.List[models.Message] = []
|
16
|
+
|
17
|
+
# Handle dict response
|
18
|
+
if isinstance(response, dict):
|
19
|
+
# Check for error field
|
20
|
+
if "error" in response:
|
21
|
+
error_data = response["error"]
|
22
|
+
errors.append(
|
23
|
+
models.Message(
|
24
|
+
carrier_id=settings.carrier_id,
|
25
|
+
carrier_name=settings.carrier_name,
|
26
|
+
code=error_data.get("code", "UNKNOWN"),
|
27
|
+
message=error_data.get("message", "Unknown error occurred"),
|
28
|
+
details={
|
29
|
+
"details": error_data.get("details", ""),
|
30
|
+
},
|
31
|
+
)
|
32
|
+
)
|
33
|
+
|
34
|
+
# Check for validation errors field
|
35
|
+
if "errors" in response and isinstance(response["errors"], dict):
|
36
|
+
for field, messages in response["errors"].items():
|
37
|
+
for message in messages if isinstance(messages, list) else [messages]:
|
38
|
+
errors.append(
|
39
|
+
models.Message(
|
40
|
+
carrier_id=settings.carrier_id,
|
41
|
+
carrier_name=settings.carrier_name,
|
42
|
+
code="VALIDATION_ERROR",
|
43
|
+
message=f"{field}: {message}",
|
44
|
+
details={**kwargs, "field": field},
|
45
|
+
)
|
46
|
+
)
|
47
|
+
|
48
|
+
# Check for simple message field (common in APIs)
|
49
|
+
if not errors and "message" in response:
|
50
|
+
errors.append(
|
51
|
+
models.Message(
|
52
|
+
carrier_id=settings.carrier_id,
|
53
|
+
carrier_name=settings.carrier_name,
|
54
|
+
code=response.get("code", "ERROR"),
|
55
|
+
message=response["message"],
|
56
|
+
details={**kwargs},
|
57
|
+
)
|
58
|
+
)
|
59
|
+
|
60
|
+
# Handle list response (batch errors)
|
61
|
+
elif isinstance(response, list):
|
62
|
+
for idx, error in enumerate(response):
|
63
|
+
if isinstance(error, dict):
|
64
|
+
errors.append(
|
65
|
+
models.Message(
|
66
|
+
carrier_id=settings.carrier_id,
|
67
|
+
carrier_name=settings.carrier_name,
|
68
|
+
code=error.get("code", "ERROR"),
|
69
|
+
message=error.get("message", f"Error at index {idx}"),
|
70
|
+
details={**kwargs, "index": idx},
|
71
|
+
)
|
72
|
+
)
|
73
|
+
|
74
|
+
return errors
|
@@ -0,0 +1,14 @@
|
|
1
|
+
"""Karrio SendCloud pickup API imports."""
|
2
|
+
|
3
|
+
from karrio.providers.sendcloud.pickup.create import (
|
4
|
+
parse_pickup_response,
|
5
|
+
pickup_request,
|
6
|
+
)
|
7
|
+
from karrio.providers.sendcloud.pickup.update import (
|
8
|
+
parse_pickup_update_response,
|
9
|
+
pickup_update_request,
|
10
|
+
)
|
11
|
+
from karrio.providers.sendcloud.pickup.cancel import (
|
12
|
+
parse_pickup_cancel_response,
|
13
|
+
pickup_cancel_request,
|
14
|
+
)
|
@@ -0,0 +1,53 @@
|
|
1
|
+
"""Karrio SendCloud pickup cancellation API implementation."""
|
2
|
+
|
3
|
+
import typing
|
4
|
+
import karrio.lib as lib
|
5
|
+
import karrio.core.models as models
|
6
|
+
import karrio.providers.sendcloud.error as error
|
7
|
+
import karrio.providers.sendcloud.utils as provider_utils
|
8
|
+
|
9
|
+
|
10
|
+
def parse_pickup_cancel_response(
|
11
|
+
_response: lib.Deserializable[dict],
|
12
|
+
settings: provider_utils.Settings,
|
13
|
+
) -> typing.Tuple[models.ConfirmationDetails, typing.List[models.Message]]:
|
14
|
+
"""Parse pickup cancellation response from SendCloud API"""
|
15
|
+
response = _response.deserialize()
|
16
|
+
messages = error.parse_error_response(response, settings)
|
17
|
+
|
18
|
+
# Check if cancellation was successful
|
19
|
+
success = _extract_cancellation_status(response)
|
20
|
+
confirmation = (
|
21
|
+
models.ConfirmationDetails(
|
22
|
+
carrier_id=settings.carrier_id,
|
23
|
+
carrier_name=settings.carrier_name,
|
24
|
+
success=success,
|
25
|
+
operation="Cancel Pickup",
|
26
|
+
) if success else None
|
27
|
+
)
|
28
|
+
|
29
|
+
return confirmation, messages
|
30
|
+
|
31
|
+
|
32
|
+
def _extract_cancellation_status(
|
33
|
+
response: dict
|
34
|
+
) -> bool:
|
35
|
+
"""Extract cancellation success status from SendCloud response"""
|
36
|
+
return response.get("success", False)
|
37
|
+
|
38
|
+
|
39
|
+
|
40
|
+
def pickup_cancel_request(
|
41
|
+
payload: models.PickupCancelRequest,
|
42
|
+
settings: provider_utils.Settings,
|
43
|
+
) -> lib.Serializable:
|
44
|
+
"""Create pickup cancellation request for SendCloud API"""
|
45
|
+
return lib.Serializable(
|
46
|
+
dict(
|
47
|
+
confirmation_number=payload.confirmation_number,
|
48
|
+
reason=payload.reason,
|
49
|
+
),
|
50
|
+
lib.to_dict,
|
51
|
+
ctx=dict(pickup_id=payload.confirmation_number),
|
52
|
+
)
|
53
|
+
|
@@ -0,0 +1,66 @@
|
|
1
|
+
"""Karrio SendCloud pickup API implementation."""
|
2
|
+
|
3
|
+
import typing
|
4
|
+
import karrio.lib as lib
|
5
|
+
import karrio.core.models as models
|
6
|
+
import karrio.providers.sendcloud.error as error
|
7
|
+
import karrio.providers.sendcloud.utils as provider_utils
|
8
|
+
|
9
|
+
|
10
|
+
def parse_pickup_response(
|
11
|
+
_response: lib.Deserializable[dict],
|
12
|
+
settings: provider_utils.Settings,
|
13
|
+
) -> typing.Tuple[models.PickupDetails, typing.List[models.Message]]:
|
14
|
+
"""Parse pickup response from SendCloud API."""
|
15
|
+
response = _response.deserialize()
|
16
|
+
messages = error.parse_error_response(response, settings)
|
17
|
+
|
18
|
+
# Extract pickup details if successful
|
19
|
+
pickup = _extract_details(response, settings) if not messages else None
|
20
|
+
|
21
|
+
return pickup, messages
|
22
|
+
|
23
|
+
|
24
|
+
def _extract_details(
|
25
|
+
response: dict,
|
26
|
+
settings: provider_utils.Settings,
|
27
|
+
) -> models.PickupDetails:
|
28
|
+
"""Extract pickup details from SendCloud response."""
|
29
|
+
pickup_id = response.get("id")
|
30
|
+
pickup_date = response.get("pickup_date")
|
31
|
+
|
32
|
+
return models.PickupDetails(
|
33
|
+
carrier_id=settings.carrier_id,
|
34
|
+
carrier_name=settings.carrier_name,
|
35
|
+
confirmation_number=str(pickup_id),
|
36
|
+
pickup_date=lib.fdate(pickup_date),
|
37
|
+
pickup_charge=lib.to_money(0),
|
38
|
+
ready_time=lib.ftime(response.get("ready_time")),
|
39
|
+
closing_time=lib.ftime(response.get("closing_time")),
|
40
|
+
)
|
41
|
+
|
42
|
+
|
43
|
+
def pickup_request(
|
44
|
+
payload: models.PickupRequest,
|
45
|
+
settings: provider_utils.Settings,
|
46
|
+
) -> lib.Serializable:
|
47
|
+
"""Create a pickup request for SendCloud API."""
|
48
|
+
return lib.Serializable(
|
49
|
+
dict(
|
50
|
+
pickup_date=lib.fdate(payload.pickup_date),
|
51
|
+
ready_time=lib.ftime(payload.ready_time),
|
52
|
+
closing_time=lib.ftime(payload.closing_time),
|
53
|
+
address=dict(
|
54
|
+
company_name=payload.address.company_name,
|
55
|
+
contact_name=payload.address.person_name,
|
56
|
+
address_line1=payload.address.address_line1,
|
57
|
+
city=payload.address.city,
|
58
|
+
postal_code=payload.address.postal_code,
|
59
|
+
country_code=payload.address.country_code,
|
60
|
+
phone_number=payload.address.phone_number,
|
61
|
+
email=payload.address.email,
|
62
|
+
),
|
63
|
+
parcels=payload.parcels,
|
64
|
+
),
|
65
|
+
lib.to_dict,
|
66
|
+
)
|
@@ -0,0 +1,67 @@
|
|
1
|
+
"""Karrio SendCloud pickup update API implementation."""
|
2
|
+
|
3
|
+
import typing
|
4
|
+
import karrio.lib as lib
|
5
|
+
import karrio.core.models as models
|
6
|
+
import karrio.providers.sendcloud.error as error
|
7
|
+
import karrio.providers.sendcloud.utils as provider_utils
|
8
|
+
|
9
|
+
|
10
|
+
def parse_pickup_update_response(
|
11
|
+
_response: lib.Deserializable[dict],
|
12
|
+
settings: provider_utils.Settings,
|
13
|
+
) -> typing.Tuple[models.PickupDetails, typing.List[models.Message]]:
|
14
|
+
"""Parse pickup update response from SendCloud API."""
|
15
|
+
response = _response.deserialize()
|
16
|
+
messages = error.parse_error_response(response, settings)
|
17
|
+
|
18
|
+
# Extract updated pickup details
|
19
|
+
pickup = _extract_details(response, settings) if not messages else None
|
20
|
+
|
21
|
+
return pickup, messages
|
22
|
+
|
23
|
+
|
24
|
+
def _extract_details(
|
25
|
+
response: dict,
|
26
|
+
settings: provider_utils.Settings,
|
27
|
+
) -> models.PickupDetails:
|
28
|
+
"""Extract pickup details from SendCloud response."""
|
29
|
+
pickup_id = response.get("id")
|
30
|
+
pickup_date = response.get("pickup_date")
|
31
|
+
|
32
|
+
return models.PickupDetails(
|
33
|
+
carrier_id=settings.carrier_id,
|
34
|
+
carrier_name=settings.carrier_name,
|
35
|
+
confirmation_number=str(pickup_id),
|
36
|
+
pickup_date=lib.fdate(pickup_date),
|
37
|
+
pickup_charge=lib.to_money(0),
|
38
|
+
ready_time=lib.ftime(response.get("ready_time")),
|
39
|
+
closing_time=lib.ftime(response.get("closing_time")),
|
40
|
+
)
|
41
|
+
|
42
|
+
|
43
|
+
def pickup_update_request(
|
44
|
+
payload: models.PickupUpdateRequest,
|
45
|
+
settings: provider_utils.Settings,
|
46
|
+
) -> lib.Serializable:
|
47
|
+
"""Create a pickup update request for SendCloud API."""
|
48
|
+
return lib.Serializable(
|
49
|
+
dict(
|
50
|
+
pickup_date=lib.fdate(payload.pickup_date),
|
51
|
+
ready_time=lib.ftime(payload.ready_time),
|
52
|
+
closing_time=lib.ftime(payload.closing_time),
|
53
|
+
address=dict(
|
54
|
+
company_name=payload.address.company_name,
|
55
|
+
contact_name=payload.address.person_name,
|
56
|
+
address_line1=payload.address.address_line1,
|
57
|
+
city=payload.address.city,
|
58
|
+
postal_code=payload.address.postal_code,
|
59
|
+
country_code=payload.address.country_code,
|
60
|
+
phone_number=payload.address.phone_number,
|
61
|
+
email=payload.address.email,
|
62
|
+
),
|
63
|
+
),
|
64
|
+
lib.to_dict,
|
65
|
+
ctx=dict(pickup_id=payload.confirmation_number),
|
66
|
+
)
|
67
|
+
|
@@ -0,0 +1,139 @@
|
|
1
|
+
"""Karrio SendCloud rate API implementation."""
|
2
|
+
|
3
|
+
import karrio.schemas.sendcloud.rate_request as sendcloud
|
4
|
+
import karrio.schemas.sendcloud.rate_response as rating
|
5
|
+
|
6
|
+
import typing
|
7
|
+
import karrio.lib as lib
|
8
|
+
import karrio.core.units as units
|
9
|
+
import karrio.core.models as models
|
10
|
+
import karrio.providers.sendcloud.error as error
|
11
|
+
import karrio.providers.sendcloud.utils as provider_utils
|
12
|
+
import karrio.providers.sendcloud.units as provider_units
|
13
|
+
|
14
|
+
|
15
|
+
def parse_rate_response(
|
16
|
+
_response: lib.Deserializable[dict],
|
17
|
+
settings: provider_utils.Settings,
|
18
|
+
) -> typing.Tuple[typing.List[models.RateDetails], typing.List[models.Message]]:
|
19
|
+
response = _response.deserialize()
|
20
|
+
|
21
|
+
messages = error.parse_error_response(response, settings)
|
22
|
+
rates = [
|
23
|
+
_extract_details(option, settings)
|
24
|
+
for option in response.get("data", [])
|
25
|
+
]
|
26
|
+
|
27
|
+
return rates, messages
|
28
|
+
|
29
|
+
|
30
|
+
def _extract_details(
|
31
|
+
data: dict,
|
32
|
+
settings: provider_utils.Settings,
|
33
|
+
) -> models.RateDetails:
|
34
|
+
"""Extract rate details from SendCloud multi-carrier response."""
|
35
|
+
details = lib.to_object(rating.DatumType, data)
|
36
|
+
|
37
|
+
# Create composite service ID for hub carrier pattern
|
38
|
+
service_id = f"sendcloud_{details.carrier.code}_{details.product.code}"
|
39
|
+
|
40
|
+
# Extract pricing information
|
41
|
+
total_charge = 0.0
|
42
|
+
currency = "EUR"
|
43
|
+
transit_days = None
|
44
|
+
|
45
|
+
if details.quotes and len(details.quotes) > 0:
|
46
|
+
quote = details.quotes[0]
|
47
|
+
if quote.price and quote.price.total:
|
48
|
+
total_charge = float(quote.price.total.value or 0)
|
49
|
+
currency = quote.price.total.currency or "EUR"
|
50
|
+
transit_days = getattr(quote, 'leadtime', None)
|
51
|
+
|
52
|
+
# Extract extra charges from breakdown
|
53
|
+
extra_charges = []
|
54
|
+
if details.quotes and len(details.quotes) > 0 and details.quotes[0].price:
|
55
|
+
for breakdown in details.quotes[0].price.breakdown or []:
|
56
|
+
if breakdown.price and float(breakdown.price.value or 0) > 0:
|
57
|
+
extra_charges.append(
|
58
|
+
models.ChargeDetails(
|
59
|
+
name=breakdown.label,
|
60
|
+
amount=lib.to_money(breakdown.price.value),
|
61
|
+
currency=breakdown.price.currency,
|
62
|
+
)
|
63
|
+
)
|
64
|
+
|
65
|
+
return models.RateDetails(
|
66
|
+
carrier_id=settings.carrier_id,
|
67
|
+
carrier_name=settings.carrier_name,
|
68
|
+
service=service_id,
|
69
|
+
total_charge=lib.to_money(total_charge),
|
70
|
+
currency=currency,
|
71
|
+
transit_days=transit_days,
|
72
|
+
extra_charges=extra_charges,
|
73
|
+
meta=dict(
|
74
|
+
rate_provider=details.carrier.name,
|
75
|
+
service_name=details.name,
|
76
|
+
carrier_code=details.carrier.code,
|
77
|
+
product_code=details.product.code,
|
78
|
+
sendcloud_code=details.code,
|
79
|
+
contract_id=details.contract.id if details.contract else None,
|
80
|
+
# Functionalities metadata
|
81
|
+
signature=getattr(details.functionalities, 'signature', False),
|
82
|
+
tracked=getattr(details.functionalities, 'tracked', False),
|
83
|
+
insurance_available=getattr(details.functionalities, 'insurance', None) is not None,
|
84
|
+
age_check=getattr(details.functionalities, 'agecheck', None),
|
85
|
+
delivery_deadline=getattr(details.functionalities, 'deliverydeadline', None),
|
86
|
+
weekend_delivery=getattr(details.functionalities, 'weekenddelivery', None),
|
87
|
+
# Weight limits
|
88
|
+
min_weight=lib.failsafe(lambda: details.weight.min.value),
|
89
|
+
max_weight=lib.failsafe(lambda: details.weight.max.value),
|
90
|
+
weight_unit=lib.failsafe(lambda: details.weight.min.unit),
|
91
|
+
),
|
92
|
+
)
|
93
|
+
|
94
|
+
|
95
|
+
def rate_request(
|
96
|
+
payload: models.RateRequest,
|
97
|
+
settings: provider_utils.Settings,
|
98
|
+
) -> lib.Serializable:
|
99
|
+
"""Create a rate request for SendCloud's fetch-shipping-options API."""
|
100
|
+
shipper = lib.to_address(payload.shipper)
|
101
|
+
recipient = lib.to_address(payload.recipient)
|
102
|
+
packages = lib.to_packages(payload.parcels)
|
103
|
+
options = lib.to_shipping_options(
|
104
|
+
payload.options,
|
105
|
+
package_options=packages.options,
|
106
|
+
initializer=provider_units.shipping_options_initializer,
|
107
|
+
)
|
108
|
+
|
109
|
+
# SendCloud expects total weight and dimensions
|
110
|
+
total_weight = sum(pkg.weight.KG for pkg in packages)
|
111
|
+
|
112
|
+
# Get max dimensions from all packages
|
113
|
+
max_length = max((pkg.length.CM for pkg in packages if pkg.length), default=0)
|
114
|
+
max_width = max((pkg.width.CM for pkg in packages if pkg.width), default=0)
|
115
|
+
max_height = max((pkg.height.CM for pkg in packages if pkg.height), default=0)
|
116
|
+
|
117
|
+
# Map data to SendCloud rate request format
|
118
|
+
request = sendcloud.RateRequestType(
|
119
|
+
fromcountry=shipper.country_code,
|
120
|
+
tocountry=recipient.country_code,
|
121
|
+
frompostalcode=shipper.postal_code,
|
122
|
+
topostalcode=recipient.postal_code,
|
123
|
+
weight=total_weight,
|
124
|
+
length=int(max_length) if max_length > 0 else None,
|
125
|
+
width=int(max_width) if max_width > 0 else None,
|
126
|
+
height=int(max_height) if max_height > 0 else None,
|
127
|
+
isreturn=lib.identity(
|
128
|
+
options.sendcloud_is_return.state
|
129
|
+
if options.sendcloud_is_return.state is not None
|
130
|
+
else False
|
131
|
+
),
|
132
|
+
requestlabelasync=lib.identity(
|
133
|
+
settings.connection_config.request_label_async.state
|
134
|
+
if settings.connection_config.request_label_async.state is not None
|
135
|
+
else False
|
136
|
+
),
|
137
|
+
)
|
138
|
+
|
139
|
+
return lib.Serializable(request, lib.to_dict)
|