shipthisapi-python 3.0.4__tar.gz → 3.1.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shipthisapi-python
3
- Version: 3.0.4
3
+ Version: 3.1.0
4
4
  Summary: ShipthisAPI async utility package
5
5
  Home-page: https://github.com/shipthisco/shipthisapi-python
6
6
  Author: Mayur Rawte
@@ -1,5 +1,5 @@
1
1
  # __variables__ with double-quoted values will be available in setup.py
2
- __version__ = "3.0.4"
2
+ __version__ = "3.0.5"
3
3
 
4
4
  from .shipthisapi import (
5
5
  ShipthisAPI,
@@ -107,6 +107,7 @@ class ShipthisAPI:
107
107
  self.custom_headers = custom_headers or {}
108
108
  self.organisation_info = None
109
109
  self.is_connected = False
110
+ self._client: httpx.AsyncClient = None
110
111
 
111
112
  def set_region_location(self, region_id: str, location_id: str) -> None:
112
113
  """Set the region and location for subsequent requests.
@@ -146,6 +147,19 @@ class ShipthisAPI:
146
147
  headers.update(override_headers)
147
148
  return headers
148
149
 
150
+ async def _ensure_client(self) -> httpx.AsyncClient:
151
+ """Get or create the shared HTTP client."""
152
+ if self._client is None:
153
+ self._client = httpx.AsyncClient(timeout=self.timeout)
154
+ return self._client
155
+
156
+ async def __aenter__(self):
157
+ await self.connect()
158
+ return self
159
+
160
+ async def __aexit__(self, *args):
161
+ await self.disconnect()
162
+
149
163
  async def _make_request(
150
164
  self,
151
165
  method: str,
@@ -172,31 +186,31 @@ class ShipthisAPI:
172
186
  """
173
187
  url = self.base_api_endpoint + path
174
188
  request_headers = self._get_headers(headers)
189
+ client = await self._ensure_client()
175
190
 
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
- )
191
+ try:
192
+ response = await client.request(
193
+ method,
194
+ url,
195
+ headers=request_headers,
196
+ params=query_params,
197
+ json=request_data,
198
+ )
199
+ except httpx.TimeoutException:
200
+ raise ShipthisRequestError(
201
+ message="Request timed out",
202
+ status_code=408,
203
+ )
204
+ except httpx.ConnectError as e:
205
+ raise ShipthisRequestError(
206
+ message=f"Connection error: {str(e)}",
207
+ status_code=0,
208
+ )
209
+ except httpx.RequestError as e:
210
+ raise ShipthisRequestError(
211
+ message=f"Request failed: {str(e)}",
212
+ status_code=0,
213
+ )
200
214
 
201
215
  # Handle authentication errors
202
216
  if response.status_code == 401:
@@ -275,9 +289,13 @@ class ShipthisAPI:
275
289
  "organisation": self.organisation_info,
276
290
  }
277
291
 
278
- def disconnect(self) -> None:
279
- """Disconnect and clear credentials."""
292
+ async def disconnect(self) -> None:
293
+ """Disconnect, close the HTTP client, and clear credentials."""
294
+ if self._client is not None:
295
+ await self._client.aclose()
296
+ self._client = None
280
297
  self.x_api_key = None
298
+ self.organisation_info = None
281
299
  self.is_connected = False
282
300
 
283
301
  # ==================== Info ====================
@@ -321,14 +339,18 @@ class ShipthisAPI:
321
339
  params = {}
322
340
  if only_fields:
323
341
  params["only"] = only_fields
324
- return await self._make_request("GET", path, query_params=params if params else None)
342
+ return await self._make_request(
343
+ "GET", path, query_params=params if params else None
344
+ )
325
345
  else:
326
346
  params = {}
327
347
  if filters:
328
348
  params["query_filter_v2"] = json.dumps(filters)
329
349
  if only_fields:
330
350
  params["only"] = only_fields
331
- resp = await self._make_request("GET", f"incollection/{collection_name}", params)
351
+ resp = await self._make_request(
352
+ "GET", f"incollection/{collection_name}", params
353
+ )
332
354
  if isinstance(resp, dict) and resp.get("items"):
333
355
  return resp.get("items")[0]
334
356
  return None
