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,3 @@
1
+ from karrio.mappers.tnt.mapper import Mapper
2
+ from karrio.mappers.tnt.proxy import Proxy
3
+ from karrio.mappers.tnt.settings import Settings
@@ -0,0 +1,40 @@
1
+ """Karrio TNT client mapper."""
2
+
3
+ import typing
4
+ import karrio.lib as lib
5
+ import karrio.api.mapper as mapper
6
+ import karrio.core.models as models
7
+ import karrio.providers.tnt as provider
8
+ import karrio.mappers.tnt.settings as provider_settings
9
+
10
+
11
+ class Mapper(mapper.Mapper):
12
+ settings: provider_settings.Settings
13
+
14
+ def create_rate_request(self, payload: models.RateRequest) -> lib.Serializable:
15
+ return provider.rate_request(payload, self.settings)
16
+
17
+ def create_tracking_request(
18
+ self, payload: models.TrackingRequest
19
+ ) -> lib.Serializable:
20
+ return provider.tracking_request(payload, self.settings)
21
+
22
+ def create_shipment_request(
23
+ self, payload: models.ShipmentRequest
24
+ ) -> lib.Serializable:
25
+ return provider.shipment_request(payload, self.settings)
26
+
27
+ def parse_rate_response(
28
+ self, response: lib.Deserializable
29
+ ) -> typing.Tuple[typing.List[models.RateDetails], typing.List[models.Message]]:
30
+ return provider.parse_rate_response(response, self.settings)
31
+
32
+ def parse_shipment_response(
33
+ self, response: lib.Deserializable
34
+ ) -> typing.Tuple[models.ShipmentDetails, typing.List[models.Message]]:
35
+ return provider.parse_shipment_response(response, self.settings)
36
+
37
+ def parse_tracking_response(
38
+ self, response: lib.Deserializable
39
+ ) -> typing.Tuple[typing.List[models.TrackingDetails], typing.List[models.Message]]:
40
+ return provider.parse_tracking_response(response, self.settings)
@@ -0,0 +1,97 @@
1
+ import urllib.parse
2
+ import karrio.lib as lib
3
+ import karrio.api.proxy as proxy
4
+ import karrio.providers.tnt.error as provider_error
5
+ import karrio.providers.tnt.utils as provider_utils
6
+ import karrio.mappers.tnt.settings as provider_settings
7
+
8
+
9
+ class Proxy(proxy.Proxy):
10
+ settings: provider_settings.Settings
11
+
12
+ """ Proxy Methods """
13
+
14
+ def get_rates(self, request: lib.Serializable) -> lib.Deserializable[str]:
15
+ response = lib.request(
16
+ url=f"{self.settings.server_url}/expressconnect/pricing/getprice",
17
+ data=urllib.parse.urlencode(dict(xml_in=request.serialize())),
18
+ trace=self.trace_as("xml"),
19
+ method="POST",
20
+ headers={
21
+ "Content-Type": "application/x-www-form-urlencoded",
22
+ "Authorization": f"Basic {self.settings.authorization}",
23
+ },
24
+ )
25
+
26
+ return lib.Deserializable(response, lib.to_element)
27
+
28
+ def create_shipment(self, request: lib.Serializable) -> lib.Deserializable[str]:
29
+ ctx: dict = {}
30
+ shipment_response = lib.request(
31
+ url=f"{self.settings.server_url}/expressconnect/shipping/ship",
32
+ data=urllib.parse.urlencode(dict(xml_in=request.serialize())),
33
+ trace=self.trace_as("xml"),
34
+ method="POST",
35
+ headers={
36
+ "Content-Type": "application/x-www-form-urlencoded",
37
+ "Authorization": f"Basic {self.settings.authorization}",
38
+ },
39
+ )
40
+
41
+ label_request = provider_utils.create_label_request(
42
+ shipment_response, self.settings, request.ctx
43
+ )
44
+
45
+ if label_request is not None:
46
+ label_response = lib.request(
47
+ url=f"{self.settings.server_url}/expresslabel/documentation/getlabel",
48
+ trace=self.trace_as("xml"),
49
+ method="POST",
50
+ headers={
51
+ "Content-Type": "application/x-www-form-urlencoded",
52
+ "Authorization": f"Basic {self.settings.authorization}",
53
+ },
54
+ data=urllib.parse.urlencode(dict(xml_in=label_request.serialize())),
55
+ )
56
+
57
+ label_errors = provider_error.parse_error_response(
58
+ lib.to_element(label_response), self.settings
59
+ )
60
+
61
+ if not any(label_errors):
62
+ label = lib.request(
63
+ url=f"{self.settings.server_url}/expresswebservices-website/app/render.html",
64
+ trace=self.trace_as("xml"),
65
+ method="POST",
66
+ headers={
67
+ "Content-Type": "application/x-www-form-urlencoded",
68
+ "Authorization": f"Basic {self.settings.authorization}",
69
+ },
70
+ data=urllib.parse.urlencode(
71
+ dict(
72
+ responseXml=label_response,
73
+ documentType="routingLabel",
74
+ contentType="pdf",
75
+ ),
76
+ ),
77
+ decoder=lib.encode_base64,
78
+ )
79
+
80
+ ctx.update(label=label)
81
+
82
+ return lib.Deserializable(shipment_response, lib.to_element, ctx=ctx)
83
+
84
+ def get_tracking(self, request: lib.Serializable) -> lib.Deserializable[str]:
85
+ response = lib.request(
86
+ url="https://necta.az.fxei.fedex.com/ectrack/track",
87
+ data=urllib.parse.urlencode(dict(xml_in=request.serialize())),
88
+ trace=self.trace_as("xml"),
89
+ method="POST",
90
+ headers={
91
+ "accept": "text/xml; charset=UTF-8",
92
+ "Content-Type": "application/x-www-form-urlencoded",
93
+ "Authorization": f"Basic {self.settings.authorization}",
94
+ },
95
+ )
96
+
97
+ return lib.Deserializable(response, lib.to_element)
@@ -0,0 +1,22 @@
1
+ """Karrio TNT settings."""
2
+
3
+ import attr
4
+ import karrio.providers.tnt.utils as provider_utils
5
+
6
+
7
+ @attr.s(auto_attribs=True)
8
+ class Settings(provider_utils.Settings):
9
+ """TNT connection settings."""
10
+
11
+ # Carrier specific properties
12
+ username: str
13
+ password: str
14
+ account_number: str = None
15
+ account_country_code: str = None
16
+ metadata: dict = {}
17
+ config: dict = {}
18
+
19
+ # Base properties
20
+ id: str = None
21
+ test_mode: bool = False
22
+ carrier_id: str = "tnt"
@@ -0,0 +1,24 @@
1
+ import karrio.core.metadata as metadata
2
+ import karrio.mappers.tnt as mappers
3
+ import karrio.providers.tnt.units as units
4
+
5
+
6
+ METADATA = metadata.PluginMetadata(
7
+ status="beta",
8
+ id="tnt",
9
+ label="TNT",
10
+ # Integrations
11
+ Mapper=mappers.Mapper,
12
+ Proxy=mappers.Proxy,
13
+ Settings=mappers.Settings,
14
+ # Data Units
15
+ options=units.ShippingOption,
16
+ services=units.ShippingService,
17
+ packaging_types=units.PackageType,
18
+ package_presets=units.PackagePresets,
19
+ connection_configs=units.ConnectionConfig,
20
+ has_intl_accounts=True,
21
+ # New fields
22
+ website="https://www.tnt.com",
23
+ description="TNT is an international courier delivery services company with headquarters in the Netherlands.",
24
+ )
@@ -0,0 +1,10 @@
1
+ from karrio.providers.tnt.utils import Settings
2
+ from karrio.providers.tnt.rate import parse_rate_response, rate_request
3
+ from karrio.providers.tnt.shipment import (
4
+ parse_shipment_response,
5
+ shipment_request,
6
+ )
7
+ from karrio.providers.tnt.tracking import (
8
+ parse_tracking_response,
9
+ tracking_request,
10
+ )
@@ -0,0 +1,129 @@
1
+ import karrio.schemas.tnt.rating_response as tnt
2
+ import typing
3
+ import karrio.lib as lib
4
+ import karrio.core.models as models
5
+ import karrio.providers.tnt.utils as provider_utils
6
+
7
+
8
+ def parse_error_response(
9
+ response,
10
+ settings: provider_utils.Settings,
11
+ ) -> typing.List[models.Message]:
12
+ structure_errors = lib.find_element("ErrorStructure", response)
13
+ broken_rules_nodes = lib.find_element("brokenRules", response)
14
+ broken_rule_nodes = lib.find_element("brokenRule", response)
15
+ runtime_errors = lib.find_element("runtime_error", response)
16
+ parse_errors = lib.find_element("parse_error", response)
17
+ ERRORS = lib.find_element("ERROR", response)
18
+ errors = lib.find_element("Error", response)
19
+ faults = lib.find_element("fault", response)
20
+
21
+ return [
22
+ *[_extract_structure_error(node, settings) for node in structure_errors],
23
+ *[_extract_broken_rules(node, settings) for node in broken_rules_nodes],
24
+ *[_extract_broken_rule(node, settings) for node in broken_rule_nodes],
25
+ *[_extract_runtime_error(node, settings) for node in runtime_errors],
26
+ *[_extract_parse_error(node, settings) for node in parse_errors],
27
+ *[_extract_structure_error(node, settings) for node in errors],
28
+ *[_extract_error(node, settings) for node in ERRORS],
29
+ *[_extract_faut(node, settings) for node in faults],
30
+ ]
31
+
32
+
33
+ def _extract_structure_error(
34
+ node: lib.Element, settings: provider_utils.Settings
35
+ ) -> models.Message:
36
+ return models.Message(
37
+ # context info
38
+ carrier_name=settings.carrier_name,
39
+ carrier_id=settings.carrier_id,
40
+ # carrier error info
41
+ code=lib.find_element("Code", node, first=True).text,
42
+ message=lib.find_element("Message", node, first=True).text,
43
+ )
44
+
45
+
46
+ def _extract_broken_rules(
47
+ node: lib.Element, settings: provider_utils.Settings
48
+ ) -> models.Message:
49
+ return models.Message(
50
+ # context info
51
+ carrier_name=settings.carrier_name,
52
+ carrier_id=settings.carrier_id,
53
+ # carrier error info
54
+ code=lib.find_element("errorCode", node, first=True).text,
55
+ message=lib.find_element("errorMessage", node, first=True).text,
56
+ )
57
+
58
+
59
+ def _extract_broken_rule(
60
+ node: lib.Element, settings: provider_utils.Settings
61
+ ) -> models.Message:
62
+ error = lib.to_object(tnt.brokenRule, node)
63
+
64
+ return models.Message(
65
+ # context info
66
+ carrier_name=settings.carrier_name,
67
+ carrier_id=settings.carrier_id,
68
+ # carrier error info
69
+ code=getattr(error, "code", None),
70
+ message=getattr(error, "description", None),
71
+ details=dict(messageType=getattr(error, "messageType", None)),
72
+ )
73
+
74
+
75
+ def _extract_runtime_error(
76
+ node: lib.Element, settings: provider_utils.Settings
77
+ ) -> models.Message:
78
+ error = lib.to_object(tnt.runtimeError, node)
79
+
80
+ return models.Message(
81
+ # context info
82
+ carrier_name=settings.carrier_name,
83
+ carrier_id=settings.carrier_id,
84
+ # carrier error info
85
+ code="runtime",
86
+ message=getattr(error, "errorReason", None),
87
+ details=dict(srcTxt=getattr(error, "errorSrcText", None)),
88
+ )
89
+
90
+
91
+ def _extract_parse_error(
92
+ node: lib.Element, settings: provider_utils.Settings
93
+ ) -> models.Message:
94
+ error = lib.to_object(tnt.parseError, node)
95
+
96
+ return models.Message(
97
+ # context info
98
+ carrier_name=settings.carrier_name,
99
+ carrier_id=settings.carrier_id,
100
+ # carrier error info
101
+ code="parsing",
102
+ message=getattr(error, "errorReason", None),
103
+ details=dict(srcText=getattr(error, "errorSrcText", None)),
104
+ )
105
+
106
+
107
+ def _extract_error(
108
+ node: lib.Element, settings: provider_utils.Settings
109
+ ) -> models.Message:
110
+ return models.Message(
111
+ # context info
112
+ carrier_name=settings.carrier_name,
113
+ carrier_id=settings.carrier_id,
114
+ # carrier error info
115
+ code=lib.find_element("CODE", node, first=True).text,
116
+ message=lib.find_element("DESCRIPTION", node, first=True).text,
117
+ )
118
+
119
+
120
+ def _extract_faut(
121
+ node: lib.Element, settings: provider_utils.Settings
122
+ ) -> models.Message:
123
+ return models.Message(
124
+ # context info
125
+ carrier_name=settings.carrier_name,
126
+ carrier_id=settings.carrier_id,
127
+ # carrier error info
128
+ code=lib.find_element("key", node, first=True).text,
129
+ )
@@ -0,0 +1,184 @@
1
+ import functools
2
+ import karrio.schemas.tnt.rating_request as tnt
3
+ import karrio.schemas.tnt.rating_response as rating
4
+ import typing
5
+ import karrio.lib as lib
6
+ import karrio.core.models as models
7
+ import karrio.providers.tnt.error as provider_error
8
+ import karrio.providers.tnt.units as provider_units
9
+ import karrio.providers.tnt.utils as provider_utils
10
+
11
+
12
+ def parse_rate_response(
13
+ _response: lib.Deserializable[lib.Element],
14
+ settings: provider_utils.Settings,
15
+ ) -> typing.Tuple[typing.List[models.RateDetails], typing.List[models.Message]]:
16
+ response = _response.deserialize()
17
+
18
+ messages = provider_error.parse_error_response(response, settings)
19
+ services: typing.List[rating.ratedServices] = lib.find_element(
20
+ "ratedServices", response, rating.ratedServices
21
+ )
22
+ rates: typing.List[models.RateDetails] = sum(
23
+ [
24
+ [
25
+ _extract_details((rate, svc.currency), settings)
26
+ for rate in svc.ratedService
27
+ ]
28
+ for svc in services
29
+ ],
30
+ start=[],
31
+ )
32
+
33
+ return rates, messages
34
+
35
+
36
+ def _extract_details(
37
+ details: typing.Tuple[rating.ratedService, str],
38
+ settings: provider_utils.Settings,
39
+ ) -> models.RateDetails:
40
+ rate, currency = details
41
+ service = provider_units.ShippingService.map(rate.product.id)
42
+ charges = [
43
+ ("Base charge", rate.totalPriceExclVat),
44
+ ("VAT", rate.vatAmount),
45
+ *(
46
+ (charge.description, charge.chargeValue)
47
+ for charge in rate.chargeElements.chargeElement
48
+ ),
49
+ ]
50
+
51
+ return models.RateDetails(
52
+ carrier_name=settings.carrier_name,
53
+ carrier_id=settings.carrier_id,
54
+ currency=currency,
55
+ service=service.name_or_key,
56
+ total_charge=lib.to_decimal(rate.totalPrice),
57
+ extra_charges=[
58
+ models.ChargeDetails(
59
+ name=name,
60
+ amount=lib.to_decimal(amount),
61
+ currency=currency,
62
+ )
63
+ for name, amount in charges
64
+ if amount is not None
65
+ ],
66
+ meta=dict(service_name=rate.product.productDesc),
67
+ )
68
+
69
+
70
+ def rate_request(
71
+ payload: models.RateRequest,
72
+ settings: provider_utils.Settings,
73
+ ) -> lib.Serializable:
74
+ service = lib.to_services(payload.services, provider_units.ShippingService).first
75
+ packages = lib.to_packages(
76
+ payload.parcels,
77
+ presets=provider_units.PackagePresets,
78
+ package_option_type=provider_units.PackageType,
79
+ )
80
+ options = lib.to_shipping_options(
81
+ payload.options,
82
+ package_options=packages.options,
83
+ shipper_country_code=payload.shipper.country_code,
84
+ recipient_country_code=payload.recipient.country_code,
85
+ is_international=(
86
+ payload.shipper.country_code != payload.recipient.country_code
87
+ ),
88
+ initializer=provider_units.shipping_options_initializer,
89
+ )
90
+ is_document = all([parcel.is_document for parcel in payload.parcels])
91
+
92
+ request = tnt.priceRequest(
93
+ appId=settings.connection_config.app_id.state or "PC",
94
+ appVersion="3.2",
95
+ priceCheck=[
96
+ tnt.priceCheck(
97
+ rateId=None,
98
+ sender=tnt.address(
99
+ country=payload.shipper.country_code,
100
+ town=payload.shipper.city,
101
+ postcode=payload.shipper.postal_code,
102
+ ),
103
+ delivery=tnt.address(
104
+ country=payload.recipient.country_code,
105
+ town=payload.recipient.city,
106
+ postcode=payload.recipient.postal_code,
107
+ ),
108
+ collectionDateTime=lib.fdatetime(
109
+ options.shipment_date.state,
110
+ current_format="%Y-%m-%d",
111
+ output_format="%Y-%m-%dT%H:%M:%S",
112
+ ),
113
+ product=tnt.product(
114
+ id=getattr(service, "value", None),
115
+ division=next(
116
+ (
117
+ option.code
118
+ for key, option in options.items()
119
+ if "division" in key and option.state is True
120
+ ),
121
+ None,
122
+ ),
123
+ productDesc=None,
124
+ type_=("D" if is_document else "N"),
125
+ options=(
126
+ tnt.options(
127
+ option=[
128
+ tnt.option(optionCode=option.code)
129
+ for key, option in options.items()
130
+ if "division" not in key
131
+ ]
132
+ )
133
+ if any(options.items())
134
+ else None
135
+ ),
136
+ ),
137
+ account=(
138
+ tnt.account(
139
+ accountNumber=settings.account_number,
140
+ accountCountry=settings.account_country_code,
141
+ )
142
+ if any([settings.account_number, settings.account_country_code])
143
+ else None
144
+ ),
145
+ insurance=(
146
+ tnt.insurance(
147
+ insuranceValue=options.insurance.state,
148
+ goodsValue=options.declared_value.state,
149
+ )
150
+ if options.insurance.state is not None
151
+ else None
152
+ ),
153
+ termsOfPayment=provider_units.PaymentType.sender.value,
154
+ currency=options.currency.state,
155
+ priceBreakDown=True,
156
+ consignmentDetails=tnt.consignmentDetails(
157
+ totalWeight=packages.weight.KG,
158
+ totalVolume=packages.volume.m3,
159
+ totalNumberOfPieces=len(packages),
160
+ ),
161
+ pieceLine=[
162
+ tnt.pieceLine(
163
+ numberOfPieces=1,
164
+ pieceMeasurements=tnt.pieceMeasurements(
165
+ length=package.length.M,
166
+ width=package.width.M,
167
+ height=package.height.M,
168
+ weight=package.weight.KG,
169
+ ),
170
+ pallet=(package.packaging_type == "pallet"),
171
+ )
172
+ for package in packages
173
+ ],
174
+ )
175
+ ],
176
+ )
177
+
178
+ return lib.Serializable(
179
+ request,
180
+ functools.partial(
181
+ lib.to_xml,
182
+ namespacedef_='xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"',
183
+ ),
184
+ )
@@ -0,0 +1,4 @@
1
+ from karrio.providers.tnt.shipment.create import (
2
+ parse_shipment_response,
3
+ shipment_request,
4
+ )