karrio-tnt 2025.5rc1__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,226 @@
1
+ import karrio.schemas.tnt.shipping_request as tnt
2
+ import karrio.schemas.tnt.shipping_response as shipping
3
+ import uuid
4
+ import typing
5
+ import karrio.lib as lib
6
+ import karrio.core.units as units
7
+ import karrio.core.models as models
8
+ import karrio.providers.tnt.error as provider_error
9
+ import karrio.providers.tnt.utils as provider_utils
10
+ import karrio.providers.tnt.units as provider_units
11
+
12
+
13
+ def parse_shipment_response(
14
+ _response: lib.Deserializable[lib.Element],
15
+ settings: provider_utils.Settings,
16
+ ) -> typing.Tuple[models.ShipmentDetails, typing.List[models.Message]]:
17
+ response = _response.deserialize()
18
+
19
+ messages = provider_error.parse_error_response(response, settings)
20
+ create: shipping.CREATE = lib.find_element(
21
+ "CREATE", response, element_type=shipping.CREATE, first=True
22
+ )
23
+ shipment = (
24
+ _extract_detail(create, settings, ctx=_response.ctx)
25
+ if getattr(create, "SUCCESS", None) == "Y"
26
+ else None
27
+ )
28
+
29
+ return shipment, messages
30
+
31
+
32
+ def _extract_detail(
33
+ detail: shipping.CREATE,
34
+ settings: provider_utils.Settings,
35
+ ctx: dict,
36
+ ) -> typing.Optional[models.ShipmentDetails]:
37
+ label = ctx.get("label")
38
+
39
+ return models.ShipmentDetails(
40
+ carrier_name=settings.carrier_name,
41
+ carrier_id=settings.carrier_id,
42
+ tracking_number=detail.CONNUMBER,
43
+ shipment_identifier=detail.CONREF,
44
+ docs=models.Documents(label=label),
45
+ )
46
+
47
+
48
+ def shipment_request(
49
+ payload: models.ShipmentRequest,
50
+ settings: provider_utils.Settings,
51
+ ) -> lib.Serializable:
52
+ shipper = lib.to_address(payload.shipper)
53
+ recipient = lib.to_address(payload.recipient)
54
+ packages = lib.to_packages(payload.parcels)
55
+ service = provider_units.ShippingService.map(payload.service).value_or_key
56
+ payment = payload.payment or models.Payment(paid_by="sender")
57
+ is_document = all([parcel.is_document for parcel in payload.parcels])
58
+ options = lib.to_shipping_options(
59
+ payload.options,
60
+ package_options=packages.options,
61
+ shipper_country_code=payload.shipper.country_code,
62
+ recipient_country_code=payload.recipient.country_code,
63
+ is_international=(
64
+ payload.shipper.country_code != payload.recipient.country_code
65
+ ),
66
+ initializer=provider_units.shipping_options_initializer,
67
+ )
68
+ ref = payload.parcels[0].reference_number or f"ref_{uuid.uuid4()}"
69
+ insurance = options.tnt_insurance.state
70
+
71
+ request = tnt.ESHIPPER(
72
+ LOGIN=tnt.LOGIN(
73
+ COMPANY=settings.username,
74
+ PASSWORD=settings.password,
75
+ APPID=settings.connection_config.app_id.state or "EC",
76
+ APPVERSION="3.1",
77
+ ),
78
+ CONSIGNMENTBATCH=tnt.CONSIGNMENTBATCH(
79
+ GROUPCODE=None,
80
+ SENDER=tnt.SENDER(
81
+ COMPANYNAME=shipper.company_name,
82
+ STREETADDRESS1=shipper.address_line1,
83
+ STREETADDRESS2=shipper.address_line2,
84
+ STREETADDRESS3=None,
85
+ CITY=shipper.city,
86
+ PROVINCE=shipper.state_code,
87
+ POSTCODE=shipper.postal_code,
88
+ COUNTRY=shipper.country_code,
89
+ ACCOUNT=settings.account_number,
90
+ VAT=shipper.tax_id,
91
+ CONTACTNAME=shipper.person_name,
92
+ CONTACTDIALCODE=None,
93
+ CONTACTTELEPHONE=shipper.phone_number,
94
+ CONTACTEMAIL=shipper.email,
95
+ COLLECTION=tnt.COLLECTION(
96
+ COLLECTIONADDRESS=None,
97
+ SHIPDATE=lib.fdatetime(
98
+ options.shipment_date.state,
99
+ current_format="%Y-%m-%d",
100
+ output_format="%d/%m/%Y",
101
+ ),
102
+ PREFCOLLECTTIME=None,
103
+ ALTCOLLECTTIME=None,
104
+ COLLINSTRUCTIONS=None,
105
+ CONFIRMATIONEMAILADDRESS=None,
106
+ ),
107
+ ),
108
+ CONSIGNMENT=[
109
+ tnt.CONSIGNMENT(
110
+ CONREF=ref,
111
+ DETAILS=tnt.DETAILS(
112
+ RECEIVER=tnt.RECEIVER(
113
+ COMPANYNAME=recipient.company_name,
114
+ STREETADDRESS1=recipient.address_line1,
115
+ STREETADDRESS2=recipient.address_line2,
116
+ STREETADDRESS3=None,
117
+ CITY=recipient.city,
118
+ PROVINCE=recipient.state_code,
119
+ POSTCODE=recipient.postal_code,
120
+ COUNTRY=recipient.country_code,
121
+ VAT=recipient.tax_id,
122
+ CONTACTNAME=recipient.person_name,
123
+ CONTACTDIALCODE=None,
124
+ CONTACTTELEPHONE=recipient.phone_number,
125
+ CONTACTEMAIL=recipient.email,
126
+ ACCOUNT=None,
127
+ ACCOUNTCOUNTRY=None,
128
+ ),
129
+ DELIVERY=None,
130
+ CONNUMBER=None,
131
+ CUSTOMERREF=payload.reference,
132
+ CONTYPE=("D" if is_document else "N"),
133
+ PAYMENTIND=provider_units.PaymentType.map(
134
+ payment.paid_by
135
+ ).value,
136
+ ITEMS=len(packages),
137
+ TOTALWEIGHT=packages.weight.KG,
138
+ TOTALVOLUME=packages.volume.m3,
139
+ CURRENCY=options.currency.state,
140
+ GOODSVALUE=options.declared_value.state,
141
+ INSURANCEVALUE=insurance,
142
+ INSURANCECURRENCY=options.currency.state,
143
+ DIVISION=None,
144
+ SERVICE=service,
145
+ OPTION=[
146
+ option.code
147
+ for _, option in options.items()
148
+ if "division" not in _
149
+ ],
150
+ DESCRIPTION=None,
151
+ DELIVERYINST=None,
152
+ CUSTOMCONTROLIN=None,
153
+ HAZARDOUS=None,
154
+ UNNUMBER=None,
155
+ PACKINGGROUP=None,
156
+ PACKAGE=[
157
+ tnt.PACKAGE(
158
+ ITEMS=package.items.quantity,
159
+ DESCRIPTION=package.parcel.description,
160
+ LENGTH=package.length.M,
161
+ HEIGHT=package.height.M,
162
+ WIDTH=package.width.M,
163
+ WEIGHT=package.weight.KG,
164
+ ARTICLE=(
165
+ [
166
+ tnt.ARTICLE(
167
+ ITEMS=item.quantity,
168
+ DESCRIPTION=lib.text(
169
+ item.title or item.description or "N/A",
170
+ max=35,
171
+ ),
172
+ WEIGHT=units.Weight(
173
+ item.weight,
174
+ units.WeightUnit[item.weight_unit],
175
+ ).KG,
176
+ INVOICEVALUE=item.value_amount,
177
+ INVOICEDESC=lib.text(
178
+ item.description or item.title,
179
+ max=35,
180
+ ),
181
+ HTS=item.hs_code or item.sku,
182
+ COUNTRY=item.origin_country,
183
+ )
184
+ for item in package.items
185
+ ]
186
+ if len(package.items) > 0
187
+ else None
188
+ ),
189
+ )
190
+ for package in packages
191
+ ],
192
+ ),
193
+ )
194
+ ],
195
+ ),
196
+ ACTIVITY=tnt.ACTIVITY(
197
+ CREATE=tnt.CREATE(CONREF=[ref]),
198
+ RATE=tnt.RATE(CONREF=[ref]),
199
+ BOOK=tnt.BOOK(CONREF=[ref]),
200
+ SHIP=tnt.SHIP(CONREF=[ref]),
201
+ PRINT=tnt.PRINT(
202
+ REQUIRED=tnt.REQUIRED(CONREF=[ref]),
203
+ CONNOTE=tnt.CONNOTE(CONREF=[ref]),
204
+ LABEL=tnt.LABEL(CONREF=[ref]),
205
+ MANIFEST=tnt.MANIFEST(CONREF=[ref]),
206
+ INVOICE=tnt.INVOICE(CONREF=[ref]),
207
+ EMAILTO=(
208
+ tnt.EMAILTO(
209
+ type_=None,
210
+ valueOf_=(
211
+ options.email_notification_to.state or recipient.email
212
+ ),
213
+ )
214
+ if (
215
+ options.email_notification.state
216
+ and any([options.email_notification_to.state, recipient.email])
217
+ )
218
+ else None
219
+ ),
220
+ EMAILFROM=settings.connection_config.email_from.state,
221
+ ),
222
+ SHOW_GROUPCODE=tnt.SHOW_GROUPCODE(),
223
+ ),
224
+ )
225
+
226
+ return lib.Serializable(request, lib.to_xml, dict(payload=payload))
@@ -0,0 +1,71 @@
1
+ import karrio.schemas.tnt.tracking_request as tnt
2
+ import karrio.schemas.tnt.tracking_response as tracking
3
+ import typing
4
+ import karrio.lib as lib
5
+ import karrio.core.models as models
6
+ import karrio.providers.tnt.error as provider_error
7
+ import karrio.providers.tnt.utils as provider_utils
8
+ import karrio.providers.tnt.units as provider_units
9
+
10
+
11
+ def parse_tracking_response(
12
+ _response: lib.Deserializable[lib.Element],
13
+ settings: provider_utils.Settings,
14
+ ) -> typing.Tuple[typing.List[models.TrackingDetails], typing.List[models.Message]]:
15
+ response = _response.deserialize()
16
+ messages = provider_error.parse_error_response(response, settings)
17
+ tracking_details = [
18
+ _extract_detail(node, settings)
19
+ for node in lib.find_element("Consignment", response)
20
+ ]
21
+
22
+ return tracking_details, messages
23
+
24
+
25
+ def _extract_detail(
26
+ node: dict, settings: provider_utils.Settings
27
+ ) -> models.TrackingDetails:
28
+ detail = lib.to_object(tracking.ConsignmentType, node)
29
+ events: typing.List[tracking.StatusStructure] = detail.StatusData
30
+
31
+ return models.TrackingDetails(
32
+ carrier_name=settings.carrier_name,
33
+ carrier_id=settings.carrier_id,
34
+ tracking_number=detail.ConsignmentNumber,
35
+ events=[
36
+ models.TrackingEvent(
37
+ date=lib.fdate(status.LocalEventDate.valueOf_, "%Y%m%d"),
38
+ description=status.StatusDescription,
39
+ location=lib.join(
40
+ status.Depot, status.DepotName, join=True, separator="-"
41
+ ),
42
+ code=status.StatusCode,
43
+ time=lib.flocaltime(status.LocalEventTime.valueOf_, "%H%M"),
44
+ )
45
+ for status in events
46
+ ],
47
+ delivered=(detail.SummaryCode == "DEL"),
48
+ )
49
+
50
+
51
+ def tracking_request(
52
+ payload: models.TrackingRequest,
53
+ settings: provider_utils.Settings,
54
+ ) -> lib.Serializable:
55
+ request = tnt.TrackRequest(
56
+ locale=settings.connection_config.locale.state or "en_US",
57
+ version="3.1",
58
+ SearchCriteria=tnt.SearchCriteriaType(
59
+ marketType="INTERNATIONAL",
60
+ originCountry=(settings.account_country_code or "US"),
61
+ ConsignmentNumber=payload.tracking_numbers,
62
+ ),
63
+ LevelOfDetail=tnt.LevelOfDetailType(
64
+ Complete=tnt.CompleteType(
65
+ originAddress=True,
66
+ destinationAddress=True,
67
+ )
68
+ ),
69
+ )
70
+
71
+ return lib.Serializable(request, lib.to_xml)
@@ -0,0 +1,166 @@
1
+ """ TNT Native Types """
2
+
3
+ import karrio.lib as lib
4
+
5
+ PRESET_DEFAULTS = dict(dimension_unit="CM", weight_unit="KG")
6
+
7
+
8
+ class PackagePresets(lib.Enum):
9
+ tnt_envelope_doc = lib.units.PackagePreset(
10
+ **dict(width=35.0, height=1.0, length=27.5, packaging_type="envelope"),
11
+ **PRESET_DEFAULTS
12
+ )
13
+ tnt_satchel_bag1 = lib.units.PackagePreset(
14
+ **dict(weight=2.0, width=40.0, height=1.0, length=30.0, packaging_type="pak"),
15
+ **PRESET_DEFAULTS
16
+ )
17
+ tnt_satchel_bag2 = lib.units.PackagePreset(
18
+ **dict(weight=4.0, width=47.5, height=1.0, length=38.0, packaging_type="pak"),
19
+ **PRESET_DEFAULTS
20
+ )
21
+ tnt_box_B = lib.units.PackagePreset(
22
+ **dict(
23
+ weight=4.0,
24
+ width=29.5,
25
+ height=19.0,
26
+ length=40.0,
27
+ packaging_type="medium_box",
28
+ ),
29
+ **PRESET_DEFAULTS
30
+ )
31
+ tnt_box_C = lib.units.PackagePreset(
32
+ **dict(
33
+ weight=6.0,
34
+ width=29.5,
35
+ height=29.0,
36
+ length=40.0,
37
+ packaging_type="medium_box",
38
+ ),
39
+ **PRESET_DEFAULTS
40
+ )
41
+ tnt_box_D = lib.units.PackagePreset(
42
+ **dict(
43
+ weight=10.0,
44
+ width=39.5,
45
+ height=29.0,
46
+ length=50.0,
47
+ packaging_type="medium_box",
48
+ ),
49
+ **PRESET_DEFAULTS
50
+ )
51
+ tnt_box_E = lib.units.PackagePreset(
52
+ **dict(
53
+ weight=15.0,
54
+ width=39.5,
55
+ height=49.5,
56
+ length=44.0,
57
+ packaging_type="medium_box",
58
+ ),
59
+ **PRESET_DEFAULTS
60
+ )
61
+ tnt_medpack_ambient = lib.units.PackagePreset(
62
+ **dict(width=18.0, height=12.0, length=23.0, packaging_type="medium_box"),
63
+ **PRESET_DEFAULTS
64
+ )
65
+ tnt_medpack_fronzen_10 = lib.units.PackagePreset(
66
+ **dict(width=37.0, height=35.5, length=40.0, packaging_type="large_box"),
67
+ **PRESET_DEFAULTS
68
+ )
69
+
70
+
71
+ class PackageType(lib.StrEnum):
72
+ tnt_envelope = "envelope"
73
+ tnt_satchel = "satchel"
74
+ tnt_box = "box"
75
+ tnt_cylinder = "cylinder"
76
+ tnt_pallet = "pallet"
77
+
78
+ """ Unified Packaging type mapping """
79
+ envelope = tnt_envelope
80
+ pak = tnt_satchel
81
+ tube = tnt_cylinder
82
+ pallet = tnt_pallet
83
+ small_box = tnt_box
84
+ medium_box = tnt_box
85
+ large_box = tnt_box
86
+ your_packaging = "your_packaging"
87
+
88
+
89
+ class PaymentType(lib.StrEnum):
90
+ sender = "S"
91
+ recipient = "R"
92
+ third_party = recipient
93
+
94
+
95
+ class ConnectionConfig(lib.Enum):
96
+ app_id = lib.OptionEnum("app_id")
97
+ email_from = lib.OptionEnum("email_from")
98
+
99
+
100
+ class ShippingService(lib.StrEnum):
101
+ tnt_special_express = "1N"
102
+ tnt_9_00_express = "09N"
103
+ tnt_10_00_express = "10N"
104
+ tnt_12_00_express = "12N"
105
+ tnt_express = "EX"
106
+ tnt_economy_express = "48N"
107
+ tnt_global_express = "15N"
108
+
109
+
110
+ class ShippingOption(lib.Enum):
111
+ tnt_priority = lib.OptionEnum("PR")
112
+ tnt_insurance = lib.OptionEnum("IN", lib.to_money)
113
+ tnt_enhanced_liability = lib.OptionEnum("EL")
114
+ tnt_dangerous_goods_fully_regulated = lib.OptionEnum("HZ")
115
+ tnt_dangerous_goods_in_limited_quantities = lib.OptionEnum("LQ")
116
+ tnt_dry_ice_shipments = lib.OptionEnum("DI")
117
+ tnt_biological_substances = lib.OptionEnum("BB")
118
+ tnt_lithium_batteries = lib.OptionEnum("LB")
119
+ tnt_dangerous_goods_in_excepted_quantities = lib.OptionEnum("EQ")
120
+ tnt_radioactive_materials_in_excepted_packages = lib.OptionEnum("XP")
121
+ tnt_pre_delivery_notification = lib.OptionEnum("SMS")
122
+
123
+ tnt_division_international_shipments = lib.OptionEnum("G", bool)
124
+ tnt_division_global_link_domestic = lib.OptionEnum("D", bool)
125
+ tnt_division_german_domestic = lib.OptionEnum("H", bool)
126
+ tnt_division_uk_domestic = lib.OptionEnum("010", bool)
127
+
128
+ insurance = tnt_insurance
129
+
130
+
131
+ def shipping_options_initializer(
132
+ options: dict,
133
+ package_options: lib.units.Options = None,
134
+ is_international: bool = None,
135
+ shipper_country_code: str = None,
136
+ recipient_country_code: str = None,
137
+ ) -> lib.units.Options:
138
+ """
139
+ Apply default values to the given options.
140
+ """
141
+ _options = options.copy()
142
+
143
+ if package_options is not None:
144
+ _options.update(package_options.content)
145
+
146
+ if is_international:
147
+ _options.update(tnt_division_international_shipments=True)
148
+
149
+ if shipper_country_code == "DE" and recipient_country_code == "DE":
150
+ _options.update(tnt_division_german_domestic=True)
151
+
152
+ if shipper_country_code == "GB" and recipient_country_code == "GB":
153
+ _options.update(tnt_division_uk_domestic=True)
154
+
155
+ if shipper_country_code == recipient_country_code and shipper_country_code not in [
156
+ "DE",
157
+ "GB",
158
+ ]:
159
+ _options.update(tnt_division_global_link_domestic=True)
160
+
161
+ def items_filter(key: str) -> bool:
162
+ return key in ShippingOption # type: ignore
163
+
164
+ return lib.units.ShippingOptions(
165
+ _options, ShippingOption, items_filter=items_filter
166
+ )
@@ -0,0 +1,162 @@
1
+ import karrio.schemas.tnt.label_request as tnt
2
+ import karrio.schemas.tnt.shipping_response as shipping
3
+ import typing
4
+ import base64
5
+ import karrio.lib as lib
6
+ import karrio.core as core
7
+ import karrio.core.models as models
8
+
9
+
10
+ class Settings(core.Settings):
11
+ """TNT connection settings."""
12
+
13
+ username: str
14
+ password: str
15
+
16
+ account_number: str = None
17
+ account_country_code: str = None
18
+ metadata: dict = {}
19
+ config: dict = {}
20
+
21
+ @property
22
+ def carrier_name(self):
23
+ return "tnt"
24
+
25
+ @property
26
+ def server_url(self):
27
+ return "https://express.tnt.com"
28
+
29
+ @property
30
+ def authorization(self):
31
+ pair = "%s:%s" % (self.username, self.password)
32
+ return base64.b64encode(pair.encode("utf-8")).decode("ascii")
33
+
34
+ @property
35
+ def connection_config(self) -> lib.units.Options:
36
+ from karrio.providers.tnt.units import ConnectionConfig
37
+
38
+ return lib.to_connection_config(
39
+ self.config or {},
40
+ option_type=ConnectionConfig,
41
+ )
42
+
43
+
44
+ def create_label_request(
45
+ shipment_response: str,
46
+ settings: Settings,
47
+ ctx: dict,
48
+ ) -> typing.Optional[lib.Serializable]:
49
+ import karrio.providers.tnt.units as provider_units
50
+
51
+ payload: models.ShipmentRequest = ctx.get("payload")
52
+ response = lib.to_element(shipment_response)
53
+ consignment = lib.find_element("CONNUMBER", response, first=True)
54
+ groupcode = lib.find_element("GROUPCODE", response, first=True)
55
+ price: shipping.document = lib.find_element(
56
+ "PRICE", response, shipping.PRICE, first=True
57
+ )
58
+
59
+ if consignment is None or groupcode is None or price is None:
60
+ return None
61
+
62
+ shipper = lib.to_address(payload.shipper)
63
+ recipient = lib.to_address(payload.recipient)
64
+ packages = lib.to_packages(payload.parcels)
65
+ customs = lib.to_customs_info(payload.customs)
66
+
67
+ request = tnt.labelRequest(
68
+ consignment=[
69
+ tnt.labelConsignmentsType(
70
+ key="1",
71
+ consignmentIdentity=tnt.consignmentIdentityType(
72
+ consignmentNumber=getattr(consignment, "text", None),
73
+ customerReference=payload.reference,
74
+ ),
75
+ collectionDateTime=None,
76
+ sender=tnt.nameAndAddressRequestType(
77
+ name=recipient.contact,
78
+ addressLine1=shipper.street,
79
+ addressLine2=shipper.address_line2,
80
+ addressLine3=None,
81
+ town=shipper.city,
82
+ exactMatch=None,
83
+ province=shipper.state_code,
84
+ postcode=shipper.postal_code,
85
+ country=shipper.country_code,
86
+ ),
87
+ delivery=tnt.nameAndAddressRequestType(
88
+ name=recipient.contact,
89
+ addressLine1=recipient.street,
90
+ addressLine2=recipient.address_line2,
91
+ addressLine3=None,
92
+ town=recipient.city,
93
+ exactMatch=None,
94
+ province=recipient.state_code,
95
+ postcode=recipient.postal_code,
96
+ country=recipient.country_code,
97
+ ),
98
+ contact=tnt.contactType(
99
+ name=shipper.person_name,
100
+ telephoneNumber=shipper.phone_number,
101
+ emailAddress=shipper.email,
102
+ ),
103
+ product=tnt.productType(
104
+ lineOfBusiness=None,
105
+ groupId=getattr(groupcode, "text", None),
106
+ subGroupId=None,
107
+ id=price.SERVICE,
108
+ type_=price.SERVICEDESC,
109
+ option=price.OPTION,
110
+ ),
111
+ account=tnt.accountType(
112
+ accountNumber=settings.account_number,
113
+ accountCountry=settings.account_country_code,
114
+ ),
115
+ cashAmount=price.RATE,
116
+ cashCurrency=price.CURRENCY,
117
+ cashType=None,
118
+ ncolNumber=None,
119
+ specialInstructions=None,
120
+ bulkShipment="N",
121
+ customControlled=("N" if payload.customs is None else "Y"),
122
+ termsOfPayment=provider_units.PaymentType.map(
123
+ getattr(payload.payment, "paidby", "sender")
124
+ ).value,
125
+ totalNumberOfPieces=len(packages),
126
+ pieceLine=[
127
+ tnt.pieceLineType(
128
+ identifier=1,
129
+ goodsDescription=package.parcel.description,
130
+ barcodeForCustomer="Y",
131
+ pieceMeasurements=tnt.measurementsType(
132
+ length=package.length.M,
133
+ width=package.width.M,
134
+ height=package.height.M,
135
+ weight=package.weight.KG,
136
+ ),
137
+ pieces=(
138
+ [
139
+ tnt.pieceType(
140
+ sequenceNumbers=(index + 1),
141
+ pieceReference=piece.sku or piece.hs_code,
142
+ )
143
+ for index, piece in enumerate(
144
+ (
145
+ package.items
146
+ if len(package.items) > 0
147
+ else customs.commodities
148
+ ),
149
+ start=1,
150
+ )
151
+ ]
152
+ if len(package.items) > 0 or len(customs.commodities) > 0
153
+ else None
154
+ ),
155
+ )
156
+ for package in packages
157
+ ],
158
+ )
159
+ ]
160
+ )
161
+
162
+ return lib.Serializable(request, lib.to_xml)
File without changes