karrio-parcelone 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,250 @@
1
+ """Karrio ParcelOne shipment creation implementation."""
2
+
3
+ import typing
4
+ import karrio.schemas.parcelone as parcelone
5
+ import karrio.lib as lib
6
+ import karrio.core.models as models
7
+ import karrio.providers.parcelone.error as error
8
+ import karrio.providers.parcelone.utils as provider_utils
9
+ import karrio.providers.parcelone.units as provider_units
10
+
11
+
12
+ def parse_shipment_response(
13
+ _response: lib.Deserializable[dict],
14
+ settings: provider_utils.Settings,
15
+ ) -> typing.Tuple[typing.Optional[models.ShipmentDetails], typing.List[models.Message]]:
16
+ """Parse shipment creation response from ParcelOne REST API."""
17
+ response = _response.deserialize()
18
+ messages = error.parse_error_response(response, settings)
19
+ shipment = lib.identity(
20
+ _extract_details(response, settings, ctx=_response.ctx)
21
+ if response.get("success") == 1 and response.get("results")
22
+ else None
23
+ )
24
+
25
+ return shipment, messages
26
+
27
+
28
+ def _extract_details(
29
+ data: dict,
30
+ settings: provider_utils.Settings,
31
+ ctx: dict = None,
32
+ ) -> typing.Optional[models.ShipmentDetails]:
33
+ """Extract shipment details from API response."""
34
+ result = lib.to_object(parcelone.ShipmentResultType, data.get("results") or {})
35
+
36
+ # Check action result for success
37
+ action = result.ActionResult
38
+ if action is None or action.Success != 1:
39
+ return None
40
+
41
+ # Get tracking IDs from package results
42
+ packages = result.PackageResults or []
43
+ tracking_ids = [p.TrackingID for p in packages if p.TrackingID]
44
+ tracking_urls = [p.TrackingURL for p in packages if p.TrackingURL]
45
+
46
+ # Get labels from package results - use list comprehension
47
+ labels = [p.Label for p in packages if p.Label]
48
+
49
+ # Bundle labels if multiple, otherwise use single label
50
+ label_format = ctx.get("label_format") if ctx else "PDF"
51
+ label = lib.bundle_base64(labels, label_format) if len(labels) > 1 else lib.failsafe(lambda: labels[0])
52
+
53
+ # Calculate total charges if available
54
+ total_charge = lib.failsafe(lambda: float(result.TotalCharges.Value)) if result.TotalCharges else None
55
+
56
+ return models.ShipmentDetails(
57
+ carrier_name=settings.carrier_name,
58
+ carrier_id=settings.carrier_id,
59
+ tracking_number=action.TrackingID or lib.failsafe(lambda: tracking_ids[0]),
60
+ shipment_identifier=action.ShipmentID or action.TrackingID,
61
+ label_type=label_format,
62
+ docs=models.Documents(label=label),
63
+ meta=dict(
64
+ shipment_id=action.ShipmentID,
65
+ shipment_ref=action.ShipmentRef,
66
+ tracking_numbers=tracking_ids,
67
+ tracking_urls=tracking_urls,
68
+ label_url=result.LabelURL,
69
+ carrier_tracking_link=settings.tracking_link.format(
70
+ action.TrackingID or lib.failsafe(lambda: tracking_ids[0])
71
+ ),
72
+ total_charge=total_charge,
73
+ currency=lib.failsafe(lambda: result.TotalCharges.Currency) or "EUR",
74
+ ),
75
+ )
76
+
77
+
78
+ def shipment_request(
79
+ payload: models.ShipmentRequest,
80
+ settings: provider_utils.Settings,
81
+ ) -> lib.Serializable:
82
+ """Create ParcelOne shipment request."""
83
+ shipper = lib.to_address(payload.shipper)
84
+ recipient = lib.to_address(payload.recipient)
85
+ packages = lib.to_packages(payload.parcels, required=["weight"])
86
+ options = lib.to_shipping_options(
87
+ payload.options,
88
+ package_options=packages.options,
89
+ initializer=provider_units.shipping_options_initializer,
90
+ )
91
+ customs = payload.customs
92
+ is_international = shipper.country_code != recipient.country_code
93
+
94
+ # Parse service to get CEP and product
95
+ service = provider_units.ShippingService.map(payload.service)
96
+ service_code = service.value_or_key
97
+ cep_id, product_id = provider_units.parse_service_code(service_code)
98
+ cep_id = cep_id or settings.connection_config.cep_id.state
99
+ product_id = product_id or settings.connection_config.product_id.state
100
+
101
+ # Determine label format
102
+ label_format = provider_units.LabelFormat.map(
103
+ payload.label_type or settings.connection_config.label_format.state
104
+ ).value or "PDF"
105
+ label_size = settings.connection_config.label_size.state or "A6"
106
+
107
+ request = parcelone.ShippingDataRequestType(
108
+ ShippingData=parcelone.ShipmentType(
109
+ ShipmentRef=payload.reference,
110
+ CEPID=cep_id,
111
+ ProductID=product_id,
112
+ MandatorID=settings.mandator_id,
113
+ ConsignerID=settings.consigner_id,
114
+ ShipToData=parcelone.ShipToType(
115
+ Name1=recipient.company_name or recipient.person_name,
116
+ Name2=recipient.person_name if recipient.company_name else None,
117
+ ShipmentAddress=parcelone.AddressType(
118
+ Street=recipient.street,
119
+ Streetno=recipient.street_number,
120
+ PostalCode=recipient.postal_code,
121
+ City=recipient.city,
122
+ State=recipient.state_code,
123
+ Country=recipient.country_code,
124
+ ),
125
+ ShipmentContact=parcelone.ContactType(
126
+ Email=recipient.email,
127
+ Phone=recipient.phone_number,
128
+ ),
129
+ PrivateAddressIndicator=1 if recipient.residential else 0,
130
+ ),
131
+ ShipFromData=parcelone.ShipFromType(
132
+ Name1=shipper.company_name or shipper.person_name,
133
+ Name2=shipper.person_name if shipper.company_name else None,
134
+ ShipmentAddress=parcelone.AddressType(
135
+ Street=shipper.street,
136
+ Streetno=shipper.street_number,
137
+ PostalCode=shipper.postal_code,
138
+ City=shipper.city,
139
+ State=shipper.state_code,
140
+ Country=shipper.country_code,
141
+ ),
142
+ ShipmentContact=parcelone.ContactType(
143
+ Email=shipper.email,
144
+ Phone=shipper.phone_number,
145
+ ),
146
+ ),
147
+ ReturnShipmentIndicator=1 if options.is_return.state else 0,
148
+ PrintLabel=1,
149
+ LabelFormat=parcelone.FormatType(
150
+ Type=label_format,
151
+ Size=label_size,
152
+ ),
153
+ PrintDocuments=1 if is_international else 0,
154
+ Software="Karrio",
155
+ Packages=[
156
+ parcelone.ShipmentPackageType(
157
+ PackageRef=pkg.parcel.id or str(index),
158
+ PackageWeight=parcelone.MeasurementType(
159
+ Value=str(pkg.weight.KG),
160
+ Unit="kg",
161
+ ),
162
+ PackageDimensions=(
163
+ parcelone.DimensionsType(
164
+ Length=str(pkg.length.CM),
165
+ Width=str(pkg.width.CM),
166
+ Height=str(pkg.height.CM),
167
+ )
168
+ if pkg.length.CM and pkg.width.CM and pkg.height.CM
169
+ else None
170
+ ),
171
+ IntDocData=(
172
+ parcelone.IntDocDataType(
173
+ InvoiceNo=customs.invoice,
174
+ ItemCategory=1,
175
+ CustomDetails=[
176
+ parcelone.CustomDetailType(
177
+ Contents=item.description or item.title,
178
+ Quantity=item.quantity,
179
+ ItemValue=item.value_amount,
180
+ NetWeight=item.weight,
181
+ Origin=item.origin_country,
182
+ TariffNumber=item.hs_code,
183
+ )
184
+ for item in (customs.commodities or [])
185
+ ] if customs.commodities else None,
186
+ )
187
+ if is_international and customs
188
+ else None
189
+ ),
190
+ )
191
+ for index, pkg in enumerate(packages, 1)
192
+ ],
193
+ Services=[
194
+ *(
195
+ [
196
+ parcelone.ShipmentServiceType(
197
+ ServiceID="COD",
198
+ Value=parcelone.AmountType(
199
+ Value=str(options.cash_on_delivery.state),
200
+ Currency=options.parcelone_cod_currency.state or "EUR",
201
+ ),
202
+ )
203
+ ]
204
+ if options.cash_on_delivery.state
205
+ else []
206
+ ),
207
+ *(
208
+ [
209
+ parcelone.ShipmentServiceType(
210
+ ServiceID="INS",
211
+ Value=parcelone.AmountType(
212
+ Value=str(options.insurance.state),
213
+ Currency=options.parcelone_insurance_currency.state or "EUR",
214
+ ),
215
+ )
216
+ ]
217
+ if options.insurance.state
218
+ else []
219
+ ),
220
+ *([parcelone.ShipmentServiceType(ServiceID="SIG")] if options.signature_required.state else []),
221
+ *([parcelone.ShipmentServiceType(ServiceID="SDO")] if options.saturday_delivery.state else []),
222
+ *(
223
+ [
224
+ parcelone.ShipmentServiceType(
225
+ ServiceID="MAIL",
226
+ Parameters=options.parcelone_notification_email.state,
227
+ )
228
+ ]
229
+ if options.parcelone_notification_email.state
230
+ else []
231
+ ),
232
+ *(
233
+ [
234
+ parcelone.ShipmentServiceType(
235
+ ServiceID="SMS",
236
+ Parameters=options.parcelone_notification_sms.state,
237
+ )
238
+ ]
239
+ if options.parcelone_notification_sms.state
240
+ else []
241
+ ),
242
+ ],
243
+ ),
244
+ )
245
+
246
+ return lib.Serializable(
247
+ request,
248
+ lib.to_dict,
249
+ dict(label_format=label_format),
250
+ )
@@ -0,0 +1,141 @@
1
+ """Karrio ParcelOne tracking implementation."""
2
+
3
+ import typing
4
+ import karrio.schemas.parcelone as parcelone
5
+ import karrio.lib as lib
6
+ import karrio.core.models as models
7
+ import karrio.providers.parcelone.error as error
8
+ import karrio.providers.parcelone.utils as provider_utils
9
+ import karrio.providers.parcelone.units as provider_units
10
+
11
+
12
+ def parse_tracking_response(
13
+ _response: lib.Deserializable[typing.List[typing.Tuple[str, dict]]],
14
+ settings: provider_utils.Settings,
15
+ ) -> typing.Tuple[typing.List[models.TrackingDetails], typing.List[models.Message]]:
16
+ """Parse tracking response from ParcelOne TrackLMC REST API."""
17
+ responses = _response.deserialize()
18
+ messages: typing.List[models.Message] = []
19
+ tracking_details: typing.List[models.TrackingDetails] = []
20
+
21
+ for tracking_number, response in responses:
22
+ # Parse errors for this response
23
+ response_messages = error.parse_error_response(
24
+ response,
25
+ settings,
26
+ tracking_number=tracking_number,
27
+ )
28
+ messages.extend(response_messages)
29
+
30
+ # Extract tracking details if successful
31
+ if response.get("success") == 1 and response.get("results"):
32
+ details = _extract_tracking_details(
33
+ response.get("results"),
34
+ tracking_number,
35
+ settings,
36
+ )
37
+ if details:
38
+ tracking_details.append(details)
39
+
40
+ return tracking_details, messages
41
+
42
+
43
+ def _extract_tracking_details(
44
+ result: dict,
45
+ tracking_number: str,
46
+ settings: provider_utils.Settings,
47
+ ) -> typing.Optional[models.TrackingDetails]:
48
+ """Extract tracking details from API response."""
49
+ tracking_result = lib.to_object(parcelone.TrackingResultType, result)
50
+
51
+ # Parse events
52
+ events = sorted(
53
+ [
54
+ _parse_tracking_event(event)
55
+ for event in (tracking_result.Events or [])
56
+ ],
57
+ key=lambda e: e.timestamp or e.date or "",
58
+ reverse=True,
59
+ )
60
+
61
+ if not events:
62
+ return None
63
+
64
+ latest_event = events[0] if events else None
65
+ status = lib.identity(
66
+ provider_units.TrackingStatus.find(
67
+ tracking_result.StatusCode or (latest_event.code if latest_event else None)
68
+ )
69
+ )
70
+
71
+ return models.TrackingDetails(
72
+ carrier_name=settings.carrier_name,
73
+ carrier_id=settings.carrier_id,
74
+ tracking_number=tracking_number,
75
+ events=events,
76
+ delivered=(status.name == "delivered") if status else False,
77
+ status=status.name if status else None,
78
+ estimated_delivery=lib.fdate(tracking_result.EstimatedDelivery),
79
+ info=models.TrackingInfo(
80
+ carrier_tracking_link=settings.tracking_link.format(tracking_number),
81
+ signed_by=tracking_result.SignedBy,
82
+ ),
83
+ meta=dict(
84
+ carrier_tracking_id=tracking_result.CarrierTrackingID,
85
+ last_mile_carrier=tracking_result.CarrierIDLMC,
86
+ ),
87
+ )
88
+
89
+
90
+ def _parse_tracking_event(event: parcelone.TrackingEventType) -> models.TrackingEvent:
91
+ """Parse a single tracking event."""
92
+ datetime_str = event.DateTime or ""
93
+ date = lib.fdate(
94
+ datetime_str,
95
+ try_formats=["%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S", "%Y-%m-%d"],
96
+ )
97
+ time = lib.flocaltime(
98
+ datetime_str,
99
+ try_formats=["%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S"],
100
+ )
101
+ status = provider_units.TrackingStatus.find(event.StatusCode)
102
+
103
+ return models.TrackingEvent(
104
+ date=date,
105
+ time=time,
106
+ description=event.Description or event.Status,
107
+ code=event.StatusCode,
108
+ location=event.Location,
109
+ timestamp=lib.fiso_timestamp(
110
+ datetime_str,
111
+ current_format="%Y-%m-%dT%H:%M:%S",
112
+ ),
113
+ status=status.name if status else None,
114
+ )
115
+
116
+
117
+ def tracking_request(
118
+ payload: models.TrackingRequest,
119
+ settings: provider_utils.Settings,
120
+ ) -> lib.Serializable:
121
+ """Create ParcelOne tracking request.
122
+
123
+ The TrackLMC API uses: GET /tracking/{CarrierIDLMC}/{TrackingID}
124
+ For ParcelOne tracking numbers, CarrierIDLMC is typically not needed
125
+ as the tracking ID encodes the carrier information.
126
+ """
127
+ # For each tracking number, we'll need to make a separate API call
128
+ # The carrier_id can be passed as an option if known
129
+ requests = [
130
+ dict(
131
+ tracking_id=tracking_number,
132
+ carrier_id=lib.identity(
133
+ payload.options.get("carrier_id")
134
+ or payload.options.get(f"{tracking_number}_carrier_id")
135
+ or "PA1" # Default to ParcelOne
136
+ ),
137
+ )
138
+ for tracking_number in payload.tracking_numbers
139
+ ]
140
+
141
+ return lib.Serializable(requests, lib.to_dict)