karrio-fedex 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.
Files changed (42) hide show
  1. karrio/mappers/fedex/__init__.py +3 -0
  2. karrio/mappers/fedex/mapper.py +89 -0
  3. karrio/mappers/fedex/proxy.py +144 -0
  4. karrio/mappers/fedex/settings.py +24 -0
  5. karrio/plugins/fedex/__init__.py +23 -0
  6. karrio/providers/fedex/__init__.py +24 -0
  7. karrio/providers/fedex/document.py +88 -0
  8. karrio/providers/fedex/error.py +66 -0
  9. karrio/providers/fedex/pickup/__init__.py +9 -0
  10. karrio/providers/fedex/pickup/cancel.py +79 -0
  11. karrio/providers/fedex/pickup/create.py +148 -0
  12. karrio/providers/fedex/pickup/update.py +162 -0
  13. karrio/providers/fedex/rate.py +357 -0
  14. karrio/providers/fedex/shipment/__init__.py +9 -0
  15. karrio/providers/fedex/shipment/cancel.py +46 -0
  16. karrio/providers/fedex/shipment/create.py +748 -0
  17. karrio/providers/fedex/tracking.py +154 -0
  18. karrio/providers/fedex/units.py +501 -0
  19. karrio/providers/fedex/utils.py +199 -0
  20. karrio/schemas/fedex/__init__.py +0 -0
  21. karrio/schemas/fedex/cancel_pickup_request.py +31 -0
  22. karrio/schemas/fedex/cancel_pickup_response.py +24 -0
  23. karrio/schemas/fedex/cancel_request.py +17 -0
  24. karrio/schemas/fedex/cancel_response.py +25 -0
  25. karrio/schemas/fedex/error_response.py +16 -0
  26. karrio/schemas/fedex/paperless_request.py +30 -0
  27. karrio/schemas/fedex/paperless_response.py +21 -0
  28. karrio/schemas/fedex/pickup_request.py +106 -0
  29. karrio/schemas/fedex/pickup_response.py +25 -0
  30. karrio/schemas/fedex/rating_request.py +478 -0
  31. karrio/schemas/fedex/rating_responses.py +208 -0
  32. karrio/schemas/fedex/shipping_request.py +731 -0
  33. karrio/schemas/fedex/shipping_responses.py +584 -0
  34. karrio/schemas/fedex/tracking_document_request.py +30 -0
  35. karrio/schemas/fedex/tracking_document_response.py +30 -0
  36. karrio/schemas/fedex/tracking_request.py +23 -0
  37. karrio/schemas/fedex/tracking_response.py +350 -0
  38. karrio_fedex-2025.5rc1.dist-info/METADATA +45 -0
  39. karrio_fedex-2025.5rc1.dist-info/RECORD +42 -0
  40. karrio_fedex-2025.5rc1.dist-info/WHEEL +5 -0
  41. karrio_fedex-2025.5rc1.dist-info/entry_points.txt +2 -0
  42. karrio_fedex-2025.5rc1.dist-info/top_level.txt +3 -0
