From 530f2169e09b1608b826e8b49e0f5ffceb77263f Mon Sep 17 00:00:00 2001 From: mayurrawte Date: Fri, 6 Feb 2026 13:30:11 +0530 Subject: [PATCH 1/3] feat: add custom headers support and webhook methods - Add custom_headers parameter to override default headers - Make x_api_key optional (can use custom_headers for auth) - Add per-request header override support - Add webhook_sync method for synchronous updates - Add webhook_update method for async updates with actions - Add create_reference_linked_field method - Bump version to 2.2.0 --- ShipthisAPI/__init__.py | 2 +- ShipthisAPI/shipthisapi.py | 146 ++++++++++++++++++++++++++++++++++--- setup.py | 2 +- 3 files changed, 139 insertions(+), 11 deletions(-) diff --git a/ShipthisAPI/__init__.py b/ShipthisAPI/__init__.py index 8fa6255..c6227fb 100644 --- a/ShipthisAPI/__init__.py +++ b/ShipthisAPI/__init__.py @@ -1,5 +1,5 @@ # __variables__ with double-quoted values will be available in setup.py -__version__ = "2.1.0" +__version__ = "2.2.0" from .shipthisapi import ( ShipthisAPI, diff --git a/ShipthisAPI/shipthisapi.py b/ShipthisAPI/shipthisapi.py index d878141..18e55c6 100644 --- a/ShipthisAPI/shipthisapi.py +++ b/ShipthisAPI/shipthisapi.py @@ -5,6 +5,7 @@ Usage: from ShipthisAPI import ShipthisAPI + # Standard usage with API key client = ShipthisAPI( organisation="your_org_id", x_api_key="your_api_key", @@ -12,6 +13,15 @@ location_id="your_location" ) + # Server-to-server usage with custom headers + client = ShipthisAPI( + organisation="your_org_id", + custom_headers={ + "st-server-key": "your_server_key", + "st-server-name": "your_service_name", + } + ) + # Connect and validate client.connect() @@ -20,6 +30,9 @@ # Get a single item item = client.get_one_item("shipment", doc_id="abc123") + + # Webhook sync update + client.webhook_sync("fcl_load", doc_id, fields=[{"status": "completed"}]) """ from typing import Any, Dict, List, Optional, Union @@ -60,6 +73,7 @@ class ShipthisAPI: region_id: Region ID for requests. location_id: Location ID for requests. timeout: Request timeout in seconds. + custom_headers: Custom headers to override defaults. """ DEFAULT_TIMEOUT = 30 @@ -68,28 +82,28 @@ class ShipthisAPI: def __init__( self, organisation: str, - x_api_key: str, + x_api_key: str = None, user_type: str = "employee", region_id: str = None, location_id: str = None, timeout: int = None, base_url: str = None, + custom_headers: Dict[str, str] = None, ) -> None: """Initialize the Shipthis API client. Args: organisation: Your organisation ID. - x_api_key: Your API key. + x_api_key: Your API key (optional if using custom_headers with auth). user_type: User type for requests (default: "employee"). region_id: Region ID for requests. location_id: Location ID for requests. timeout: Request timeout in seconds (default: 30). base_url: Custom base URL (optional, for testing). + custom_headers: Custom headers that override defaults (e.g., for server-to-server auth). """ if not organisation: raise ValueError("organisation is required") - if not x_api_key: - raise ValueError("x_api_key is required") self.x_api_key = x_api_key self.organisation_id = organisation @@ -98,6 +112,7 @@ def __init__( self.location_id = location_id self.timeout = timeout or self.DEFAULT_TIMEOUT self.base_api_endpoint = base_url or self.BASE_API_ENDPOINT + self.custom_headers = custom_headers or {} self.organisation_info = None self.is_connected = False @@ -111,23 +126,32 @@ def set_region_location(self, region_id: str, location_id: str) -> None: self.region_id = region_id self.location_id = location_id - def _get_headers(self) -> Dict[str, str]: + def _get_headers(self, override_headers: Dict[str, str] = None) -> Dict[str, str]: """Build request headers. + Args: + override_headers: Headers to override for this specific request. + Returns: Dictionary of headers. """ headers = { - "x-api-key": self.x_api_key, "organisation": self.organisation_id, "usertype": self.user_type, "Content-Type": "application/json", "Accept": "application/json", } + if self.x_api_key: + headers["x-api-key"] = self.x_api_key if self.region_id: headers["region"] = self.region_id if self.location_id: headers["location"] = self.location_id + # Apply custom headers from init + headers.update(self.custom_headers) + # Apply per-request override headers + if override_headers: + headers.update(override_headers) return headers def _make_request( @@ -136,6 +160,7 @@ def _make_request( path: str, query_params: Dict[str, Any] = None, request_data: Dict[str, Any] = None, + headers: Dict[str, str] = None, ) -> Dict[str, Any]: """Make an HTTP request to the API. @@ -144,6 +169,7 @@ def _make_request( path: API endpoint path. query_params: Query parameters. request_data: Request body data. + headers: Headers to override for this request. Returns: API response data. @@ -153,7 +179,7 @@ def _make_request( ShipthisRequestError: If the request fails. """ url = self.base_api_endpoint + path - headers = self._get_headers() + request_headers = self._get_headers(headers) try: if request_data: @@ -161,7 +187,7 @@ def _make_request( method, url, json=request_data, - headers=headers, + headers=request_headers, params=query_params, timeout=self.timeout, ) @@ -169,7 +195,7 @@ def _make_request( response = requests.request( method, url, - headers=headers, + headers=request_headers, params=query_params, timeout=self.timeout, ) @@ -959,4 +985,106 @@ def upload_file( raise ShipthisRequestError( message=f"Upload failed with status {response.status_code}", status_code=response.status_code, + ) + + # ==================== Webhook Operations ==================== + + def webhook_sync( + self, + view_name: str, + doc_id: str, + fields: List[Dict[str, Any]], + ) -> Dict[str, Any]: + """Update document via webhook-sync (synchronous update). + + Args: + view_name: View/collection name (e.g., "sea_shipment", "fcl_load"). + doc_id: Document ID. + fields: List of field dicts to update. + + Returns: + API response with success status and data. + + Raises: + ShipthisAPIError: If the request fails. + + Example: + client.webhook_sync( + "fcl_load", + "68a4f906743189ad061429a7", + fields=[{"container_no": "CONT123"}, {"seal_no": "SEAL456"}] + ) + """ + payload = {"fields": fields} + return self._make_request( + "PUT", + f"webhook-sync/{self.organisation_id}/{view_name}/{doc_id}", + request_data=payload, + ) + + def webhook_update( + self, + view_name: str, + doc_id: str, + actions: List[Dict[str, Any]], + ) -> Dict[str, Any]: + """Update document via webhook (async update with actions). + + Args: + view_name: View/collection name. + doc_id: Document ID. + actions: List of action dicts to execute. + + Returns: + API response with success status and data. + + Raises: + ShipthisAPIError: If the request fails. + + Example: + client.webhook_update( + "sea_shipment", + "68a4f906743189ad061429a7", + actions=[{"action": "update_status", "value": "completed"}] + ) + """ + payload = {"actions": actions} + return self._make_request( + "PUT", + f"webhook/{self.organisation_id}/{view_name}/{doc_id}", + request_data=payload, + ) + + # ==================== Reference Linked Fields ==================== + + def create_reference_linked_field( + self, + collection_name: str, + doc_id: str, + payload: Dict[str, Any], + ) -> Dict[str, Any]: + """Create a reference-linked field on a document. + + Args: + collection_name: Collection name. + doc_id: Document ID. + payload: Field data to create. + + Returns: + API response. + + Raises: + ShipthisAPIError: If the request fails. + + Example: + client.create_reference_linked_field( + "sea_shipment", + "68a4f906743189ad061429a7", + payload={"field_name": "containers", "data": {...}} + ) + """ + return self._make_request( + "POST", + f"incollection/create-reference-linked-field/{collection_name}/{doc_id}", + request_data=payload, ) \ No newline at end of file diff --git a/setup.py b/setup.py index d6e6561..ef3da4f 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setuptools.setup( name='shipthisapi-python', - version='2.1.0', + version='2.2.0', author="Mayur Rawte", author_email="mayur@shipthis.co", description="ShipthisAPI utility package", From fa6afb64efdf08b75710e38f68aa306244e6bd32 Mon Sep 17 00:00:00 2001 From: mayurrawte Date: Fri, 6 Feb 2026 13:41:34 +0530 Subject: [PATCH 2/3] Remove internal auth example from public docs --- ShipthisAPI/shipthisapi.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/ShipthisAPI/shipthisapi.py b/ShipthisAPI/shipthisapi.py index 18e55c6..7574481 100644 --- a/ShipthisAPI/shipthisapi.py +++ b/ShipthisAPI/shipthisapi.py @@ -5,7 +5,7 @@ Usage: from ShipthisAPI import ShipthisAPI - # Standard usage with API key + # Initialize the client client = ShipthisAPI( organisation="your_org_id", x_api_key="your_api_key", @@ -13,15 +13,6 @@ location_id="your_location" ) - # Server-to-server usage with custom headers - client = ShipthisAPI( - organisation="your_org_id", - custom_headers={ - "st-server-key": "your_server_key", - "st-server-name": "your_service_name", - } - ) - # Connect and validate client.connect() From 2fa39d795242a3c54f045ff7c6aaeef3906deb85 Mon Sep 17 00:00:00 2001 From: mayurrawte Date: Fri, 6 Feb 2026 13:51:46 +0530 Subject: [PATCH 3/3] Remove webhook_sync/webhook_update, enhance patch_item docs --- ShipthisAPI/shipthisapi.py | 82 ++++++-------------------------------- 1 file changed, 12 insertions(+), 70 deletions(-) diff --git a/ShipthisAPI/shipthisapi.py b/ShipthisAPI/shipthisapi.py index 7574481..4bd3896 100644 --- a/ShipthisAPI/shipthisapi.py +++ b/ShipthisAPI/shipthisapi.py @@ -483,16 +483,26 @@ def patch_item( ) -> Dict[str, Any]: """Patch specific fields of an item. + This is the recommended way to update document fields. It goes through + full field validation, workflow triggers, audit logging, and business logic. + Args: - collection_name: Name of the collection. + collection_name: Name of the collection (e.g., "sea_shipment", "fcl_load"). object_id: Document ID. - update_fields: Fields to update. + update_fields: Dictionary of field_id to value mappings. Returns: Updated document data. Raises: ShipthisAPIError: If the request fails. + + Example: + client.patch_item( + "fcl_load", + "68a4f906743189ad061429a7", + update_fields={"container_no": "CONT123", "seal_no": "SEAL456"} + ) """ return self._make_request( "PATCH", @@ -978,74 +988,6 @@ def upload_file( status_code=response.status_code, ) - # ==================== Webhook Operations ==================== - - def webhook_sync( - self, - view_name: str, - doc_id: str, - fields: List[Dict[str, Any]], - ) -> Dict[str, Any]: - """Update document via webhook-sync (synchronous update). - - Args: - view_name: View/collection name (e.g., "sea_shipment", "fcl_load"). - doc_id: Document ID. - fields: List of field dicts to update. - - Returns: - API response with success status and data. - - Raises: - ShipthisAPIError: If the request fails. - - Example: - client.webhook_sync( - "fcl_load", - "68a4f906743189ad061429a7", - fields=[{"container_no": "CONT123"}, {"seal_no": "SEAL456"}] - ) - """ - payload = {"fields": fields} - return self._make_request( - "PUT", - f"webhook-sync/{self.organisation_id}/{view_name}/{doc_id}", - request_data=payload, - ) - - def webhook_update( - self, - view_name: str, - doc_id: str, - actions: List[Dict[str, Any]], - ) -> Dict[str, Any]: - """Update document via webhook (async update with actions). - - Args: - view_name: View/collection name. - doc_id: Document ID. - actions: List of action dicts to execute. - - Returns: - API response with success status and data. - - Raises: - ShipthisAPIError: If the request fails. - - Example: - client.webhook_update( - "sea_shipment", - "68a4f906743189ad061429a7", - actions=[{"action": "update_status", "value": "completed"}] - ) - """ - payload = {"actions": actions} - return self._make_request( - "PUT", - f"webhook/{self.organisation_id}/{view_name}/{doc_id}", - request_data=payload, - ) - # ==================== Reference Linked Fields ==================== def create_reference_linked_field(