karrio-hermes 2026.1__py3-none-any.whl → 2026.1.3__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 +156 -0
- karrio/providers/hermes/units.py +261 -29
- karrio/providers/hermes/utils.py +26 -0
- karrio/schemas/hermes/__init__.py +10 -0
- karrio/schemas/hermes/tracking_response.py +64 -0
- {karrio_hermes-2026.1.dist-info → karrio_hermes-2026.1.3.dist-info}/METADATA +1 -1
- karrio_hermes-2026.1.3.dist-info/RECORD +29 -0
- {karrio_hermes-2026.1.dist-info → karrio_hermes-2026.1.3.dist-info}/WHEEL +1 -1
- karrio_hermes-2026.1.dist-info/RECORD +0 -27
- {karrio_hermes-2026.1.dist-info → karrio_hermes-2026.1.3.dist-info}/entry_points.txt +0 -0
- {karrio_hermes-2026.1.dist-info → karrio_hermes-2026.1.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,156 @@
|
|
|
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 TrackingIncidentReason enum values."""
|
|
24
|
+
if not code:
|
|
25
|
+
return None
|
|
26
|
+
for reason in list(provider_units.TrackingIncidentReason):
|
|
27
|
+
if code in reason.value:
|
|
28
|
+
return reason.name
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def parse_tracking_response(
|
|
33
|
+
_response: lib.Deserializable[dict],
|
|
34
|
+
settings: provider_utils.Settings,
|
|
35
|
+
) -> typing.Tuple[typing.List[models.TrackingDetails], typing.List[models.Message]]:
|
|
36
|
+
"""Parse tracking response from Hermes Shipment Info API."""
|
|
37
|
+
response = _response.deserialize()
|
|
38
|
+
|
|
39
|
+
# Parse the response using generated schema
|
|
40
|
+
tracking_response = lib.to_object(hermes_res.TrackingResponseType, response)
|
|
41
|
+
|
|
42
|
+
# Collect error messages
|
|
43
|
+
messages: typing.List[models.Message] = []
|
|
44
|
+
|
|
45
|
+
# Extract tracking details for each shipment
|
|
46
|
+
tracking_details: typing.List[models.TrackingDetails] = []
|
|
47
|
+
|
|
48
|
+
for shipment_info in tracking_response.shipmentinfo or []:
|
|
49
|
+
# Check for errors in individual shipment result
|
|
50
|
+
if shipment_info.result and shipment_info.result.code:
|
|
51
|
+
if shipment_info.result.code.startswith("e"):
|
|
52
|
+
messages.append(
|
|
53
|
+
models.Message(
|
|
54
|
+
carrier_id=settings.carrier_id,
|
|
55
|
+
carrier_name=settings.carrier_name,
|
|
56
|
+
code=shipment_info.result.code,
|
|
57
|
+
message=shipment_info.result.message or "",
|
|
58
|
+
details=dict(shipment_id=shipment_info.shipmentID),
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
# Extract tracking details
|
|
64
|
+
details = _extract_details(shipment_info, settings)
|
|
65
|
+
if details:
|
|
66
|
+
tracking_details.append(details)
|
|
67
|
+
|
|
68
|
+
return tracking_details, messages
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _extract_details(
|
|
72
|
+
shipment_info: hermes_res.ShipmentinfoType,
|
|
73
|
+
settings: provider_utils.Settings,
|
|
74
|
+
) -> typing.Optional[models.TrackingDetails]:
|
|
75
|
+
"""Extract tracking details from Hermes shipment info."""
|
|
76
|
+
if not shipment_info.shipmentID:
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
# Get status events (already in chronological order, most recent last in API)
|
|
80
|
+
# Reverse to have most recent first for Karrio convention
|
|
81
|
+
status_list = list(reversed(shipment_info.status or []))
|
|
82
|
+
|
|
83
|
+
# Get latest event code for overall status
|
|
84
|
+
latest_code = status_list[0].code if status_list else None
|
|
85
|
+
overall_status = _match_status(latest_code) or provider_units.TrackingStatus.in_transit.name
|
|
86
|
+
|
|
87
|
+
# Build tracking events with all required fields per CARRIER_INTEGRATION_GUIDE.md
|
|
88
|
+
events = [
|
|
89
|
+
models.TrackingEvent(
|
|
90
|
+
date=lib.fdate(event.timestamp, "%Y-%m-%dT%H:%M:%S%z"),
|
|
91
|
+
time=lib.flocaltime(event.timestamp, "%Y-%m-%dT%H:%M:%S%z"),
|
|
92
|
+
description=event.description or "",
|
|
93
|
+
code=event.code,
|
|
94
|
+
location=lib.join(
|
|
95
|
+
event.scanningUnit.city if event.scanningUnit else None,
|
|
96
|
+
event.scanningUnit.countryCode if event.scanningUnit else None,
|
|
97
|
+
join=True,
|
|
98
|
+
separator=", ",
|
|
99
|
+
),
|
|
100
|
+
# REQUIRED: timestamp in ISO 8601 format (already provided by Hermes)
|
|
101
|
+
timestamp=event.timestamp,
|
|
102
|
+
# REQUIRED: normalized status at event level
|
|
103
|
+
status=_match_status(event.code),
|
|
104
|
+
# Incident reason for exception events
|
|
105
|
+
reason=_match_reason(event.code),
|
|
106
|
+
)
|
|
107
|
+
for event in status_list
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
# Build delivery forecast info if available
|
|
111
|
+
estimated_delivery = None
|
|
112
|
+
if shipment_info.deliveryForecast and shipment_info.deliveryForecast.date:
|
|
113
|
+
estimated_delivery = shipment_info.deliveryForecast.date
|
|
114
|
+
|
|
115
|
+
return models.TrackingDetails(
|
|
116
|
+
carrier_id=settings.carrier_id,
|
|
117
|
+
carrier_name=settings.carrier_name,
|
|
118
|
+
tracking_number=shipment_info.shipmentID,
|
|
119
|
+
events=events,
|
|
120
|
+
delivered=overall_status == "delivered",
|
|
121
|
+
status=overall_status,
|
|
122
|
+
estimated_delivery=estimated_delivery,
|
|
123
|
+
info=models.TrackingInfo(
|
|
124
|
+
carrier_tracking_link=shipment_info.trackingLink,
|
|
125
|
+
customer_name=None,
|
|
126
|
+
shipment_destination_country=(
|
|
127
|
+
shipment_info.receiverAddress.countryCode
|
|
128
|
+
if shipment_info.receiverAddress
|
|
129
|
+
else None
|
|
130
|
+
),
|
|
131
|
+
shipment_destination_postal_code=(
|
|
132
|
+
str(shipment_info.receiverAddress.zipCode)
|
|
133
|
+
if shipment_info.receiverAddress and shipment_info.receiverAddress.zipCode
|
|
134
|
+
else None
|
|
135
|
+
),
|
|
136
|
+
),
|
|
137
|
+
meta=dict(
|
|
138
|
+
client_id=shipment_info.clientID,
|
|
139
|
+
client_reference=shipment_info.clientReference,
|
|
140
|
+
client_reference2=shipment_info.clientReference2,
|
|
141
|
+
part_number=shipment_info.partNumber,
|
|
142
|
+
international_shipment_id=shipment_info.internationalShipmentID,
|
|
143
|
+
),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def tracking_request(
|
|
148
|
+
payload: models.TrackingRequest,
|
|
149
|
+
settings: provider_utils.Settings,
|
|
150
|
+
) -> lib.Serializable:
|
|
151
|
+
"""Create tracking request for Hermes Shipment Info API.
|
|
152
|
+
|
|
153
|
+
Hermes uses GET requests with query parameters, so we just return
|
|
154
|
+
the tracking numbers to be used as shipmentID query params.
|
|
155
|
+
"""
|
|
156
|
+
return lib.Serializable(payload.tracking_numbers)
|
karrio/providers/hermes/units.py
CHANGED
|
@@ -66,41 +66,41 @@ class ShippingOption(lib.Enum):
|
|
|
66
66
|
|
|
67
67
|
# Hermes services as options
|
|
68
68
|
hermes_tan_service = lib.OptionEnum("tanService", bool)
|
|
69
|
-
hermes_limited_quantities = lib.OptionEnum("limitedQuantitiesService", bool)
|
|
69
|
+
hermes_limited_quantities = lib.OptionEnum("limitedQuantitiesService", bool, meta=dict(category="DANGEROUS_GOOD"))
|
|
70
70
|
hermes_bulk_goods = lib.OptionEnum("bulkGoodService", bool)
|
|
71
|
-
hermes_household_signature = lib.OptionEnum("householdSignatureService", bool)
|
|
71
|
+
hermes_household_signature = lib.OptionEnum("householdSignatureService", bool, meta=dict(category="SIGNATURE"))
|
|
72
72
|
hermes_compact_parcel = lib.OptionEnum("compactParcelService", bool)
|
|
73
|
-
hermes_next_day = lib.OptionEnum("nextDayService", bool)
|
|
74
|
-
hermes_signature = lib.OptionEnum("signatureService", bool)
|
|
75
|
-
hermes_redirection_prohibited = lib.OptionEnum("redirectionProhibitedService", bool)
|
|
76
|
-
hermes_exclude_parcel_shop_auth = lib.OptionEnum("excludeParcelShopAuthorization", bool)
|
|
73
|
+
hermes_next_day = lib.OptionEnum("nextDayService", bool, meta=dict(category="DELIVERY_OPTIONS"))
|
|
74
|
+
hermes_signature = lib.OptionEnum("signatureService", bool, meta=dict(category="SIGNATURE"))
|
|
75
|
+
hermes_redirection_prohibited = lib.OptionEnum("redirectionProhibitedService", bool, meta=dict(category="DELIVERY_OPTIONS"))
|
|
76
|
+
hermes_exclude_parcel_shop_auth = lib.OptionEnum("excludeParcelShopAuthorization", bool, meta=dict(category="PUDO"))
|
|
77
77
|
hermes_late_injection = lib.OptionEnum("lateInjectionService", bool)
|
|
78
78
|
|
|
79
79
|
# Cash on delivery
|
|
80
|
-
hermes_cod_amount = lib.OptionEnum("codAmount", float)
|
|
81
|
-
hermes_cod_currency = lib.OptionEnum("codCurrency", str)
|
|
80
|
+
hermes_cod_amount = lib.OptionEnum("codAmount", float, meta=dict(category="COD"))
|
|
81
|
+
hermes_cod_currency = lib.OptionEnum("codCurrency", str, meta=dict(category="COD"))
|
|
82
82
|
|
|
83
83
|
# Customer alert service
|
|
84
|
-
hermes_notification_email = lib.OptionEnum("notificationEmail", str)
|
|
85
|
-
hermes_notification_type = lib.OptionEnum("notificationType", str) # EMAIL, SMS, EMAIL_SMS
|
|
84
|
+
hermes_notification_email = lib.OptionEnum("notificationEmail", str, meta=dict(category="NOTIFICATION"))
|
|
85
|
+
hermes_notification_type = lib.OptionEnum("notificationType", str, meta=dict(category="NOTIFICATION")) # EMAIL, SMS, EMAIL_SMS
|
|
86
86
|
|
|
87
87
|
# Stated day service
|
|
88
|
-
hermes_stated_day = lib.OptionEnum("statedDay", str) # YYYY-MM-DD format
|
|
88
|
+
hermes_stated_day = lib.OptionEnum("statedDay", str, meta=dict(category="DELIVERY_OPTIONS")) # YYYY-MM-DD format
|
|
89
89
|
|
|
90
90
|
# Stated time service
|
|
91
|
-
hermes_time_slot = lib.OptionEnum("timeSlot", str) # FORENOON, NOON, AFTERNOON, EVENING
|
|
91
|
+
hermes_time_slot = lib.OptionEnum("timeSlot", str, meta=dict(category="DELIVERY_OPTIONS")) # FORENOON, NOON, AFTERNOON, EVENING
|
|
92
92
|
|
|
93
93
|
# Ident service
|
|
94
|
-
hermes_ident_id = lib.OptionEnum("identID", str)
|
|
95
|
-
hermes_ident_type = lib.OptionEnum("identType", str) # GERMAN_IDENTITY_CARD, etc.
|
|
96
|
-
hermes_ident_fsk = lib.OptionEnum("identVerifyFsk", str) # 18
|
|
97
|
-
hermes_ident_birthday = lib.OptionEnum("identVerifyBirthday", str) # YYYY-MM-DD
|
|
94
|
+
hermes_ident_id = lib.OptionEnum("identID", str, meta=dict(category="SIGNATURE"))
|
|
95
|
+
hermes_ident_type = lib.OptionEnum("identType", str, meta=dict(category="SIGNATURE")) # GERMAN_IDENTITY_CARD, etc.
|
|
96
|
+
hermes_ident_fsk = lib.OptionEnum("identVerifyFsk", str, meta=dict(category="SIGNATURE")) # 18
|
|
97
|
+
hermes_ident_birthday = lib.OptionEnum("identVerifyBirthday", str, meta=dict(category="SIGNATURE")) # YYYY-MM-DD
|
|
98
98
|
|
|
99
99
|
# Parcel shop delivery
|
|
100
|
-
hermes_parcel_shop_id = lib.OptionEnum("psID", str)
|
|
101
|
-
hermes_parcel_shop_selection_rule = lib.OptionEnum("psSelectionRule", str) # SELECT_BY_ID, SELECT_BY_RECEIVER_ADDRESS
|
|
102
|
-
hermes_parcel_shop_customer_firstname = lib.OptionEnum("psCustomerFirstName", str)
|
|
103
|
-
hermes_parcel_shop_customer_lastname = lib.OptionEnum("psCustomerLastName", str)
|
|
100
|
+
hermes_parcel_shop_id = lib.OptionEnum("psID", str, meta=dict(category="PUDO"))
|
|
101
|
+
hermes_parcel_shop_selection_rule = lib.OptionEnum("psSelectionRule", str, meta=dict(category="PUDO")) # SELECT_BY_ID, SELECT_BY_RECEIVER_ADDRESS
|
|
102
|
+
hermes_parcel_shop_customer_firstname = lib.OptionEnum("psCustomerFirstName", str, meta=dict(category="PUDO"))
|
|
103
|
+
hermes_parcel_shop_customer_lastname = lib.OptionEnum("psCustomerLastName", str, meta=dict(category="PUDO"))
|
|
104
104
|
|
|
105
105
|
# Multipart service
|
|
106
106
|
hermes_part_number = lib.OptionEnum("partNumber", int)
|
|
@@ -128,15 +128,247 @@ def shipping_options_initializer(
|
|
|
128
128
|
|
|
129
129
|
|
|
130
130
|
class TrackingStatus(lib.Enum):
|
|
131
|
-
"""Hermes tracking status mapping.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
131
|
+
"""Hermes tracking status mapping.
|
|
132
|
+
|
|
133
|
+
Maps Hermes 2x2 event codes (4-digit) to Karrio unified statuses.
|
|
134
|
+
Based on Hermes Germany Eventcodes.csv.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
pending = [
|
|
138
|
+
"0000", # Die Sendung wurde Hermes elektronisch angekündigt
|
|
139
|
+
"0600", # Shipment has not arrived at the depot (1st notice)
|
|
140
|
+
"0700", # Shipment has not arrived at the depot (2nd notice)
|
|
141
|
+
"0800", # Shipment has not arrived at the depot (7 days)
|
|
142
|
+
"0900", # Shipment has not arrived at the depot (28 days)
|
|
143
|
+
]
|
|
144
|
+
picked_up = [
|
|
145
|
+
"1000", # Die Sendung hat das Lager des Auftraggebers verlassen
|
|
146
|
+
"1900", # Shipment accepted after collection
|
|
147
|
+
"1901", # Shipment arrived at branch by self-delivery
|
|
148
|
+
"1910", # Shipment received on tour
|
|
149
|
+
]
|
|
150
|
+
in_transit = [
|
|
151
|
+
"1510", # Sendung am LC umgeschlagen (sorted at logistics center)
|
|
152
|
+
"1610", # Shipment in international transit
|
|
153
|
+
"1710", # Customs export created
|
|
154
|
+
"1720", # Customs clearance completed
|
|
155
|
+
"1810", # Handed to partner carrier (international)
|
|
156
|
+
"1820", # Handed to partner carrier (international)
|
|
157
|
+
"2000", # Die Sendung ist eingetroffen (arrived at depot)
|
|
158
|
+
"2100", # Arrived without advice data
|
|
159
|
+
"2300", # Automatic shipment received
|
|
160
|
+
"2400", # Shipment handed in at ParcelShop
|
|
161
|
+
]
|
|
162
|
+
out_for_delivery = [
|
|
163
|
+
"3000", # Die Sendung ist auf Zustelltour gegangen
|
|
164
|
+
"3010", # Shipment has left depot on tour
|
|
165
|
+
"3300", # Sorted for delivery tour (automatic)
|
|
166
|
+
]
|
|
167
|
+
ready_for_pickup = [
|
|
168
|
+
"3410", # Die Sendung liegt im PaketShop zur Abholung bereit
|
|
169
|
+
"3430", # Handed over to island carrier
|
|
170
|
+
]
|
|
171
|
+
delivered = [
|
|
172
|
+
"3500", # Die Sendung wurde zugestellt
|
|
173
|
+
"3510", # Shipment delivered (with scanner)
|
|
174
|
+
"3511", # Delivered in letterbox
|
|
175
|
+
"3520", # Shipment delivered (without scanner)
|
|
176
|
+
"3530", # Collected by recipient from ParcelShop
|
|
177
|
+
"7500", # Return shipment arrived at client
|
|
178
|
+
]
|
|
179
|
+
delivery_failed = [
|
|
180
|
+
"3710", # Annahmeverweigerung (refused)
|
|
181
|
+
"3715", # COD not paid
|
|
182
|
+
"3720", # Address not found
|
|
183
|
+
"3731", # Recipient not present (1st attempt)
|
|
184
|
+
"3732", # Recipient not present (2nd attempt)
|
|
185
|
+
"3733", # Recipient not present (3rd attempt)
|
|
186
|
+
"3734", # Recipient not present (4th attempt)
|
|
187
|
+
"3740", # Damage detected
|
|
188
|
+
"3750", # Tour cancellation
|
|
189
|
+
"3751", # Incorrect TAN (1st attempt)
|
|
190
|
+
"3752", # Incorrect TAN (2nd attempt)
|
|
191
|
+
"3753", # Incorrect TAN (3rd attempt)
|
|
192
|
+
"3754", # Incorrect TAN (4th attempt)
|
|
193
|
+
"3760", # Return shipment collected
|
|
194
|
+
"3761", # Return shipment taken
|
|
195
|
+
"3780", # Misdirected
|
|
196
|
+
"3782", # Ident failed - photo mismatch
|
|
197
|
+
"3783", # Ident failed - name mismatch
|
|
198
|
+
"3784", # Ident failed - DOB mismatch
|
|
199
|
+
"3785", # Ident failed - document mismatch
|
|
200
|
+
"3786", # Ident failed - PIN code
|
|
201
|
+
"3787", # Ident failed - age verification
|
|
202
|
+
"3795", # Shipment stopped
|
|
203
|
+
]
|
|
204
|
+
on_hold = [
|
|
205
|
+
"1730", # Held by customs
|
|
206
|
+
"1751", # Rejected by customs
|
|
207
|
+
"4100", # Sendung wird aufbewahrt (shipment stored)
|
|
208
|
+
"4500", # Stored (stocktaking)
|
|
209
|
+
"4610", # ParcelShop - high volume, cannot pick up
|
|
210
|
+
"4620", # ParcelShop - shipment not available
|
|
211
|
+
"4630", # ParcelShop - shipment not found
|
|
212
|
+
"4690", # Status corrected at ParcelShop
|
|
213
|
+
]
|
|
214
|
+
delivery_delayed = [
|
|
215
|
+
"4010", # Return - refused
|
|
216
|
+
"4015", # Return - COD not paid
|
|
217
|
+
"4020", # Return - address not found
|
|
218
|
+
"4024", # Return - too large/heavy for ParcelShop
|
|
219
|
+
"4025", # Shipment returned to depot
|
|
220
|
+
"4031", # Return - N1 (1st attempt failed)
|
|
221
|
+
"4032", # Return - N2 (2nd attempt failed)
|
|
222
|
+
"4033", # Return - N3 (3rd attempt failed)
|
|
223
|
+
"4034", # Return - N4 (4th attempt failed)
|
|
224
|
+
"4035", # Return - not collected from ParcelShop
|
|
225
|
+
"4040", # Return - damage
|
|
226
|
+
"4050", # Return - tour cancellation
|
|
227
|
+
"4051", # Return - TAN 1
|
|
228
|
+
"4052", # Return - TAN 2
|
|
229
|
+
"4053", # Return - TAN 3
|
|
230
|
+
"4054", # Return - TAN 4
|
|
231
|
+
"4060", # Return shipment received (pickup)
|
|
232
|
+
"4061", # Return shipment received (take-away)
|
|
233
|
+
"4062", # Return handed in at ParcelShop
|
|
234
|
+
"4070", # Tour departure cancelled
|
|
235
|
+
"4072", # Return cancelled (correction)
|
|
236
|
+
"4080", # Misdirected
|
|
237
|
+
"4081", # Ident failed
|
|
238
|
+
"4082", # Ident failed - photo
|
|
239
|
+
"4083", # Ident failed - name
|
|
240
|
+
"4084", # Ident failed - DOB
|
|
241
|
+
"4085", # Ident failed - document
|
|
242
|
+
"4086", # Ident failed - PIN
|
|
243
|
+
"4087", # Ident failed - age
|
|
244
|
+
"4095", # Delivery stopped
|
|
245
|
+
]
|
|
246
|
+
return_to_sender = [
|
|
247
|
+
"1520", # Return shipment sorted
|
|
248
|
+
"6080", # Rückversand (return shipment)
|
|
249
|
+
"6081", # Return - refused
|
|
250
|
+
"6082", # Return - address not readable
|
|
251
|
+
"6083", # Return - address not found
|
|
252
|
+
"6084", # Return - receiver not met
|
|
253
|
+
"6085", # Return - damage
|
|
254
|
+
"6086", # Return - sorting error
|
|
255
|
+
"6087", # Return - technical issue
|
|
256
|
+
"6088", # Redirected at receiver request
|
|
257
|
+
"6089", # Return - returns
|
|
258
|
+
"6090", # Forwarded to logistics center
|
|
259
|
+
"6092", # Return - ident failed
|
|
260
|
+
"6093", # Return - not collected from ParcelShop
|
|
261
|
+
"6094", # Return - COD not paid
|
|
262
|
+
"6096", # Return - too large/heavy for ParcelShop
|
|
263
|
+
"6098", # Return - incorrect TAN
|
|
264
|
+
"6099", # Return - delivery stopped
|
|
265
|
+
]
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class TrackingIncidentReason(lib.Enum):
|
|
269
|
+
"""Maps Hermes exception codes to normalized incident reasons.
|
|
270
|
+
|
|
271
|
+
Based on Hermes Germany Eventcodes.csv.
|
|
272
|
+
Maps carrier-specific exception/status codes to standardized
|
|
273
|
+
incident reasons for tracking events.
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
# Consignee-caused issues
|
|
277
|
+
consignee_refused = [
|
|
278
|
+
"3710", # Annahmeverweigerung
|
|
279
|
+
"4010", # Return - refused
|
|
280
|
+
"6081", # Return - refused
|
|
281
|
+
]
|
|
282
|
+
consignee_not_home = [
|
|
283
|
+
"3731", # Recipient not present (1st attempt)
|
|
284
|
+
"3732", # Recipient not present (2nd attempt)
|
|
285
|
+
"3733", # Recipient not present (3rd attempt)
|
|
286
|
+
"3734", # Recipient not present (4th attempt)
|
|
287
|
+
"4031", # Return - N1
|
|
288
|
+
"4032", # Return - N2
|
|
289
|
+
"4033", # Return - N3
|
|
290
|
+
"4034", # Return - N4
|
|
291
|
+
"6084", # Return - receiver not met
|
|
292
|
+
]
|
|
293
|
+
consignee_incorrect_address = [
|
|
294
|
+
"3720", # Address not found
|
|
295
|
+
"4020", # Return - address not found
|
|
296
|
+
"6082", # Return - address not readable
|
|
297
|
+
"6083", # Return - address not found
|
|
298
|
+
]
|
|
299
|
+
consignee_not_available = [
|
|
300
|
+
"4035", # Not collected from ParcelShop
|
|
301
|
+
"6093", # Return - not collected from ParcelShop
|
|
302
|
+
]
|
|
303
|
+
consignee_cod_unpaid = [
|
|
304
|
+
"3715", # COD not paid
|
|
305
|
+
"4015", # Return - COD not paid
|
|
306
|
+
"6094", # Return - COD not paid
|
|
307
|
+
]
|
|
308
|
+
consignee_id_failed = [
|
|
309
|
+
"3782", # Ident failed - photo mismatch
|
|
310
|
+
"3783", # Ident failed - name mismatch
|
|
311
|
+
"3784", # Ident failed - DOB mismatch
|
|
312
|
+
"3785", # Ident failed - document mismatch
|
|
313
|
+
"3786", # Ident failed - PIN code
|
|
314
|
+
"3787", # Ident failed - age verification
|
|
315
|
+
"4081", # Ident failed
|
|
316
|
+
"4082", # Ident failed - photo
|
|
317
|
+
"4083", # Ident failed - name
|
|
318
|
+
"4084", # Ident failed - DOB
|
|
319
|
+
"4085", # Ident failed - document
|
|
320
|
+
"4086", # Ident failed - PIN
|
|
321
|
+
"4087", # Ident failed - age
|
|
322
|
+
"6092", # Return - ident failed
|
|
323
|
+
]
|
|
324
|
+
consignee_tan_invalid = [
|
|
325
|
+
"3751", # Incorrect TAN (1st attempt)
|
|
326
|
+
"3752", # Incorrect TAN (2nd attempt)
|
|
327
|
+
"3753", # Incorrect TAN (3rd attempt)
|
|
328
|
+
"3754", # Incorrect TAN (4th attempt)
|
|
329
|
+
"4051", # Return - TAN 1
|
|
330
|
+
"4052", # Return - TAN 2
|
|
331
|
+
"4053", # Return - TAN 3
|
|
332
|
+
"4054", # Return - TAN 4
|
|
333
|
+
"6098", # Return - incorrect TAN
|
|
334
|
+
]
|
|
335
|
+
|
|
336
|
+
# Carrier-caused issues
|
|
337
|
+
carrier_damaged_parcel = [
|
|
338
|
+
"3740", # Damage detected
|
|
339
|
+
"4040", # Return - damage
|
|
340
|
+
"6085", # Return - damage
|
|
341
|
+
]
|
|
342
|
+
carrier_sorting_error = [
|
|
343
|
+
"3780", # Misdirected
|
|
344
|
+
"4080", # Misdirected
|
|
345
|
+
"6086", # Return - sorting error
|
|
346
|
+
"6087", # Return - technical issue
|
|
347
|
+
]
|
|
348
|
+
carrier_not_enough_time = [
|
|
349
|
+
"3750", # Tour cancellation
|
|
350
|
+
"4050", # Return - tour cancellation
|
|
351
|
+
"4070", # Tour departure cancelled
|
|
352
|
+
]
|
|
353
|
+
carrier_parcel_too_large = [
|
|
354
|
+
"4024", # Return - too large/heavy for ParcelShop
|
|
355
|
+
"6096", # Return - too large/heavy for ParcelShop
|
|
356
|
+
]
|
|
357
|
+
|
|
358
|
+
# Customs-related issues
|
|
359
|
+
customs_delay = [
|
|
360
|
+
"1730", # Held by customs
|
|
361
|
+
]
|
|
362
|
+
customs_rejected = [
|
|
363
|
+
"1751", # Rejected by customs
|
|
364
|
+
]
|
|
365
|
+
|
|
366
|
+
# Shipment stopped
|
|
367
|
+
shipment_stopped = [
|
|
368
|
+
"3795", # Shipment stopped
|
|
369
|
+
"4095", # Delivery stopped
|
|
370
|
+
"6099", # Return - delivery stopped
|
|
371
|
+
]
|
|
140
372
|
|
|
141
373
|
|
|
142
374
|
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,64 @@
|
|
|
1
|
+
import attr
|
|
2
|
+
import jstruct
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@attr.s(auto_attribs=True)
|
|
7
|
+
class TimeSlotType:
|
|
8
|
+
timeSlotfrom: typing.Optional[str] = None
|
|
9
|
+
to: typing.Optional[str] = None
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@attr.s(auto_attribs=True)
|
|
13
|
+
class DeliveryForecastType:
|
|
14
|
+
fixed: typing.Optional[bool] = None
|
|
15
|
+
date: typing.Optional[str] = None
|
|
16
|
+
timeSlot: typing.Optional[TimeSlotType] = jstruct.JStruct[TimeSlotType]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@attr.s(auto_attribs=True)
|
|
20
|
+
class ReceiverAddressType:
|
|
21
|
+
city: typing.Optional[str] = None
|
|
22
|
+
zipCode: typing.Optional[int] = None
|
|
23
|
+
countryCode: typing.Optional[str] = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@attr.s(auto_attribs=True)
|
|
27
|
+
class ResultType:
|
|
28
|
+
code: typing.Optional[str] = None
|
|
29
|
+
message: typing.Optional[str] = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@attr.s(auto_attribs=True)
|
|
33
|
+
class ScanningUnitType:
|
|
34
|
+
name: typing.Optional[str] = None
|
|
35
|
+
city: typing.Optional[str] = None
|
|
36
|
+
countryCode: typing.Optional[str] = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@attr.s(auto_attribs=True)
|
|
40
|
+
class StatusType:
|
|
41
|
+
timestamp: typing.Optional[str] = None
|
|
42
|
+
code: typing.Optional[str] = None
|
|
43
|
+
description: typing.Optional[str] = None
|
|
44
|
+
scanningUnit: typing.Optional[ScanningUnitType] = jstruct.JStruct[ScanningUnitType]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@attr.s(auto_attribs=True)
|
|
48
|
+
class ShipmentinfoType:
|
|
49
|
+
shipmentID: typing.Optional[str] = None
|
|
50
|
+
partNumber: typing.Optional[int] = None
|
|
51
|
+
clientID: typing.Optional[int] = None
|
|
52
|
+
clientReference: typing.Optional[str] = None
|
|
53
|
+
clientReference2: typing.Optional[int] = None
|
|
54
|
+
internationalShipmentID: typing.Optional[str] = None
|
|
55
|
+
trackingLink: typing.Optional[str] = None
|
|
56
|
+
result: typing.Optional[ResultType] = jstruct.JStruct[ResultType]
|
|
57
|
+
receiverAddress: typing.Optional[ReceiverAddressType] = jstruct.JStruct[ReceiverAddressType]
|
|
58
|
+
deliveryForecast: typing.Optional[DeliveryForecastType] = jstruct.JStruct[DeliveryForecastType]
|
|
59
|
+
status: typing.Optional[typing.List[StatusType]] = jstruct.JList[StatusType]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@attr.s(auto_attribs=True)
|
|
63
|
+
class TrackingResponseType:
|
|
64
|
+
shipmentinfo: typing.Optional[typing.List[ShipmentinfoType]] = jstruct.JList[ShipmentinfoType]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
karrio/mappers/hermes/__init__.py,sha256=3WcvIpOkQAy1p54BhLGfmi1hyY9Dhe0qSG2yZY9mPes,145
|
|
2
|
+
karrio/mappers/hermes/mapper.py,sha256=4xOrR9eFX-MKv6LKB32WD8hZuhnscB_OTgeKVVrGo6Q,2700
|
|
3
|
+
karrio/mappers/hermes/proxy.py,sha256=zGbLwJTwunrWp8b1Lg_hs3AwxPSd8Ogy6sydl99YaSs,4492
|
|
4
|
+
karrio/mappers/hermes/settings.py,sha256=4BTM7ncSQqTBP6J_oJTksBDGhaFHOWoHDnpq0iOWHVo,1151
|
|
5
|
+
karrio/plugins/hermes/__init__.py,sha256=zUfOhtzuErd1H1ICL4rQt68e6BWRQf_B6_kW5UVzd9M,1020
|
|
6
|
+
karrio/providers/hermes/__init__.py,sha256=wMiVnPz7EgKdbu1pGBuvalfyuHj5QwSeMijjvEP6q4w,590
|
|
7
|
+
karrio/providers/hermes/error.py,sha256=YWmYC_l7_nyJCxiqM0Kgf6yV9IpJ04tkdZyyMT7QnkY,2974
|
|
8
|
+
karrio/providers/hermes/tracking.py,sha256=F_NYX_G7hnQe04zPbeXfc_IuN69XLOjYC9v9ezHB0Kg,5958
|
|
9
|
+
karrio/providers/hermes/units.py,sha256=voG1oHUfF2Fjzo0w7FOVqrNGmGLTCWDai1zlMng-9mQ,18148
|
|
10
|
+
karrio/providers/hermes/utils.py,sha256=NOfS_K0Zw7_wxJGBJfkdXwZP6hs0bsaZye5uypztGc4,4163
|
|
11
|
+
karrio/providers/hermes/pickup/__init__.py,sha256=E59ks-qJAsNmfQgaZ7X0tZuBetITsdRSMJUAZnqxabg,307
|
|
12
|
+
karrio/providers/hermes/pickup/cancel.py,sha256=JYVGL9decPB0QOHsjrYU5cwT_4vzrpsaEjapC9F3t7A,1587
|
|
13
|
+
karrio/providers/hermes/pickup/create.py,sha256=ICSG7HgF45h8XY0Ypc8voA5TWFSVmysGerh1F-fha24,3993
|
|
14
|
+
karrio/providers/hermes/shipment/__init__.py,sha256=B_zc5eaS4st1bnIyB_RwYY9XJJ9DyjB1Tb0SaIEJRJ8,209
|
|
15
|
+
karrio/providers/hermes/shipment/create.py,sha256=DAkC5O9XHs6QOlLKBTQ8zKQlLNf6h5Ty29_qFhMSBWM,16677
|
|
16
|
+
karrio/schemas/hermes/__init__.py,sha256=zcJLsZ0HvWk9q7J07BAel3ib_eXFoN6_5joxbdzBLV0,1530
|
|
17
|
+
karrio/schemas/hermes/error_response.py,sha256=BJhI7P8wursdYsYACfDLT8cs-bsnKlJDs0OzurYMPR4,343
|
|
18
|
+
karrio/schemas/hermes/pickup_cancel_request.py,sha256=rZgm3BpL3iziO_TC_y-cAELMsWjXyvRYWmXYyExhdR0,148
|
|
19
|
+
karrio/schemas/hermes/pickup_cancel_response.py,sha256=9-STL_Aq0EvXWrNMfvc4DiPK658e1JPp0uhW695XOGI,397
|
|
20
|
+
karrio/schemas/hermes/pickup_create_request.py,sha256=0V14LMl0XVLOZ_zXRzoTLUZ8nB_TZHCmr2G03NPl4tw,1375
|
|
21
|
+
karrio/schemas/hermes/pickup_create_response.py,sha256=sAgq92J0TZOUolhMcvDkp6qSRvxOqWCIZHNQWQN7KeU,397
|
|
22
|
+
karrio/schemas/hermes/shipment_request.py,sha256=FMqrSlCDG4eGDkuqPDc_DDCbtV_vGRyrRBFCiNTFkmM,7415
|
|
23
|
+
karrio/schemas/hermes/shipment_response.py,sha256=BI2kUemG-wTzAFpok9KEnlwjm6eK_LemBA_Cyga_e6A,3477
|
|
24
|
+
karrio/schemas/hermes/tracking_response.py,sha256=Ye4Uo6UEnEy-A5VX4MDmyyePXZMM_meMiecJlht9qMc,2013
|
|
25
|
+
karrio_hermes-2026.1.3.dist-info/METADATA,sha256=P2iafkpDRgz5fQhi9oQvv_GWYDj2N23CwqdgPnnqvhc,982
|
|
26
|
+
karrio_hermes-2026.1.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
27
|
+
karrio_hermes-2026.1.3.dist-info/entry_points.txt,sha256=R8XDrYBqWXTPqaO0iTi4Buz8iQiiBiuHkJVNtZIXTkU,57
|
|
28
|
+
karrio_hermes-2026.1.3.dist-info/top_level.txt,sha256=9Nasa6abG7pPPG8MGzlemnqw1ohIqgouzQ7HGBnOFLg,27
|
|
29
|
+
karrio_hermes-2026.1.3.dist-info/RECORD,,
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
karrio/mappers/hermes/__init__.py,sha256=3WcvIpOkQAy1p54BhLGfmi1hyY9Dhe0qSG2yZY9mPes,145
|
|
2
|
-
karrio/mappers/hermes/mapper.py,sha256=0fZbXPHGg5R4bbhRyJbzfEGNiKXSINW1FJQnhz-4MRM,2258
|
|
3
|
-
karrio/mappers/hermes/proxy.py,sha256=FG_OcaMEXiZxZp37m25pgrOtcdCziVk3Kft9HjoEnT8,2858
|
|
4
|
-
karrio/mappers/hermes/settings.py,sha256=4BTM7ncSQqTBP6J_oJTksBDGhaFHOWoHDnpq0iOWHVo,1151
|
|
5
|
-
karrio/plugins/hermes/__init__.py,sha256=zUfOhtzuErd1H1ICL4rQt68e6BWRQf_B6_kW5UVzd9M,1020
|
|
6
|
-
karrio/providers/hermes/__init__.py,sha256=lwP0P714ipPISsbd_6xXZ4oaCK32yTEHktP8yxmF4NQ,490
|
|
7
|
-
karrio/providers/hermes/error.py,sha256=YWmYC_l7_nyJCxiqM0Kgf6yV9IpJ04tkdZyyMT7QnkY,2974
|
|
8
|
-
karrio/providers/hermes/units.py,sha256=MO1ap8ItQUlwfTTx7_CqxJqx96KY-FyLm4pMSIXRg8g,8665
|
|
9
|
-
karrio/providers/hermes/utils.py,sha256=84N70gICmOmP2vjYmnzJbjzLhFaEHvXLKgHALZiE-2k,3077
|
|
10
|
-
karrio/providers/hermes/pickup/__init__.py,sha256=E59ks-qJAsNmfQgaZ7X0tZuBetITsdRSMJUAZnqxabg,307
|
|
11
|
-
karrio/providers/hermes/pickup/cancel.py,sha256=JYVGL9decPB0QOHsjrYU5cwT_4vzrpsaEjapC9F3t7A,1587
|
|
12
|
-
karrio/providers/hermes/pickup/create.py,sha256=q4AbBqI-_oXglD2DffJpqBp93zZo2hz-gf32DtX5lg8,3556
|
|
13
|
-
karrio/providers/hermes/shipment/__init__.py,sha256=B_zc5eaS4st1bnIyB_RwYY9XJJ9DyjB1Tb0SaIEJRJ8,209
|
|
14
|
-
karrio/providers/hermes/shipment/create.py,sha256=UcKTavylyrdbeFmzXY7pqCUI1lfOxmMO2Cr07xoB1YY,13046
|
|
15
|
-
karrio/schemas/hermes/__init__.py,sha256=yw5kUm6WMQ_UJEVW-LQUY-916m7Y-qxp0yLbqIMb2b4,1272
|
|
16
|
-
karrio/schemas/hermes/error_response.py,sha256=BJhI7P8wursdYsYACfDLT8cs-bsnKlJDs0OzurYMPR4,343
|
|
17
|
-
karrio/schemas/hermes/pickup_cancel_request.py,sha256=rZgm3BpL3iziO_TC_y-cAELMsWjXyvRYWmXYyExhdR0,148
|
|
18
|
-
karrio/schemas/hermes/pickup_cancel_response.py,sha256=9-STL_Aq0EvXWrNMfvc4DiPK658e1JPp0uhW695XOGI,397
|
|
19
|
-
karrio/schemas/hermes/pickup_create_request.py,sha256=0V14LMl0XVLOZ_zXRzoTLUZ8nB_TZHCmr2G03NPl4tw,1375
|
|
20
|
-
karrio/schemas/hermes/pickup_create_response.py,sha256=sAgq92J0TZOUolhMcvDkp6qSRvxOqWCIZHNQWQN7KeU,397
|
|
21
|
-
karrio/schemas/hermes/shipment_request.py,sha256=FMqrSlCDG4eGDkuqPDc_DDCbtV_vGRyrRBFCiNTFkmM,7415
|
|
22
|
-
karrio/schemas/hermes/shipment_response.py,sha256=BI2kUemG-wTzAFpok9KEnlwjm6eK_LemBA_Cyga_e6A,3477
|
|
23
|
-
karrio_hermes-2026.1.dist-info/METADATA,sha256=yF6_59XBxGBqORO5gj9Gjp5TJTdk4GOUJ_TxlGzA4eE,980
|
|
24
|
-
karrio_hermes-2026.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
25
|
-
karrio_hermes-2026.1.dist-info/entry_points.txt,sha256=R8XDrYBqWXTPqaO0iTi4Buz8iQiiBiuHkJVNtZIXTkU,57
|
|
26
|
-
karrio_hermes-2026.1.dist-info/top_level.txt,sha256=9Nasa6abG7pPPG8MGzlemnqw1ohIqgouzQ7HGBnOFLg,27
|
|
27
|
-
karrio_hermes-2026.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|