karrio-teleship 2025.5__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.
Files changed (49) hide show
  1. karrio/mappers/teleship/__init__.py +4 -0
  2. karrio/mappers/teleship/hooks.py +27 -0
  3. karrio/mappers/teleship/mapper.py +114 -0
  4. karrio/mappers/teleship/proxy.py +239 -0
  5. karrio/mappers/teleship/settings.py +21 -0
  6. karrio/plugins/teleship/__init__.py +32 -0
  7. karrio/providers/teleship/__init__.py +41 -0
  8. karrio/providers/teleship/duties.py +115 -0
  9. karrio/providers/teleship/error.py +44 -0
  10. karrio/providers/teleship/hooks/__init__.py +5 -0
  11. karrio/providers/teleship/hooks/event.py +163 -0
  12. karrio/providers/teleship/hooks/oauth.py +103 -0
  13. karrio/providers/teleship/manifest.py +68 -0
  14. karrio/providers/teleship/pickup/__init__.py +8 -0
  15. karrio/providers/teleship/pickup/cancel.py +43 -0
  16. karrio/providers/teleship/pickup/schedule.py +66 -0
  17. karrio/providers/teleship/rate.py +287 -0
  18. karrio/providers/teleship/shipment/__init__.py +9 -0
  19. karrio/providers/teleship/shipment/cancel.py +48 -0
  20. karrio/providers/teleship/shipment/create.py +322 -0
  21. karrio/providers/teleship/tracking.py +100 -0
  22. karrio/providers/teleship/units.py +154 -0
  23. karrio/providers/teleship/utils.py +57 -0
  24. karrio/providers/teleship/webhook/__init__.py +8 -0
  25. karrio/providers/teleship/webhook/deregister.py +47 -0
  26. karrio/providers/teleship/webhook/register.py +48 -0
  27. karrio/schemas/teleship/__init__.py +0 -0
  28. karrio/schemas/teleship/duties_taxes_request.py +82 -0
  29. karrio/schemas/teleship/duties_taxes_response.py +28 -0
  30. karrio/schemas/teleship/error_response.py +17 -0
  31. karrio/schemas/teleship/manifest_request.py +39 -0
  32. karrio/schemas/teleship/manifest_response.py +31 -0
  33. karrio/schemas/teleship/pickup_request.py +31 -0
  34. karrio/schemas/teleship/pickup_response.py +48 -0
  35. karrio/schemas/teleship/rate_request.py +128 -0
  36. karrio/schemas/teleship/rate_response.py +66 -0
  37. karrio/schemas/teleship/shipment_cancel_request.py +8 -0
  38. karrio/schemas/teleship/shipment_cancel_response.py +17 -0
  39. karrio/schemas/teleship/shipment_request.py +137 -0
  40. karrio/schemas/teleship/shipment_response.py +247 -0
  41. karrio/schemas/teleship/tracking_request.py +8 -0
  42. karrio/schemas/teleship/tracking_response.py +52 -0
  43. karrio/schemas/teleship/webhook_request.py +18 -0
  44. karrio/schemas/teleship/webhook_response.py +20 -0
  45. karrio_teleship-2025.5.dist-info/METADATA +44 -0
  46. karrio_teleship-2025.5.dist-info/RECORD +49 -0
  47. karrio_teleship-2025.5.dist-info/WHEEL +5 -0
  48. karrio_teleship-2025.5.dist-info/entry_points.txt +2 -0
  49. karrio_teleship-2025.5.dist-info/top_level.txt +4 -0