@@ -0,0 +1,199 @@
1
+ import karrio.schemas.fedex.tracking_document_request as fedex
2
+ import gzip
3
+ import typing
4
+ import datetime
5
+ import urllib.parse
6
+ import karrio.lib as lib
7
+ import karrio.core as core
8
+ import karrio.core.errors as errors
9
+
10
+
11
+ class Settings(core.Settings):
12
+ """FedEx connection settings."""
13
+
14
+ api_key: str = None
15
+ secret_key: str = None
16
+ account_number: str = None
17
+ track_api_key: str = None
18
+ track_secret_key: str = None
19
+
20
+ account_country_code: str = None
21
+ metadata: dict = {}
22
+ config: dict = {}
23
+ id: str = None
24
+
25
+ @property
26
+ def carrier_name(self):
27
+ return "fedex"
28
+
29
+ @property
30
+ def server_url(self):
31
+ return (
32
+ "https://apis-sandbox.fedex.com"
33
+ if self.test_mode
34
+ else "https://apis.fedex.com"
35
+ )
36
+
37
+ @property
38
+ def tracking_url(self):
39
+ return "https://www.fedex.com/fedextrack/?trknbr={}"
40
+
41
+ @property
42
+ def connection_config(self) -> lib.units.Options:
43
+ from karrio.providers.fedex.units import ConnectionConfig
44
+
45
+ return lib.to_connection_config(
46
+ self.config or {},
47
+ option_type=ConnectionConfig,
48
+ )
49
+
50
+ @property
51
+ def default_currency(self) -> typing.Optional[str]:
52
+ return lib.units.CountryCurrency.map(self.account_country_code).value
53
+
54
+ @property
55
+ def access_token(self):
56
+ """Retrieve the access_token using the api_key|secret_key pair
57
+ or collect it from the cache if an unexpired access_token exist.
58
+ """
59
+ if not all([self.api_key, self.secret_key, self.account_number]):
60
+ raise Exception(
61
+ "The api_key, secret_key and account_number are required for Rate, Ship and Other API requests."
62
+ )
63
+
64
+ cache_key = f"{self.carrier_name}|{self.api_key}|{self.secret_key}"
65
+ now = datetime.datetime.now() + datetime.timedelta(minutes=30)
66
+
67
+ auth = self.connection_cache.get(cache_key) or {}
68
+ token = auth.get("access_token")
69
+ expiry = lib.to_date(auth.get("expiry"), current_format="%Y-%m-%d %H:%M:%S")
70
+
71
+ if token is not None and expiry is not None and expiry > now:
72
+ return token
73
+
74
+ self.connection_cache.set(
75
+ cache_key,
76
+ lambda: login(
77
+ self,
78
+ client_id=self.api_key,
79
+ client_secret=self.secret_key,
80
+ ),
81
+ )
82
+ new_auth = self.connection_cache.get(cache_key)
83
+
84
+ return new_auth["access_token"]
85
+
86
+ @property
87
+ def track_access_token(self):
88
+ """Retrieve the access_token using the track_api_key|track_secret_key pair
89
+ or collect it from the cache if an unexpired access_token exist.
90
+ """
91
+ if not all([self.track_api_key, self.track_secret_key]):
92
+ raise Exception(
93
+ "The track_api_key and track_secret_key are required for Track API requests."
94
+ )
95
+
96
+ cache_key = f"{self.carrier_name}|{self.track_api_key}|{self.track_secret_key}"
97
+ now = datetime.datetime.now() + datetime.timedelta(minutes=30)
98
+
99
+ auth = self.connection_cache.get(cache_key) or {}
100
+ token = auth.get("access_token")
101
+ expiry = lib.to_date(auth.get("expiry"), current_format="%Y-%m-%d %H:%M:%S")
102
+
103
+ if token is not None and expiry is not None and expiry > now:
104
+ return token
105
+
106
+ self.connection_cache.set(
107
+ cache_key,
108
+ lambda: login(
109
+ self,
110
+ client_id=self.track_api_key,
111
+ client_secret=self.track_secret_key,
112
+ ),
113
+ )
114
+ new_auth = self.connection_cache.get(cache_key)
115
+
116
+ return new_auth["access_token"]
117
+
118
+
119
+ def login(settings: Settings, client_id: str = None, client_secret: str = None):
120
+ import karrio.providers.fedex.error as error
121
+
122
+ result = lib.request(
123
+ url=f"{settings.server_url}/oauth/token",
124
+ method="POST",
125
+ headers={
126
+ "content-Type": "application/x-www-form-urlencoded",
127
+ },
128
+ data=urllib.parse.urlencode(
129
+ dict(
130
+ grant_type="client_credentials",
131
+ client_id=client_id,
132
+ client_secret=client_secret,
133
+ )
134
+ ),
135
+ )
136
+
137
+ response = lib.to_dict(result)
138
+ messages = error.parse_error_response(response, settings)
139
+
140
+ if any(messages):
141
+ raise errors.ParsedMessagesError(messages)
142
+
143
+ expiry = datetime.datetime.now() + datetime.timedelta(
144
+ seconds=float(response.get("expires_in", 0))
145
+ )
146
+
147
+ return {**response, "expiry": lib.fdatetime(expiry)}
148
+
149
+
150
+ def get_proof_of_delivery(tracking_number: str, settings: Settings):
151
+ import karrio.providers.fedex.error as error
152
+
153
+ request = fedex.TrackingDocumentRequestType(
154
+ trackDocumentSpecification=[
155
+ fedex.TrackDocumentSpecificationType(
156
+ trackingNumberInfo=fedex.TrackingNumberInfoType(
157
+ trackingNumber=tracking_number
158
+ )
159
+ )
160
+ ],
161
+ trackDocumentDetail=fedex.TrackDocumentDetailType(
162
+ documentType="SIGNATURE_PROOF_OF_DELIVERY",
163
+ documentFormat="PNG",
164
+ ),
165
+ )
166
+ response = lib.to_dict(
167
+ lib.request(
168
+ url=f"{settings.server_url}/track/v1/trackingdocuments",
169
+ data=lib.to_json(request),
170
+ method="POST",
171
+ decoder=parse_response,
172
+ on_error=lambda b: parse_response(b.read()),
173
+ )
174
+ )
175
+
176
+ messages = error.parse_error_response(response, settings)
177
+
178
+ if any(messages):
179
+ return None
180
+
181
+ return lib.failsafe(
182
+ lambda: lib.bundle_base64(response["output"]["documents"], format="PNG")
183
+ )
184
+
185
+
186
+ def parse_response(binary_string):
187
+ content = lib.failsafe(lambda: gzip.decompress(binary_string)) or binary_string
188
+ return lib.decode(content)
189
+
190
+
191
+ def state_code(address: lib.units.ComputedAddress) -> str:
192
+ if address.state_code is None:
193
+ return None
194
+
195
+ return (
196
+ "PQ"
197
+ if address.state_code.lower() == "qc" and address.country_code == "CA"
198
+ else address.state_code
199
+ )
File without changes
@@ -0,0 +1,31 @@
1
+ import attr
2
+ import jstruct
3
+ import typing
4
+
5
+
6
+ @attr.s(auto_attribs=True)
7
+ class AccountAddressOfRecordType:
8
+ streetLines: typing.Optional[typing.List[str]] = None
9
+ urbanizationCode: typing.Optional[str] = None
10
+ city: typing.Optional[str] = None
11
+ stateOrProvinceCode: typing.Optional[str] = None
12
+ postalCode: typing.Optional[int] = None
13
+ countryCode: typing.Optional[str] = None
14
+ residential: typing.Optional[bool] = None
15
+ addressClassification: typing.Optional[str] = None
16
+
17
+
18
+ @attr.s(auto_attribs=True)
19
+ class AssociatedAccountNumberType:
20
+ value: typing.Optional[str] = None
21
+
22
+
23
+ @attr.s(auto_attribs=True)
24
+ class CancelPickupRequestType:
25
+ associatedAccountNumber: typing.Optional[AssociatedAccountNumberType] = jstruct.JStruct[AssociatedAccountNumberType]
26
+ pickupConfirmationCode: typing.Optional[int] = None
27
+ remarks: typing.Optional[str] = None
28
+ carrierCode: typing.Optional[str] = None
29
+ accountAddressOfRecord: typing.Optional[AccountAddressOfRecordType] = jstruct.JStruct[AccountAddressOfRecordType]
30
+ scheduledDate: typing.Optional[str] = None
31
+ location: typing.Optional[str] = None
@@ -0,0 +1,24 @@
1
+ import attr
2
+ import jstruct
3
+ import typing
4
+
5
+
6
+ @attr.s(auto_attribs=True)
7
+ class AlertType:
8
+ code: typing.Optional[str] = None
9
+ alertType: typing.Optional[str] = None
10
+ message: typing.Optional[str] = None
11
+
12
+
13
+ @attr.s(auto_attribs=True)
14
+ class OutputType:
15
+ pickupConfirmationCode: typing.Optional[str] = None
16
+ cancelConfirmationMessage: typing.Optional[str] = None
17
+ alerts: typing.Optional[typing.List[AlertType]] = jstruct.JList[AlertType]
18
+
19
+
20
+ @attr.s(auto_attribs=True)
21
+ class CancelPickupResponseType:
22
+ transactionId: typing.Optional[str] = None
23
+ customerTransactionId: typing.Optional[str] = None
24
+ output: typing.Optional[OutputType] = jstruct.JStruct[OutputType]
@@ -0,0 +1,17 @@
1
+ import attr
2
+ import jstruct
3
+ import typing
4
+
5
+
6
+ @attr.s(auto_attribs=True)
7
+ class AccountNumberType:
8
+ value: typing.Optional[str] = None
9
+
10
+
11
+ @attr.s(auto_attribs=True)
12
+ class CancelRequestType:
13
+ accountNumber: typing.Optional[AccountNumberType] = jstruct.JStruct[AccountNumberType]
14
+ emailShipment: typing.Optional[bool] = None
15
+ senderCountryCode: typing.Optional[str] = None
16
+ deletionControl: typing.Optional[str] = None
17
+ trackingNumber: typing.Optional[str] = None
@@ -0,0 +1,25 @@
1
+ import attr
2
+ import jstruct
3
+ import typing
4
+
5
+
6
+ @attr.s(auto_attribs=True)
7
+ class AlertType:
8
+ code: typing.Optional[str] = None
9
+ alertType: typing.Optional[str] = None
10
+ message: typing.Optional[str] = None
11
+
12
+
13
+ @attr.s(auto_attribs=True)
14
+ class OutputType:
15
+ cancelledShipment: typing.Optional[bool] = None
16
+ cancelledHistory: typing.Optional[bool] = None
17
+ successMessage: typing.Optional[str] = None
18
+ alerts: typing.Optional[typing.List[AlertType]] = jstruct.JList[AlertType]
19
+
20
+
21
+ @attr.s(auto_attribs=True)
22
+ class CancelResponseType:
23
+ transactionId: typing.Optional[str] = None
24
+ customerTransactionId: typing.Optional[str] = None
25
+ output: typing.Optional[OutputType] = jstruct.JStruct[OutputType]
@@ -0,0 +1,16 @@
1
+ import attr
2
+ import jstruct
3
+ import typing
4
+
5
+
6
+ @attr.s(auto_attribs=True)
7
+ class ErrorType:
8
+ code: typing.Optional[str] = None
9
+ message: typing.Optional[str] = None
10
+
11
+
12
+ @attr.s(auto_attribs=True)
13
+ class ErrorResponseType:
14
+ transactionId: typing.Optional[str] = None
15
+ customerTransactionId: typing.Optional[str] = None
16
+ errors: typing.Optional[typing.List[ErrorType]] = jstruct.JList[ErrorType]
@@ -0,0 +1,30 @@
1
+ import attr
2
+ import jstruct
3
+ import typing
4
+
5
+
6
+ @attr.s(auto_attribs=True)
7
+ class MetaType:
8
+ shipDocumentType: typing.Optional[str] = None
9
+ formCode: typing.Optional[str] = None
10
+ trackingNumber: typing.Optional[str] = None
11
+ shipmentDate: typing.Optional[str] = None
12
+ originLocationCode: typing.Optional[str] = None
13
+ originCountryCode: typing.Optional[str] = None
14
+ destinationLocationCode: typing.Optional[str] = None
15
+ destinationCountryCode: typing.Optional[str] = None
16
+
17
+
18
+ @attr.s(auto_attribs=True)
19
+ class DocumentType:
20
+ workflowName: typing.Optional[str] = None
21
+ carrierCode: typing.Optional[str] = None
22
+ name: typing.Optional[str] = None
23
+ contentType: typing.Optional[str] = None
24
+ meta: typing.Optional[MetaType] = jstruct.JStruct[MetaType]
25
+
26
+
27
+ @attr.s(auto_attribs=True)
28
+ class PaperlessRequestType:
29
+ document: typing.Optional[DocumentType] = jstruct.JStruct[DocumentType]
30
+ attachment: typing.Optional[str] = None
@@ -0,0 +1,21 @@
1
+ import attr
2
+ import jstruct
3
+ import typing
4
+
5
+
6
+ @attr.s(auto_attribs=True)
7
+ class MetaType:
8
+ documentType: typing.Optional[str] = None
9
+ docId: typing.Optional[str] = None
10
+ folderId: typing.Optional[typing.List[str]] = None
11
+
12
+
13
+ @attr.s(auto_attribs=True)
14
+ class OutputType:
15
+ meta: typing.Optional[MetaType] = jstruct.JStruct[MetaType]
16
+
17
+
18
+ @attr.s(auto_attribs=True)
19
+ class PaperlessResponseType:
20
+ output: typing.Optional[OutputType] = jstruct.JStruct[OutputType]
21
+ customerTransactionId: typing.Optional[str] = None
@@ -0,0 +1,106 @@
1
+ import attr
2
+ import jstruct
3
+ import typing
4
+
5
+
6
+ @attr.s(auto_attribs=True)
7
+ class AccountAddressOfRecordType:
8
+ streetLines: typing.Optional[typing.List[str]] = None
9
+ city: typing.Optional[str] = None
10
+ stateOrProvinceCode: typing.Optional[str] = None
11
+ postalCode: typing.Optional[int] = None
12
+ countryCode: typing.Optional[str] = None
13
+ residential: typing.Optional[bool] = None
14
+ addressClassification: typing.Optional[str] = None
15
+ urbanizationCode: typing.Optional[str] = None
16
+
17
+
18
+ @attr.s(auto_attribs=True)
19
+ class AccountNumberType:
20
+ value: typing.Optional[str] = None
21
+
22
+
23
+ @attr.s(auto_attribs=True)
24
+ class DimensionsType:
25
+ length: typing.Optional[int] = None
26
+ width: typing.Optional[int] = None
27
+ height: typing.Optional[int] = None
28
+ units: typing.Optional[str] = None
29
+
30
+
31
+ @attr.s(auto_attribs=True)
32
+ class ExpressFreightDetailType:
33
+ truckType: typing.Optional[str] = None
34
+ service: typing.Optional[str] = None
35
+ trailerLength: typing.Optional[str] = None
36
+ bookingNumber: typing.Optional[str] = None
37
+ dimensions: typing.Optional[DimensionsType] = jstruct.JStruct[DimensionsType]
38
+
39
+
40
+ @attr.s(auto_attribs=True)
41
+ class ContactType:
42
+ companyName: typing.Optional[str] = None
43
+ personName: typing.Optional[str] = None
44
+ phoneNumber: typing.Optional[str] = None
45
+ phoneExtension: typing.Optional[str] = None
46
+
47
+
48
+ @attr.s(auto_attribs=True)
49
+ class PickupLocationType:
50
+ contact: typing.Optional[ContactType] = jstruct.JStruct[ContactType]
51
+ address: typing.Optional[AccountAddressOfRecordType] = jstruct.JStruct[AccountAddressOfRecordType]
52
+ accountNumber: typing.Optional[AccountNumberType] = jstruct.JStruct[AccountNumberType]
53
+ deliveryInstructions: typing.Optional[str] = None
54
+
55
+
56
+ @attr.s(auto_attribs=True)
57
+ class OriginDetailType:
58
+ pickupAddressType: typing.Optional[str] = None
59
+ pickupLocation: typing.Optional[PickupLocationType] = jstruct.JStruct[PickupLocationType]
60
+ readyDateTimestamp: typing.Optional[str] = None
61
+ customerCloseTime: typing.Optional[str] = None
62
+ pickupDateType: typing.Optional[str] = None
63
+ packageLocation: typing.Optional[str] = None
64
+ buildingPart: typing.Optional[str] = None
65
+ buildingPartDescription: typing.Optional[int] = None
66
+ earlyPickup: typing.Optional[bool] = None
67
+ suppliesRequested: typing.Optional[str] = None
68
+ geographicalPostalCode: typing.Optional[str] = None
69
+
70
+
71
+ @attr.s(auto_attribs=True)
72
+ class EmailDetailType:
73
+ address: typing.Optional[str] = None
74
+ locale: typing.Optional[str] = None
75
+
76
+
77
+ @attr.s(auto_attribs=True)
78
+ class PickupNotificationDetailType:
79
+ emailDetails: typing.Optional[typing.List[EmailDetailType]] = jstruct.JList[EmailDetailType]
80
+ format: typing.Optional[str] = None
81
+ userMessage: typing.Optional[str] = None
82
+
83
+
84
+ @attr.s(auto_attribs=True)
85
+ class TotalWeightType:
86
+ units: typing.Optional[str] = None
87
+ value: typing.Optional[int] = None
88
+
89
+
90
+ @attr.s(auto_attribs=True)
91
+ class PickupRequestType:
92
+ associatedAccountNumber: typing.Optional[AccountNumberType] = jstruct.JStruct[AccountNumberType]
93
+ originDetail: typing.Optional[OriginDetailType] = jstruct.JStruct[OriginDetailType]
94
+ associatedAccountNumberType: typing.Optional[str] = None
95
+ totalWeight: typing.Optional[TotalWeightType] = jstruct.JStruct[TotalWeightType]
96
+ packageCount: typing.Optional[int] = None
97
+ carrierCode: typing.Optional[str] = None
98
+ accountAddressOfRecord: typing.Optional[AccountAddressOfRecordType] = jstruct.JStruct[AccountAddressOfRecordType]
99
+ remarks: typing.Optional[str] = None
100
+ countryRelationships: typing.Optional[str] = None
101
+ pickupType: typing.Optional[str] = None
102
+ trackingNumber: typing.Optional[str] = None
103
+ commodityDescription: typing.Optional[str] = None
104
+ expressFreightDetail: typing.Optional[ExpressFreightDetailType] = jstruct.JStruct[ExpressFreightDetailType]
105
+ oversizePackageCount: typing.Optional[int] = None
106
+ pickupNotificationDetail: typing.Optional[PickupNotificationDetailType] = jstruct.JStruct[PickupNotificationDetailType]
@@ -0,0 +1,25 @@
1
+ import attr
2
+ import jstruct
3
+ import typing
4
+
5
+
6
+ @attr.s(auto_attribs=True)
7
+ class AlertType:
8
+ code: typing.Optional[str] = None
9
+ alertType: typing.Optional[str] = None
10
+ message: typing.Optional[str] = None
11
+
12
+
13
+ @attr.s(auto_attribs=True)
14
+ class OutputType:
15
+ pickupConfirmationCode: typing.Optional[int] = None
16
+ message: typing.Optional[str] = None
17
+ location: typing.Optional[str] = None
18
+ alerts: typing.Optional[typing.List[AlertType]] = jstruct.JList[AlertType]
19
+
20
+
21
+ @attr.s(auto_attribs=True)
22
+ class PickupResponseType:
23
+ transactionId: typing.Optional[str] = None
24
+ customerTransactionId: typing.Optional[str] = None
25
+ output: typing.Optional[OutputType] = jstruct.JStruct[OutputType]