karrio-usps 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 (37) hide show
  1. karrio/mappers/usps/__init__.py +3 -0
  2. karrio/mappers/usps/mapper.py +94 -0
  3. karrio/mappers/usps/proxy.py +155 -0
  4. karrio/mappers/usps/settings.py +26 -0
  5. karrio/plugins/usps/__init__.py +24 -0
  6. karrio/providers/usps/__init__.py +26 -0
  7. karrio/providers/usps/error.py +56 -0
  8. karrio/providers/usps/manifest.py +100 -0
  9. karrio/providers/usps/pickup/__init__.py +4 -0
  10. karrio/providers/usps/pickup/cancel.py +40 -0
  11. karrio/providers/usps/pickup/create.py +102 -0
  12. karrio/providers/usps/pickup/update.py +109 -0
  13. karrio/providers/usps/rate.py +204 -0
  14. karrio/providers/usps/shipment/__init__.py +9 -0
  15. karrio/providers/usps/shipment/cancel.py +53 -0
  16. karrio/providers/usps/shipment/create.py +279 -0
  17. karrio/providers/usps/tracking.py +112 -0
  18. karrio/providers/usps/units.py +303 -0
  19. karrio/providers/usps/utils.py +320 -0
  20. karrio/schemas/usps/__init__.py +0 -0
  21. karrio/schemas/usps/error_response.py +31 -0
  22. karrio/schemas/usps/label_request.py +142 -0
  23. karrio/schemas/usps/label_response.py +84 -0
  24. karrio/schemas/usps/pickup_request.py +49 -0
  25. karrio/schemas/usps/pickup_response.py +58 -0
  26. karrio/schemas/usps/pickup_update_request.py +55 -0
  27. karrio/schemas/usps/pickup_update_response.py +58 -0
  28. karrio/schemas/usps/rate_request.py +38 -0
  29. karrio/schemas/usps/rate_response.py +89 -0
  30. karrio/schemas/usps/scan_form_request.py +36 -0
  31. karrio/schemas/usps/scan_form_response.py +46 -0
  32. karrio/schemas/usps/tracking_response.py +113 -0
  33. karrio_usps-2025.5rc1.dist-info/METADATA +45 -0
  34. karrio_usps-2025.5rc1.dist-info/RECORD +37 -0
  35. karrio_usps-2025.5rc1.dist-info/WHEEL +5 -0
  36. karrio_usps-2025.5rc1.dist-info/entry_points.txt +2 -0
  37. karrio_usps-2025.5rc1.dist-info/top_level.txt +3 -0
