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,326 @@
1
+ """ParcelOne units and enums."""
2
+
3
+ import csv
4
+ import pathlib
5
+ import typing
6
+ import karrio.lib as lib
7
+ import karrio.core.units as units
8
+ import karrio.core.models as models
9
+
10
+
11
+ class LabelFormat(lib.StrEnum):
12
+ """Supported label formats."""
13
+
14
+ PDF = "PDF"
15
+ ZPL = "ZPL"
16
+ PNG = "PNG"
17
+
18
+
19
+ class LabelSize(lib.StrEnum):
20
+ """Supported label sizes."""
21
+
22
+ A6 = "A6"
23
+ A4 = "A4"
24
+
25
+
26
+ class WeightUnit(lib.StrEnum):
27
+ """Supported weight units."""
28
+
29
+ kg = "kg"
30
+ g = "g"
31
+
32
+
33
+ class CEP(lib.StrEnum):
34
+ """Available carriers through ParcelOne (CEP = Courier, Express, Parcel)."""
35
+
36
+ PA1 = "PA1" # Parcel.One
37
+ DHL = "DHL"
38
+ UPS = "UPS"
39
+
40
+
41
+ class PackagingType(lib.StrEnum):
42
+ """Carrier specific packaging type."""
43
+
44
+ PACKAGE = "PACKAGE"
45
+ PALLET = "PALLET"
46
+
47
+ # Unified Packaging type mapping
48
+ envelope = PACKAGE
49
+ pak = PACKAGE
50
+ tube = PACKAGE
51
+ pallet = PALLET
52
+ small_box = PACKAGE
53
+ medium_box = PACKAGE
54
+ your_packaging = PACKAGE
55
+
56
+
57
+ class ConnectionConfig(lib.Enum):
58
+ """ParcelOne connection configuration options."""
59
+
60
+ cep_id = lib.OptionEnum("cep_id", str, "PA1") # Default carrier (PA1, DHL, UPS)
61
+ product_id = lib.OptionEnum("product_id", str, "eco") # Default product code
62
+ label_format = lib.OptionEnum("label_format", LabelFormat)
63
+ label_size = lib.OptionEnum("label_size", LabelSize)
64
+ shipping_services = lib.OptionEnum("shipping_services", list)
65
+ shipping_options = lib.OptionEnum("shipping_options", list)
66
+
67
+
68
+ class ShippingService(lib.StrEnum):
69
+ """ParcelOne shipping services.
70
+
71
+ Format: parcelone_{cep}_{product}
72
+ The service code maps to CEPID and ProductID internally.
73
+ """
74
+
75
+ # Parcel.One (PA1) services
76
+ parcelone_pa1_basic = "PA1_basic"
77
+ parcelone_pa1_eco = "PA1_eco"
78
+ parcelone_pa1_premium = "PA1_premium"
79
+ parcelone_pa1_express = "PA1_express"
80
+
81
+ # DHL services (via ParcelOne)
82
+ parcelone_dhl_paket = "DHL_PAKET"
83
+ parcelone_dhl_paket_international = "DHL_PAKETINT"
84
+ parcelone_dhl_express = "DHL_EXPRESS"
85
+ parcelone_dhl_retoure = "DHL_RETOURE"
86
+
87
+ # UPS services (via ParcelOne)
88
+ parcelone_ups_standard = "UPS_STANDARD"
89
+ parcelone_ups_express = "UPS_EXPRESS"
90
+ parcelone_ups_express_saver = "UPS_EXPSAVER"
91
+
92
+
93
+ def parse_service_code(service_code: str) -> typing.Tuple[str, str]:
94
+ """Parse a service code to extract CEP ID and Product ID.
95
+
96
+ Args:
97
+ service_code: Service code in format "CEPID_PRODUCTID"
98
+
99
+ Returns:
100
+ Tuple of (cep_id, product_id)
101
+ """
102
+ if "_" in service_code:
103
+ parts = service_code.split("_", 1)
104
+ return parts[0], parts[1] if len(parts) > 1 else ""
105
+ return service_code, ""
106
+
107
+
108
+ class ShippingOption(lib.Enum):
109
+ """Carrier specific shipping options.
110
+
111
+ ParcelOne ServiceID values that can be added to shipments or packages.
112
+ """
113
+
114
+ # Delivery options
115
+ parcelone_saturday_delivery = lib.OptionEnum("SDO", bool) # Saturday delivery only
116
+ parcelone_return_label = lib.OptionEnum("SRL", bool) # Return label
117
+
118
+ # Payment services
119
+ parcelone_cod = lib.OptionEnum("COD", float) # Cash on delivery
120
+ parcelone_cod_currency = lib.OptionEnum("COD_CURRENCY")
121
+ parcelone_insurance = lib.OptionEnum("INS", float) # Insurance
122
+ parcelone_insurance_currency = lib.OptionEnum("INS_CURRENCY")
123
+
124
+ # Notification services
125
+ parcelone_notification_email = lib.OptionEnum("MAIL") # Email notification
126
+ parcelone_notification_sms = lib.OptionEnum("SMS") # SMS notification
127
+
128
+ # Delivery confirmation
129
+ parcelone_signature = lib.OptionEnum("SIG", bool) # Signature required
130
+ parcelone_ident_check = lib.OptionEnum("IDENT", bool) # Identity check
131
+ parcelone_age_check = lib.OptionEnum("AGE", int) # Age verification (16, 18)
132
+ parcelone_personally = lib.OptionEnum("PERS", bool) # Personal delivery only
133
+
134
+ # Delivery location options
135
+ parcelone_neighbor_delivery = lib.OptionEnum("NEIGHBOR", bool)
136
+ parcelone_no_neighbor = lib.OptionEnum("NONEIGHBOR", bool)
137
+ parcelone_drop_off_point = lib.OptionEnum("DROP") # Parcel shop delivery (PUDO ID)
138
+
139
+ # Premium services
140
+ parcelone_premium = lib.OptionEnum("PREMIUM", bool)
141
+ parcelone_bulky_goods = lib.OptionEnum("BULKY", bool)
142
+
143
+ # Unified option mappings
144
+ cash_on_delivery = parcelone_cod
145
+ insurance = parcelone_insurance
146
+ signature_required = parcelone_signature
147
+ saturday_delivery = parcelone_saturday_delivery
148
+ email_notification = parcelone_notification_email
149
+
150
+
151
+ def shipping_options_initializer(
152
+ options: dict,
153
+ package_options: units.ShippingOptions = None,
154
+ ) -> units.ShippingOptions:
155
+ """Apply default values to the given options."""
156
+ if package_options is not None:
157
+ options.update(package_options.content)
158
+
159
+ def items_filter(key: str) -> bool:
160
+ return key in ShippingOption # type: ignore
161
+
162
+ return units.ShippingOptions(options, ShippingOption, items_filter=items_filter)
163
+
164
+
165
+ class TrackingStatus(lib.Enum):
166
+ """Carrier tracking status mapping.
167
+
168
+ Maps ParcelOne/last-mile-carrier tracking status codes to Karrio unified status.
169
+ """
170
+
171
+ pending = [
172
+ "CREATED",
173
+ "REGISTERED",
174
+ "DATA_RECEIVED",
175
+ "LABEL_PRINTED",
176
+ "0",
177
+ "1",
178
+ ]
179
+ delivered = [
180
+ "DELIVERED",
181
+ "POD",
182
+ "DELIVERED_NEIGHBOR",
183
+ "DELIVERED_SAFE_PLACE",
184
+ "DELIVERED_PARCELSHOP",
185
+ "90",
186
+ ]
187
+ in_transit = [
188
+ "IN_TRANSIT",
189
+ "DEPARTED",
190
+ "ARRIVED",
191
+ "PROCESSED",
192
+ "SORTING",
193
+ "IN_DELIVERY_VEHICLE",
194
+ "EXPORTED",
195
+ "IMPORTED",
196
+ "SHIPPED",
197
+ "10",
198
+ "20",
199
+ "30",
200
+ ]
201
+ out_for_delivery = [
202
+ "OUT_FOR_DELIVERY",
203
+ "ON_DELIVERY_VEHICLE",
204
+ "DELIVERY_IN_PROGRESS",
205
+ "80",
206
+ ]
207
+ on_hold = [
208
+ "HELD",
209
+ "CUSTOMS",
210
+ "CUSTOMS_CLEARANCE",
211
+ "PAYMENT_REQUIRED",
212
+ "AWAITING_PICKUP",
213
+ "40",
214
+ ]
215
+ delivery_failed = [
216
+ "FAILED",
217
+ "EXCEPTION",
218
+ "NOT_DELIVERED",
219
+ "REFUSED",
220
+ "ADDRESSEE_NOT_FOUND",
221
+ "WRONG_ADDRESS",
222
+ "99",
223
+ ]
224
+ delivery_delayed = [
225
+ "DELAYED",
226
+ "RESCHEDULED",
227
+ "REDIRECTED",
228
+ ]
229
+ ready_for_pickup = [
230
+ "READY_FOR_PICKUP",
231
+ "AT_PARCELSHOP",
232
+ "AVAILABLE_FOR_COLLECTION",
233
+ "70",
234
+ ]
235
+
236
+
237
+ def load_services_from_csv() -> list:
238
+ """
239
+ Load service definitions from CSV file.
240
+ CSV format: service_code,service_name,zone_label,country_codes,min_weight,max_weight,max_length,max_width,max_height,rate,currency,transit_days,domicile,international
241
+ """
242
+ csv_path = pathlib.Path(__file__).resolve().parent / "services.csv"
243
+
244
+ if not csv_path.exists():
245
+ # Fallback to simple default if CSV doesn't exist
246
+ return [
247
+ models.ServiceLevel(
248
+ service_name="Parcel.One Eco",
249
+ service_code="parcelone_pa1_eco",
250
+ currency="EUR",
251
+ domicile=True,
252
+ zones=[models.ServiceZone(rate=0.0)],
253
+ )
254
+ ]
255
+
256
+ # Group zones by service
257
+ services_dict: dict[str, dict] = {}
258
+
259
+ with open(csv_path, "r", encoding="utf-8") as f:
260
+ reader = csv.DictReader(f)
261
+ for row in reader:
262
+ service_code = row["service_code"]
263
+ service_name = row["service_name"]
264
+
265
+ # Map carrier service code to karrio service code
266
+ karrio_service_code = ShippingService.map(service_code).name_or_key
267
+
268
+ # Initialize service if not exists
269
+ if karrio_service_code not in services_dict:
270
+ services_dict[karrio_service_code] = {
271
+ "service_name": service_name,
272
+ "service_code": karrio_service_code,
273
+ "currency": row.get("currency", "EUR"),
274
+ "min_weight": (
275
+ float(row["min_weight"]) if row.get("min_weight") else None
276
+ ),
277
+ "max_weight": (
278
+ float(row["max_weight"]) if row.get("max_weight") else None
279
+ ),
280
+ "max_length": (
281
+ float(row["max_length"]) if row.get("max_length") else None
282
+ ),
283
+ "max_width": (
284
+ float(row["max_width"]) if row.get("max_width") else None
285
+ ),
286
+ "max_height": (
287
+ float(row["max_height"]) if row.get("max_height") else None
288
+ ),
289
+ "weight_unit": "KG",
290
+ "dimension_unit": "CM",
291
+ "domicile": row.get("domicile", "").lower() == "true",
292
+ "international": (
293
+ True if row.get("international", "").lower() == "true" else None
294
+ ),
295
+ "zones": [],
296
+ }
297
+
298
+ # Parse country codes
299
+ country_codes = [
300
+ c.strip() for c in row.get("country_codes", "").split(",") if c.strip()
301
+ ]
302
+
303
+ # Parse transit days (handle "1-3" format)
304
+ transit_days = None
305
+ if row.get("transit_days"):
306
+ transit_str = row["transit_days"].split("-")[0]
307
+ if transit_str.isdigit():
308
+ transit_days = int(transit_str)
309
+
310
+ # Create zone
311
+ zone = models.ServiceZone(
312
+ label=row.get("zone_label", "Default Zone"),
313
+ rate=float(row.get("rate", 0.0)),
314
+ transit_days=transit_days,
315
+ country_codes=country_codes if country_codes else None,
316
+ )
317
+
318
+ services_dict[karrio_service_code]["zones"].append(zone)
319
+
320
+ # Convert to ServiceLevel objects
321
+ return [
322
+ models.ServiceLevel(**service_data) for service_data in services_dict.values()
323
+ ]
324
+
325
+
326
+ DEFAULT_SERVICES = load_services_from_csv()
@@ -0,0 +1,63 @@
1
+ """ParcelOne REST API connection utilities and settings."""
2
+
3
+ import base64
4
+ import karrio.lib as lib
5
+ import karrio.core as core
6
+
7
+
8
+ class Settings(core.Settings):
9
+ """ParcelOne REST API connection settings."""
10
+
11
+ # Required credentials
12
+ username: str
13
+ password: str
14
+ mandator_id: str
15
+ consigner_id: str
16
+
17
+ # Generic properties
18
+ id: str = None
19
+ test_mode: bool = False
20
+ carrier_id: str = "parcelone"
21
+ account_country_code: str = "DE"
22
+ metadata: dict = {}
23
+ config: dict = {}
24
+
25
+ @property
26
+ def carrier_name(self):
27
+ return "parcelone"
28
+
29
+ @property
30
+ def server_url(self):
31
+ return (
32
+ "https://sandboxapi.parcel.one/v1"
33
+ if self.test_mode
34
+ else "https://api.parcel.one/v1"
35
+ )
36
+
37
+ @property
38
+ def tracking_url(self):
39
+ return (
40
+ "https://sandboxapi.parcel.one/v1/tracklmc"
41
+ if self.test_mode
42
+ else "https://api.parcel.one/v1/tracklmc"
43
+ )
44
+
45
+ @property
46
+ def tracking_link(self):
47
+ return "https://tracking.parcel.one/?trackingNumber={}"
48
+
49
+ @property
50
+ def authorization(self):
51
+ """HTTP Basic Auth header value."""
52
+ credentials = f"{self.username}:{self.password}"
53
+ encoded = base64.b64encode(credentials.encode()).decode()
54
+ return f"Basic {encoded}"
55
+
56
+ @property
57
+ def connection_config(self) -> lib.units.Options:
58
+ from karrio.providers.parcelone.units import ConnectionConfig
59
+
60
+ return lib.to_connection_config(
61
+ self.config or {},
62
+ option_type=ConnectionConfig,
63
+ )
@@ -0,0 +1,6 @@
1
+ """ParcelOne API schema types."""
2
+
3
+ from karrio.schemas.parcelone.shipping_request import *
4
+ from karrio.schemas.parcelone.shipping_response import *
5
+ from karrio.schemas.parcelone.tracking_response import *
6
+ from karrio.schemas.parcelone.error import *
@@ -0,0 +1,27 @@
1
+ """ParcelOne REST API v1 - Error Types."""
2
+
3
+ import attr
4
+ import jstruct
5
+ import typing
6
+
7
+
8
+ @attr.s(auto_attribs=True)
9
+ class ErrorDetailType:
10
+ """Error detail information."""
11
+
12
+ ErrorNo: typing.Optional[str] = None
13
+ Message: typing.Optional[str] = None
14
+ StatusCode: typing.Optional[str] = None
15
+
16
+
17
+ @attr.s(auto_attribs=True)
18
+ class ErrorResponseType:
19
+ """API error response."""
20
+
21
+ status: typing.Optional[int] = None
22
+ success: typing.Optional[int] = None
23
+ message: typing.Optional[str] = None
24
+ type: typing.Optional[str] = None
25
+ instance: typing.Optional[str] = None
26
+ errors: typing.Optional[typing.List[ErrorDetailType]] = jstruct.JList[ErrorDetailType]
27
+ UniqId: typing.Optional[str] = None
@@ -0,0 +1,202 @@
1
+ """ParcelOne Shipping REST API v1 - Request Types."""
2
+
3
+ import attr
4
+ import jstruct
5
+ import typing
6
+
7
+
8
+ @attr.s(auto_attribs=True)
9
+ class AddressType:
10
+ """Address data format."""
11
+
12
+ Street: typing.Optional[str] = None
13
+ Streetno: typing.Optional[str] = None
14
+ PostalCode: typing.Optional[str] = None
15
+ City: typing.Optional[str] = None
16
+ District: typing.Optional[str] = None
17
+ State: typing.Optional[str] = None
18
+ Country: typing.Optional[str] = None
19
+
20
+
21
+ @attr.s(auto_attribs=True)
22
+ class ContactType:
23
+ """Contact information."""
24
+
25
+ Email: typing.Optional[str] = None
26
+ Phone: typing.Optional[str] = None
27
+ Mobile: typing.Optional[str] = None
28
+ Fax: typing.Optional[str] = None
29
+ AttentionName: typing.Optional[str] = None
30
+
31
+
32
+ @attr.s(auto_attribs=True)
33
+ class ShipToType:
34
+ """Recipient/consignee address data."""
35
+
36
+ Name1: typing.Optional[str] = None
37
+ Name2: typing.Optional[str] = None
38
+ Name3: typing.Optional[str] = None
39
+ Reference: typing.Optional[str] = None
40
+ ShipmentAddress: typing.Optional[AddressType] = jstruct.JStruct[AddressType]
41
+ ShipmentContact: typing.Optional[ContactType] = jstruct.JStruct[ContactType]
42
+ PrivateAddressIndicator: typing.Optional[int] = None
43
+ SalesTaxID: typing.Optional[str] = None
44
+ CustomsID: typing.Optional[str] = None
45
+ BranchID: typing.Optional[str] = None
46
+ CEPCustID: typing.Optional[str] = None
47
+
48
+
49
+ @attr.s(auto_attribs=True)
50
+ class ShipFromType:
51
+ """Consigner/sender address data."""
52
+
53
+ Name1: typing.Optional[str] = None
54
+ Name2: typing.Optional[str] = None
55
+ Name3: typing.Optional[str] = None
56
+ Reference: typing.Optional[str] = None
57
+ ShipmentAddress: typing.Optional[AddressType] = jstruct.JStruct[AddressType]
58
+ ShipmentContact: typing.Optional[ContactType] = jstruct.JStruct[ContactType]
59
+ SalesTaxID: typing.Optional[str] = None
60
+ CustomsID: typing.Optional[str] = None
61
+
62
+
63
+ @attr.s(auto_attribs=True)
64
+ class FormatType:
65
+ """Document format specification."""
66
+
67
+ Type: typing.Optional[str] = None
68
+ Size: typing.Optional[str] = None
69
+ Unit: typing.Optional[str] = None
70
+ Orientation: typing.Optional[int] = None
71
+ Height: typing.Optional[str] = None
72
+ Width: typing.Optional[str] = None
73
+
74
+
75
+ @attr.s(auto_attribs=True)
76
+ class MeasurementType:
77
+ """Weight or volume measurement."""
78
+
79
+ Unit: typing.Optional[str] = None
80
+ Value: typing.Optional[str] = None
81
+
82
+
83
+ @attr.s(auto_attribs=True)
84
+ class DimensionsType:
85
+ """Package dimensions."""
86
+
87
+ Length: typing.Optional[str] = None
88
+ Width: typing.Optional[str] = None
89
+ Height: typing.Optional[str] = None
90
+
91
+
92
+ @attr.s(auto_attribs=True)
93
+ class AmountType:
94
+ """Monetary amount."""
95
+
96
+ Currency: typing.Optional[str] = None
97
+ Value: typing.Optional[str] = None
98
+ Description: typing.Optional[str] = None
99
+
100
+
101
+ @attr.s(auto_attribs=True)
102
+ class ShipmentServiceType:
103
+ """Service for shipment or package."""
104
+
105
+ ServiceID: typing.Optional[str] = None
106
+ Value: typing.Optional[AmountType] = jstruct.JStruct[AmountType]
107
+ Parameters: typing.Optional[str] = None
108
+
109
+
110
+ @attr.s(auto_attribs=True)
111
+ class CustomDetailType:
112
+ """Customs detail line item."""
113
+
114
+ Contents: typing.Optional[str] = None
115
+ ItemValue: typing.Optional[float] = None
116
+ ItemValuePerItem: typing.Optional[float] = None
117
+ NetWeight: typing.Optional[float] = None
118
+ NetWeightPerItem: typing.Optional[float] = None
119
+ Origin: typing.Optional[str] = None
120
+ Quantity: typing.Optional[int] = None
121
+ TariffNumber: typing.Optional[str] = None
122
+
123
+
124
+ @attr.s(auto_attribs=True)
125
+ class InternationalDocFormatType:
126
+ """International document format."""
127
+
128
+ Type: typing.Optional[str] = None
129
+ Size: typing.Optional[str] = None
130
+
131
+
132
+ @attr.s(auto_attribs=True)
133
+ class IntDocDataType:
134
+ """International/customs documentation data."""
135
+
136
+ ConsignerCustomsID: typing.Optional[str] = None
137
+ Invoice: typing.Optional[int] = None
138
+ InvoiceNo: typing.Optional[str] = None
139
+ PrintInternationalDocuments: typing.Optional[int] = None
140
+ InternationalDocumentFormat: typing.Optional[InternationalDocFormatType] = jstruct.JStruct[InternationalDocFormatType]
141
+ ShipToRef: typing.Optional[str] = None
142
+ TotalWeightkg: typing.Optional[float] = None
143
+ Postage: typing.Optional[float] = None
144
+ ItemCategory: typing.Optional[int] = None
145
+ CustomDetails: typing.Optional[typing.List[CustomDetailType]] = jstruct.JList[CustomDetailType]
146
+
147
+
148
+ @attr.s(auto_attribs=True)
149
+ class ShipmentPackageType:
150
+ """Package within a shipment."""
151
+
152
+ PackageRef: typing.Optional[str] = None
153
+ PackageType: typing.Optional[str] = None
154
+ PackageWeight: typing.Optional[MeasurementType] = jstruct.JStruct[MeasurementType]
155
+ PackageDimensions: typing.Optional[DimensionsType] = jstruct.JStruct[DimensionsType]
156
+ PackageVolume: typing.Optional[MeasurementType] = jstruct.JStruct[MeasurementType]
157
+ Services: typing.Optional[typing.List[ShipmentServiceType]] = jstruct.JList[ShipmentServiceType]
158
+ IntDocData: typing.Optional[IntDocDataType] = jstruct.JStruct[IntDocDataType]
159
+ Remarks: typing.Optional[str] = None
160
+
161
+
162
+ @attr.s(auto_attribs=True)
163
+ class CEPSpecialType:
164
+ """CEP-specific special options."""
165
+
166
+ Key: typing.Optional[str] = None
167
+ Value: typing.Optional[str] = None
168
+
169
+
170
+ @attr.s(auto_attribs=True)
171
+ class ShipmentType:
172
+ """Complete shipment registration request."""
173
+
174
+ ShipmentRef: typing.Optional[str] = None
175
+ CEPID: typing.Optional[str] = None
176
+ ProductID: typing.Optional[str] = None
177
+ MandatorID: typing.Optional[str] = None
178
+ ConsignerID: typing.Optional[str] = None
179
+ ShipToData: typing.Optional[ShipToType] = jstruct.JStruct[ShipToType]
180
+ ShipFromData: typing.Optional[ShipFromType] = jstruct.JStruct[ShipFromType]
181
+ ReturnShipmentIndicator: typing.Optional[int] = None
182
+ PrintLabel: typing.Optional[int] = None
183
+ LabelFormat: typing.Optional[FormatType] = jstruct.JStruct[FormatType]
184
+ PrintDocuments: typing.Optional[int] = None
185
+ DocumentFormat: typing.Optional[FormatType] = jstruct.JStruct[FormatType]
186
+ ReturnCharges: typing.Optional[int] = None
187
+ MaxCharges: typing.Optional[AmountType] = jstruct.JStruct[AmountType]
188
+ Software: typing.Optional[str] = None
189
+ Packages: typing.Optional[typing.List[ShipmentPackageType]] = jstruct.JList[ShipmentPackageType]
190
+ Services: typing.Optional[typing.List[ShipmentServiceType]] = jstruct.JList[ShipmentServiceType]
191
+ CEPSpecials: typing.Optional[typing.List[CEPSpecialType]] = jstruct.JList[CEPSpecialType]
192
+ CostCenter: typing.Optional[str] = None
193
+ Other1: typing.Optional[str] = None
194
+ Other2: typing.Optional[str] = None
195
+ Other3: typing.Optional[str] = None
196
+
197
+
198
+ @attr.s(auto_attribs=True)
199
+ class ShippingDataRequestType:
200
+ """Root shipping data request wrapper."""
201
+
202
+ ShippingData: typing.Optional[ShipmentType] = jstruct.JStruct[ShipmentType]