shipthisapi-python 3.0.2__py3-none-any.whl → 3.0.4__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.
- ShipthisAPI/__init__.py +15 -1
- ShipthisAPI/shipthisapi.py +1007 -16
- {shipthisapi_python-3.0.2.dist-info → shipthisapi_python-3.0.4.dist-info}/METADATA +1 -1
- shipthisapi_python-3.0.4.dist-info/RECORD +6 -0
- shipthisapi_python-3.0.2.dist-info/RECORD +0 -6
- {shipthisapi_python-3.0.2.dist-info → shipthisapi_python-3.0.4.dist-info}/WHEEL +0 -0
- {shipthisapi_python-3.0.2.dist-info → shipthisapi_python-3.0.4.dist-info}/top_level.txt +0 -0
ShipthisAPI/__init__.py
CHANGED
|
@@ -1,2 +1,16 @@
|
|
|
1
1
|
# __variables__ with double-quoted values will be available in setup.py
|
|
2
|
-
__version__ = "0.
|
|
2
|
+
__version__ = "3.0.4"
|
|
3
|
+
|
|
4
|
+
from .shipthisapi import (
|
|
5
|
+
ShipthisAPI,
|
|
6
|
+
ShipthisAPIError,
|
|
7
|
+
ShipthisAuthError,
|
|
8
|
+
ShipthisRequestError,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"ShipthisAPI",
|
|
13
|
+
"ShipthisAPIError",
|
|
14
|
+
"ShipthisAuthError",
|
|
15
|
+
"ShipthisRequestError",
|
|
16
|
+
]
|
ShipthisAPI/shipthisapi.py
CHANGED
|
@@ -1,24 +1,1015 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
"""Shipthis API Client.
|
|
2
|
+
|
|
3
|
+
An async Python client for the Shipthis public API.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
import asyncio
|
|
7
|
+
from ShipthisAPI import ShipthisAPI
|
|
8
|
+
|
|
9
|
+
async def main():
|
|
10
|
+
# Initialize the client
|
|
11
|
+
client = ShipthisAPI(
|
|
12
|
+
organisation="your_org_id",
|
|
13
|
+
x_api_key="your_api_key",
|
|
14
|
+
region_id="your_region",
|
|
15
|
+
location_id="your_location"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# Connect and validate
|
|
19
|
+
await client.connect()
|
|
20
|
+
|
|
21
|
+
# Get items from a collection
|
|
22
|
+
items = await client.get_list("shipment")
|
|
23
|
+
|
|
24
|
+
# Patch document fields
|
|
25
|
+
await client.patch_item("fcl_load", doc_id, {"status": "completed"})
|
|
26
|
+
|
|
27
|
+
asyncio.run(main())
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from typing import Any, Dict, List, Optional
|
|
31
|
+
import json
|
|
32
|
+
import httpx
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ShipthisAPIError(Exception):
|
|
36
|
+
"""Base exception for Shipthis API errors."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, message: str, status_code: int = None, details: dict = None):
|
|
39
|
+
self.message = message
|
|
40
|
+
self.status_code = status_code
|
|
41
|
+
self.details = details or {}
|
|
42
|
+
super().__init__(self.message)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ShipthisAuthError(ShipthisAPIError):
|
|
46
|
+
"""Raised when authentication fails."""
|
|
47
|
+
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ShipthisRequestError(ShipthisAPIError):
|
|
52
|
+
"""Raised when a request fails."""
|
|
53
|
+
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
|
|
3
57
|
class ShipthisAPI:
|
|
4
|
-
|
|
58
|
+
"""Async Shipthis API client for public API access.
|
|
59
|
+
|
|
60
|
+
Attributes:
|
|
61
|
+
base_api_endpoint: The base URL for the API.
|
|
62
|
+
organisation_id: Your organisation ID.
|
|
63
|
+
x_api_key: Your API key.
|
|
64
|
+
user_type: User type for requests (default: "employee").
|
|
65
|
+
region_id: Region ID for requests.
|
|
66
|
+
location_id: Location ID for requests.
|
|
67
|
+
timeout: Request timeout in seconds.
|
|
68
|
+
custom_headers: Custom headers to override defaults.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
DEFAULT_TIMEOUT = 30
|
|
72
|
+
BASE_API_ENDPOINT = "https://api.shipthis.co/api/v3/"
|
|
73
|
+
|
|
74
|
+
def __init__(
|
|
75
|
+
self,
|
|
76
|
+
organisation: str,
|
|
77
|
+
x_api_key: str = None,
|
|
78
|
+
user_type: str = "employee",
|
|
79
|
+
region_id: str = None,
|
|
80
|
+
location_id: str = None,
|
|
81
|
+
timeout: int = None,
|
|
82
|
+
base_url: str = None,
|
|
83
|
+
custom_headers: Dict[str, str] = None,
|
|
84
|
+
) -> None:
|
|
85
|
+
"""Initialize the Shipthis API client.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
organisation: Your organisation ID.
|
|
89
|
+
x_api_key: Your API key (optional if using custom_headers with auth).
|
|
90
|
+
user_type: User type for requests (default: "employee").
|
|
91
|
+
region_id: Region ID for requests.
|
|
92
|
+
location_id: Location ID for requests.
|
|
93
|
+
timeout: Request timeout in seconds (default: 30).
|
|
94
|
+
base_url: Custom base URL (optional, for testing).
|
|
95
|
+
custom_headers: Custom headers that override defaults.
|
|
96
|
+
"""
|
|
97
|
+
if not organisation:
|
|
98
|
+
raise ValueError("organisation is required")
|
|
5
99
|
|
|
6
|
-
def __init__(self, base_url: str, organisation: str, x_api_key:str, user_type='employee') -> None:
|
|
7
100
|
self.x_api_key = x_api_key
|
|
8
101
|
self.organisation_id = organisation
|
|
9
|
-
self.base_url = base_url
|
|
10
102
|
self.user_type = user_type
|
|
11
|
-
|
|
12
|
-
|
|
103
|
+
self.region_id = region_id
|
|
104
|
+
self.location_id = location_id
|
|
105
|
+
self.timeout = timeout or self.DEFAULT_TIMEOUT
|
|
106
|
+
self.base_api_endpoint = base_url or self.BASE_API_ENDPOINT
|
|
107
|
+
self.custom_headers = custom_headers or {}
|
|
108
|
+
self.organisation_info = None
|
|
109
|
+
self.is_connected = False
|
|
110
|
+
|
|
111
|
+
def set_region_location(self, region_id: str, location_id: str) -> None:
|
|
112
|
+
"""Set the region and location for subsequent requests.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
region_id: Region ID.
|
|
116
|
+
location_id: Location ID.
|
|
117
|
+
"""
|
|
118
|
+
self.region_id = region_id
|
|
119
|
+
self.location_id = location_id
|
|
120
|
+
|
|
121
|
+
def _get_headers(self, override_headers: Dict[str, str] = None) -> Dict[str, str]:
|
|
122
|
+
"""Build request headers.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
override_headers: Headers to override for this specific request.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Dictionary of headers.
|
|
129
|
+
"""
|
|
13
130
|
headers = {
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
131
|
+
"organisation": self.organisation_id,
|
|
132
|
+
"usertype": self.user_type,
|
|
133
|
+
"Content-Type": "application/json",
|
|
134
|
+
"Accept": "application/json",
|
|
135
|
+
}
|
|
136
|
+
if self.x_api_key:
|
|
137
|
+
headers["x-api-key"] = self.x_api_key
|
|
138
|
+
if self.region_id:
|
|
139
|
+
headers["region"] = self.region_id
|
|
140
|
+
if self.location_id:
|
|
141
|
+
headers["location"] = self.location_id
|
|
142
|
+
# Apply custom headers from init
|
|
143
|
+
headers.update(self.custom_headers)
|
|
144
|
+
# Apply per-request override headers
|
|
145
|
+
if override_headers:
|
|
146
|
+
headers.update(override_headers)
|
|
147
|
+
return headers
|
|
148
|
+
|
|
149
|
+
async def _make_request(
|
|
150
|
+
self,
|
|
151
|
+
method: str,
|
|
152
|
+
path: str,
|
|
153
|
+
query_params: Dict[str, Any] = None,
|
|
154
|
+
request_data: Dict[str, Any] = None,
|
|
155
|
+
headers: Dict[str, str] = None,
|
|
156
|
+
) -> Dict[str, Any]:
|
|
157
|
+
"""Make an async HTTP request to the API.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
method: HTTP method (GET, POST, PUT, PATCH, DELETE).
|
|
161
|
+
path: API endpoint path.
|
|
162
|
+
query_params: Query parameters.
|
|
163
|
+
request_data: Request body data.
|
|
164
|
+
headers: Headers to override for this request.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
API response data.
|
|
168
|
+
|
|
169
|
+
Raises:
|
|
170
|
+
ShipthisAuthError: If authentication fails.
|
|
171
|
+
ShipthisRequestError: If the request fails.
|
|
172
|
+
"""
|
|
173
|
+
url = self.base_api_endpoint + path
|
|
174
|
+
request_headers = self._get_headers(headers)
|
|
175
|
+
|
|
176
|
+
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
177
|
+
try:
|
|
178
|
+
response = await client.request(
|
|
179
|
+
method,
|
|
180
|
+
url,
|
|
181
|
+
headers=request_headers,
|
|
182
|
+
params=query_params,
|
|
183
|
+
json=request_data,
|
|
184
|
+
)
|
|
185
|
+
except httpx.TimeoutException:
|
|
186
|
+
raise ShipthisRequestError(
|
|
187
|
+
message="Request timed out",
|
|
188
|
+
status_code=408,
|
|
189
|
+
)
|
|
190
|
+
except httpx.ConnectError as e:
|
|
191
|
+
raise ShipthisRequestError(
|
|
192
|
+
message=f"Connection error: {str(e)}",
|
|
193
|
+
status_code=0,
|
|
194
|
+
)
|
|
195
|
+
except httpx.RequestError as e:
|
|
196
|
+
raise ShipthisRequestError(
|
|
197
|
+
message=f"Request failed: {str(e)}",
|
|
198
|
+
status_code=0,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# Handle authentication errors
|
|
202
|
+
if response.status_code == 401:
|
|
203
|
+
raise ShipthisAuthError(
|
|
204
|
+
message="Authentication failed. Check your API key.",
|
|
205
|
+
status_code=401,
|
|
206
|
+
)
|
|
207
|
+
if response.status_code == 403:
|
|
208
|
+
raise ShipthisAuthError(
|
|
209
|
+
message="Access denied. Check your permissions.",
|
|
210
|
+
status_code=403,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# Parse response
|
|
214
|
+
try:
|
|
215
|
+
result = response.json()
|
|
216
|
+
except json.JSONDecodeError:
|
|
217
|
+
raise ShipthisRequestError(
|
|
218
|
+
message=f"Invalid JSON response: {response.text[:200]}",
|
|
219
|
+
status_code=response.status_code,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Handle success
|
|
223
|
+
if response.status_code in [200, 201]:
|
|
224
|
+
if result.get("success"):
|
|
225
|
+
return result.get("data")
|
|
226
|
+
else:
|
|
227
|
+
errors = result.get("errors", [])
|
|
228
|
+
if errors and isinstance(errors, list) and len(errors) > 0:
|
|
229
|
+
error_msg = errors[0].get("message", "Unknown error")
|
|
230
|
+
else:
|
|
231
|
+
error_msg = result.get("message", "API call failed")
|
|
232
|
+
raise ShipthisRequestError(
|
|
233
|
+
message=error_msg,
|
|
234
|
+
status_code=response.status_code,
|
|
235
|
+
details=result,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# Handle other error status codes
|
|
239
|
+
raise ShipthisRequestError(
|
|
240
|
+
message=f"Request failed with status {response.status_code}",
|
|
241
|
+
status_code=response.status_code,
|
|
242
|
+
details=result if isinstance(result, dict) else {"response": str(result)},
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
# ==================== Connection ====================
|
|
246
|
+
|
|
247
|
+
async def connect(self) -> Dict[str, Any]:
|
|
248
|
+
"""Connect and validate the API connection.
|
|
249
|
+
|
|
250
|
+
Fetches organisation info and validates region/location.
|
|
251
|
+
If no region/location is set, uses the first available one.
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Dictionary with region_id and location_id.
|
|
255
|
+
|
|
256
|
+
Raises:
|
|
257
|
+
ShipthisAPIError: If connection fails.
|
|
258
|
+
"""
|
|
259
|
+
info = await self.info()
|
|
260
|
+
self.organisation_info = info.get("organisation")
|
|
261
|
+
|
|
262
|
+
if not self.region_id or not self.location_id:
|
|
263
|
+
# Use first available region/location
|
|
264
|
+
regions = self.organisation_info.get("regions", [])
|
|
265
|
+
if regions:
|
|
266
|
+
self.region_id = regions[0].get("region_id")
|
|
267
|
+
locations = regions[0].get("locations", [])
|
|
268
|
+
if locations:
|
|
269
|
+
self.location_id = locations[0].get("location_id")
|
|
270
|
+
|
|
271
|
+
self.is_connected = True
|
|
272
|
+
return {
|
|
273
|
+
"region_id": self.region_id,
|
|
274
|
+
"location_id": self.location_id,
|
|
275
|
+
"organisation": self.organisation_info,
|
|
17
276
|
}
|
|
18
|
-
resp = requests.request(method, self.base_api_endpoint + path, data=request_data or {}, headers=headers)
|
|
19
|
-
if resp.status_code == 200:
|
|
20
|
-
return resp.json
|
|
21
277
|
|
|
22
|
-
def
|
|
23
|
-
|
|
24
|
-
|
|
278
|
+
def disconnect(self) -> None:
|
|
279
|
+
"""Disconnect and clear credentials."""
|
|
280
|
+
self.x_api_key = None
|
|
281
|
+
self.is_connected = False
|
|
282
|
+
|
|
283
|
+
# ==================== Info ====================
|
|
284
|
+
|
|
285
|
+
async def info(self) -> Dict[str, Any]:
|
|
286
|
+
"""Get organisation and user info.
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
Dictionary with organisation and user information.
|
|
290
|
+
|
|
291
|
+
Raises:
|
|
292
|
+
ShipthisAPIError: If the request fails.
|
|
293
|
+
"""
|
|
294
|
+
return await self._make_request("GET", "user-auth/info")
|
|
295
|
+
|
|
296
|
+
# ==================== Collection CRUD ====================
|
|
297
|
+
|
|
298
|
+
async def get_one_item(
|
|
299
|
+
self,
|
|
300
|
+
collection_name: str,
|
|
301
|
+
doc_id: str = None,
|
|
302
|
+
filters: Dict[str, Any] = None,
|
|
303
|
+
only_fields: str = None,
|
|
304
|
+
) -> Optional[Dict[str, Any]]:
|
|
305
|
+
"""Get a single item from a collection.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
collection_name: Name of the collection.
|
|
309
|
+
doc_id: Document ID (optional, if not provided returns first item).
|
|
310
|
+
filters: Query filters (optional).
|
|
311
|
+
only_fields: Comma-separated list of fields to return.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
Document data or None if not found.
|
|
315
|
+
|
|
316
|
+
Raises:
|
|
317
|
+
ShipthisAPIError: If the request fails.
|
|
318
|
+
"""
|
|
319
|
+
if doc_id:
|
|
320
|
+
path = f"incollection/{collection_name}/{doc_id}"
|
|
321
|
+
params = {}
|
|
322
|
+
if only_fields:
|
|
323
|
+
params["only"] = only_fields
|
|
324
|
+
return await self._make_request("GET", path, query_params=params if params else None)
|
|
325
|
+
else:
|
|
326
|
+
params = {}
|
|
327
|
+
if filters:
|
|
328
|
+
params["query_filter_v2"] = json.dumps(filters)
|
|
329
|
+
if only_fields:
|
|
330
|
+
params["only"] = only_fields
|
|
331
|
+
resp = await self._make_request("GET", f"incollection/{collection_name}", params)
|
|
332
|
+
if isinstance(resp, dict) and resp.get("items"):
|
|
333
|
+
return resp.get("items")[0]
|
|
334
|
+
return None
|
|
335
|
+
|
|
336
|
+
async def get_list(
|
|
337
|
+
self,
|
|
338
|
+
collection_name: str,
|
|
339
|
+
filters: Dict[str, Any] = None,
|
|
340
|
+
search_query: str = None,
|
|
341
|
+
page: int = 1,
|
|
342
|
+
count: int = 20,
|
|
343
|
+
only_fields: str = None,
|
|
344
|
+
sort: List[Dict[str, Any]] = None,
|
|
345
|
+
output_type: str = None,
|
|
346
|
+
meta: bool = True,
|
|
347
|
+
) -> List[Dict[str, Any]]:
|
|
348
|
+
"""Get a list of items from a collection.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
collection_name: Name of the collection.
|
|
352
|
+
filters: Query filters (optional).
|
|
353
|
+
search_query: Search query string (optional).
|
|
354
|
+
page: Page number (default: 1).
|
|
355
|
+
count: Items per page (default: 20).
|
|
356
|
+
only_fields: Comma-separated list of fields to return (optional).
|
|
357
|
+
sort: List of sort objects [{"field": "name", "order": "asc"}] (optional).
|
|
358
|
+
output_type: Output type (optional).
|
|
359
|
+
meta: Include metadata (default: True).
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
List of documents.
|
|
363
|
+
|
|
364
|
+
Raises:
|
|
365
|
+
ShipthisAPIError: If the request fails.
|
|
366
|
+
"""
|
|
367
|
+
params = {"page": page, "count": count}
|
|
368
|
+
if filters:
|
|
369
|
+
params["query_filter_v2"] = json.dumps(filters)
|
|
370
|
+
if search_query:
|
|
371
|
+
params["search_query"] = search_query
|
|
372
|
+
if only_fields:
|
|
373
|
+
params["only"] = only_fields
|
|
374
|
+
if sort:
|
|
375
|
+
params["multi_sort"] = json.dumps(sort)
|
|
376
|
+
if output_type:
|
|
377
|
+
params["output_type"] = output_type
|
|
378
|
+
if not meta:
|
|
379
|
+
params["meta"] = "false"
|
|
380
|
+
|
|
381
|
+
response = await self._make_request("GET", f"incollection/{collection_name}", params)
|
|
382
|
+
|
|
383
|
+
if isinstance(response, dict):
|
|
384
|
+
return response.get("items", [])
|
|
385
|
+
return []
|
|
386
|
+
|
|
387
|
+
async def search(
|
|
388
|
+
self,
|
|
389
|
+
collection_name: str,
|
|
390
|
+
query: str,
|
|
391
|
+
page: int = 1,
|
|
392
|
+
count: int = 20,
|
|
393
|
+
only_fields: str = None,
|
|
394
|
+
) -> List[Dict[str, Any]]:
|
|
395
|
+
"""Search for items in a collection.
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
collection_name: Name of the collection.
|
|
399
|
+
query: Search query string.
|
|
400
|
+
page: Page number (default: 1).
|
|
401
|
+
count: Items per page (default: 20).
|
|
402
|
+
only_fields: Comma-separated list of fields to return (optional).
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
List of matching documents.
|
|
406
|
+
|
|
407
|
+
Raises:
|
|
408
|
+
ShipthisAPIError: If the request fails.
|
|
409
|
+
"""
|
|
410
|
+
return await self.get_list(
|
|
411
|
+
collection_name,
|
|
412
|
+
search_query=query,
|
|
413
|
+
page=page,
|
|
414
|
+
count=count,
|
|
415
|
+
only_fields=only_fields,
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
async def create_item(
|
|
419
|
+
self, collection_name: str, data: Dict[str, Any]
|
|
420
|
+
) -> Dict[str, Any]:
|
|
421
|
+
"""Create a new item in a collection.
|
|
422
|
+
|
|
423
|
+
Args:
|
|
424
|
+
collection_name: Name of the collection.
|
|
425
|
+
data: Document data.
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
Created document data.
|
|
429
|
+
|
|
430
|
+
Raises:
|
|
431
|
+
ShipthisAPIError: If the request fails.
|
|
432
|
+
"""
|
|
433
|
+
resp = await self._make_request(
|
|
434
|
+
"POST",
|
|
435
|
+
f"incollection/{collection_name}",
|
|
436
|
+
request_data={"reqbody": data},
|
|
437
|
+
)
|
|
438
|
+
if isinstance(resp, dict) and resp.get("data"):
|
|
439
|
+
return resp.get("data")
|
|
440
|
+
return resp
|
|
441
|
+
|
|
442
|
+
async def update_item(
|
|
443
|
+
self,
|
|
444
|
+
collection_name: str,
|
|
445
|
+
object_id: str,
|
|
446
|
+
updated_data: Dict[str, Any],
|
|
447
|
+
) -> Dict[str, Any]:
|
|
448
|
+
"""Update an existing item (full replacement).
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
collection_name: Name of the collection.
|
|
452
|
+
object_id: Document ID.
|
|
453
|
+
updated_data: Updated document data.
|
|
454
|
+
|
|
455
|
+
Returns:
|
|
456
|
+
Updated document data.
|
|
457
|
+
|
|
458
|
+
Raises:
|
|
459
|
+
ShipthisAPIError: If the request fails.
|
|
460
|
+
"""
|
|
461
|
+
resp = await self._make_request(
|
|
462
|
+
"PUT",
|
|
463
|
+
f"incollection/{collection_name}/{object_id}",
|
|
464
|
+
request_data={"reqbody": updated_data},
|
|
465
|
+
)
|
|
466
|
+
if isinstance(resp, dict) and resp.get("data"):
|
|
467
|
+
return resp.get("data")
|
|
468
|
+
return resp
|
|
469
|
+
|
|
470
|
+
async def patch_item(
|
|
471
|
+
self,
|
|
472
|
+
collection_name: str,
|
|
473
|
+
object_id: str,
|
|
474
|
+
update_fields: Dict[str, Any],
|
|
475
|
+
) -> Dict[str, Any]:
|
|
476
|
+
"""Patch specific fields of an item.
|
|
477
|
+
|
|
478
|
+
This is the recommended way to update document fields. It goes through
|
|
479
|
+
full field validation, workflow triggers, audit logging, and business logic.
|
|
480
|
+
|
|
481
|
+
Args:
|
|
482
|
+
collection_name: Name of the collection (e.g., "sea_shipment", "fcl_load").
|
|
483
|
+
object_id: Document ID.
|
|
484
|
+
update_fields: Dictionary of field_id to value mappings.
|
|
485
|
+
|
|
486
|
+
Returns:
|
|
487
|
+
Updated document data.
|
|
488
|
+
|
|
489
|
+
Raises:
|
|
490
|
+
ShipthisAPIError: If the request fails.
|
|
491
|
+
|
|
492
|
+
Example:
|
|
493
|
+
await client.patch_item(
|
|
494
|
+
"fcl_load",
|
|
495
|
+
"68a4f906743189ad061429a7",
|
|
496
|
+
update_fields={"container_no": "CONT123", "seal_no": "SEAL456"}
|
|
497
|
+
)
|
|
498
|
+
"""
|
|
499
|
+
return await self._make_request(
|
|
500
|
+
"PATCH",
|
|
501
|
+
f"incollection/{collection_name}/{object_id}",
|
|
502
|
+
request_data={"update_fields": update_fields},
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
async def delete_item(self, collection_name: str, object_id: str) -> Dict[str, Any]:
|
|
506
|
+
"""Delete an item.
|
|
507
|
+
|
|
508
|
+
Args:
|
|
509
|
+
collection_name: Name of the collection.
|
|
510
|
+
object_id: Document ID.
|
|
511
|
+
|
|
512
|
+
Returns:
|
|
513
|
+
Deletion response.
|
|
514
|
+
|
|
515
|
+
Raises:
|
|
516
|
+
ShipthisAPIError: If the request fails.
|
|
517
|
+
"""
|
|
518
|
+
return await self._make_request(
|
|
519
|
+
"DELETE",
|
|
520
|
+
f"incollection/{collection_name}/{object_id}",
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
# ==================== Workflow Operations ====================
|
|
524
|
+
|
|
525
|
+
async def get_job_status(
|
|
526
|
+
self, collection_name: str, object_id: str
|
|
527
|
+
) -> Dict[str, Any]:
|
|
528
|
+
"""Get the job status for a document.
|
|
529
|
+
|
|
530
|
+
Args:
|
|
531
|
+
collection_name: Name of the collection.
|
|
532
|
+
object_id: Document ID.
|
|
533
|
+
|
|
534
|
+
Returns:
|
|
535
|
+
Job status data.
|
|
536
|
+
|
|
537
|
+
Raises:
|
|
538
|
+
ShipthisAPIError: If the request fails.
|
|
539
|
+
"""
|
|
540
|
+
return await self._make_request(
|
|
541
|
+
"GET",
|
|
542
|
+
f"workflow/{collection_name}/job_status/{object_id}",
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
async def set_job_status(
|
|
546
|
+
self,
|
|
547
|
+
collection_name: str,
|
|
548
|
+
object_id: str,
|
|
549
|
+
action_index: int,
|
|
550
|
+
) -> Dict[str, Any]:
|
|
551
|
+
"""Set the job status for a document.
|
|
552
|
+
|
|
553
|
+
Args:
|
|
554
|
+
collection_name: Name of the collection.
|
|
555
|
+
object_id: Document ID.
|
|
556
|
+
action_index: The index of the action to execute.
|
|
557
|
+
|
|
558
|
+
Returns:
|
|
559
|
+
Updated status data.
|
|
560
|
+
|
|
561
|
+
Raises:
|
|
562
|
+
ShipthisAPIError: If the request fails.
|
|
563
|
+
"""
|
|
564
|
+
return await self._make_request(
|
|
565
|
+
"POST",
|
|
566
|
+
f"workflow/{collection_name}/job_status/{object_id}",
|
|
567
|
+
request_data={"action_index": action_index},
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
async def get_workflow(self, object_id: str) -> Dict[str, Any]:
|
|
571
|
+
"""Get a workflow configuration.
|
|
572
|
+
|
|
573
|
+
Args:
|
|
574
|
+
object_id: Workflow ID.
|
|
575
|
+
|
|
576
|
+
Returns:
|
|
577
|
+
Workflow data.
|
|
578
|
+
|
|
579
|
+
Raises:
|
|
580
|
+
ShipthisAPIError: If the request fails.
|
|
581
|
+
"""
|
|
582
|
+
return await self._make_request("GET", f"incollection/workflow/{object_id}")
|
|
583
|
+
|
|
584
|
+
# ==================== Reports ====================
|
|
585
|
+
|
|
586
|
+
async def get_report_view(
|
|
587
|
+
self,
|
|
588
|
+
report_name: str,
|
|
589
|
+
start_date: str,
|
|
590
|
+
end_date: str,
|
|
591
|
+
post_data: Dict[str, Any] = None,
|
|
592
|
+
output_type: str = "json",
|
|
593
|
+
skip_meta: bool = True,
|
|
594
|
+
) -> Dict[str, Any]:
|
|
595
|
+
"""Get a report view.
|
|
596
|
+
|
|
597
|
+
Args:
|
|
598
|
+
report_name: Name of the report.
|
|
599
|
+
start_date: Start date (YYYY-MM-DD or timestamp).
|
|
600
|
+
end_date: End date (YYYY-MM-DD or timestamp).
|
|
601
|
+
post_data: Additional filter data (optional).
|
|
602
|
+
output_type: Output format (default: "json").
|
|
603
|
+
skip_meta: Skip metadata (default: True).
|
|
604
|
+
|
|
605
|
+
Returns:
|
|
606
|
+
Report data.
|
|
607
|
+
|
|
608
|
+
Raises:
|
|
609
|
+
ShipthisAPIError: If the request fails.
|
|
610
|
+
"""
|
|
611
|
+
params = {
|
|
612
|
+
"start_date": start_date,
|
|
613
|
+
"end_date": end_date,
|
|
614
|
+
"output_type": output_type,
|
|
615
|
+
"skip_meta": "true" if skip_meta else "false",
|
|
616
|
+
}
|
|
617
|
+
if self.location_id:
|
|
618
|
+
params["location"] = self.location_id
|
|
619
|
+
|
|
620
|
+
return await self._make_request(
|
|
621
|
+
"POST",
|
|
622
|
+
f"report-view/{report_name}",
|
|
623
|
+
query_params=params,
|
|
624
|
+
request_data=post_data,
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
# ==================== Third-party Services ====================
|
|
628
|
+
|
|
629
|
+
async def get_exchange_rate(
|
|
630
|
+
self,
|
|
631
|
+
source_currency: str,
|
|
632
|
+
target_currency: str = "USD",
|
|
633
|
+
date: int = None,
|
|
634
|
+
) -> Dict[str, Any]:
|
|
635
|
+
"""Get exchange rate between currencies.
|
|
636
|
+
|
|
637
|
+
Args:
|
|
638
|
+
source_currency: Source currency code (e.g., "EUR").
|
|
639
|
+
target_currency: Target currency code (default: "USD").
|
|
640
|
+
date: Date timestamp in milliseconds (optional, defaults to now).
|
|
641
|
+
|
|
642
|
+
Returns:
|
|
643
|
+
Exchange rate data.
|
|
644
|
+
|
|
645
|
+
Raises:
|
|
646
|
+
ShipthisAPIError: If the request fails.
|
|
647
|
+
"""
|
|
648
|
+
import time
|
|
649
|
+
if date is None:
|
|
650
|
+
date = int(time.time() * 1000)
|
|
651
|
+
|
|
652
|
+
return await self._make_request(
|
|
653
|
+
"GET",
|
|
654
|
+
f"thirdparty/currency?source={source_currency}&target={target_currency}&date={date}",
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
async def autocomplete(
|
|
658
|
+
self,
|
|
659
|
+
reference_name: str,
|
|
660
|
+
data: Dict[str, Any],
|
|
661
|
+
) -> List[Dict[str, Any]]:
|
|
662
|
+
"""Get autocomplete suggestions for a reference field.
|
|
663
|
+
|
|
664
|
+
Args:
|
|
665
|
+
reference_name: Name of the reference (e.g., "port", "airport").
|
|
666
|
+
data: Search data with query.
|
|
667
|
+
|
|
668
|
+
Returns:
|
|
669
|
+
List of suggestions.
|
|
670
|
+
|
|
671
|
+
Raises:
|
|
672
|
+
ShipthisAPIError: If the request fails.
|
|
673
|
+
"""
|
|
674
|
+
params = {}
|
|
675
|
+
if self.location_id:
|
|
676
|
+
params["location"] = self.location_id
|
|
677
|
+
|
|
678
|
+
return await self._make_request(
|
|
679
|
+
"POST",
|
|
680
|
+
f"autocomplete-reference/{reference_name}",
|
|
681
|
+
query_params=params if params else None,
|
|
682
|
+
request_data=data,
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
async def search_location(self, query: str) -> List[Dict[str, Any]]:
|
|
686
|
+
"""Search for locations using Google Places.
|
|
687
|
+
|
|
688
|
+
Args:
|
|
689
|
+
query: Search query string.
|
|
690
|
+
|
|
691
|
+
Returns:
|
|
692
|
+
List of location suggestions.
|
|
693
|
+
|
|
694
|
+
Raises:
|
|
695
|
+
ShipthisAPIError: If the request fails.
|
|
696
|
+
"""
|
|
697
|
+
return await self._make_request(
|
|
698
|
+
"GET",
|
|
699
|
+
f"thirdparty/search-place-autocomplete?query={query}",
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
async def get_place_details(
|
|
703
|
+
self,
|
|
704
|
+
place_id: str,
|
|
705
|
+
description: str = "",
|
|
706
|
+
) -> Dict[str, Any]:
|
|
707
|
+
"""Get details for a Google Place.
|
|
708
|
+
|
|
709
|
+
Args:
|
|
710
|
+
place_id: Google Place ID.
|
|
711
|
+
description: Place description (optional).
|
|
712
|
+
|
|
713
|
+
Returns:
|
|
714
|
+
Place details.
|
|
715
|
+
|
|
716
|
+
Raises:
|
|
717
|
+
ShipthisAPIError: If the request fails.
|
|
718
|
+
"""
|
|
719
|
+
return await self._make_request(
|
|
720
|
+
"GET",
|
|
721
|
+
f"thirdparty/select-google-place?query={place_id}&description={description}",
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
# ==================== Conversations ====================
|
|
725
|
+
|
|
726
|
+
async def create_conversation(
|
|
727
|
+
self,
|
|
728
|
+
view_name: str,
|
|
729
|
+
document_id: str,
|
|
730
|
+
conversation_data: Dict[str, Any],
|
|
731
|
+
) -> Dict[str, Any]:
|
|
732
|
+
"""Create a conversation/message on a document.
|
|
733
|
+
|
|
734
|
+
Args:
|
|
735
|
+
view_name: Collection/view name.
|
|
736
|
+
document_id: Document ID.
|
|
737
|
+
conversation_data: Conversation data (message, type, etc.).
|
|
738
|
+
|
|
739
|
+
Returns:
|
|
740
|
+
Created conversation data.
|
|
741
|
+
|
|
742
|
+
Raises:
|
|
743
|
+
ShipthisAPIError: If the request fails.
|
|
744
|
+
"""
|
|
745
|
+
payload = {
|
|
746
|
+
"conversation": conversation_data,
|
|
747
|
+
"document_id": document_id,
|
|
748
|
+
"view_name": view_name,
|
|
749
|
+
"message_type": conversation_data.get("type", ""),
|
|
750
|
+
}
|
|
751
|
+
return await self._make_request("POST", "conversation", request_data=payload)
|
|
752
|
+
|
|
753
|
+
async def get_conversations(
|
|
754
|
+
self,
|
|
755
|
+
view_name: str,
|
|
756
|
+
document_id: str,
|
|
757
|
+
message_type: str = "all",
|
|
758
|
+
page: int = 1,
|
|
759
|
+
count: int = 100,
|
|
760
|
+
) -> Dict[str, Any]:
|
|
761
|
+
"""Get conversations for a document.
|
|
762
|
+
|
|
763
|
+
Args:
|
|
764
|
+
view_name: Collection/view name.
|
|
765
|
+
document_id: Document ID.
|
|
766
|
+
message_type: Filter by message type (default: "all").
|
|
767
|
+
page: Page number (default: 1).
|
|
768
|
+
count: Items per page (default: 100).
|
|
769
|
+
|
|
770
|
+
Returns:
|
|
771
|
+
Conversations data.
|
|
772
|
+
|
|
773
|
+
Raises:
|
|
774
|
+
ShipthisAPIError: If the request fails.
|
|
775
|
+
"""
|
|
776
|
+
params = {
|
|
777
|
+
"view_name": view_name,
|
|
778
|
+
"document_id": document_id,
|
|
779
|
+
"page": str(page),
|
|
780
|
+
"count": str(count),
|
|
781
|
+
"message_type": message_type,
|
|
782
|
+
"version": "2",
|
|
783
|
+
}
|
|
784
|
+
return await self._make_request("GET", "conversation", query_params=params)
|
|
785
|
+
|
|
786
|
+
# ==================== Bulk Operations ====================
|
|
787
|
+
|
|
788
|
+
async def bulk_edit(
|
|
789
|
+
self,
|
|
790
|
+
collection_name: str,
|
|
791
|
+
ids: List[str],
|
|
792
|
+
update_data: Dict[str, Any],
|
|
793
|
+
external_update_data: Dict[str, Any] = None,
|
|
794
|
+
) -> Dict[str, Any]:
|
|
795
|
+
"""Bulk edit multiple items in a collection.
|
|
796
|
+
|
|
797
|
+
Args:
|
|
798
|
+
collection_name: Name of the collection.
|
|
799
|
+
ids: List of document IDs to update.
|
|
800
|
+
update_data: Key-value pairs of fields to update.
|
|
801
|
+
external_update_data: Extra data for external updates (optional).
|
|
802
|
+
|
|
803
|
+
Returns:
|
|
804
|
+
Update response.
|
|
805
|
+
|
|
806
|
+
Raises:
|
|
807
|
+
ShipthisAPIError: If the request fails.
|
|
808
|
+
|
|
809
|
+
Example:
|
|
810
|
+
await client.bulk_edit(
|
|
811
|
+
"customer",
|
|
812
|
+
ids=["5fdc00487f7636c97b9fa064", "608fe19fc33215427867f34e"],
|
|
813
|
+
update_data={"company.fax_no": "12323231", "address.state": "California"}
|
|
814
|
+
)
|
|
815
|
+
"""
|
|
816
|
+
payload = {
|
|
817
|
+
"data": {
|
|
818
|
+
"ids": ids,
|
|
819
|
+
"update_data": update_data,
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
if external_update_data:
|
|
823
|
+
payload["data"]["external_update_data"] = external_update_data
|
|
824
|
+
|
|
825
|
+
return await self._make_request(
|
|
826
|
+
"POST",
|
|
827
|
+
f"incollection_group_edit/{collection_name}",
|
|
828
|
+
request_data=payload,
|
|
829
|
+
)
|
|
830
|
+
|
|
831
|
+
# ==================== Workflow Actions ====================
|
|
832
|
+
|
|
833
|
+
async def primary_workflow_action(
|
|
834
|
+
self,
|
|
835
|
+
collection: str,
|
|
836
|
+
workflow_id: str,
|
|
837
|
+
object_id: str,
|
|
838
|
+
action_index: int,
|
|
839
|
+
intended_state_id: str,
|
|
840
|
+
start_state_id: str = None,
|
|
841
|
+
) -> Dict[str, Any]:
|
|
842
|
+
"""Trigger a primary workflow transition (status change on a record).
|
|
843
|
+
|
|
844
|
+
Args:
|
|
845
|
+
collection: Target collection (e.g., "pickup_delivery", "sea_shipment").
|
|
846
|
+
workflow_id: Workflow status key (e.g., "job_status").
|
|
847
|
+
object_id: The document's ID.
|
|
848
|
+
action_index: Index of action within the status.
|
|
849
|
+
intended_state_id: Intended resulting state ID (e.g., "ops_complete").
|
|
850
|
+
start_state_id: Current/starting state ID (optional).
|
|
851
|
+
|
|
852
|
+
Returns:
|
|
853
|
+
Workflow action response with success status and resulting state.
|
|
854
|
+
|
|
855
|
+
Raises:
|
|
856
|
+
ShipthisAPIError: If the request fails.
|
|
857
|
+
|
|
858
|
+
Example:
|
|
859
|
+
await client.primary_workflow_action(
|
|
860
|
+
collection="pickup_delivery",
|
|
861
|
+
workflow_id="job_status",
|
|
862
|
+
object_id="68a4f906743189ad061429a7",
|
|
863
|
+
action_index=0,
|
|
864
|
+
intended_state_id="ops_complete",
|
|
865
|
+
start_state_id="closed"
|
|
866
|
+
)
|
|
867
|
+
"""
|
|
868
|
+
payload = {
|
|
869
|
+
"action_index": action_index,
|
|
870
|
+
"intended_state_id": intended_state_id,
|
|
871
|
+
}
|
|
872
|
+
if start_state_id:
|
|
873
|
+
payload["start_state_id"] = start_state_id
|
|
874
|
+
|
|
875
|
+
return await self._make_request(
|
|
876
|
+
"POST",
|
|
877
|
+
f"workflow/{collection}/{workflow_id}/{object_id}",
|
|
878
|
+
request_data=payload,
|
|
879
|
+
)
|
|
880
|
+
|
|
881
|
+
async def secondary_workflow_action(
|
|
882
|
+
self,
|
|
883
|
+
collection: str,
|
|
884
|
+
workflow_id: str,
|
|
885
|
+
object_id: str,
|
|
886
|
+
target_state: str,
|
|
887
|
+
additional_data: Dict[str, Any] = None,
|
|
888
|
+
) -> Dict[str, Any]:
|
|
889
|
+
"""Trigger a secondary workflow transition (sub-status change).
|
|
890
|
+
|
|
891
|
+
Args:
|
|
892
|
+
collection: Target collection (e.g., "pickup_delivery").
|
|
893
|
+
workflow_id: Secondary status key (e.g., "driver_status").
|
|
894
|
+
object_id: The document's ID.
|
|
895
|
+
target_state: Resulting sub-state (e.g., "to_pick_up").
|
|
896
|
+
additional_data: Optional additional data to send with the request.
|
|
897
|
+
|
|
898
|
+
Returns:
|
|
899
|
+
Workflow action response.
|
|
900
|
+
|
|
901
|
+
Raises:
|
|
902
|
+
ShipthisAPIError: If the request fails.
|
|
903
|
+
|
|
904
|
+
Example:
|
|
905
|
+
await client.secondary_workflow_action(
|
|
906
|
+
collection="pickup_delivery",
|
|
907
|
+
workflow_id="driver_status",
|
|
908
|
+
object_id="67ed10859b7cf551a19f813e",
|
|
909
|
+
target_state="to_pick_up"
|
|
910
|
+
)
|
|
911
|
+
"""
|
|
912
|
+
payload = additional_data or {}
|
|
913
|
+
|
|
914
|
+
return await self._make_request(
|
|
915
|
+
"POST",
|
|
916
|
+
f"workflow/{collection}/{workflow_id}/{object_id}/{target_state}",
|
|
917
|
+
request_data=payload,
|
|
918
|
+
)
|
|
919
|
+
|
|
920
|
+
# ==================== File Upload ====================
|
|
921
|
+
|
|
922
|
+
async def upload_file(
|
|
923
|
+
self,
|
|
924
|
+
file_path: str,
|
|
925
|
+
file_name: str = None,
|
|
926
|
+
) -> Dict[str, Any]:
|
|
927
|
+
"""Upload a file.
|
|
928
|
+
|
|
929
|
+
Args:
|
|
930
|
+
file_path: Path to the file to upload.
|
|
931
|
+
file_name: Custom file name (optional).
|
|
932
|
+
|
|
933
|
+
Returns:
|
|
934
|
+
Upload response with file URL.
|
|
935
|
+
|
|
936
|
+
Raises:
|
|
937
|
+
ShipthisAPIError: If the request fails.
|
|
938
|
+
"""
|
|
939
|
+
import os
|
|
940
|
+
|
|
941
|
+
if file_name is None:
|
|
942
|
+
file_name = os.path.basename(file_path)
|
|
943
|
+
|
|
944
|
+
upload_url = self.base_api_endpoint.replace("/api/v3/", "").rstrip("/")
|
|
945
|
+
upload_url = upload_url.replace("api.", "upload.")
|
|
946
|
+
upload_url = f"{upload_url}/api/v3/file-upload"
|
|
947
|
+
|
|
948
|
+
headers = self._get_headers()
|
|
949
|
+
# Remove Content-Type for multipart
|
|
950
|
+
headers.pop("Content-Type", None)
|
|
951
|
+
|
|
952
|
+
try:
|
|
953
|
+
with open(file_path, "rb") as f:
|
|
954
|
+
files = {"file": (file_name, f)}
|
|
955
|
+
async with httpx.AsyncClient(timeout=self.timeout * 2) as client:
|
|
956
|
+
response = await client.post(
|
|
957
|
+
upload_url,
|
|
958
|
+
headers=headers,
|
|
959
|
+
files=files,
|
|
960
|
+
)
|
|
961
|
+
except FileNotFoundError:
|
|
962
|
+
raise ShipthisRequestError(
|
|
963
|
+
message=f"File not found: {file_path}",
|
|
964
|
+
status_code=0,
|
|
965
|
+
)
|
|
966
|
+
except httpx.RequestError as e:
|
|
967
|
+
raise ShipthisRequestError(
|
|
968
|
+
message=f"Upload failed: {str(e)}",
|
|
969
|
+
status_code=0,
|
|
970
|
+
)
|
|
971
|
+
|
|
972
|
+
if response.status_code == 200:
|
|
973
|
+
try:
|
|
974
|
+
return response.json()
|
|
975
|
+
except json.JSONDecodeError:
|
|
976
|
+
return {"url": response.text}
|
|
977
|
+
|
|
978
|
+
raise ShipthisRequestError(
|
|
979
|
+
message=f"Upload failed with status {response.status_code}",
|
|
980
|
+
status_code=response.status_code,
|
|
981
|
+
)
|
|
982
|
+
|
|
983
|
+
# ==================== Reference Linked Fields ====================
|
|
984
|
+
|
|
985
|
+
async def create_reference_linked_field(
|
|
986
|
+
self,
|
|
987
|
+
collection_name: str,
|
|
988
|
+
doc_id: str,
|
|
989
|
+
payload: Dict[str, Any],
|
|
990
|
+
) -> Dict[str, Any]:
|
|
991
|
+
"""Create a reference-linked field on a document.
|
|
992
|
+
|
|
993
|
+
Args:
|
|
994
|
+
collection_name: Collection name.
|
|
995
|
+
doc_id: Document ID.
|
|
996
|
+
payload: Field data to create.
|
|
997
|
+
|
|
998
|
+
Returns:
|
|
999
|
+
API response.
|
|
1000
|
+
|
|
1001
|
+
Raises:
|
|
1002
|
+
ShipthisAPIError: If the request fails.
|
|
1003
|
+
|
|
1004
|
+
Example:
|
|
1005
|
+
await client.create_reference_linked_field(
|
|
1006
|
+
"sea_shipment",
|
|
1007
|
+
"68a4f906743189ad061429a7",
|
|
1008
|
+
payload={"field_name": "containers", "data": {...}}
|
|
1009
|
+
)
|
|
1010
|
+
"""
|
|
1011
|
+
return await self._make_request(
|
|
1012
|
+
"POST",
|
|
1013
|
+
f"incollection/create-reference-linked-field/{collection_name}/{doc_id}",
|
|
1014
|
+
request_data=payload,
|
|
1015
|
+
)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
ShipthisAPI/__init__.py,sha256=6bJiEUPwRd3vgRVABju-NlGaTCyh074gNkd7Im8zswU,322
|
|
2
|
+
ShipthisAPI/shipthisapi.py,sha256=__SzjYFohS3mOjK0tXShWSBpLEuVsZm4diLguR7IPfw,31512
|
|
3
|
+
shipthisapi_python-3.0.4.dist-info/METADATA,sha256=siKVxarQlEk9I_cXF3Pa2IiWh21UL0v04qM3Z84d8eg,2213
|
|
4
|
+
shipthisapi_python-3.0.4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
5
|
+
shipthisapi_python-3.0.4.dist-info/top_level.txt,sha256=35r4y5buufpRwYKMsKPB59GoDeEprokRcBpgf-PVEFg,12
|
|
6
|
+
shipthisapi_python-3.0.4.dist-info/RECORD,,
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
ShipthisAPI/__init__.py,sha256=eEx4pJ9QtrqOUJqCNtp_u5GEehhUFSG_QpRCJNHOl2Q,93
|
|
2
|
-
ShipthisAPI/shipthisapi.py,sha256=pUQRZSEJph3eSQCf_UGqy3vv34pjNznRrBIckDi0NiI,933
|
|
3
|
-
shipthisapi_python-3.0.2.dist-info/METADATA,sha256=faMFG39_GdM0C9I8OYJR3CxtRhza8rGZrQ1GUMO_YI0,2213
|
|
4
|
-
shipthisapi_python-3.0.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
5
|
-
shipthisapi_python-3.0.2.dist-info/top_level.txt,sha256=35r4y5buufpRwYKMsKPB59GoDeEprokRcBpgf-PVEFg,12
|
|
6
|
-
shipthisapi_python-3.0.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|