@@ -378,7 +400,9 @@ class ShipthisAPI:
378
400
  if not meta:
379
401
  params["meta"] = "false"
380
402
 
381
- response = await self._make_request("GET", f"incollection/{collection_name}", params)
403
+ response = await self._make_request(
404
+ "GET", f"incollection/{collection_name}", params
405
+ )
382
406
 
383
407
  if isinstance(response, dict):
384
408
  return response.get("items", [])
@@ -416,13 +440,25 @@ class ShipthisAPI:
416
440
  )
417
441
 
418
442
  async def create_item(
419
- self, collection_name: str, data: Dict[str, Any]
443
+ self,
444
+ collection_name: str,
445
+ data: Dict[str, Any],
446
+ ignore_new_required: bool = False,
447
+ skip_sequence_if_exists: bool = False,
448
+ replicate_count: int = 0,
449
+ input_filters: Optional[Dict[str, Any]] = None,
450
+ action_op_data: Optional[Dict[str, Any]] = None,
420
451
  ) -> Dict[str, Any]:
421
- """Create a new item in a collection.
452
+ """Create a new item in a collection with all advanced Shipthis features.
422
453
 
423
454
  Args:
424
455
  collection_name: Name of the collection.
425
456
  data: Document data.
457
+ ignore_new_required: Ignore new required fields (default: False).
458
+ skip_sequence_if_exists: Skip sequence if exists (default: False).
459
+ replicate_count: Number of times to replicate the item (default: 0).
460
+ input_filters: Input filters (optional).
461
+ action_op_data: Action operation data (optional).
426
462
 
427
463
  Returns:
428
464
  Created document data.
@@ -430,10 +466,26 @@ class ShipthisAPI:
430
466
  Raises:
431
467
  ShipthisAPIError: If the request fails.
432
468
  """
469
+ params = {}
470
+ if replicate_count > 0:
471
+ params["replicate_count"] = min(replicate_count, 100)
472
+ if input_filters:
473
+ params["input_filters"] = json.dumps(input_filters)
474
+
475
+ request_payload = {
476
+ "reqbody": data,
477
+ "ignore_new_required": ignore_new_required,
478
+ "skip_sequence_if_exists": skip_sequence_if_exists,
479
+ }
480
+
481
+ if action_op_data:
482
+ request_payload["action_op_data"] = action_op_data
483
+
433
484
  resp = await self._make_request(
434
485
  "POST",
435
486
  f"incollection/{collection_name}",
436
- request_data={"reqbody": data},
487
+ query_params=params,
488
+ request_data=request_payload,
437
489
  )
438
490
  if isinstance(resp, dict) and resp.get("data"):
439
491
  return resp.get("data")
@@ -471,9 +523,10 @@ class ShipthisAPI:
471
523
  self,
472
524
  collection_name: str,
473
525
  object_id: str,
474
- update_fields: Dict[str, Any],
526
+ update_fields: Dict[str, Any] = None,
527
+ workflow: List[Dict[str, Any]] = None,
475
528
  ) -> Dict[str, Any]:
476
- """Patch specific fields of an item.
529
+ """Patch specific fields of an item and/or trigger workflow transitions.
477
530
 
478
531
  This is the recommended way to update document fields. It goes through
479
532
  full field validation, workflow triggers, audit logging, and business logic.
@@ -482,24 +535,57 @@ class ShipthisAPI:
482
535
  collection_name: Name of the collection (e.g., "sea_shipment", "fcl_load").
483
536
  object_id: Document ID.
484
537
  update_fields: Dictionary of field_id to value mappings.
538
+ workflow: List of workflow actions to execute. Each action is a dict with:
539
+ - workflow_id (str): The workflow identifier (e.g., "status").
540
+ - value (str, optional): Target state for direct-mode workflows.
541
+ - action_id (str, optional): Action ID for action-based workflows.
542
+ - payload (any, optional): Extra data for the action.
485
543
 
486
544
  Returns:
487
- Updated document data.
545
+ Updated document data and/or workflow results.
488
546
 
489
547
  Raises:
490
548
  ShipthisAPIError: If the request fails.
491
549
 
492
550
  Example:
551
+ # Update fields only
493
552
  await client.patch_item(
494
553
  "fcl_load",
495
554
  "68a4f906743189ad061429a7",
496
555
  update_fields={"container_no": "CONT123", "seal_no": "SEAL456"}
497
556
  )
557
+
558
+ # Workflow transition only (direct mode)
559
+ await client.patch_item(
560
+ "sea_shipment",
561
+ "68a4f906743189ad061429a7",
562
+ workflow=[{"workflow_id": "status", "value": "approved"}]
563
+ )
564
+
565
+ # Workflow transition only (action-based)
566
+ await client.patch_item(
567
+ "sea_shipment",
568
+ "68a4f906743189ad061429a7",
569
+ workflow=[{"workflow_id": "status", "action_id": "submit_review"}]
570
+ )
571
+
572
+ # Fields + workflow in one call
573
+ await client.patch_item(
574
+ "sea_shipment",
575
+ "68a4f906743189ad061429a7",
576
+ update_fields={"notes": "ready"},
577
+ workflow=[{"workflow_id": "status", "action_id": "submit_review"}]
578
+ )
498
579
  """
