@@ -107,6 +107,7 @@ def __init__(
107107 self .custom_headers = custom_headers or {}
108108 self .organisation_info = None
109109 self .is_connected = False
110+ self ._client : httpx .AsyncClient = None
110111
111112 def set_region_location (self , region_id : str , location_id : str ) -> None :
112113 """Set the region and location for subsequent requests.
@@ -146,6 +147,19 @@ def _get_headers(self, override_headers: Dict[str, str] = None) -> Dict[str, str
146147 headers .update (override_headers )
147148 return headers
148149
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+
149163 async def _make_request (
150164 self ,
151165 method : str ,
@@ -172,31 +186,31 @@ async def _make_request(
172186 """
173187 url = self .base_api_endpoint + path
174188 request_headers = self ._get_headers (headers )
189+ client = await self ._ensure_client ()
175190
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+ )
200214
201215 # Handle authentication errors
202216 if response .status_code == 401 :
@@ -275,9 +289,13 @@ async def connect(self) -> Dict[str, Any]:
275289 "organisation" : self .organisation_info ,
276290 }
277291
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
280297 self .x_api_key = None
298+ self .organisation_info = None
281299 self .is_connected = False
282300
283301 # ==================== Info ====================
@@ -505,9 +523,10 @@ async def patch_item(
505523 self ,
506524 collection_name : str ,
507525 object_id : str ,
508- update_fields : Dict [str , Any ],
526+ update_fields : Dict [str , Any ] = None ,
527+ workflow : List [Dict [str , Any ]] = None ,
509528 ) -> Dict [str , Any ]:
510- """Patch specific fields of an item.
529+ """Patch specific fields of an item and/or trigger workflow transitions .
511530
512531 This is the recommended way to update document fields. It goes through
513532 full field validation, workflow triggers, audit logging, and business logic.
@@ -516,24 +535,57 @@ async def patch_item(
516535 collection_name: Name of the collection (e.g., "sea_shipment", "fcl_load").
517536 object_id: Document ID.
518537 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.
519543
520544 Returns:
521- Updated document data.
545+ Updated document data and/or workflow results .
522546
523547 Raises:
524548 ShipthisAPIError: If the request fails.
525549
526550 Example:
551+ # Update fields only
527552 await client.patch_item(
528553 "fcl_load",
529554 "68a4f906743189ad061429a7",
530555 update_fields={"container_no": "CONT123", "seal_no": "SEAL456"}
531556 )
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+ )
532579 """
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
533585 return await self ._make_request (
534586 "PATCH" ,
535587 f"incollection/{ collection_name } /{ object_id } " ,
536- request_data = { "update_fields" : update_fields } ,
588+ request_data = request_data ,
537589 )
538590
539591 async def delete_item (self , collection_name : str , object_id : str ) -> Dict [str , Any ]:
@@ -686,7 +738,12 @@ async def get_exchange_rate(
686738
687739 return await self ._make_request (
688740 "GET" ,
689- 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+ },
690747 )
691748
692749 async def autocomplete (
@@ -731,7 +788,8 @@ async def search_location(self, query: str) -> List[Dict[str, Any]]:
731788 """
732789 return await self ._make_request (
733790 "GET" ,
734- f"thirdparty/search-place-autocomplete?query={ query } " ,
791+ "thirdparty/search-place-autocomplete" ,
792+ query_params = {"query" : query },
735793 )
736794
737795 async def get_place_details (
@@ -753,7 +811,8 @@ async def get_place_details(
753811 """
754812 return await self ._make_request (
755813 "GET" ,
756- f"thirdparty/select-google-place?query={ place_id } &description={ description } " ,
814+ "thirdparty/select-google-place" ,
815+ query_params = {"query" : place_id , "description" : description },
757816 )
758817
759818 # ==================== Conversations ====================
@@ -987,8 +1046,8 @@ async def upload_file(
9871046 try :
9881047 with open (file_path , "rb" ) as f :
9891048 files = {"file" : (file_name , f )}
990- async with httpx .AsyncClient (timeout = self .timeout * 2 ) as client :
991- response = await client .post (
1049+ async with httpx .AsyncClient (timeout = self .timeout * 2 ) as upload_client :
1050+ response = await upload_client .post (
9921051 upload_url ,
9931052 headers = headers ,
9941053 files = files ,
0 commit comments