@@ -0,0 +1,320 @@
1
+ """USPS connection settings."""
2
+
3
+ import re
4
+ import typing
5
+ import datetime
6
+ import karrio.lib as lib
7
+ import karrio.core as core
8
+ import karrio.core.errors as errors
9
+
10
+ AccountType = lib.units.create_enum(
11
+ "AccountType",
12
+ ["EPS", "PERMIT", "METER"],
13
+ )
14
+
15
+
16
+ class Settings(core.Settings):
17
+ """USPS connection settings."""
18
+
19
+ # Add carrier specific api connection properties here
20
+ client_id: str
21
+ client_secret: str
22
+ account_number: str = None
23
+ account_type: AccountType = "EPS" # type: ignore
24
+ manifest_MID: str = None
25
+ CRID: str = None
26
+ MID: str = None
27
+
28
+ @property
29
+ def carrier_name(self):
30
+ return "usps"
31
+
32
+ @property
33
+ def server_url(self):
34
+ return "https://api-cat.usps.com" if self.test_mode else "https://apis.usps.com"
35
+
36
+ @property
37
+ def tracking_url(self):
38
+ return "https://tools.usps.com/go/TrackConfirmAction?tLabels={}"
39
+
40
+ @property
41
+ def connection_config(self) -> lib.units.Options:
42
+ return lib.to_connection_config(
43
+ self.config or {},
44
+ option_type=ConnectionConfig,
45
+ )
46
+
47
+ @property
48
+ def access_token(self):
49
+ """Retrieve the access_token using the client_id|client_secret pair
50
+ or collect it from the cache if an unexpired access_token exist.
51
+ """
52
+ cache_key = f"access|{self.carrier_name}|{self.client_id}|{self.client_secret}"
53
+ now = datetime.datetime.now() + datetime.timedelta(minutes=30)
54
+
55
+ auth = self.connection_cache.get(cache_key) or {}
56
+ token = auth.get("access_token")
57
+ expiry = lib.to_date(auth.get("expiry"), current_format="%Y-%m-%d %H:%M:%S")
58
+
59
+ if token is not None and expiry is not None and expiry > now:
60
+ return token
61
+
62
+ self.connection_cache.set(cache_key, lambda: oauth2_login(self))
63
+ new_auth = self.connection_cache.get(cache_key)
64
+
65
+ return new_auth["access_token"]
66
+
67
+ @property
68
+ def payment_token(self):
69
+ """Retrieve the paymentAuthorizationToken using the client_id|client_secret pair
70
+ or collect it from the cache if an unexpired paymentAuthorizationToken exist.
71
+ """
72
+ cache_key = f"payment|{self.carrier_name}|{self.client_id}|{self.client_secret}"
73
+ now = datetime.datetime.now() + datetime.timedelta(minutes=45)
74
+
75
+ auth = self.connection_cache.get(cache_key) or {}
76
+ token = auth.get("paymentAuthorizationToken")
77
+ expiry = lib.to_date(auth.get("expiry"), current_format="%Y-%m-%d %H:%M:%S")
78
+
79
+ if token is not None and expiry is not None and expiry > now:
80
+ return token
81
+
82
+ self.connection_cache.set(cache_key, lambda: payment_auth(self))
83
+ new_auth = self.connection_cache.get(cache_key)
84
+
85
+ return new_auth["paymentAuthorizationToken"]
86
+
87
+
88
+ class ConnectionConfig(lib.Enum):
89
+ permit_ZIP = lib.OptionEnum("permit_ZIP")
90
+ permit_number = lib.OptionEnum("permit_number")
91
+ shipping_options = lib.OptionEnum("shipping_options", list)
92
+ shipping_services = lib.OptionEnum("shipping_services", list)
93
+
94
+
95
+ def oauth2_login(settings: Settings):
96
+ import karrio.providers.usps.error as error
97
+
98
+ API_SCOPES = [
99
+ "addresses",
100
+ "international-prices",
101
+ "subscriptions",
102
+ "payments",
103
+ "pickup",
104
+ "tracking",
105
+ "labels",
106
+ "scan-forms",
107
+ "companies",
108
+ "service-delivery-standards",
109
+ "locations",
110
+ "international-labels",
111
+ "prices",
112
+ "shipments",
113
+ ]
114
+ result = lib.request(
115
+ url=f"{settings.server_url}/oauth2/v3/token",
116
+ method="POST",
117
+ headers={"content-Type": "application/x-www-form-urlencoded"},
118
+ data=lib.to_query_string(
119
+ dict(
120
+ grant_type="client_credentials",
121
+ client_id=settings.client_id,
122
+ client_secret=settings.client_secret,
123
+ scope=" ".join(API_SCOPES),
124
+ )
125
+ ),
126
+ )
127
+
128
+ response = lib.to_dict(result)
129
+ messages = error.parse_error_response(response, settings)
130
+
131
+ if any(messages):
132
+ raise errors.ParsedMessagesError(messages)
133
+
134
+ expiry = datetime.datetime.now() + datetime.timedelta(
135
+ seconds=float(response.get("expires_in", 0))
136
+ )
137
+
138
+ return {**response, "expiry": lib.fdatetime(expiry)}
139
+
140
+
141
+ def payment_auth(settings: Settings):
142
+ import karrio.providers.usps.error as error
143
+
144
+ result = lib.request(
145
+ url=f"{settings.server_url}/payments/v3/payment-authorization",
146
+ method="POST",
147
+ headers={
148
+ "content-Type": "application/json",
149
+ "Authorization": f"Bearer {settings.access_token}",
150
+ },
151
+ data=lib.to_json(
152
+ {
153
+ "roles": [
154
+ {
155
+ "roleName": "LABEL_OWNER",
156
+ "CRID": settings.CRID,
157
+ "MID": settings.MID,
158
+ "accountType": settings.account_type or "EPS",
159
+ "accountNumber": settings.account_number,
160
+ "manifestMID": settings.manifest_MID,
161
+ # "permitNumber": settings.connection_config.permit_number.state,
162
+ # "permitZIP": settings.connection_config.permit_ZIP.state,
163
+ },
164
+ {
165
+ "roleName": "PAYER",
166
+ "CRID": settings.CRID,
167
+ "MID": settings.MID,
168
+ "accountType": settings.account_type or "EPS",
169
+ "accountNumber": settings.account_number,
170
+ # "permitNumber": settings.connection_config.permit_number.state,
171
+ # "permitZIP": settings.connection_config.permit_zip.state,
172
+ },
173
+ ]
174
+ }
175
+ ),
176
+ )
177
+
178
+ response = lib.to_dict(result)
179
+ messages = error.parse_error_response(response, settings)
180
+
181
+ if any(messages):
182
+ raise errors.ParsedMessagesError(messages)
183
+
184
+ expiry = datetime.datetime.now() + datetime.timedelta(minutes=50)
185
+
186
+ return {**response, "expiry": lib.fdatetime(expiry)}
187
+
188
+
189
+ def normalize_multipart_response(response: str) -> str:
190
+ """Normalize a multipart response string to have consistent formatting"""
191
+ # Find boundary
192
+ boundary_match = re.search(r"--[a-zA-Z0-9\+/=_-]+", response)
193
+ if not boundary_match:
194
+ return response
195
+
196
+ boundary = boundary_match.group(0)
197
+
198
+ # Split into parts using boundary
199
+ parts = response.split(boundary)
200
+
201
+ # Format each part
202
+ formatted_parts = []
203
+ for part in parts:
204
+ if (
205
+ not part.strip() or part.strip() == "--"
206
+ ): # Skip empty parts and end boundary
207
+ continue
208
+
209
+ # Remove excess whitespace and normalize line endings
210
+ part = part.strip().replace("\r\n", "\n").replace("\n\n\n", "\n\n")
211
+
212
+ # Extract headers and content
213
+ if "Content-Type" in part:
214
+ # Split headers and content
215
+ headers = []
216
+ content = ""
217
+
218
+ # Process line by line
219
+ lines = part.split("\n")
220
+ in_headers = True
221
+
222
+ for line in lines:
223
+ if in_headers:
224
+ if line.startswith("Content-"):
225
+ headers.append(line)
226
+ elif not line.strip():
227
+ in_headers = False
228
+ continue
229
+ content += line + "\n"
230
+
231
+ # Reconstruct part with proper formatting
232
+ formatted_part = "\n".join(headers) + "\n\n" + content.strip()
233
+ formatted_parts.append(formatted_part)
234
+
235
+ # Reconstruct full response
236
+ formatted_response = f"\n{boundary}\n".join([""] + formatted_parts + ["--"])
237
+
238
+ return formatted_response
239
+
240
+
241
+ def parse_response(response) -> dict:
242
+ json_data = lib.failsafe(lambda: lib.to_dict(response))
243
+
244
+ if json_data:
245
+ return json_data
246
+
247
+ # Normalize response format first
248
+ normalized_response = normalize_multipart_response(response)
249
+
250
+ # Extract boundary dynamically
251
+ boundary_match = re.search(r"--[a-zA-Z0-9\-]+", normalized_response)
252
+ if not boundary_match:
253
+ return dict(
254
+ error=dict(
255
+ code="SHIPPING_SDK_ERROR",
256
+ message="Failed to parse multipart response",
257
+ )
258
+ )
259
+
260
+ boundary = boundary_match.group(0).strip()
261
+
262
+ # Handle multipart form-data
263
+ parts = normalized_response.split(boundary)
264
+ data = {}
265
+
266
+ for part in parts:
267
+ if not part.strip() or part.strip() == "--":
268
+ continue # Skip empty parts and the final boundary marker
269
+
270
+ part_data = {}
271
+ headers_content, content = (
272
+ part.split("\n\n", 1) if "\n\n" in part else (part, "")
273
+ )
274
+ headers: typing.List[str] = headers_content.strip().split("\n")
275
+
276
+ # Extract Content-Disposition and Content-Type
277
+ for header in headers:
278
+ if "Content-Type" in header:
279
+ part_data["content_type"] = header.split(":")[1].strip()
280
+ elif "Content-Disposition" in header:
281
+ disposition = header.split(":")[1].strip()
282
+ if "filename=" in disposition:
283
+ filename = re.search(r'filename="([^"]+)"', disposition)
284
+ if filename:
285
+ part_data["filename"] = filename.group(1)
286
+ if "name=" in disposition:
287
+ name = re.search(r' name="([^"]+)"', disposition)
288
+ if name:
289
+ part_data["name"] = name.group(1)
290
+
291
+ # Parse content based on type
292
+ if part_data.get("content_type") == "application/json":
293
+ data[part_data["name"]] = {**lib.to_dict(content.strip())}
294
+ elif part_data.get("filename"):
295
+ data[part_data["name"]] = content.strip() # type: ignore
296
+
297
+ return data
298
+
299
+
300
+ def parse_error_response(response) -> dict:
301
+ # Check if the response is JSON
302
+ content = lib.failsafe(lambda: response.read())
303
+ json_data = lib.failsafe(lambda: lib.to_dict(content))
304
+
305
+ if json_data:
306
+ return json_data
307
+
308
+ # the response is plain text
309
+ return dict(
310
+ error=dict(
311
+ code=response.code,
312
+ message=response.strip(),
313
+ )
314
+ )
315
+
316
+ def parse_phone_number(number: str) -> typing.Optional[str]:
317
+ if number is None:
318
+ return None
319
+
320
+ return number.replace(" ", "").replace("-", "").replace("+", "")[-10:]
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 SourceType:
8
+ parameter: typing.Optional[str] = None
9
+ example: typing.Optional[str] = None
10
+
11
+
12
+ @attr.s(auto_attribs=True)
13
+ class ErrorElementType:
14
+ status: typing.Optional[str] = None
15
+ code: typing.Optional[str] = None
16
+ title: typing.Optional[str] = None
17
+ detail: typing.Optional[str] = None
18
+ source: typing.Optional[SourceType] = jstruct.JStruct[SourceType]
19
+
20
+
21
+ @attr.s(auto_attribs=True)
22
+ class ErrorResponseErrorType:
23
+ code: typing.Optional[str] = None
24
+ message: typing.Optional[str] = None
25
+ errors: typing.Optional[typing.List[ErrorElementType]] = jstruct.JList[ErrorElementType]
26
+
27
+
28
+ @attr.s(auto_attribs=True)
29
+ class ErrorResponseType:
30
+ apiVersion: typing.Optional[str] = None
31
+ error: typing.Optional[ErrorResponseErrorType] = jstruct.JStruct[ErrorResponseErrorType]
@@ -0,0 +1,142 @@
1
+ import attr
2
+ import jstruct
3
+ import typing
4
+
5
+
6
+ @attr.s(auto_attribs=True)
7
+ class ContentType:
8
+ itemDescription: typing.Optional[str] = None
9
+ itemQuantity: typing.Optional[int] = None
10
+ itemValue: typing.Optional[int] = None
11
+ itemTotalValue: typing.Optional[int] = None
12
+ weightUOM: typing.Optional[str] = None
13
+ itemWeight: typing.Optional[float] = None
14
+ itemTotalWeight: typing.Optional[float] = None
15
+ HSTariffNumber: typing.Optional[str] = None
16
+ countryofOrigin: typing.Optional[str] = None
17
+ itemCategory: typing.Optional[str] = None
18
+ itemSubcategory: typing.Optional[str] = None
19
+
20
+
21
+ @attr.s(auto_attribs=True)
22
+ class CustomsFormType:
23
+ contentComments: typing.Optional[str] = None
24
+ restrictionType: typing.Optional[str] = None
25
+ restrictionComments: typing.Optional[str] = None
26
+ AESITN: typing.Optional[str] = None
27
+ invoiceNumber: typing.Optional[str] = None
28
+ licenseNumber: typing.Optional[str] = None
29
+ certificateNumber: typing.Optional[str] = None
30
+ customsContentType: typing.Optional[str] = None
31
+ importersReference: typing.Optional[str] = None
32
+ importersContact: typing.Optional[str] = None
33
+ exportersReference: typing.Optional[str] = None
34
+ exportersContact: typing.Optional[str] = None
35
+ contents: typing.Optional[typing.List[ContentType]] = jstruct.JList[ContentType]
36
+
37
+
38
+ @attr.s(auto_attribs=True)
39
+ class AddressType:
40
+ streetAddress: typing.Optional[str] = None
41
+ secondaryAddress: typing.Optional[str] = None
42
+ city: typing.Optional[str] = None
43
+ state: typing.Optional[str] = None
44
+ ZIPCode: typing.Optional[str] = None
45
+ ZIPPlus4: typing.Optional[str] = None
46
+ urbanization: typing.Optional[str] = None
47
+ firstName: typing.Optional[str] = None
48
+ lastName: typing.Optional[str] = None
49
+ firm: typing.Optional[str] = None
50
+ phone: typing.Optional[str] = None
51
+ email: typing.Optional[str] = None
52
+ ignoreBadAddress: typing.Optional[bool] = None
53
+ platformUserId: typing.Optional[str] = None
54
+ parcelLockerDelivery: typing.Optional[bool] = None
55
+ holdForPickup: typing.Optional[bool] = None
56
+ facilityId: typing.Optional[str] = None
57
+
58
+
59
+ @attr.s(auto_attribs=True)
60
+ class ImageInfoType:
61
+ imageType: typing.Optional[str] = None
62
+ labelType: typing.Optional[str] = None
63
+ shipInfo: typing.Optional[bool] = None
64
+ receiptOption: typing.Optional[str] = None
65
+ suppressPostage: typing.Optional[bool] = None
66
+ suppressMailDate: typing.Optional[bool] = None
67
+ returnLabel: typing.Optional[bool] = None
68
+
69
+
70
+ @attr.s(auto_attribs=True)
71
+ class CustomerReferenceType:
72
+ referenceNumber: typing.Optional[str] = None
73
+ printReferenceNumber: typing.Optional[bool] = None
74
+
75
+
76
+ @attr.s(auto_attribs=True)
77
+ class DestinationEntryFacilityAddressType:
78
+ streetAddress: typing.Optional[str] = None
79
+ secondaryAddress: typing.Optional[str] = None
80
+ city: typing.Optional[str] = None
81
+ state: typing.Optional[str] = None
82
+ ZIPCode: typing.Optional[str] = None
83
+ ZIPPlus4: typing.Optional[str] = None
84
+ urbanization: typing.Optional[str] = None
85
+
86
+
87
+ @attr.s(auto_attribs=True)
88
+ class ContainerType:
89
+ containerID: typing.Optional[str] = None
90
+ sortType: typing.Optional[str] = None
91
+
92
+
93
+ @attr.s(auto_attribs=True)
94
+ class OriginalPackageType:
95
+ originalTrackingNumber: typing.Optional[str] = None
96
+ originalConstructCode: typing.Optional[str] = None
97
+
98
+
99
+ @attr.s(auto_attribs=True)
100
+ class PackageOptionsType:
101
+ packageValue: typing.Optional[int] = None
102
+ nonDeliveryOption: typing.Optional[str] = None
103
+ redirectAddress: typing.Optional[AddressType] = jstruct.JStruct[AddressType]
104
+ contentType: typing.Optional[str] = None
105
+ generateGXEvent: typing.Optional[bool] = None
106
+ containers: typing.Optional[typing.List[ContainerType]] = jstruct.JList[ContainerType]
107
+ ancillaryServiceEndorsements: typing.Optional[str] = None
108
+ originalPackage: typing.Optional[OriginalPackageType] = jstruct.JStruct[OriginalPackageType]
109
+
110
+
111
+ @attr.s(auto_attribs=True)
112
+ class PackageDescriptionType:
113
+ weightUOM: typing.Optional[str] = None
114
+ weight: typing.Optional[int] = None
115
+ dimensionsUOM: typing.Optional[str] = None
116
+ length: typing.Optional[int] = None
117
+ height: typing.Optional[int] = None
118
+ width: typing.Optional[int] = None
119
+ girth: typing.Optional[int] = None
120
+ mailClass: typing.Optional[str] = None
121
+ rateIndicator: typing.Optional[str] = None
122
+ processingCategory: typing.Optional[str] = None
123
+ destinationEntryFacilityType: typing.Optional[str] = None
124
+ destinationEntryFacilityAddress: typing.Optional[DestinationEntryFacilityAddressType] = jstruct.JStruct[DestinationEntryFacilityAddressType]
125
+ packageOptions: typing.Optional[PackageOptionsType] = jstruct.JStruct[PackageOptionsType]
126
+ customerReference: typing.Optional[typing.List[CustomerReferenceType]] = jstruct.JList[CustomerReferenceType]
127
+ extraServices: typing.Optional[typing.List[int]] = None
128
+ mailingDate: typing.Optional[str] = None
129
+ carrierRelease: typing.Optional[bool] = None
130
+ physicalSignatureRequired: typing.Optional[bool] = None
131
+ inductionZIPCode: typing.Optional[str] = None
132
+
133
+
134
+ @attr.s(auto_attribs=True)
135
+ class LabelRequestType:
136
+ imageInfo: typing.Optional[ImageInfoType] = jstruct.JStruct[ImageInfoType]
137
+ toAddress: typing.Optional[AddressType] = jstruct.JStruct[AddressType]
138
+ fromAddress: typing.Optional[AddressType] = jstruct.JStruct[AddressType]
139
+ senderAddress: typing.Optional[AddressType] = jstruct.JStruct[AddressType]
140
+ returnAddress: typing.Optional[AddressType] = jstruct.JStruct[AddressType]
141
+ packageDescription: typing.Optional[PackageDescriptionType] = jstruct.JStruct[PackageDescriptionType]
142
+ customsForm: typing.Optional[CustomsFormType] = jstruct.JStruct[CustomsFormType]
@@ -0,0 +1,84 @@
1
+ import attr
2
+ import jstruct
3
+ import typing
4
+
5
+
6
+ @attr.s(auto_attribs=True)
7
+ class CommitmentType:
8
+ name: typing.Optional[str] = None
9
+ scheduleDeliveryDate: typing.Optional[str] = None
10
+
11
+
12
+ @attr.s(auto_attribs=True)
13
+ class ExtraServiceType:
14
+ name: typing.Optional[str] = None
15
+ SKU: typing.Optional[str] = None
16
+ price: typing.Optional[int] = None
17
+
18
+
19
+ @attr.s(auto_attribs=True)
20
+ class InductionTypeObjectType:
21
+ pass
22
+
23
+
24
+ @attr.s(auto_attribs=True)
25
+ class LabelAddressType:
26
+ streetAddress: typing.Optional[str] = None
27
+ streetAddressAbbreviation: typing.Optional[str] = None
28
+ secondaryAddress: typing.Optional[str] = None
29
+ cityAbbreviation: typing.Optional[str] = None
30
+ city: typing.Optional[str] = None
31
+ state: typing.Optional[str] = None
32
+ ZIPCode: typing.Optional[str] = None
33
+ ZIPPlus4: typing.Optional[str] = None
34
+ urbanization: typing.Optional[str] = None
35
+ firstName: typing.Optional[str] = None
36
+ lastName: typing.Optional[str] = None
37
+ firm: typing.Optional[str] = None
38
+ phone: typing.Optional[str] = None
39
+ email: typing.Optional[str] = None
40
+ ignoreBadAddress: typing.Optional[bool] = None
41
+
42
+
43
+ @attr.s(auto_attribs=True)
44
+ class LinkType:
45
+ rel: typing.Optional[typing.List[str]] = None
46
+ title: typing.Optional[str] = None
47
+ href: typing.Optional[str] = None
48
+ method: typing.Optional[str] = None
49
+ submissionMediaType: typing.Optional[str] = None
50
+ targetMediaType: typing.Optional[str] = None
51
+
52
+
53
+ @attr.s(auto_attribs=True)
54
+ class LabelMetadataType:
55
+ labelAddress: typing.Optional[LabelAddressType] = jstruct.JStruct[LabelAddressType]
56
+ routingInformation: typing.Optional[str] = None
57
+ trackingNumber: typing.Optional[str] = None
58
+ constructCode: typing.Optional[str] = None
59
+ SKU: typing.Optional[str] = None
60
+ postage: typing.Optional[int] = None
61
+ extraServices: typing.Optional[typing.List[ExtraServiceType]] = jstruct.JList[ExtraServiceType]
62
+ zone: typing.Optional[str] = None
63
+ commitment: typing.Optional[CommitmentType] = jstruct.JStruct[CommitmentType]
64
+ weightUOM: typing.Optional[str] = None
65
+ weight: typing.Optional[int] = None
66
+ dimensionalWeight: typing.Optional[int] = None
67
+ fees: typing.Optional[typing.List[ExtraServiceType]] = jstruct.JList[ExtraServiceType]
68
+ permitHolderName: typing.Optional[str] = None
69
+ inductionType: typing.Optional[InductionTypeObjectType] = jstruct.JStruct[InductionTypeObjectType]
70
+ labelBrokerID: typing.Optional[str] = None
71
+ links: typing.Optional[typing.List[LinkType]] = jstruct.JList[LinkType]
72
+ bannerText: typing.Optional[str] = None
73
+ retailDistributionCode: typing.Optional[str] = None
74
+ serviceTypeCode: typing.Optional[int] = None
75
+
76
+
77
+ @attr.s(auto_attribs=True)
78
+ class LabelResponseType:
79
+ labelMetadata: typing.Optional[LabelMetadataType] = jstruct.JStruct[LabelMetadataType]
80
+ returnLabelMetadata: typing.Optional[LabelMetadataType] = jstruct.JStruct[LabelMetadataType]
81
+ labelImage: typing.Optional[str] = None
82
+ receiptImage: typing.Optional[str] = None
83
+ returnLabelImage: typing.Optional[str] = None
84
+ returnReceiptImage: typing.Optional[str] = None
@@ -0,0 +1,49 @@
1
+ import attr
2
+ import jstruct
3
+ import typing
4
+
5
+
6
+ @attr.s(auto_attribs=True)
7
+ class PackageType:
8
+ packageType: typing.Optional[str] = None
9
+ packageCount: typing.Optional[int] = None
10
+
11
+
12
+ @attr.s(auto_attribs=True)
13
+ class AddressType:
14
+ streetAddress: typing.Optional[str] = None
15
+ secondaryAddress: typing.Optional[str] = None
16
+ city: typing.Optional[str] = None
17
+ state: typing.Optional[str] = None
18
+ ZIPCode: typing.Optional[str] = None
19
+ ZIPPlus4: typing.Optional[str] = None
20
+ urbanization: typing.Optional[str] = None
21
+
22
+
23
+ @attr.s(auto_attribs=True)
24
+ class ContactType:
25
+ email: typing.Optional[str] = None
26
+
27
+
28
+ @attr.s(auto_attribs=True)
29
+ class PickupAddressType:
30
+ firstName: typing.Optional[str] = None
31
+ lastName: typing.Optional[str] = None
32
+ firm: typing.Optional[str] = None
33
+ address: typing.Optional[AddressType] = jstruct.JStruct[AddressType]
34
+ contact: typing.Optional[typing.List[ContactType]] = jstruct.JList[ContactType]
35
+
36
+
37
+ @attr.s(auto_attribs=True)
38
+ class PickupLocationType:
39
+ packageLocation: typing.Optional[str] = None
40
+ specialInstructions: typing.Optional[str] = None
41
+
42
+
43
+ @attr.s(auto_attribs=True)
44
+ class PickupRequestType:
45
+ pickupDate: typing.Optional[str] = None
46
+ pickupAddress: typing.Optional[PickupAddressType] = jstruct.JStruct[PickupAddressType]
47
+ packages: typing.Optional[typing.List[PackageType]] = jstruct.JList[PackageType]
48
+ estimatedWeight: typing.Optional[int] = None
49
+ pickupLocation: typing.Optional[PickupLocationType] = jstruct.JStruct[PickupLocationType]
@@ -0,0 +1,58 @@
1
+ import attr
2
+ import jstruct
3
+ import typing
4
+
5
+
6
+ @attr.s(auto_attribs=True)
7
+ class PackageType:
8
+ packageType: typing.Optional[str] = None
9
+ packageCount: typing.Optional[int] = None
10
+
11
+
12
+ @attr.s(auto_attribs=True)
13
+ class AddressType:
14
+ streetAddress: typing.Optional[str] = None
15
+ streetAddressAbbreviation: typing.Optional[str] = None
16
+ secondaryAddress: typing.Optional[str] = None
17
+ cityAbbreviation: typing.Optional[str] = None
18
+ city: typing.Optional[str] = None
19
+ state: typing.Optional[str] = None
20
+ ZIPCode: typing.Optional[str] = None
21
+ ZIPPlus4: typing.Optional[str] = None
22
+ urbanization: typing.Optional[str] = None
23
+
24
+
25
+ @attr.s(auto_attribs=True)
26
+ class ContactType:
27
+ email: typing.Optional[str] = None
28
+
29
+
30
+ @attr.s(auto_attribs=True)
31
+ class PickupAddressType:
32
+ firstName: typing.Optional[str] = None
33
+ lastName: typing.Optional[str] = None
34
+ firm: typing.Optional[str] = None
35
+ address: typing.Optional[AddressType] = jstruct.JStruct[AddressType]
36
+ contact: typing.Optional[typing.List[ContactType]] = jstruct.JList[ContactType]
37
+
38
+
39
+ @attr.s(auto_attribs=True)
40
+ class PickupLocationType:
41
+ packageLocation: typing.Optional[str] = None
42
+ specialInstructions: typing.Optional[str] = None
43
+
44
+
45
+ @attr.s(auto_attribs=True)
46
+ class CarrierPickupRequestType:
47
+ pickupDate: typing.Optional[str] = None
48
+ pickupAddress: typing.Optional[PickupAddressType] = jstruct.JStruct[PickupAddressType]
49
+ packages: typing.Optional[typing.List[PackageType]] = jstruct.JList[PackageType]
50
+ estimatedWeight: typing.Optional[int] = None
51
+ pickupLocation: typing.Optional[PickupLocationType] = jstruct.JStruct[PickupLocationType]
52
+
53
+
54
+ @attr.s(auto_attribs=True)
55
+ class PickupResponseType:
56
+ confirmationNumber: typing.Optional[str] = None
57
+ pickupDate: typing.Optional[str] = None
58
+ carrierPickupRequest: typing.Optional[CarrierPickupRequestType] = jstruct.JStruct[CarrierPickupRequestType]