580
+ request_data = {}
581
+ if update_fields is not None:
582
+ request_data["update_fields"] = update_fields
583
+ if workflow is not None:
584
+ request_data["workflow"] = workflow
499
585
  return await self._make_request(
500
586
  "PATCH",
501
587
  f"incollection/{collection_name}/{object_id}",
502
- request_data={"update_fields": update_fields},
588
+ request_data=request_data,
503
589
  )
504
590
 
505
591
  async def delete_item(self, collection_name: str, object_id: str) -> Dict[str, Any]:
@@ -646,12 +732,18 @@ class ShipthisAPI:
646
732
  ShipthisAPIError: If the request fails.
647
733
  """
648
734
  import time
735
+
649
736
  if date is None:
650
737
  date = int(time.time() * 1000)
651
738
 
652
739
  return await self._make_request(
653
740
  "GET",
654
- f"thirdparty/currency?source={source_currency}&target={target_currency}&date={date}",
741
+ "thirdparty/currency",
742
+ query_params={
743
+ "source": source_currency,
744
+ "target": target_currency,
745
+ "date": date,
746
+ },
655
747
  )
656
748
 
657
749
  async def autocomplete(
@@ -696,7 +788,8 @@ class ShipthisAPI:
696
788
  """
697
789
  return await self._make_request(
698
790
  "GET",
699
- f"thirdparty/search-place-autocomplete?query={query}",
791
+ "thirdparty/search-place-autocomplete",
792
+ query_params={"query": query},
700
793
  )
701
794
 
702
795
  async def get_place_details(
@@ -718,7 +811,8 @@ class ShipthisAPI:
718
811
  """
719
812
  return await self._make_request(
720
813
  "GET",
721
- f"thirdparty/select-google-place?query={place_id}&description={description}",
814
+ "thirdparty/select-google-place",
815
+ query_params={"query": place_id, "description": description},
722
816
  )
723
817
 
724
818
  # ==================== Conversations ====================
@@ -952,8 +1046,8 @@ class ShipthisAPI:
952
1046
  try:
953
1047
  with open(file_path, "rb") as f:
954
1048
  files = {"file": (file_name, f)}
955
- async with httpx.AsyncClient(timeout=self.timeout * 2) as client:
956
- response = await client.post(
1049
+ async with httpx.AsyncClient(timeout=self.timeout * 2) as upload_client:
1050
+ response = await upload_client.post(
957
1051
  upload_url,
958
1052
  headers=headers,
959
1053
  files=files,
@@ -6,7 +6,7 @@ with open("README.md", "r") as fh:
6
6
 
7
7
  setuptools.setup(
8
8
  name='shipthisapi-python',
9
- version='3.0.4',
9
+ version='3.1.0',
10
10
  author="Mayur Rawte",
11
11
  author_email="mayur@shipthis.co",
12
12
  description="ShipthisAPI async utility package",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shipthisapi-python
3
- Version: 3.0.4
3
+ Version: 3.1.0
4
4
  Summary: ShipthisAPI async utility package
5
5
  Home-page: https://github.com/shipthisco/shipthisapi-python
6
6
  Author: Mayur Rawte