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.
Files changed (33) hide show
  1. karrio/mappers/sendcloud/__init__.py +20 -0
  2. karrio/mappers/sendcloud/mapper.py +84 -0
  3. karrio/mappers/sendcloud/proxy.py +114 -0
  4. karrio/mappers/sendcloud/settings.py +21 -0
  5. karrio/plugins/sendcloud/__init__.py +2 -0
  6. karrio/providers/sendcloud/__init__.py +24 -0
  7. karrio/providers/sendcloud/error.py +74 -0
  8. karrio/providers/sendcloud/pickup/__init__.py +14 -0
  9. karrio/providers/sendcloud/pickup/cancel.py +53 -0
  10. karrio/providers/sendcloud/pickup/create.py +66 -0
  11. karrio/providers/sendcloud/pickup/update.py +67 -0
  12. karrio/providers/sendcloud/rate.py +139 -0
  13. karrio/providers/sendcloud/shipment/__init__.py +9 -0
  14. karrio/providers/sendcloud/shipment/cancel.py +57 -0
  15. karrio/providers/sendcloud/shipment/create.py +128 -0
  16. karrio/providers/sendcloud/tracking.py +115 -0
  17. karrio/providers/sendcloud/units.py +87 -0
  18. karrio/providers/sendcloud/utils.py +108 -0
  19. karrio/schemas/sendcloud/__init__.py +0 -0
  20. karrio/schemas/sendcloud/auth_request.py +32 -0
  21. karrio/schemas/sendcloud/auth_response.py +13 -0
  22. karrio/schemas/sendcloud/error.py +22 -0
  23. karrio/schemas/sendcloud/rate_request.py +17 -0
  24. karrio/schemas/sendcloud/rate_response.py +144 -0
  25. karrio/schemas/sendcloud/shipment_request.py +45 -0
  26. karrio/schemas/sendcloud/shipment_response.py +207 -0
  27. karrio/schemas/sendcloud/tracking_request.py +22 -0
  28. karrio/schemas/sendcloud/tracking_response.py +129 -0
  29. karrio_sendcloud-2025.5rc7.dist-info/METADATA +44 -0
  30. karrio_sendcloud-2025.5rc7.dist-info/RECORD +33 -0
  31. karrio_sendcloud-2025.5rc7.dist-info/WHEEL +5 -0
  32. karrio_sendcloud-2025.5rc7.dist-info/entry_points.txt +2 -0
  33. 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,2 @@
1
+ # Import metadata from mappers module to avoid circular imports
2
+ from karrio.mappers.sendcloud import METADATA
@@ -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)