karrio-seko 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.
@@ -0,0 +1,3 @@
1
+ from karrio.mappers.seko.mapper import Mapper
2
+ from karrio.mappers.seko.proxy import Proxy
3
+ from karrio.mappers.seko.settings import Settings
@@ -0,0 +1,64 @@
1
+ """Karrio SEKO Logistics 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.seko as provider
8
+ import karrio.mappers.seko.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_cancel_shipment_request(
30
+ self, payload: models.ShipmentCancelRequest
31
+ ) -> lib.Serializable[str]:
32
+ return provider.shipment_cancel_request(payload, self.settings)
33
+
34
+ def create_manifest_request(
35
+ self, payload: models.ManifestRequest
36
+ ) -> lib.Serializable:
37
+ return provider.manifest_request(payload, self.settings)
38
+
39
+
40
+ def parse_cancel_shipment_response(
41
+ self, response: lib.Deserializable[str]
42
+ ) -> typing.Tuple[models.ConfirmationDetails, typing.List[models.Message]]:
43
+ return provider.parse_shipment_cancel_response(response, self.settings)
44
+
45
+ def parse_rate_response(
46
+ self, response: lib.Deserializable[str]
47
+ ) -> typing.Tuple[typing.List[models.RateDetails], typing.List[models.Message]]:
48
+ return provider.parse_rate_response(response, self.settings)
49
+
50
+ def parse_shipment_response(
51
+ self, response: lib.Deserializable[str]
52
+ ) -> typing.Tuple[models.ShipmentDetails, typing.List[models.Message]]:
53
+ return provider.parse_shipment_response(response, self.settings)
54
+
55
+ def parse_tracking_response(
56
+ self, response: lib.Deserializable[str]
57
+ ) -> typing.Tuple[typing.List[models.TrackingDetails], typing.List[models.Message]]:
58
+ return provider.parse_tracking_response(response, self.settings)
59
+
60
+ def parse_manifest_response(
61
+ self, response: lib.Deserializable[str]
62
+ ) -> typing.Tuple[models.ManifestDetails, typing.List[models.Message]]:
63
+ return provider.parse_manifest_response(response, self.settings)
64
+
@@ -0,0 +1,88 @@
1
+ """Karrio SEKO Logistics client proxy."""
2
+
3
+ import karrio.lib as lib
4
+ import karrio.api.proxy as proxy
5
+ import karrio.providers.seko.utils as provider_utils
6
+ import karrio.mappers.seko.settings as provider_settings
7
+
8
+
9
+ class Proxy(proxy.Proxy):
10
+ settings: provider_settings.Settings
11
+
12
+ def get_rates(self, request: lib.Serializable) -> lib.Deserializable[str]:
13
+ response = lib.request(
14
+ url=f"{self.settings.server_url}/ratesqueryv1/availablerates",
15
+ data=lib.to_json(request.serialize()),
16
+ trace=self.trace_as("json"),
17
+ method="POST",
18
+ headers={
19
+ "Content-Type": "application/json; charset=utf-8",
20
+ "access_key": f"{self.settings.access_key}",
21
+ },
22
+ on_error=provider_utils.parse_error_response,
23
+ )
24
+
25
+ return lib.Deserializable(response, lib.to_dict)
26
+
27
+ def create_shipment(self, request: lib.Serializable) -> lib.Deserializable[str]:
28
+ response = lib.request(
29
+ url=f"{self.settings.server_url}/labels/printshipment",
30
+ data=lib.to_json(request.serialize()),
31
+ trace=self.trace_as("json"),
32
+ method="POST",
33
+ headers={
34
+ "Content-Type": "application/json; charset=utf-8",
35
+ "access_key": f"{self.settings.access_key}",
36
+ },
37
+ on_error=provider_utils.parse_error_response,
38
+ )
39
+
40
+ return lib.Deserializable(response, lib.to_dict, request.ctx)
41
+
42
+ def cancel_shipment(self, request: lib.Serializable) -> lib.Deserializable[str]:
43
+ response = lib.request(
44
+ url=f"{self.settings.server_url}/labels/delete",
45
+ data=lib.to_json(request.serialize()),
46
+ trace=self.trace_as("json"),
47
+ method="POST",
48
+ headers={
49
+ "Content-Type": "application/json; charset=utf-8",
50
+ "access_key": f"{self.settings.access_key}",
51
+ },
52
+ on_error=provider_utils.parse_error_response,
53
+ )
54
+
55
+ return lib.Deserializable(
56
+ response,
57
+ lib.to_dict,
58
+ )
59
+
60
+ def get_tracking(self, request: lib.Serializable) -> lib.Deserializable[str]:
61
+ response = lib.request(
62
+ url=f"{self.settings.server_url}/labels/statusv2",
63
+ data=lib.to_json(request.serialize()),
64
+ trace=self.trace_as("json"),
65
+ method="POST",
66
+ headers={
67
+ "Content-Type": "application/json; charset=utf-8",
68
+ "access_key": f"{self.settings.access_key}",
69
+ },
70
+ on_error=provider_utils.parse_error_response,
71
+ )
72
+
73
+ return lib.Deserializable(response, lib.to_dict)
74
+
75
+ def create_manifest(self, request: lib.Serializable) -> lib.Deserializable[str]:
76
+ response = lib.request(
77
+ url=f"{self.settings.server_url}/v2/publishmanifestv4",
78
+ data=lib.to_json(request.serialize()),
79
+ trace=self.trace_as("json"),
80
+ method="POST",
81
+ headers={
82
+ "Content-Type": "application/json; charset=utf-8",
83
+ "access_key": f"{self.settings.access_key}",
84
+ },
85
+ on_error=provider_utils.parse_error_response,
86
+ )
87
+
88
+ return lib.Deserializable(response, lib.to_dict)
@@ -0,0 +1,20 @@
1
+ """Karrio SEKO Logistics client settings."""
2
+
3
+ import attr
4
+ import karrio.providers.seko.utils as provider_utils
5
+
6
+
7
+ @attr.s(auto_attribs=True)
8
+ class Settings(provider_utils.Settings):
9
+ """SEKO Logistics connection settings."""
10
+
11
+ # Add carrier specific API connection properties here
12
+ access_key: str
13
+
14
+ # generic properties
15
+ id: str = None
16
+ test_mode: bool = False
17
+ carrier_id: str = "seko"
18
+ account_country_code: str = None
19
+ metadata: dict = {}
20
+ config: dict = {}
@@ -0,0 +1,20 @@
1
+ import karrio.core.metadata as metadata
2
+ import karrio.mappers.seko as mappers
3
+ import karrio.providers.seko.units as units
4
+ import karrio.providers.seko.utils as utils
5
+
6
+
7
+ METADATA = metadata.PluginMetadata(
8
+ status="production-ready",
9
+ id="seko",
10
+ label="SEKO Logistics",
11
+ # Integrations
12
+ Mapper=mappers.Mapper,
13
+ Proxy=mappers.Proxy,
14
+ Settings=mappers.Settings,
15
+ # Data Units
16
+ is_hub=False,
17
+ options=units.ShippingOption,
18
+ services=units.ShippingService,
19
+ connection_configs=utils.ConnectionConfig,
20
+ )
@@ -0,0 +1,17 @@
1
+ """Karrio SEKO Logistics provider imports."""
2
+ from karrio.providers.seko.utils import Settings
3
+ from karrio.providers.seko.rate import parse_rate_response, rate_request
4
+ from karrio.providers.seko.shipment import (
5
+ parse_shipment_cancel_response,
6
+ parse_shipment_response,
7
+ shipment_cancel_request,
8
+ shipment_request,
9
+ )
10
+ from karrio.providers.seko.tracking import (
11
+ parse_tracking_response,
12
+ tracking_request,
13
+ )
14
+ from karrio.providers.seko.manifest import (
15
+ parse_manifest_response,
16
+ manifest_request,
17
+ )
@@ -0,0 +1,73 @@
1
+ """Karrio SEKO Logistics error parser."""
2
+
3
+ import typing
4
+ import karrio.lib as lib
5
+ import karrio.core.models as models
6
+ import karrio.providers.seko.utils as provider_utils
7
+
8
+
9
+ def parse_error_response(
10
+ response: typing.Union[typing.List[dict], dict],
11
+ settings: provider_utils.Settings,
12
+ **kwargs,
13
+ ) -> typing.List[models.Message]:
14
+ responses = response if isinstance(response, list) else [response]
15
+ errors: list = []
16
+
17
+ for response_item in responses:
18
+ if validation_errors := response_item.get("ValidationErrors"):
19
+ if isinstance(validation_errors, dict):
20
+ errors.append(
21
+ {
22
+ "code": "ValidationError",
23
+ "message": validation_errors.get(
24
+ "Message", next(iter(validation_errors.values()))
25
+ ),
26
+ **{
27
+ k: v for k, v in validation_errors.items() if k != "Message"
28
+ },
29
+ }
30
+ )
31
+ else:
32
+ errors.append(
33
+ {"code": "ValidationError", "message": str(validation_errors)}
34
+ )
35
+ break
36
+
37
+ if rejected := response_item.get("Rejected", []):
38
+ error = rejected[0]
39
+ errors.append(
40
+ {
41
+ "code": "Rejected",
42
+ "message": error.get("Reason"),
43
+ **{k: v for k, v in error.items() if k != "Reason"},
44
+ }
45
+ )
46
+ break
47
+
48
+ if general_errors := (
49
+ response_item.get("Errors", []) or response_item.get("Error", [])
50
+ ):
51
+ error = general_errors[0]
52
+ errors.append(
53
+ {
54
+ "code": "Error",
55
+ "message": error.get("Message"),
56
+ **{k: v for k, v in error.items() if k != "Message"},
57
+ }
58
+ )
59
+ break
60
+
61
+ return [
62
+ models.Message(
63
+ carrier_id=settings.carrier_id,
64
+ carrier_name=settings.carrier_name,
65
+ code=error["code"],
66
+ message=error["message"],
67
+ details={
68
+ **kwargs,
69
+ **{k: v for k, v in error.items() if k not in ["code", "message"]},
70
+ },
71
+ )
72
+ for error in errors
73
+ ]
@@ -0,0 +1,62 @@
1
+ """Karrio SEKO Logistics manifest API implementation."""
2
+
3
+ import karrio.schemas.seko.manifest_response as manifest
4
+
5
+ import typing
6
+ import karrio.lib as lib
7
+ import karrio.core.models as models
8
+ import karrio.providers.seko.error as error
9
+ import karrio.providers.seko.utils as provider_utils
10
+ import karrio.providers.seko.units as provider_units
11
+
12
+
13
+ def parse_manifest_response(
14
+ _response: lib.Deserializable[dict],
15
+ settings: provider_utils.Settings,
16
+ ) -> typing.Tuple[models.ManifestDetails, typing.List[models.Message]]:
17
+ response = _response.deserialize()
18
+
19
+ messages = error.parse_error_response(response, settings)
20
+ details = lib.identity(
21
+ _extract_details(response, settings)
22
+ if any(response.get("OutboundManifest") or [])
23
+ else None
24
+ )
25
+
26
+ return details, messages
27
+
28
+
29
+ def _extract_details(
30
+ data: dict,
31
+ settings: provider_utils.Settings,
32
+ ) -> models.ManifestDetails:
33
+ details = lib.to_object(manifest.ManifestResponseType, data)
34
+ manifest_numbers = [_.ManifestNumber for _ in details.OutboundManifest]
35
+ manifest_connotes: list = sum(
36
+ [_.ManifestedConnotes for _ in details.OutboundManifest], []
37
+ )
38
+ manifest_doc = lib.bundle_base64(
39
+ [_.ManifestContent for _ in details.OutboundManifest], "PDF"
40
+ )
41
+
42
+ return models.ManifestDetails(
43
+ carrier_id=settings.carrier_id,
44
+ carrier_name=settings.carrier_id,
45
+ doc=models.ManifestDocument(manifest=manifest_doc),
46
+ meta=dict(
47
+ ManifestNumber=manifest_numbers[0],
48
+ ManifestNumbers=manifest_numbers,
49
+ ManifestConnotes=manifest_connotes,
50
+ ),
51
+ )
52
+
53
+
54
+ def manifest_request(
55
+ payload: models.ManifestRequest,
56
+ settings: provider_utils.Settings,
57
+ ) -> lib.Serializable:
58
+
59
+ # map data to convert karrio model to seko specific type
60
+ request = payload.shipment_identifiers
61
+
62
+ return lib.Serializable(request, lib.to_dict)
@@ -0,0 +1,107 @@
1
+ """Karrio SEKO Logistics rating API implementation."""
2
+
3
+ import karrio.schemas.seko.rating_request as seko
4
+ import karrio.schemas.seko.rating_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.seko.error as error
11
+ import karrio.providers.seko.utils as provider_utils
12
+ import karrio.providers.seko.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 = [_extract_details(rate, settings) for rate in response.get("Available", [])]
23
+
24
+ return rates, messages
25
+
26
+
27
+ def _extract_details(
28
+ data: dict,
29
+ settings: provider_utils.Settings,
30
+ ) -> models.RateDetails:
31
+ details = lib.to_object(rating.AvailableType, data)
32
+ service = provider_units.ShippingService.map(details.DeliveryType)
33
+
34
+ return models.RateDetails(
35
+ carrier_id=settings.carrier_id,
36
+ carrier_name=settings.carrier_name,
37
+ service=service.name_or_key,
38
+ total_charge=lib.to_money(details.Cost),
39
+ currency=settings.connection_config.currency.state or "USD",
40
+ meta=dict(
41
+ service_name=service.value_or_key,
42
+ seko_carrier=details.CarrierName,
43
+ Route=details.Route,
44
+ QuoteId=details.QuoteId,
45
+ DeliveryType=details.DeliveryType,
46
+ CarrierServiceType=details.CarrierServiceType,
47
+ IsFreightForward=details.IsFreightForward,
48
+ IsRuralDelivery=details.IsRuralDelivery,
49
+ IsSaturdayDelivery=details.IsSaturdayDelivery,
50
+ ),
51
+ )
52
+
53
+
54
+ def rate_request(
55
+ payload: models.RateRequest,
56
+ settings: provider_utils.Settings,
57
+ ) -> lib.Serializable:
58
+ recipient = lib.to_address(payload.recipient)
59
+ options = lib.to_shipping_options(
60
+ payload.options,
61
+ initializer=provider_units.shipping_options_initializer,
62
+ )
63
+ packages = lib.to_packages(
64
+ payload.parcels,
65
+ options=options,
66
+ shipping_options_initializer=provider_units.shipping_options_initializer,
67
+ )
68
+
69
+ # map data to convert karrio model to seko specific type
70
+ request = seko.RatingRequestType(
71
+ DeliveryReference=payload.reference,
72
+ Destination=seko.DestinationType(
73
+ Id=options.seko_destination_id.state,
74
+ Name=recipient.company_name,
75
+ Address=seko.AddressType(
76
+ BuildingName="",
77
+ StreetAddress=recipient.street,
78
+ Suburb=recipient.city,
79
+ City=recipient.state_code,
80
+ PostCode=recipient.postal_code,
81
+ CountryCode=recipient.country_code,
82
+ ),
83
+ ContactPerson=recipient.contact,
84
+ PhoneNumber=recipient.phone_number,
85
+ Email=recipient.email,
86
+ DeliveryInstructions=options.destination_instructions.state,
87
+ RecipientTaxId=recipient.tax_id,
88
+ ),
89
+ IsSaturdayDelivery=options.seko_is_saturday_delivery.state,
90
+ IsSignatureRequired=options.seko_is_signature_required.state,
91
+ Packages=[
92
+ seko.PackageType(
93
+ Height=package.height.CM,
94
+ Length=package.length.CM,
95
+ Id=package.options.seko_package_id.state,
96
+ Width=package.width.CM,
97
+ Kg=package.weight.KG,
98
+ Name=lib.text(package.description, max=50),
99
+ Type=provider_units.PackagingType.map(
100
+ package.packaging_type or "your_packaging"
101
+ ).value,
102
+ )
103
+ for package in packages
104
+ ],
105
+ )
106
+
107
+ return lib.Serializable(request, lib.to_dict)
@@ -0,0 +1,9 @@
1
+
2
+ from karrio.providers.seko.shipment.create import (
3
+ parse_shipment_response,
4
+ shipment_request,
5
+ )
6
+ from karrio.providers.seko.shipment.cancel import (
7
+ parse_shipment_cancel_response,
8
+ shipment_cancel_request,
9
+ )
@@ -0,0 +1,61 @@
1
+ import typing
2
+ import karrio.lib as lib
3
+ import karrio.core.models as models
4
+ import karrio.providers.seko.error as error
5
+ import karrio.providers.seko.utils as provider_utils
6
+ import karrio.providers.seko.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(
15
+ dict(
16
+ Errors=[
17
+ {"ConsignmentId": _, "Message": __}
18
+ for _, __ in response.items()
19
+ if isinstance(__, str) and "Deleted" not in __
20
+ ]
21
+ ),
22
+ settings,
23
+ )
24
+ success = any(["Deleted" in _ for _ in response.values() if isinstance(_, str)])
25
+
26
+ confirmation = (
27
+ models.ConfirmationDetails(
28
+ carrier_id=settings.carrier_id,
29
+ carrier_name=settings.carrier_name,
30
+ operation="Cancel Shipment",
31
+ success=success,
32
+ )
33
+ if success
34
+ else None
35
+ )
36
+
37
+ return confirmation, messages
38
+
39
+
40
+ def shipment_cancel_request(
41
+ payload: models.ShipmentCancelRequest,
42
+ settings: provider_utils.Settings,
43
+ ) -> lib.Serializable:
44
+ options = lib.units.Options(
45
+ payload.options,
46
+ option_type=lib.units.create_enum(
47
+ "PickupOptions",
48
+ # fmt: off
49
+ {
50
+ "shipment_identifiers": lib.OptionEnum("shipment_identifiers", lib.to_list),
51
+ },
52
+ # fmt: on
53
+ ),
54
+ )
55
+
56
+ # map data to convert karrio model to seko specific type
57
+ request = lib.identity(
58
+ options.shipment_identifiers.state or [payload.shipment_identifier]
59
+ )
60
+
61
+ return lib.Serializable(request, lib.to_dict)