From 21ea2b7d0e178477968457dcd2871e6606c3b24a Mon Sep 17 00:00:00 2001 From: Amit Ray <51674969+amitray007@users.noreply.github.com> Date: Sat, 18 Jul 2026 22:54:39 +0530 Subject: [PATCH] feat: add batch inventory and shipping listing endpoints Sync SDK with latest Etsy OAS spec. Two GET batch endpoints were missing from the SDK: - getListingsInventoryByListingIds -> get_listings_inventory_by_listing_ids (GET /v3/application/listings/batch/inventory) - getListingsShippingByListingIds -> get_listings_shipping_by_listing_ids (GET /v3/application/listings/batch/shipping) Both take a single required listing_ids query param (comma-joined), following the existing get_listings_by_listing_ids batch pattern. Adds resource methods plus unit tests, bringing OAS coverage to 100%. The rich_description property added to ShopListing/ShopListingWithAssociations is response-only and needs no SDK change. Baseline refreshed to the latest spec; all 11 audit suppressions re-verified as still valid. Co-Authored-By: Claude Opus 4.8 --- etsy_python/v3/resources/Listing.py | 18 +++ specs/baseline.json | 172 ++++++++++++++++++++++++++++ tests/test_listing_resource.py | 52 +++++++++ 3 files changed, 242 insertions(+) diff --git a/etsy_python/v3/resources/Listing.py b/etsy_python/v3/resources/Listing.py index c395700..b79bb50 100644 --- a/etsy_python/v3/resources/Listing.py +++ b/etsy_python/v3/resources/Listing.py @@ -168,6 +168,24 @@ def get_listings_by_listings_ids( ) return self.get_listings_by_listing_ids(listing_ids, includes, legacy) + def get_listings_inventory_by_listing_ids( + self, listing_ids: List[int] + ) -> Union[Response, RequestException]: + endpoint = "/listings/batch/inventory" + query_params: Dict[str, Any] = { + "listing_ids": ",".join(list(map(str, listing_ids))), + } + return self.session.make_request(endpoint, query_params=query_params) + + def get_listings_shipping_by_listing_ids( + self, listing_ids: List[int] + ) -> Union[Response, RequestException]: + endpoint = "/listings/batch/shipping" + query_params: Dict[str, Any] = { + "listing_ids": ",".join(list(map(str, listing_ids))), + } + return self.session.make_request(endpoint, query_params=query_params) + def get_featured_listings_by_shop( self, shop_id: int, limit: int = 25, offset: int = 0, legacy: Optional[bool] = None, diff --git a/specs/baseline.json b/specs/baseline.json index a3de734..4c63119 100644 --- a/specs/baseline.json +++ b/specs/baseline.json @@ -2673,6 +2673,16 @@ } } }, + "409": { + "description": "There was a request conflict with the current state of the target resource. See the error message for details.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorSchema" + } + } + } + }, "500": { "description": "The server encountered an internal error. See the error message for details.", "content": { @@ -2694,6 +2704,82 @@ ] } }, + "/v3/application/listings/batch/inventory": { + "get": { + "operationId": "getListingsInventoryByListingIds", + "description": "
General ReleaseReport bug

This endpoint is ready for production use.

\n\nRetrieves the inventory record for each listing referenced by listing ID. Requires the listings_r OAuth scope. Limit 100 listing IDs per request.", + "tags": [ + "ShopListing Inventory" + ], + "parameters": [ + { + "name": "listing_ids", + "in": "query", + "description": "The list of numeric IDS for the listings in a specific Etsy shop.", + "required": true, + "schema": { + "type": "array", + "description": "The list of numeric IDS for the listings in a specific Etsy shop.", + "items": { + "type": "integer", + "format": "int64", + "minimum": 1 + } + } + } + ], + "responses": { + "200": { + "description": "A list of listings with their inventory records.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ShopListingsWithAssociations" + } + } + } + }, + "400": { + "description": "There was a problem with the request data. See the error message for details.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorSchema" + } + } + } + }, + "404": { + "description": "A resource could not be found. See the error message for details.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorSchema" + } + } + } + }, + "500": { + "description": "The server encountered an internal error. See the error message for details.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorSchema" + } + } + } + } + }, + "security": [ + { + "api_key": [], + "oauth2": [ + "listings_r" + ] + } + ] + } + }, "/v3/application/listings/{listing_id}/inventory/products/{product_id}": { "get": { "operationId": "getListingProduct", @@ -3937,6 +4023,82 @@ } } }, + "/v3/application/listings/batch/shipping": { + "get": { + "operationId": "getListingsShippingByListingIds", + "description": "
General ReleaseReport bug

This endpoint is ready for production use.

\n\nRetrieves the shipping profile for each listing referenced by listing ID. Requires the shops_r OAuth scope. Limit 100 listing IDs per request.", + "tags": [ + "ShopListing" + ], + "parameters": [ + { + "name": "listing_ids", + "in": "query", + "description": "The list of numeric IDS for the listings in a specific Etsy shop.", + "required": true, + "schema": { + "type": "array", + "description": "The list of numeric IDS for the listings in a specific Etsy shop.", + "items": { + "type": "integer", + "format": "int64", + "minimum": 1 + } + } + } + ], + "responses": { + "200": { + "description": "A list of listings with their shipping profiles.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ShopListingsWithAssociations" + } + } + } + }, + "400": { + "description": "There was a problem with the request data. See the error message for details.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorSchema" + } + } + } + }, + "404": { + "description": "A resource could not be found. See the error message for details.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorSchema" + } + } + } + }, + "500": { + "description": "The server encountered an internal error. See the error message for details.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorSchema" + } + } + } + } + }, + "security": [ + { + "api_key": [], + "oauth2": [ + "shops_r" + ] + } + ] + } + }, "/v3/application/shops/{shop_id}/listings/{listing_id}/transactions": { "get": { "operationId": "getShopReceiptTransactionsByListing", @@ -12915,6 +13077,11 @@ "type": "string", "description": "A description string of the product for sale in the listing." }, + "rich_description": { + "type": "string", + "description": "The seller-authored HTML rich-text description of the product when the listing uses rich text; null for plain-text listings. The plain-text `description` field is always populated. This value is HTML and consumers MUST sanitize it before rendering it in any HTML context.", + "nullable": true + }, "state": { "type": "string", "description": "When _updating_ a listing, this value can be either `active` or `inactive`. Note: Setting a `draft` listing to `active` will also publish the listing on etsy.com and requires that the listing have an image set. Setting a `sold_out` listing to active will update the quantity to 1 and renew the listing on etsy.com.", @@ -13378,6 +13545,11 @@ "type": "string", "description": "A description string of the product for sale in the listing." }, + "rich_description": { + "type": "string", + "description": "The seller-authored HTML rich-text description of the product when the listing uses rich text; null for plain-text listings. The plain-text `description` field is always populated. This value is HTML and consumers MUST sanitize it before rendering it in any HTML context.", + "nullable": true + }, "state": { "type": "string", "description": "When _updating_ a listing, this value can be either `active` or `inactive`. Note: Setting a `draft` listing to `active` will also publish the listing on etsy.com and requires that the listing have an image set. Setting a `sold_out` listing to active will update the quantity to 1 and renew the listing on etsy.com.", diff --git a/tests/test_listing_resource.py b/tests/test_listing_resource.py index 527520d..9918f06 100644 --- a/tests/test_listing_resource.py +++ b/tests/test_listing_resource.py @@ -310,6 +310,58 @@ def test_legacy_forwarded_through_deprecated_alias(self, mock_session): assert qp["legacy"] is True +class TestGetListingsInventoryByListingIds: + def test_listing_ids_joined(self, mock_session): + mock_session.make_request.return_value = Response( + 200, make_shop_listing_collection() + ) + resource = ListingResource(session=mock_session) + + resource.get_listings_inventory_by_listing_ids([111, 222, 333]) + + endpoint = mock_session.make_request.call_args[0][0] + qp = mock_session.make_request.call_args[1]["query_params"] + assert endpoint == "/listings/batch/inventory" + assert qp["listing_ids"] == "111,222,333" + + def test_single_listing_id(self, mock_session): + mock_session.make_request.return_value = Response( + 200, make_shop_listing_collection() + ) + resource = ListingResource(session=mock_session) + + resource.get_listings_inventory_by_listing_ids([111]) + + qp = mock_session.make_request.call_args[1]["query_params"] + assert qp["listing_ids"] == "111" + + +class TestGetListingsShippingByListingIds: + def test_listing_ids_joined(self, mock_session): + mock_session.make_request.return_value = Response( + 200, make_shop_listing_collection() + ) + resource = ListingResource(session=mock_session) + + resource.get_listings_shipping_by_listing_ids([111, 222, 333]) + + endpoint = mock_session.make_request.call_args[0][0] + qp = mock_session.make_request.call_args[1]["query_params"] + assert endpoint == "/listings/batch/shipping" + assert qp["listing_ids"] == "111,222,333" + + def test_single_listing_id(self, mock_session): + mock_session.make_request.return_value = Response( + 200, make_shop_listing_collection() + ) + resource = ListingResource(session=mock_session) + + resource.get_listings_shipping_by_listing_ids([111]) + + qp = mock_session.make_request.call_args[1]["query_params"] + assert qp["listing_ids"] == "111" + + class TestGetFeaturedListingsByShop: def test_basic_call(self, mock_session): mock_session.make_request.return_value = Response(