karrio-hermes 2026.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ from karrio.mappers.hermes.mapper import Mapper
2
+ from karrio.mappers.hermes.proxy import Proxy
3
+ from karrio.mappers.hermes.settings import Settings
@@ -0,0 +1,59 @@
1
+ """Karrio Hermes 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.hermes as provider
8
+ import karrio.mappers.hermes.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
+ # Rating operations (using rate sheets)
16
+ def create_rate_request(self, payload: models.RateRequest) -> lib.Serializable:
17
+ return universal_provider.rate_request(payload, self.settings)
18
+
19
+ def parse_rate_response(
20
+ self, response: lib.Deserializable[str]
21
+ ) -> typing.Tuple[typing.List[models.RateDetails], typing.List[models.Message]]:
22
+ return universal_provider.parse_rate_response(response, self.settings)
23
+
24
+ # Shipment operations
25
+ def create_shipment_request(
26
+ self, payload: models.ShipmentRequest
27
+ ) -> lib.Serializable:
28
+ return provider.shipment_request(payload, self.settings)
29
+
30
+ def parse_shipment_response(
31
+ self, response: lib.Deserializable[str]
32
+ ) -> typing.Tuple[models.ShipmentDetails, typing.List[models.Message]]:
33
+ return provider.parse_shipment_response(response, self.settings)
34
+
35
+ # Pickup operations
36
+ def create_pickup_request(
37
+ self, payload: models.PickupRequest
38
+ ) -> lib.Serializable:
39
+ return provider.pickup_request(payload, self.settings)
40
+
41
+ def parse_pickup_response(
42
+ self, response: lib.Deserializable[str]
43
+ ) -> typing.Tuple[models.PickupDetails, typing.List[models.Message]]:
44
+ return provider.parse_pickup_response(response, self.settings)
45
+
46
+ def create_cancel_pickup_request(
47
+ self, payload: models.PickupCancelRequest
48
+ ) -> lib.Serializable:
49
+ return provider.pickup_cancel_request(payload, self.settings)
50
+
51
+ def parse_cancel_pickup_response(
52
+ self, response: lib.Deserializable[str]
53
+ ) -> typing.Tuple[models.ConfirmationDetails, typing.List[models.Message]]:
54
+ return provider.parse_pickup_cancel_response(response, self.settings)
55
+
56
+ # Note: Hermes API does not support:
57
+ # - cancel_shipment (no DELETE endpoint for shipments)
58
+ # - pickup_update (no PUT endpoint for pickups)
59
+
@@ -0,0 +1,78 @@
1
+ """Karrio Hermes client proxy."""
2
+
3
+ import karrio.lib as lib
4
+ import karrio.api.proxy as proxy
5
+ import karrio.mappers.hermes.settings as provider_settings
6
+ import karrio.universal.mappers.rating_proxy as rating_proxy
7
+ from karrio.providers.hermes.units import LabelType
8
+
9
+
10
+ class Proxy(rating_proxy.RatingMixinProxy, proxy.Proxy):
11
+ settings: provider_settings.Settings
12
+
13
+ def get_rates(self, request: lib.Serializable) -> lib.Deserializable[str]:
14
+ return super().get_rates(request)
15
+
16
+ def _get_headers(self, accept: str = "application/json") -> dict:
17
+ """Get common headers for Hermes API requests."""
18
+ token_data = self.settings.access_token
19
+ # Handle case where token_data might not be a dict
20
+ access_token = token_data.get("access_token") if isinstance(token_data, dict) else token_data
21
+ language = self.settings.connection_config.language.state or "DE"
22
+
23
+ return {
24
+ "Content-Type": "application/json",
25
+ "Accept": accept,
26
+ "Accept-Language": language,
27
+ "Authorization": f"Bearer {access_token}",
28
+ }
29
+
30
+ def create_shipment(self, request: lib.Serializable) -> lib.Deserializable[str]:
31
+ """Create a shipment order with label.
32
+
33
+ Endpoint: POST /shipmentorders/labels
34
+ """
35
+ label_type = self.settings.connection_config.label_type.state or "PDF"
36
+ accept_header = LabelType.map(label_type).value or "application/pdf"
37
+
38
+ response = lib.request(
39
+ url=f"{self.settings.server_url}/shipmentorders/labels",
40
+ data=lib.to_json(request.serialize()),
41
+ trace=self.trace_as("json"),
42
+ method="POST",
43
+ headers=self._get_headers(accept=accept_header),
44
+ )
45
+
46
+ return lib.Deserializable(response, lib.to_dict)
47
+
48
+ def schedule_pickup(self, request: lib.Serializable) -> lib.Deserializable[str]:
49
+ """Create a pickup order.
50
+
51
+ Endpoint: POST /pickuporders
52
+ """
53
+ response = lib.request(
54
+ url=f"{self.settings.server_url}/pickuporders",
55
+ data=lib.to_json(request.serialize()),
56
+ trace=self.trace_as("json"),
57
+ method="POST",
58
+ headers=self._get_headers(),
59
+ )
60
+
61
+ return lib.Deserializable(response, lib.to_dict)
62
+
63
+ def cancel_pickup(self, request: lib.Serializable) -> lib.Deserializable[str]:
64
+ """Cancel a pickup order.
65
+
66
+ Endpoint: DELETE /pickuporders/{pickupOrderID}
67
+ """
68
+ payload = request.serialize()
69
+ pickup_order_id = payload.get("pickupOrderID")
70
+
71
+ response = lib.request(
72
+ url=f"{self.settings.server_url}/pickuporders/{pickup_order_id}",
73
+ trace=self.trace_as("json"),
74
+ method="DELETE",
75
+ headers=self._get_headers(),
76
+ )
77
+
78
+ return lib.Deserializable(response, lib.to_dict)
@@ -0,0 +1,36 @@
1
+ """Karrio Hermes client settings."""
2
+
3
+ import attr
4
+ import typing
5
+ import jstruct
6
+ import karrio.core.models as models
7
+ import karrio.providers.hermes.units as provider_units
8
+ import karrio.providers.hermes.utils as provider_utils
9
+ import karrio.universal.mappers.rating_proxy as rating_proxy
10
+
11
+
12
+ @attr.s(auto_attribs=True)
13
+ class Settings(provider_utils.Settings, rating_proxy.RatingMixinSettings):
14
+ """Hermes connection settings."""
15
+
16
+ # OAuth2 credentials (password flow)
17
+ username: str # type:ignore
18
+ password: str # type:ignore
19
+ client_id: str # type:ignore
20
+ client_secret: str # type:ignore
21
+
22
+ # generic properties
23
+ id: str = None
24
+ test_mode: bool = False
25
+ carrier_id: str = "hermes"
26
+ services: typing.List[models.ServiceLevel] = jstruct.JList[models.ServiceLevel, False, dict(default=provider_units.DEFAULT_SERVICES)] # type: ignore
27
+ account_country_code: str = "DE"
28
+ metadata: dict = {}
29
+ config: dict = {}
30
+
31
+ @property
32
+ def shipping_services(self) -> typing.List[models.ServiceLevel]:
33
+ if any(self.services or []):
34
+ return self.services
35
+
36
+ return provider_units.DEFAULT_SERVICES
@@ -0,0 +1,29 @@
1
+ from karrio.core.metadata import PluginMetadata
2
+
3
+ from karrio.mappers.hermes.mapper import Mapper
4
+ from karrio.mappers.hermes.proxy import Proxy
5
+ from karrio.mappers.hermes.settings import Settings
6
+ import karrio.providers.hermes.units as units
7
+
8
+
9
+ # This METADATA object is used by Karrio to discover and register this plugin
10
+ # when loaded through Python entrypoints or local plugin directories.
11
+ # The entrypoint is defined in pyproject.toml under [project.entry-points."karrio.plugins"]
12
+ METADATA = PluginMetadata(
13
+ id="hermes",
14
+ label="Hermes",
15
+ description="Hermes shipping integration for Karrio",
16
+ # Integrations
17
+ Mapper=Mapper,
18
+ Proxy=Proxy,
19
+ Settings=Settings,
20
+ # Data Units
21
+ is_hub=False,
22
+ options=units.ShippingOption,
23
+ services=units.ShippingService,
24
+ service_levels=units.DEFAULT_SERVICES,
25
+ connection_configs=units.ConnectionConfig,
26
+ # Extra info
27
+ website="https://www.hermesworld.com",
28
+ documentation="https://de-api-int.hermesworld.com/docs/applications/order",
29
+ )
@@ -0,0 +1,16 @@
1
+ """Karrio Hermes provider imports."""
2
+ from karrio.providers.hermes.utils import Settings
3
+ from karrio.providers.hermes.shipment import (
4
+ parse_shipment_response,
5
+ shipment_request,
6
+ )
7
+ from karrio.providers.hermes.pickup import (
8
+ parse_pickup_cancel_response,
9
+ parse_pickup_response,
10
+ pickup_cancel_request,
11
+ pickup_request,
12
+ )
13
+
14
+ # Note: Hermes API does not support:
15
+ # - shipment cancellation (no DELETE endpoint for shipments)
16
+ # - pickup update (no PUT endpoint for pickups)
@@ -0,0 +1,94 @@
1
+ """Karrio Hermes error parser."""
2
+
3
+ import typing
4
+ import karrio.lib as lib
5
+ import karrio.core.models as models
6
+ import karrio.providers.hermes.utils as provider_utils
7
+
8
+
9
+ def parse_error_response(
10
+ response: typing.Union[dict, str, None],
11
+ settings: provider_utils.Settings,
12
+ **kwargs,
13
+ ) -> typing.List[models.Message]:
14
+ """Parse Hermes error response into karrio messages."""
15
+ messages: typing.List[models.Message] = []
16
+
17
+ if response is None:
18
+ return messages
19
+
20
+ # Handle case where response is not a dict (e.g., raw string response)
21
+ if not isinstance(response, dict):
22
+ messages.append(
23
+ models.Message(
24
+ carrier_id=settings.carrier_id,
25
+ carrier_name=settings.carrier_name,
26
+ code="PARSE_ERROR",
27
+ message=f"Unexpected response format: {str(response)[:200]}",
28
+ details={**kwargs},
29
+ )
30
+ )
31
+ return messages
32
+
33
+ # Handle OAuth2 error response
34
+ if "error" in response:
35
+ messages.append(
36
+ models.Message(
37
+ carrier_id=settings.carrier_id,
38
+ carrier_name=settings.carrier_name,
39
+ code=response.get("error", "AUTH_ERROR"),
40
+ message=response.get("error_description", "Authentication failed"),
41
+ details={**kwargs},
42
+ )
43
+ )
44
+
45
+ # Handle Hermes API error response with listOfResultCodes
46
+ result_codes = response.get("listOfResultCodes") or []
47
+ for error in result_codes:
48
+ code = error.get("code", "")
49
+ message = error.get("message", "")
50
+
51
+ # Only include actual errors (codes starting with 'e')
52
+ if code.startswith("e") or (code and not code.startswith("w")):
53
+ messages.append(
54
+ models.Message(
55
+ carrier_id=settings.carrier_id,
56
+ carrier_name=settings.carrier_name,
57
+ code=code,
58
+ message=message,
59
+ details={**kwargs},
60
+ )
61
+ )
62
+
63
+ return messages
64
+
65
+
66
+ def parse_warning_response(
67
+ response: dict,
68
+ settings: provider_utils.Settings,
69
+ **kwargs,
70
+ ) -> typing.List[models.Message]:
71
+ """Parse Hermes warning response into karrio messages."""
72
+ messages: typing.List[models.Message] = []
73
+
74
+ if response is None:
75
+ return messages
76
+
77
+ # Handle warnings from listOfResultCodes (codes starting with 'w')
78
+ result_codes = response.get("listOfResultCodes") or []
79
+ for warning in result_codes:
80
+ code = warning.get("code", "")
81
+ message = warning.get("message", "")
82
+
83
+ if code.startswith("w"):
84
+ messages.append(
85
+ models.Message(
86
+ carrier_id=settings.carrier_id,
87
+ carrier_name=settings.carrier_name,
88
+ code=code,
89
+ message=message,
90
+ details={**kwargs},
91
+ )
92
+ )
93
+
94
+ return messages
@@ -0,0 +1,12 @@
1
+ """Karrio Hermes pickup API imports."""
2
+
3
+ from karrio.providers.hermes.pickup.create import (
4
+ parse_pickup_response,
5
+ pickup_request,
6
+ )
7
+ from karrio.providers.hermes.pickup.cancel import (
8
+ parse_pickup_cancel_response,
9
+ pickup_cancel_request,
10
+ )
11
+
12
+ # Note: Hermes API does not support pickup update
@@ -0,0 +1,49 @@
1
+ """Karrio Hermes 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.hermes.error as error
7
+ import karrio.providers.hermes.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[typing.Optional[models.ConfirmationDetails], typing.List[models.Message]]:
14
+ """Parse Hermes pickup cancellation response."""
15
+ response = _response.deserialize()
16
+ messages = error.parse_error_response(response, settings)
17
+
18
+ # Check if cancellation was successful (no errors means success)
19
+ success = len(messages) == 0
20
+
21
+ confirmation = None
22
+ if success:
23
+ confirmation = models.ConfirmationDetails(
24
+ carrier_id=settings.carrier_id,
25
+ carrier_name=settings.carrier_name,
26
+ success=True,
27
+ operation="Cancel Pickup",
28
+ )
29
+
30
+ return confirmation, messages
31
+
32
+
33
+ def pickup_cancel_request(
34
+ payload: models.PickupCancelRequest,
35
+ settings: provider_utils.Settings,
36
+ ) -> lib.Serializable:
37
+ """Create Hermes pickup cancellation request.
38
+
39
+ Note: The pickup order ID is passed in the URL path, not in the body.
40
+ The proxy.cancel_pickup method extracts pickupOrderID from the payload.
41
+ """
42
+ # Hermes uses DELETE /pickuporders/{pickupOrderID}
43
+ # The confirmation_number from Karrio is the pickupOrderID
44
+ request = {
45
+ "pickupOrderID": payload.confirmation_number,
46
+ }
47
+
48
+ return lib.Serializable(request, lib.to_dict)
49
+
@@ -0,0 +1,98 @@
1
+ """Karrio Hermes pickup scheduling implementation."""
2
+
3
+ import typing
4
+ import karrio.lib as lib
5
+ import karrio.core.models as models
6
+ import karrio.providers.hermes.error as error
7
+ import karrio.providers.hermes.utils as provider_utils
8
+ import karrio.schemas.hermes.pickup_create_request as hermes_req
9
+ import karrio.schemas.hermes.pickup_create_response as hermes_res
10
+
11
+
12
+ def parse_pickup_response(
13
+ _response: lib.Deserializable[dict],
14
+ settings: provider_utils.Settings,
15
+ ) -> typing.Tuple[typing.Optional[models.PickupDetails], typing.List[models.Message]]:
16
+ """Parse Hermes pickup response."""
17
+ response = _response.deserialize()
18
+ messages = error.parse_error_response(response, settings)
19
+
20
+ # Check if we have a valid pickup order ID
21
+ pickup = None
22
+ if response.get("pickupOrderID"):
23
+ pickup = _extract_details(response, settings)
24
+
25
+ return pickup, messages
26
+
27
+
28
+ def _extract_details(
29
+ data: dict,
30
+ settings: provider_utils.Settings,
31
+ ) -> models.PickupDetails:
32
+ """Extract pickup details from Hermes response."""
33
+ response = lib.to_object(hermes_res.PickupCreateResponseType, data)
34
+
35
+ # Hermes returns pickupOrderID as the confirmation number
36
+ confirmation_number = response.pickupOrderID or ""
37
+
38
+ return models.PickupDetails(
39
+ carrier_id=settings.carrier_id,
40
+ carrier_name=settings.carrier_name,
41
+ confirmation_number=confirmation_number,
42
+ pickup_date=None, # Not returned in response
43
+ )
44
+
45
+
46
+ def pickup_request(
47
+ payload: models.PickupRequest,
48
+ settings: provider_utils.Settings,
49
+ ) -> lib.Serializable:
50
+ """Create a Hermes pickup request."""
51
+ address = lib.to_address(payload.address)
52
+
53
+ # Parse parcel counts by size (XS, S, M, L, XL)
54
+ # Default to counting all parcels as M (medium) if not specified
55
+ parcel_count = hermes_req.ParcelCountType(
56
+ pickupParcelCountXS=0,
57
+ pickupParcelCountS=0,
58
+ pickupParcelCountM=len(payload.parcels) if payload.parcels else 1,
59
+ pickupParcelCountL=0,
60
+ pickupParcelCountXL=0,
61
+ )
62
+
63
+ # Map time slot from ready_time/closing_time
64
+ # Valid values per OpenAPI: BETWEEN_10_AND_13, BETWEEN_12_AND_15, BETWEEN_14_AND_17
65
+ time_slot = None
66
+ if payload.ready_time:
67
+ hour = int(payload.ready_time.split(":")[0]) if ":" in payload.ready_time else 12
68
+ if hour < 12:
69
+ time_slot = "BETWEEN_10_AND_13"
70
+ elif hour < 14:
71
+ time_slot = "BETWEEN_12_AND_15"
72
+ else:
73
+ time_slot = "BETWEEN_14_AND_17"
74
+
75
+ # Create the request using generated schema types
76
+ request = hermes_req.PickupCreateRequestType(
77
+ pickupAddress=hermes_req.PickupAddressType(
78
+ street=address.street_name,
79
+ houseNumber=address.street_number or "",
80
+ zipCode=address.postal_code,
81
+ town=address.city,
82
+ countryCode=address.country_code,
83
+ addressAddition=address.address_line2 or address.company_name or None,
84
+ ),
85
+ pickupName=hermes_req.PickupNameType(
86
+ title=None,
87
+ gender=None,
88
+ firstname=address.person_name.split()[0] if address.person_name else None,
89
+ middlename=None,
90
+ lastname=" ".join(address.person_name.split()[1:]) if address.person_name and len(address.person_name.split()) > 1 else address.person_name,
91
+ ),
92
+ phone=address.phone_number or None,
93
+ pickupDate=payload.pickup_date,
94
+ pickupTimeSlot=time_slot,
95
+ parcelCount=parcel_count,
96
+ )
97
+
98
+ return lib.Serializable(request, lib.to_dict)
@@ -0,0 +1,8 @@
1
+ """Karrio Hermes shipment API imports."""
2
+
3
+ from karrio.providers.hermes.shipment.create import (
4
+ parse_shipment_response,
5
+ shipment_request,
6
+ )
7
+
8
+ # Note: Hermes API does not support shipment cancellation