@@ -0,0 +1,163 @@
1
+ """Karrio Teleship webhook event processing implementation."""
2
+
3
+ import hmac
4
+ import typing
5
+ import hashlib
6
+ import karrio.lib as lib
7
+ import karrio.core.models as models
8
+ import karrio.providers.teleship.error as error
9
+ import karrio.providers.teleship.utils as provider_utils
10
+ import karrio.providers.teleship.units as provider_units
11
+ import karrio.schemas.teleship.tracking_response as tracking_res
12
+
13
+
14
+ def on_webhook_event(
15
+ payload: models.RequestPayload,
16
+ settings: provider_utils.Settings,
17
+ ) -> typing.Tuple[models.WebhookEventDetails, typing.List[models.Message]]:
18
+ """
19
+ webhook payloads follow a structure:
20
+ {
21
+ "eventName": "label.generated",
22
+ "objectType": "shipment",
23
+ "objectId": "6f384ad7-f8bf-40ce-8bf0-715248738f10",
24
+ "data": {
25
+ // Event-specific data (see examples below)
26
+ }
27
+ }
28
+ """
29
+
30
+ if not verify_webhook_signature(payload, settings):
31
+ return None, [
32
+ models.Message(
33
+ code="invalid_signature",
34
+ message=f"Invalid webhook signature for {settings.carrier_name} webhook",
35
+ )
36
+ ]
37
+
38
+ messages = error.parse_error_response(payload.body, settings)
39
+ body = lib.to_dict(payload.body) if payload.body else {}
40
+ data = body.get("data") or {}
41
+
42
+ # Extract tracking details from webhook event data
43
+ tracking = lib.identity(
44
+ _extract_webhook_tracking(data, settings)
45
+ if body.get("eventName") == "shipment.updated" and data.get("trackingNumber")
46
+ else None
47
+ )
48
+ details = models.WebhookEventDetails(
49
+ carrier_id=settings.carrier_id,
50
+ carrier_name=settings.carrier_name,
51
+ tracking_number=data.get("trackingNumber"),
52
+ shipment_identifier=body.get("objectId"),
53
+ tracking=tracking,
54
+ shipment=None,
55
+ )
56
+
57
+ return details, messages
58
+
59
+
60
+ def _extract_webhook_tracking(
61
+ data: typing.Any,
62
+ settings: provider_utils.Settings,
63
+ ) -> models.TrackingDetails:
64
+ """Extract tracking details from webhook event data.
65
+
66
+ Webhook data structure differs from tracking API response - timestamps include
67
+ milliseconds and the data is nested under 'data' in the webhook payload.
68
+ """
69
+
70
+ tracking = lib.to_object(tracking_res.TrackingResponseType, data)
71
+ last_event = next(iter(tracking.events or []), None)
72
+ status = next(
73
+ (
74
+ status.name
75
+ for status in list(provider_units.TrackingStatus)
76
+ if last_event and last_event.code in status.value
77
+ ),
78
+ provider_units.TrackingStatus.in_transit.name,
79
+ )
80
+
81
+ # Teleship webhook timestamps include milliseconds: "2025-11-27T05:48:00.000Z"
82
+ timestamp_formats = ["%Y-%m-%dT%H:%M:%S.%fZ", "%Y-%m-%dT%H:%M:%SZ"]
83
+
84
+ return models.TrackingDetails(
85
+ carrier_id=settings.carrier_id,
86
+ carrier_name=settings.carrier_name,
87
+ tracking_number=tracking.trackingNumber,
88
+ events=[
89
+ models.TrackingEvent(
90
+ date=lib.fdate(event.timestamp, try_formats=timestamp_formats),
91
+ time=lib.flocaltime(event.timestamp, try_formats=timestamp_formats),
92
+ description=event.description,
93
+ code=event.code,
94
+ location=event.location,
95
+ )
96
+ for event in (tracking.events or [])
97
+ ],
98
+ delivered=status == "delivered",
99
+ status=status,
100
+ estimated_delivery=lib.fdate(
101
+ tracking.estimatedDelivery, try_formats=timestamp_formats
102
+ ),
103
+ info=models.TrackingInfo(
104
+ shipment_service=tracking.firstMile.carrier,
105
+ carrier_tracking_link=settings.tracking_url.format(tracking.trackingNumber),
106
+ customer_name=tracking.shipTo.address.city,
107
+ shipment_destination_country=tracking.shipTo.address.country,
108
+ shipment_origin_country=tracking.shipFrom.address.country,
109
+ ),
110
+ meta=dict(
111
+ shipment_id=tracking.shipmentId,
112
+ customer_reference=tracking.customerReference,
113
+ ship_date=tracking.shipDate,
114
+ last_mile_carrier=tracking.lastMile.carrier,
115
+ last_mile_tracking=tracking.lastMile.trackingNumber,
116
+ ),
117
+ )
118
+
119
+
120
+ def verify_webhook_signature(
121
+ payload: models.RequestPayload,
122
+ settings: provider_utils.Settings,
123
+ ) -> bool:
124
+ """Verify the webhook signature using HMAC-SHA256.
125
+
126
+ JavaScript equivalent:
127
+ const crypto = require('crypto');
128
+
129
+ function verifyWebhookSignature(payload, signature, secret) {
130
+ const expectedSignature = crypto
131
+ .createHmac('sha256', secret)
132
+ .update(JSON.stringify(payload))
133
+ .digest('hex');
134
+
135
+ return crypto.timingSafeEqual(
136
+ Buffer.from(signature),
137
+ Buffer.from(expectedSignature)
138
+ );
139
+ }
140
+ """
141
+
142
+ # Get the signature from headers (access dict directly - headers contain hyphens)
143
+ headers = payload.headers or {}
144
+ signature = headers.get("x-teleship-signature")
145
+ secret = settings.connection_config.webhook_secret.state
146
+
147
+ # If no signature provided, skip verification (or return False for strict mode)
148
+ if not signature or not secret:
149
+ return True
150
+
151
+ # Compute expected signature using HMAC-SHA256
152
+ # The payload body should be JSON serialized consistently
153
+ payload_bytes = lib.to_json(payload.body).encode("utf-8")
154
+ secret_bytes = secret.encode("utf-8")
155
+
156
+ expected_signature = hmac.new(
157
+ secret_bytes,
158
+ payload_bytes,
159
+ hashlib.sha256,
160
+ ).hexdigest()
161
+
162
+ # Use constant-time comparison to prevent timing attacks
163
+ return hmac.compare_digest(signature, expected_signature)
@@ -0,0 +1,103 @@
1
+ """Karrio Teleship OAuth processing implementation."""
2
+
3
+ import typing
4
+ import urllib.parse
5
+ import karrio.lib as lib
6
+ import karrio.core.models as models
7
+ import karrio.providers.teleship.error as error
8
+ import karrio.providers.teleship.utils as provider_utils
9
+
10
+
11
+ def on_oauth_authorize(
12
+ payload: models.OAuthAuthorizePayload,
13
+ settings: provider_utils.Settings,
14
+ ) -> typing.Tuple[models.OAuthAuthorizeRequest, typing.List[models.Message]]:
15
+ """Create OAuth authorize request for Teleship.
16
+
17
+ Generates the authorization URL and parameters needed to initiate
18
+ the OAuth flow with Teleship.
19
+ """
20
+ messages: typing.List[models.Message] = []
21
+
22
+ # Get OAuth credentials from system config
23
+ scope = payload.options.get("scope", "read_accounts write_shipments")
24
+
25
+ if not settings.oauth_client_id:
26
+ messages.append(
27
+ models.Message(
28
+ carrier_id=settings.carrier_id,
29
+ carrier_name=settings.carrier_name,
30
+ code="OAUTH_CONFIG_ERROR",
31
+ message="TELESHIP_OAUTH_CLIENT_ID is not configured in system settings",
32
+ )
33
+ )
34
+
35
+ # Build authorization URL parameters
36
+ auth_params = dict(
37
+ redirectUri=payload.redirect_uri,
38
+ state=payload.state,
39
+ responseType="code",
40
+ clientId=settings.oauth_client_id,
41
+ scope=scope,
42
+ )
43
+
44
+ authorization_url = lib.identity(
45
+ f"{settings.server_url}/oauth/authorize?{urllib.parse.urlencode(auth_params)}"
46
+ )
47
+
48
+ return (
49
+ models.OAuthAuthorizeRequest(
50
+ carrier_name=settings.carrier_name,
51
+ authorization_url=authorization_url,
52
+ meta=dict(scope=scope),
53
+ ),
54
+ messages,
55
+ )
56
+
57
+
58
+ def on_oauth_callback(
59
+ payload: models.RequestPayload,
60
+ settings: provider_utils.Settings,
61
+ ) -> typing.Tuple[typing.Optional[typing.Dict], typing.List[models.Message]]:
62
+ """Process OAuth authorization callback.
63
+
64
+ Extracts the authorization code and user credentials from the callback.
65
+ Teleship returns account_client_id and account_client_secret in the callback
66
+ query parameters which are the user's credentials for API access.
67
+ """
68
+ query = payload.query or {}
69
+ messages = error.parse_error_response(payload.body, settings)
70
+
71
+ code = query.get("code")
72
+ account_client_id = query.get("account_client_id")
73
+ account_client_secret = query.get("account_client_secret")
74
+
75
+ if not code:
76
+ messages.append(
77
+ models.Message(
78
+ carrier_id=settings.carrier_id,
79
+ carrier_name=settings.carrier_name,
80
+ code="OAUTH_CALLBACK_ERROR",
81
+ message="No authorization code received in callback",
82
+ )
83
+ )
84
+ return None, messages
85
+
86
+ if not account_client_id or not account_client_secret:
87
+ messages.append(
88
+ models.Message(
89
+ carrier_id=settings.carrier_id,
90
+ carrier_name=settings.carrier_name,
91
+ code="OAUTH_CALLBACK_ERROR",
92
+ message="Missing account credentials in callback",
93
+ )
94
+ )
95
+ return None, messages
96
+
97
+ # Return credentials that map to the Teleship connection settings
98
+ credentials = dict(
99
+ client_id=account_client_id,
100
+ client_secret=account_client_secret,
101
+ )
102
+
103
+ return credentials, messages
@@ -0,0 +1,68 @@
1
+ """Karrio Teleship manifest creation implementation."""
2
+
3
+ import typing
4
+ import karrio.schemas.teleship.manifest_request as teleship
5
+ import karrio.schemas.teleship.manifest_response as manifest
6
+ import karrio.lib as lib
7
+ import karrio.core.models as models
8
+ import karrio.providers.teleship.error as error
9
+ import karrio.providers.teleship.utils as provider_utils
10
+
11
+
12
+ def parse_manifest_response(
13
+ _response: lib.Deserializable[str],
14
+ settings: provider_utils.Settings,
15
+ ) -> typing.Tuple[models.ManifestDetails, typing.List[models.Message]]:
16
+ response = _response.deserialize()
17
+ messages = error.parse_error_response(response, settings)
18
+ details = lib.to_object(manifest.ManifestResponseType, response)
19
+
20
+ manifest_details = lib.identity(
21
+ models.ManifestDetails(
22
+ carrier_id=settings.carrier_id,
23
+ carrier_name=settings.carrier_name,
24
+ id=details.id,
25
+ doc=None,
26
+ meta=dict(
27
+ status=details.status,
28
+ reference=details.reference,
29
+ createdAt=details.createdAt,
30
+ ),
31
+ )
32
+ if details and details.id
33
+ else None
34
+ )
35
+
36
+ return manifest_details, messages
37
+
38
+
39
+ def manifest_request(
40
+ payload: models.ManifestRequest,
41
+ settings: provider_utils.Settings,
42
+ ) -> lib.Serializable:
43
+ address = lib.to_address(payload.address)
44
+
45
+ request = teleship.ManifestRequestType(
46
+ shipmentIds=payload.shipment_identifiers,
47
+ reference=payload.reference,
48
+ address=lib.identity(
49
+ teleship.ManifestRequestAddressType(
50
+ name=address.person_name,
51
+ email=address.email,
52
+ phone=address.phone_number,
53
+ company=address.company_name,
54
+ address=teleship.AddressAddressType(
55
+ line1=address.address_line1,
56
+ line2=address.address_line2,
57
+ city=address.city,
58
+ state=address.state_code,
59
+ postcode=address.postal_code,
60
+ country=address.country_code,
61
+ ),
62
+ )
63
+ if address
64
+ else None
65
+ ),
66
+ )
67
+
68
+ return lib.Serializable(request, lib.to_dict)
@@ -0,0 +1,8 @@
1
+ from karrio.providers.teleship.pickup.schedule import (
2
+ pickup_request,
3
+ parse_pickup_response,
4
+ )
5
+ from karrio.providers.teleship.pickup.cancel import (
6
+ cancel_pickup_request,
7
+ parse_cancel_pickup_response,
8
+ )
@@ -0,0 +1,43 @@
1
+ """Karrio Teleship pickup cancellation implementation."""
2
+
3
+ import typing
4
+ import karrio.lib as lib
5
+ import karrio.core.models as models
6
+ import karrio.providers.teleship.error as error
7
+ import karrio.providers.teleship.utils as provider_utils
8
+
9
+
10
+ def parse_cancel_pickup_response(
11
+ _response: lib.Deserializable[str],
12
+ settings: provider_utils.Settings,
13
+ ) -> typing.Tuple[models.ConfirmationDetails, typing.List[models.Message]]:
14
+ response = _response.deserialize()
15
+ messages = error.parse_error_response(response, settings)
16
+
17
+ success = not any(messages) and (
18
+ isinstance(response, dict) and response.get("success") is not False
19
+ or response == ""
20
+ or response is None
21
+ )
22
+
23
+ confirmation = (
24
+ models.ConfirmationDetails(
25
+ carrier_id=settings.carrier_id,
26
+ carrier_name=settings.carrier_name,
27
+ success=success,
28
+ operation="pickup_cancellation",
29
+ )
30
+ if success
31
+ else None
32
+ )
33
+
34
+ return confirmation, messages
35
+
36
+
37
+ def cancel_pickup_request(
38
+ payload: models.PickupCancelRequest,
39
+ settings: provider_utils.Settings,
40
+ ) -> lib.Serializable:
41
+ request = {"pickupId": payload.confirmation_number}
42
+
43
+ return lib.Serializable(request, lib.to_dict)
@@ -0,0 +1,66 @@
1
+ """Karrio Teleship pickup scheduling implementation."""
2
+
3
+ import typing
4
+ import karrio.schemas.teleship.pickup_request as teleship
5
+ import karrio.schemas.teleship.pickup_response as pickup
6
+ import karrio.lib as lib
7
+ import karrio.core.models as models
8
+ import karrio.providers.teleship.error as error
9
+ import karrio.providers.teleship.utils as provider_utils
10
+
11
+
12
+ def parse_pickup_response(
13
+ _response: lib.Deserializable[str],
14
+ settings: provider_utils.Settings,
15
+ ) -> typing.Tuple[models.PickupDetails, typing.List[models.Message]]:
16
+ response = _response.deserialize()
17
+ messages = error.parse_error_response(response, settings)
18
+ details = lib.to_object(pickup.PickupResponseType, response)
19
+
20
+ pickup_details = lib.identity(
21
+ models.PickupDetails(
22
+ carrier_id=settings.carrier_id,
23
+ carrier_name=settings.carrier_name,
24
+ confirmation_number=details.pickup.id,
25
+ pickup_date=lib.fdatetime(
26
+ details.pickup.startAt,
27
+ current_format="%Y-%m-%dT%H:%M:%S.%fZ",
28
+ output_format="%Y-%m-%d",
29
+ ),
30
+ meta=lib.to_dict(details.pickup),
31
+ )
32
+ if details and details.pickup and details.pickup.id
33
+ else None
34
+ )
35
+
36
+ return pickup_details, messages
37
+
38
+
39
+ def pickup_request(
40
+ payload: models.PickupRequest,
41
+ settings: provider_utils.Settings,
42
+ ) -> lib.Serializable:
43
+ address = lib.to_address(payload.address)
44
+
45
+ request = teleship.PickupRequestType(
46
+ startAt=payload.pickup_date,
47
+ endAt=payload.closing_time or payload.pickup_date,
48
+ shipmentIds=payload.shipment_identifiers,
49
+ address=teleship.PickupRequestAddressType(
50
+ name=address.person_name,
51
+ email=address.email,
52
+ phone=address.phone_number,
53
+ company=address.company_name,
54
+ address=teleship.AddressAddressType(
55
+ line1=address.address_line1,
56
+ line2=address.address_line2,
57
+ city=address.city,
58
+ state=address.state_code,
59
+ postcode=address.postal_code,
60
+ country=address.country_code,
61
+ ),
62
+ ),
63
+ reference=payload.instruction,
64
+ )
65
+
66
+ return lib.Serializable(request, lib.to_dict)