karrio-hermes 2026.1.1__py3-none-any.whl → 2026.1.4__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/hermes/mapper.py +11 -0
- karrio/mappers/hermes/proxy.py +60 -37
- karrio/providers/hermes/__init__.py +4 -0
- karrio/providers/hermes/pickup/create.py +8 -0
- karrio/providers/hermes/shipment/create.py +278 -245
- karrio/providers/hermes/tracking.py +155 -0
- karrio/providers/hermes/units.py +202 -51
- karrio/providers/hermes/utils.py +26 -0
- karrio/schemas/hermes/__init__.py +10 -0
- karrio/schemas/hermes/tracking_response.py +63 -0
- {karrio_hermes-2026.1.1.dist-info → karrio_hermes-2026.1.4.dist-info}/METADATA +1 -1
- {karrio_hermes-2026.1.1.dist-info → karrio_hermes-2026.1.4.dist-info}/RECORD +15 -13
- {karrio_hermes-2026.1.1.dist-info → karrio_hermes-2026.1.4.dist-info}/WHEEL +1 -1
- {karrio_hermes-2026.1.1.dist-info → karrio_hermes-2026.1.4.dist-info}/entry_points.txt +0 -0
- {karrio_hermes-2026.1.1.dist-info → karrio_hermes-2026.1.4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Karrio Hermes tracking 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
|
+
import karrio.providers.hermes.units as provider_units
|
|
9
|
+
import karrio.schemas.hermes.tracking_response as hermes_res
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _match_status(code: str) -> typing.Optional[str]:
|
|
13
|
+
"""Match Hermes event code against TrackingStatus enum values."""
|
|
14
|
+
if not code:
|
|
15
|
+
return None
|
|
16
|
+
for status in list(provider_units.TrackingStatus):
|
|
17
|
+
if code in status.value:
|
|
18
|
+
return status.name
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _match_reason(code: str) -> typing.Optional[str]:
|
|
23
|
+
"""Match Hermes event code against incident reasons.
|
|
24
|
+
|
|
25
|
+
Hermes uses numeric codes that map to statuses, not specific incident reasons.
|
|
26
|
+
Returns None as Hermes does not provide granular incident reason codes.
|
|
27
|
+
"""
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def parse_tracking_response(
|
|
32
|
+
_response: lib.Deserializable[dict],
|
|
33
|
+
settings: provider_utils.Settings,
|
|
34
|
+
) -> typing.Tuple[typing.List[models.TrackingDetails], typing.List[models.Message]]:
|
|
35
|
+
"""Parse tracking response from Hermes Shipment Info API."""
|
|
36
|
+
response = _response.deserialize()
|
|
37
|
+
|
|
38
|
+
# Parse the response using generated schema
|
|
39
|
+
tracking_response = lib.to_object(hermes_res.TrackingResponseType, response)
|
|
40
|
+
|
|
41
|
+
# Collect error messages
|
|
42
|
+
messages: typing.List[models.Message] = []
|
|
43
|
+
|
|
44
|
+
# Extract tracking details for each shipment
|
|
45
|
+
tracking_details: typing.List[models.TrackingDetails] = []
|
|
46
|
+
|
|
47
|
+
for shipment_info in tracking_response.shipmentinfo or []:
|
|
48
|
+
# Check for errors in individual shipment result
|
|
49
|
+
if shipment_info.result and shipment_info.result.code:
|
|
50
|
+
if shipment_info.result.code.startswith("e"):
|
|
51
|
+
messages.append(
|
|
52
|
+
models.Message(
|
|
53
|
+
carrier_id=settings.carrier_id,
|
|
54
|
+
carrier_name=settings.carrier_name,
|
|
55
|
+
code=shipment_info.result.code,
|
|
56
|
+
message=shipment_info.result.message or "",
|
|
57
|
+
details=dict(shipment_id=shipment_info.shipmentID),
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
# Extract tracking details
|
|
63
|
+
details = _extract_details(shipment_info, settings)
|
|
64
|
+
if details:
|
|
65
|
+
tracking_details.append(details)
|
|
66
|
+
|
|
67
|
+
return tracking_details, messages
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _extract_details(
|
|
71
|
+
shipment_info: hermes_res.ShipmentinfoType,
|
|
72
|
+
settings: provider_utils.Settings,
|
|
73
|
+
) -> typing.Optional[models.TrackingDetails]:
|
|
74
|
+
"""Extract tracking details from Hermes shipment info."""
|
|
75
|
+
if not shipment_info.shipmentID:
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
# Get status events (already in chronological order, most recent last in API)
|
|
79
|
+
# Reverse to have most recent first for Karrio convention
|
|
80
|
+
status_list = list(reversed(shipment_info.status or []))
|
|
81
|
+
|
|
82
|
+
# Get latest event code for overall status
|
|
83
|
+
latest_code = status_list[0].code if status_list else None
|
|
84
|
+
overall_status = _match_status(latest_code) or provider_units.TrackingStatus.in_transit.name
|
|
85
|
+
|
|
86
|
+
# Build tracking events with all required fields per CARRIER_INTEGRATION_GUIDE.md
|
|
87
|
+
events = [
|
|
88
|
+
models.TrackingEvent(
|
|
89
|
+
date=lib.fdate(event.timestamp, "%Y-%m-%dT%H:%M:%S%z"),
|
|
90
|
+
time=lib.flocaltime(event.timestamp, "%Y-%m-%dT%H:%M:%S%z"),
|
|
91
|
+
description=event.description or "",
|
|
92
|
+
code=event.code,
|
|
93
|
+
location=lib.join(
|
|
94
|
+
event.scanningUnit.city if event.scanningUnit else None,
|
|
95
|
+
event.scanningUnit.countryCode if event.scanningUnit else None,
|
|
96
|
+
join=True,
|
|
97
|
+
separator=", ",
|
|
98
|
+
),
|
|
99
|
+
# REQUIRED: timestamp in ISO 8601 format (already provided by Hermes)
|
|
100
|
+
timestamp=event.timestamp,
|
|
101
|
+
# REQUIRED: normalized status at event level
|
|
102
|
+
status=_match_status(event.code),
|
|
103
|
+
# Incident reason for exception events
|
|
104
|
+
reason=_match_reason(event.code),
|
|
105
|
+
)
|
|
106
|
+
for event in status_list
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
# Build delivery forecast info if available
|
|
110
|
+
estimated_delivery = None
|
|
111
|
+
if shipment_info.deliveryForecast and shipment_info.deliveryForecast.date:
|
|
112
|
+
estimated_delivery = shipment_info.deliveryForecast.date
|
|
113
|
+
|
|
114
|
+
return models.TrackingDetails(
|
|
115
|
+
carrier_id=settings.carrier_id,
|
|
116
|
+
carrier_name=settings.carrier_name,
|
|
117
|
+
tracking_number=shipment_info.shipmentID,
|
|
118
|
+
events=events,
|
|
119
|
+
delivered=overall_status == "delivered",
|
|
120
|
+
status=overall_status,
|
|
121
|
+
estimated_delivery=estimated_delivery,
|
|
122
|
+
info=models.TrackingInfo(
|
|
123
|
+
carrier_tracking_link=shipment_info.trackingLink,
|
|
124
|
+
customer_name=None,
|
|
125
|
+
shipment_destination_country=(
|
|
126
|
+
shipment_info.receiverAddress.countryCode
|
|
127
|
+
if shipment_info.receiverAddress
|
|
128
|
+
else None
|
|
129
|
+
),
|
|
130
|
+
shipment_destination_postal_code=(
|
|
131
|
+
str(shipment_info.receiverAddress.zipCode)
|
|
132
|
+
if shipment_info.receiverAddress and shipment_info.receiverAddress.zipCode
|
|
133
|
+
else None
|
|
134
|
+
),
|
|
135
|
+
),
|
|
136
|
+
meta=dict(
|
|
137
|
+
client_id=shipment_info.clientID,
|
|
138
|
+
client_reference=shipment_info.clientReference,
|
|
139
|
+
client_reference2=shipment_info.clientReference2,
|
|
140
|
+
part_number=shipment_info.partNumber,
|
|
141
|
+
international_shipment_id=shipment_info.internationalShipmentID,
|
|
142
|
+
),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def tracking_request(
|
|
147
|
+
payload: models.TrackingRequest,
|
|
148
|
+
settings: provider_utils.Settings,
|
|
149
|
+
) -> lib.Serializable:
|
|
150
|
+
"""Create tracking request for Hermes Shipment Info API.
|
|
151
|
+
|
|
152
|
+
Hermes uses GET requests with query parameters, so we just return
|
|
153
|
+
the tracking numbers to be used as shipmentID query params.
|
|
154
|
+
"""
|
|
155
|
+
return lib.Serializable(payload.tracking_numbers)
|
karrio/providers/hermes/units.py
CHANGED
|
@@ -64,48 +64,198 @@ class ShippingService(lib.StrEnum):
|
|
|
64
64
|
class ShippingOption(lib.Enum):
|
|
65
65
|
"""Carrier specific options."""
|
|
66
66
|
|
|
67
|
-
#
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
hermes_stated_day = lib.OptionEnum(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
67
|
+
# Delivery Options (Zustelloptionen tab)
|
|
68
|
+
hermes_next_day = lib.OptionEnum(
|
|
69
|
+
"nextDayService", bool,
|
|
70
|
+
help="Enable next-day delivery service",
|
|
71
|
+
meta=dict(category="DELIVERY_OPTIONS", configurable=True)
|
|
72
|
+
)
|
|
73
|
+
hermes_bulk_goods = lib.OptionEnum(
|
|
74
|
+
"bulkGoodService", bool,
|
|
75
|
+
help="Mark shipment as bulky goods (Sperrgut)",
|
|
76
|
+
meta=dict(category="DELIVERY_OPTIONS", configurable=True)
|
|
77
|
+
)
|
|
78
|
+
hermes_compact_parcel = lib.OptionEnum(
|
|
79
|
+
"compactParcelService", bool,
|
|
80
|
+
help="Enable compact parcel service",
|
|
81
|
+
meta=dict(category="DELIVERY_OPTIONS", configurable=True)
|
|
82
|
+
)
|
|
83
|
+
hermes_redirection_prohibited = lib.OptionEnum(
|
|
84
|
+
"redirectionProhibitedService", bool,
|
|
85
|
+
help="Do not allow redirection to neighbor",
|
|
86
|
+
meta=dict(category="DELIVERY_OPTIONS", configurable=True)
|
|
87
|
+
)
|
|
88
|
+
hermes_stated_day = lib.OptionEnum(
|
|
89
|
+
"statedDay", str,
|
|
90
|
+
help="Specific delivery date (YYYY-MM-DD format)",
|
|
91
|
+
meta=dict(category="DELIVERY_OPTIONS", configurable=True)
|
|
92
|
+
)
|
|
93
|
+
hermes_time_slot = lib.OptionEnum(
|
|
94
|
+
"timeSlot", str,
|
|
95
|
+
help="Delivery time slot (FORENOON, NOON, AFTERNOON, EVENING)",
|
|
96
|
+
meta=dict(category="DELIVERY_OPTIONS", configurable=True)
|
|
97
|
+
)
|
|
98
|
+
hermes_express = lib.OptionEnum(
|
|
99
|
+
"expressService", bool,
|
|
100
|
+
help="Enable express delivery service",
|
|
101
|
+
meta=dict(category="DELIVERY_OPTIONS", configurable=True)
|
|
102
|
+
)
|
|
103
|
+
hermes_after_hours_delivery = lib.OptionEnum(
|
|
104
|
+
"afterHoursDeliveryService", bool,
|
|
105
|
+
help="Enable after-hours delivery (Feierabendservice)",
|
|
106
|
+
meta=dict(category="DELIVERY_OPTIONS", configurable=True)
|
|
107
|
+
)
|
|
108
|
+
hermes_parcel_class = lib.OptionEnum(
|
|
109
|
+
"parcelClass", str,
|
|
110
|
+
help="Parcel size class (XS, S, M, L, XL)",
|
|
111
|
+
meta=dict(category="DELIVERY_OPTIONS", configurable=True)
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Signature Options
|
|
115
|
+
hermes_signature = lib.OptionEnum(
|
|
116
|
+
"signatureService", bool,
|
|
117
|
+
help="Require signature upon delivery",
|
|
118
|
+
meta=dict(category="SIGNATURE", configurable=True)
|
|
119
|
+
)
|
|
120
|
+
hermes_household_signature = lib.OptionEnum(
|
|
121
|
+
"householdSignatureService", bool,
|
|
122
|
+
help="Require household member signature",
|
|
123
|
+
meta=dict(category="SIGNATURE", configurable=True)
|
|
124
|
+
)
|
|
125
|
+
hermes_ident_id = lib.OptionEnum(
|
|
126
|
+
"identID", str,
|
|
127
|
+
help="ID number for identity verification",
|
|
128
|
+
meta=dict(category="SIGNATURE", configurable=True)
|
|
129
|
+
)
|
|
130
|
+
hermes_ident_type = lib.OptionEnum(
|
|
131
|
+
"identType", str,
|
|
132
|
+
help="Type of ID for verification (e.g., GERMAN_IDENTITY_CARD)",
|
|
133
|
+
meta=dict(category="SIGNATURE", configurable=True)
|
|
134
|
+
)
|
|
135
|
+
hermes_ident_fsk = lib.OptionEnum(
|
|
136
|
+
"identVerifyFsk", str,
|
|
137
|
+
help="Minimum age verification (e.g., 18)",
|
|
138
|
+
meta=dict(category="SIGNATURE", configurable=True)
|
|
139
|
+
)
|
|
140
|
+
hermes_ident_birthday = lib.OptionEnum(
|
|
141
|
+
"identVerifyBirthday", str,
|
|
142
|
+
help="Verify recipient birthday (YYYY-MM-DD)",
|
|
143
|
+
meta=dict(category="SIGNATURE", configurable=True)
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# PUDO Options (Parcel Shop)
|
|
147
|
+
hermes_parcel_shop_id = lib.OptionEnum(
|
|
148
|
+
"psID", str,
|
|
149
|
+
help="Hermes ParcelShop ID for delivery",
|
|
150
|
+
meta=dict(category="PUDO", configurable=True)
|
|
151
|
+
)
|
|
152
|
+
hermes_parcel_shop_selection_rule = lib.OptionEnum(
|
|
153
|
+
"psSelectionRule", str,
|
|
154
|
+
help="ParcelShop selection rule (SELECT_BY_ID, SELECT_BY_RECEIVER_ADDRESS)",
|
|
155
|
+
meta=dict(category="PUDO", configurable=True)
|
|
156
|
+
)
|
|
157
|
+
hermes_parcel_shop_customer_firstname = lib.OptionEnum(
|
|
158
|
+
"psCustomerFirstName", str,
|
|
159
|
+
help="Customer first name for ParcelShop pickup",
|
|
160
|
+
meta=dict(category="PUDO", configurable=True)
|
|
161
|
+
)
|
|
162
|
+
hermes_parcel_shop_customer_lastname = lib.OptionEnum(
|
|
163
|
+
"psCustomerLastName", str,
|
|
164
|
+
help="Customer last name for ParcelShop pickup",
|
|
165
|
+
meta=dict(category="PUDO", configurable=True)
|
|
166
|
+
)
|
|
167
|
+
hermes_exclude_parcel_shop_auth = lib.OptionEnum(
|
|
168
|
+
"excludeParcelShopAuthorization", bool,
|
|
169
|
+
help="Exclude ParcelShop delivery authorization",
|
|
170
|
+
meta=dict(category="PUDO", configurable=True)
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Notification Options
|
|
174
|
+
hermes_notification_email = lib.OptionEnum(
|
|
175
|
+
"notificationEmail", str,
|
|
176
|
+
help="Email for delivery notifications",
|
|
177
|
+
meta=dict(category="NOTIFICATION", configurable=True)
|
|
178
|
+
)
|
|
179
|
+
hermes_notification_type = lib.OptionEnum(
|
|
180
|
+
"notificationType", str,
|
|
181
|
+
help="Notification type (EMAIL, SMS, EMAIL_SMS)",
|
|
182
|
+
meta=dict(category="NOTIFICATION", configurable=True)
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# COD Options (Cash on Delivery)
|
|
186
|
+
hermes_cod_amount = lib.OptionEnum(
|
|
187
|
+
"codAmount", float,
|
|
188
|
+
help="Cash on delivery amount",
|
|
189
|
+
meta=dict(category="COD", configurable=True)
|
|
190
|
+
)
|
|
191
|
+
hermes_cod_currency = lib.OptionEnum(
|
|
192
|
+
"codCurrency", str,
|
|
193
|
+
help="Currency for COD amount",
|
|
194
|
+
meta=dict(category="COD", configurable=True)
|
|
195
|
+
)
|
|
196
|
+
hermes_cod_distribution = lib.OptionEnum(
|
|
197
|
+
"codDistribution", str,
|
|
198
|
+
help="COD distribution method (e.g., transfer, check)",
|
|
199
|
+
meta=dict(category="COD", configurable=True)
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# Dangerous Goods
|
|
203
|
+
hermes_limited_quantities = lib.OptionEnum(
|
|
204
|
+
"limitedQuantitiesService", bool,
|
|
205
|
+
help="Mark shipment as containing limited quantity hazardous materials",
|
|
206
|
+
meta=dict(category="DANGEROUS_GOOD", configurable=True)
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# Return Options
|
|
210
|
+
hermes_return_enabled = lib.OptionEnum(
|
|
211
|
+
"returnService", bool,
|
|
212
|
+
help="Enable return label for this shipment",
|
|
213
|
+
meta=dict(category="RETURN", configurable=True)
|
|
214
|
+
)
|
|
215
|
+
hermes_include_return_label = lib.OptionEnum(
|
|
216
|
+
"includeReturnLabel", bool,
|
|
217
|
+
help="Include a pre-printed return label inside the package",
|
|
218
|
+
meta=dict(category="RETURN", configurable=True)
|
|
219
|
+
)
|
|
220
|
+
hermes_digital_sales_return = lib.OptionEnum(
|
|
221
|
+
"digitalSalesReturn", bool,
|
|
222
|
+
help="Enable digital sales return (digitale Verkaufsretoure)",
|
|
223
|
+
meta=dict(category="RETURN", configurable=True)
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
# Reference/Instructions Options
|
|
227
|
+
hermes_customer_reference_1 = lib.OptionEnum(
|
|
228
|
+
"customerReference1", str,
|
|
229
|
+
help="Customer reference field 1 (Kundenreferenz 1)",
|
|
230
|
+
meta=dict(category="INSTRUCTIONS", configurable=True)
|
|
231
|
+
)
|
|
232
|
+
hermes_customer_reference_2 = lib.OptionEnum(
|
|
233
|
+
"customerReference2", str,
|
|
234
|
+
help="Customer reference field 2 (Kundenreferenz 2)",
|
|
235
|
+
meta=dict(category="INSTRUCTIONS", configurable=True)
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# Internal/Multipart Options (not configurable in shipping method editor)
|
|
239
|
+
hermes_tan_service = lib.OptionEnum(
|
|
240
|
+
"tanService", bool,
|
|
241
|
+
meta=dict(configurable=False)
|
|
242
|
+
)
|
|
243
|
+
hermes_late_injection = lib.OptionEnum(
|
|
244
|
+
"lateInjectionService", bool,
|
|
245
|
+
meta=dict(configurable=False)
|
|
246
|
+
)
|
|
247
|
+
hermes_part_number = lib.OptionEnum(
|
|
248
|
+
"partNumber", int,
|
|
249
|
+
meta=dict(configurable=False)
|
|
250
|
+
)
|
|
251
|
+
hermes_number_of_parts = lib.OptionEnum(
|
|
252
|
+
"numberOfParts", int,
|
|
253
|
+
meta=dict(configurable=False)
|
|
254
|
+
)
|
|
255
|
+
hermes_parent_shipment_order_id = lib.OptionEnum(
|
|
256
|
+
"parentShipmentOrderID", str,
|
|
257
|
+
meta=dict(configurable=False)
|
|
258
|
+
)
|
|
109
259
|
|
|
110
260
|
"""Unified Option type mapping."""
|
|
111
261
|
signature_required = hermes_signature
|
|
@@ -128,15 +278,16 @@ def shipping_options_initializer(
|
|
|
128
278
|
|
|
129
279
|
|
|
130
280
|
class TrackingStatus(lib.Enum):
|
|
131
|
-
"""Hermes tracking status mapping."""
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
281
|
+
"""Hermes tracking status mapping based on Hermes event codes."""
|
|
282
|
+
|
|
283
|
+
pending = ["0000"]
|
|
284
|
+
in_transit = ["1000", "2000"]
|
|
285
|
+
out_for_delivery = ["3000"]
|
|
286
|
+
delivered = ["3500"]
|
|
287
|
+
delivery_failed = ["4000", "4500"]
|
|
288
|
+
ready_for_pickup = ["5000"]
|
|
289
|
+
on_hold = ["6000"]
|
|
290
|
+
delivery_delayed = ["7000"]
|
|
140
291
|
|
|
141
292
|
|
|
142
293
|
class LabelType(lib.StrEnum):
|
karrio/providers/hermes/utils.py
CHANGED
|
@@ -100,3 +100,29 @@ def login(settings: Settings):
|
|
|
100
100
|
seconds=float(response.get("expires_in", 3600))
|
|
101
101
|
)
|
|
102
102
|
return {**response, "expiry": lib.fdatetime(expiry)}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def get_access_token(settings: Settings) -> str:
|
|
106
|
+
"""Get access token from settings."""
|
|
107
|
+
token_data = settings.access_token
|
|
108
|
+
return token_data.get("access_token") if isinstance(token_data, dict) else token_data
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def prepare_shipment_data(request: lib.Serializable) -> tuple:
|
|
112
|
+
"""Prepare shipment request data and multi-piece flag."""
|
|
113
|
+
requests_data = request.serialize()
|
|
114
|
+
is_multi_piece = request.ctx.get("is_multi_piece", False) if request.ctx else False
|
|
115
|
+
requests_data = requests_data if isinstance(requests_data, list) else [requests_data]
|
|
116
|
+
return requests_data, is_multi_piece
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def inject_parent_shipment_id(req_data: dict, parent_id: str) -> dict:
|
|
120
|
+
"""Inject parentShipmentOrderID for multi-piece packages 2+."""
|
|
121
|
+
if req_data.get("service", {}).get("multipartService"):
|
|
122
|
+
req_data["service"]["multipartService"]["parentShipmentOrderID"] = parent_id
|
|
123
|
+
return req_data
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def extract_shipment_order_id(response: dict) -> str:
|
|
127
|
+
"""Extract shipmentOrderID from response for multi-piece linking."""
|
|
128
|
+
return response.get("shipmentOrderID")
|
|
@@ -41,3 +41,13 @@ from karrio.schemas.hermes.pickup_create_request import (
|
|
|
41
41
|
from karrio.schemas.hermes.pickup_create_response import PickupCreateResponseType
|
|
42
42
|
from karrio.schemas.hermes.pickup_cancel_request import PickupCancelRequestType
|
|
43
43
|
from karrio.schemas.hermes.pickup_cancel_response import PickupCancelResponseType
|
|
44
|
+
from karrio.schemas.hermes.tracking_response import (
|
|
45
|
+
TrackingResponseType,
|
|
46
|
+
ShipmentinfoType,
|
|
47
|
+
ResultType,
|
|
48
|
+
StatusType,
|
|
49
|
+
ScanningUnitType,
|
|
50
|
+
ReceiverAddressType as TrackingReceiverAddressType,
|
|
51
|
+
DeliveryForecastType,
|
|
52
|
+
TimeSlotType,
|
|
53
|
+
)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import attr
|
|
2
|
+
import jstruct
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@attr.s(auto_attribs=True)
|
|
7
|
+
class TimeSlotType:
|
|
8
|
+
to: typing.Optional[str] = None
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@attr.s(auto_attribs=True)
|
|
12
|
+
class DeliveryForecastType:
|
|
13
|
+
fixed: typing.Optional[bool] = None
|
|
14
|
+
date: typing.Optional[str] = None
|
|
15
|
+
timeSlot: typing.Optional[TimeSlotType] = jstruct.JStruct[TimeSlotType]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@attr.s(auto_attribs=True)
|
|
19
|
+
class ReceiverAddressType:
|
|
20
|
+
city: typing.Optional[str] = None
|
|
21
|
+
zipCode: typing.Optional[int] = None
|
|
22
|
+
countryCode: typing.Optional[str] = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@attr.s(auto_attribs=True)
|
|
26
|
+
class ResultType:
|
|
27
|
+
code: typing.Optional[str] = None
|
|
28
|
+
message: typing.Optional[str] = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@attr.s(auto_attribs=True)
|
|
32
|
+
class ScanningUnitType:
|
|
33
|
+
name: typing.Optional[str] = None
|
|
34
|
+
city: typing.Optional[str] = None
|
|
35
|
+
countryCode: typing.Optional[str] = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@attr.s(auto_attribs=True)
|
|
39
|
+
class StatusType:
|
|
40
|
+
timestamp: typing.Optional[str] = None
|
|
41
|
+
code: typing.Optional[str] = None
|
|
42
|
+
description: typing.Optional[str] = None
|
|
43
|
+
scanningUnit: typing.Optional[ScanningUnitType] = jstruct.JStruct[ScanningUnitType]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@attr.s(auto_attribs=True)
|
|
47
|
+
class ShipmentinfoType:
|
|
48
|
+
shipmentID: typing.Optional[str] = None
|
|
49
|
+
partNumber: typing.Optional[int] = None
|
|
50
|
+
clientID: typing.Optional[int] = None
|
|
51
|
+
clientReference: typing.Optional[str] = None
|
|
52
|
+
clientReference2: typing.Optional[int] = None
|
|
53
|
+
internationalShipmentID: typing.Optional[str] = None
|
|
54
|
+
trackingLink: typing.Optional[str] = None
|
|
55
|
+
result: typing.Optional[ResultType] = jstruct.JStruct[ResultType]
|
|
56
|
+
receiverAddress: typing.Optional[ReceiverAddressType] = jstruct.JStruct[ReceiverAddressType]
|
|
57
|
+
deliveryForecast: typing.Optional[DeliveryForecastType] = jstruct.JStruct[DeliveryForecastType]
|
|
58
|
+
status: typing.Optional[typing.List[StatusType]] = jstruct.JList[StatusType]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@attr.s(auto_attribs=True)
|
|
62
|
+
class TrackingResponseType:
|
|
63
|
+
shipmentinfo: typing.Optional[typing.List[ShipmentinfoType]] = jstruct.JList[ShipmentinfoType]
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
karrio/mappers/hermes/__init__.py,sha256=3WcvIpOkQAy1p54BhLGfmi1hyY9Dhe0qSG2yZY9mPes,145
|
|
2
|
-
karrio/mappers/hermes/mapper.py,sha256=
|
|
3
|
-
karrio/mappers/hermes/proxy.py,sha256=
|
|
2
|
+
karrio/mappers/hermes/mapper.py,sha256=4xOrR9eFX-MKv6LKB32WD8hZuhnscB_OTgeKVVrGo6Q,2700
|
|
3
|
+
karrio/mappers/hermes/proxy.py,sha256=zGbLwJTwunrWp8b1Lg_hs3AwxPSd8Ogy6sydl99YaSs,4492
|
|
4
4
|
karrio/mappers/hermes/settings.py,sha256=4BTM7ncSQqTBP6J_oJTksBDGhaFHOWoHDnpq0iOWHVo,1151
|
|
5
5
|
karrio/plugins/hermes/__init__.py,sha256=zUfOhtzuErd1H1ICL4rQt68e6BWRQf_B6_kW5UVzd9M,1020
|
|
6
|
-
karrio/providers/hermes/__init__.py,sha256=
|
|
6
|
+
karrio/providers/hermes/__init__.py,sha256=wMiVnPz7EgKdbu1pGBuvalfyuHj5QwSeMijjvEP6q4w,590
|
|
7
7
|
karrio/providers/hermes/error.py,sha256=YWmYC_l7_nyJCxiqM0Kgf6yV9IpJ04tkdZyyMT7QnkY,2974
|
|
8
|
-
karrio/providers/hermes/
|
|
9
|
-
karrio/providers/hermes/
|
|
8
|
+
karrio/providers/hermes/tracking.py,sha256=olKfHEVYZuwGL9e4348Ui-mAKBGr94FLF6VyxCwtZZg,5941
|
|
9
|
+
karrio/providers/hermes/units.py,sha256=91B_i4BeY1e_XguBOG2MZ126IT3mKdz6-dvn70AJyv8,13633
|
|
10
|
+
karrio/providers/hermes/utils.py,sha256=NOfS_K0Zw7_wxJGBJfkdXwZP6hs0bsaZye5uypztGc4,4163
|
|
10
11
|
karrio/providers/hermes/pickup/__init__.py,sha256=E59ks-qJAsNmfQgaZ7X0tZuBetITsdRSMJUAZnqxabg,307
|
|
11
12
|
karrio/providers/hermes/pickup/cancel.py,sha256=JYVGL9decPB0QOHsjrYU5cwT_4vzrpsaEjapC9F3t7A,1587
|
|
12
|
-
karrio/providers/hermes/pickup/create.py,sha256=
|
|
13
|
+
karrio/providers/hermes/pickup/create.py,sha256=ICSG7HgF45h8XY0Ypc8voA5TWFSVmysGerh1F-fha24,3993
|
|
13
14
|
karrio/providers/hermes/shipment/__init__.py,sha256=B_zc5eaS4st1bnIyB_RwYY9XJJ9DyjB1Tb0SaIEJRJ8,209
|
|
14
|
-
karrio/providers/hermes/shipment/create.py,sha256=
|
|
15
|
-
karrio/schemas/hermes/__init__.py,sha256=
|
|
15
|
+
karrio/providers/hermes/shipment/create.py,sha256=DAkC5O9XHs6QOlLKBTQ8zKQlLNf6h5Ty29_qFhMSBWM,16677
|
|
16
|
+
karrio/schemas/hermes/__init__.py,sha256=zcJLsZ0HvWk9q7J07BAel3ib_eXFoN6_5joxbdzBLV0,1530
|
|
16
17
|
karrio/schemas/hermes/error_response.py,sha256=BJhI7P8wursdYsYACfDLT8cs-bsnKlJDs0OzurYMPR4,343
|
|
17
18
|
karrio/schemas/hermes/pickup_cancel_request.py,sha256=rZgm3BpL3iziO_TC_y-cAELMsWjXyvRYWmXYyExhdR0,148
|
|
18
19
|
karrio/schemas/hermes/pickup_cancel_response.py,sha256=9-STL_Aq0EvXWrNMfvc4DiPK658e1JPp0uhW695XOGI,397
|
|
@@ -20,8 +21,9 @@ karrio/schemas/hermes/pickup_create_request.py,sha256=0V14LMl0XVLOZ_zXRzoTLUZ8nB
|
|
|
20
21
|
karrio/schemas/hermes/pickup_create_response.py,sha256=sAgq92J0TZOUolhMcvDkp6qSRvxOqWCIZHNQWQN7KeU,397
|
|
21
22
|
karrio/schemas/hermes/shipment_request.py,sha256=FMqrSlCDG4eGDkuqPDc_DDCbtV_vGRyrRBFCiNTFkmM,7415
|
|
22
23
|
karrio/schemas/hermes/shipment_response.py,sha256=BI2kUemG-wTzAFpok9KEnlwjm6eK_LemBA_Cyga_e6A,3477
|
|
23
|
-
|
|
24
|
-
karrio_hermes-2026.1.
|
|
25
|
-
karrio_hermes-2026.1.
|
|
26
|
-
karrio_hermes-2026.1.
|
|
27
|
-
karrio_hermes-2026.1.
|
|
24
|
+
karrio/schemas/hermes/tracking_response.py,sha256=6-bqTSl-N65PHl8cs1puOCFubzIZB55RveLdpNsmTEE,1967
|
|
25
|
+
karrio_hermes-2026.1.4.dist-info/METADATA,sha256=cxB5FLr58U6GhdnY-8PCdSYMGHNPOPNiG46K9X-mirI,982
|
|
26
|
+
karrio_hermes-2026.1.4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
27
|
+
karrio_hermes-2026.1.4.dist-info/entry_points.txt,sha256=R8XDrYBqWXTPqaO0iTi4Buz8iQiiBiuHkJVNtZIXTkU,57
|
|
28
|
+
karrio_hermes-2026.1.4.dist-info/top_level.txt,sha256=9Nasa6abG7pPPG8MGzlemnqw1ohIqgouzQ7HGBnOFLg,27
|
|
29
|
+
karrio_hermes-2026.1.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|