From 5a10e4fa247c4d110485d643319393aba54b29fb Mon Sep 17 00:00:00 2001 From: Amit Ray <51674969+amitray007@users.noreply.github.com> Date: Sat, 25 Apr 2026 04:48:59 +0530 Subject: [PATCH 1/2] feat: add max_variations_supported param to update_listing_inventory Sync SDK with latest Etsy OAS spec. The spec added an optional max_variations_supported query param to PUT /v3/application/listings/{listing_id}/inventory that gates third-variation support ("2" or "3"). Add a MaxVariationsSupported enum and wire the param through the resource method. Also refreshes specs/baseline.json to match the latest spec snapshot. Co-Authored-By: Claude Opus 4.7 (1M context) --- etsy_python/v3/enums/ListingInventory.py | 5 + etsy_python/v3/resources/ListingInventory.py | 10 +- specs/baseline.json | 98 +++++++++----------- tests/test_remaining_resources.py | 22 ++++- 4 files changed, 79 insertions(+), 56 deletions(-) diff --git a/etsy_python/v3/enums/ListingInventory.py b/etsy_python/v3/enums/ListingInventory.py index 8f48c90..3cb470c 100644 --- a/etsy_python/v3/enums/ListingInventory.py +++ b/etsy_python/v3/enums/ListingInventory.py @@ -3,3 +3,8 @@ class Includes(Enum): LISTING = "listing" + + +class MaxVariationsSupported(Enum): + TWO = "2" + THREE = "3" diff --git a/etsy_python/v3/resources/ListingInventory.py b/etsy_python/v3/resources/ListingInventory.py index 5bdd507..a56889f 100644 --- a/etsy_python/v3/resources/ListingInventory.py +++ b/etsy_python/v3/resources/ListingInventory.py @@ -3,6 +3,7 @@ from etsy_python.v3.exceptions.RequestException import RequestException from etsy_python.v3.enums.Listing import InventoryIncludes +from etsy_python.v3.enums.ListingInventory import MaxVariationsSupported from etsy_python.v3.models.Listing import UpdateListingInventoryRequest from etsy_python.v3.resources.Session import EtsyClient from etsy_python.v3.resources.enums.Request import Method @@ -31,9 +32,16 @@ def get_listing_inventory( def update_listing_inventory( self, listing_id: int, listing_inventory: UpdateListingInventoryRequest, legacy: Optional[bool] = None, + max_variations_supported: Optional[MaxVariationsSupported] = None, ) -> Union[Response, RequestException]: endpoint = f"/listings/{listing_id}/inventory" + query_params: Dict[str, Any] = { + "legacy": legacy, + "max_variations_supported": max_variations_supported.value + if max_variations_supported is not None + else None, + } return self.session.make_request( endpoint, method=Method.PUT, payload=listing_inventory, - query_params={"legacy": legacy}, + query_params=query_params, ) diff --git a/specs/baseline.json b/specs/baseline.json index 328e267..11a08b8 100644 --- a/specs/baseline.json +++ b/specs/baseline.json @@ -499,7 +499,7 @@ }, "is_personalizable": { "type": "boolean", - "description": "When true, this listing is personalizable. The default value is false." + "description": "[DEPRECATED] When true, this listing is personalizable. The default value is false. NOTE: This field will be removed on Apr. 9th, 2026. See https://developers.etsy.com/documentation/tutorials/personalization-migration for migration details." }, "personalization_is_required": { "type": "boolean", @@ -2449,6 +2449,20 @@ "type": "boolean", "description": "This parameter needed to enable new parameters and response values related to processing profiles." } + }, + { + "name": "max_variations_supported", + "in": "query", + "description": "Coming soon: This parameter determines whether a third variation can be added to or updated for a listing. It accepts values of 2 or 3, where 3 enables third-variation support.", + "required": false, + "schema": { + "type": "string", + "description": "Coming soon: This parameter determines whether a third variation can be added to or updated for a listing. It accepts values of 2 or 3, where 3 enables third-variation support.", + "enum": [ + "2", + "3" + ] + } } ], "requestBody": { @@ -2479,13 +2493,13 @@ "properties": { "property_id": { "type": "integer", - "description": "The unique ID of an Etsy [listing property](/documentation/reference#operation/getListingProperties).", + "description": "The unique ID of an Etsy [listing property](/documentation/reference#operation/getListingInventory).", "format": "int64", "minimum": 1 }, "value_ids": { "type": "array", - "description": "An array of unique IDs of multiple Etsy [listing property](/documentation/reference#operation/getListingProperties) values. For example, if your listing offers different sizes of a product, then the value ID list contains value IDs for each size.", + "description": "An array of unique IDs of Etsy [listing property](/documentation/reference#operation/getListingInventory) values.", "items": { "type": "integer", "format": "int64", @@ -2561,28 +2575,28 @@ }, "price_on_property": { "type": "array", - "description": "An array of unique [listing property](/documentation/reference#operation/getListingProperties) ID integers for the properties that change product prices, if any. For example, if you charge specific prices for different sized products in the same listing, then this array contains the property ID for size.", + "description": "An array of unique [listing property](/documentation/reference#operation/getListingInventory) ID integers for the properties that change product prices, if any. For example, if you charge specific prices for different sized products in the same listing, then this array contains the property ID for size.", "items": { "type": "integer" } }, "quantity_on_property": { "type": "array", - "description": "An array of unique [listing property](/documentation/reference#operation/getListingProperties) ID integers for the properties that change the quantity of the products, if any. For example, if you stock specific quantities of different colored products in the same listing, then this array contains the property ID for color.", + "description": "An array of unique [listing property](/documentation/reference#operation/getListingInventory) ID integers for the properties that change the quantity of the products, if any. For example, if you stock specific quantities of different colored products in the same listing, then this array contains the property ID for color.", "items": { "type": "integer" } }, "sku_on_property": { "type": "array", - "description": "An array of unique [listing property](/documentation/reference#operation/getListingProperties) ID integers for the properties that change the product SKU, if any. For example, if you use specific skus for different colored products in the same listing, then this array contains the property ID for color.", + "description": "An array of unique [listing property](/documentation/reference#operation/getListingInventory) ID integers for the properties that change the product SKU, if any. For example, if you use specific skus for different colored products in the same listing, then this array contains the property ID for color.", "items": { "type": "integer" } }, "readiness_state_on_property": { "type": "array", - "description": "An array of unique [listing property](/documentation/reference#operation/getListingProperties) ID integers for the properties that change processing profile, if any. For example, if you need specific processing profiles for different colored products in the same listing, then this array contains the property ID for color.", + "description": "An array of unique [listing property](/documentation/reference#operation/getListingInventory) ID integers for the properties that change processing profile, if any. For example, if you need specific processing profiles for different colored products in the same listing, then this array contains the property ID for color.", "nullable": true, "items": { "type": "integer", @@ -3647,7 +3661,7 @@ "properties": { "value_ids": { "type": "array", - "description": "An array of unique IDs of multiple Etsy [listing property](/documentation/reference#operation/getListingProperties) values. For example, if your listing offers different sizes of a product, then the value ID list contains value IDs for each size.", + "description": "An array of unique IDs of multiple Etsy [listing property](/documentation/reference#operation/getListingProperties) values. For example, if your listing is composed of different materials, then the value ID list contains value IDs for each material.", "items": { "type": "integer", "format": "int64", @@ -3656,7 +3670,7 @@ }, "values": { "type": "array", - "description": "An array of value strings for multiple Etsy [listing property](/documentation/reference#operation/getListingProperties) values. For example, if your listing offers different colored products, then the values array contains the color strings for each color. Note: parenthesis characters (`(` and `)`) are not allowed.", + "description": "An array of value strings for multiple Etsy [listing property](/documentation/reference#operation/getListingProperties) values. For example, if your listing is painted in different colors, then the values array contains the color strings for each color. Note: parenthesis characters (`(` and `)`) are not allowed.", "items": { "type": "string" } @@ -4636,7 +4650,7 @@ }, "is_personalizable": { "type": "boolean", - "description": "When true, this listing is personalizable. The default value is false." + "description": "[DEPRECATED] When true, this listing is personalizable. The default value is false. NOTE: This field will be removed on Apr. 9th, 2026. See https://developers.etsy.com/documentation/tutorials/personalization-migration for migration details." }, "personalization_is_required": { "type": "boolean", @@ -12218,7 +12232,7 @@ "/v3/application/users/{user_id}": { "get": { "operationId": "getUser", - "description": "
General ReleaseReport bug

This endpoint is ready for production use.

\n\nRetrieves a user profile based on a unique user ID. Access is limited to profiles of the authenticated user or linked buyers. For the primary_email field, specific app-based permissions are required and granted case-by-case.", + "description": "
General ReleaseReport bug

This endpoint is ready for production use.

\n\nRetrieves a user profile based on a unique user ID. Access is limited to profiles of the authenticated user or linked buyers. For the primary_email field, specific app-based permissions are required and granted case-by-case.", "tags": [ "User" ], @@ -12570,11 +12584,11 @@ }, "supports_attributes": { "type": "boolean", - "description": "When true, you can use this property in listing attributes." + "description": "When true, you can use this property in listing properties." }, "supports_variations": { "type": "boolean", - "description": "When true, you can use this property in listing variations." + "description": "When true, you can use this property in listing inventory." }, "is_multivalued": { "type": "boolean", @@ -12784,20 +12798,6 @@ "type": "boolean", "description": "When true, this listing is personalizable. The default value is false." }, - "personalization_is_required": { - "type": "boolean", - "description": "[DEPRECATED] When true, this listing requires personalization. The default value is false. NOTE: This field will be removed on Apr. 9th, 2026. See https://developers.etsy.com/documentation/tutorials/personalization-migration for migration details." - }, - "personalization_char_count_max": { - "type": "integer", - "description": "[DEPRECATED] This is an integer value representing the maximum length for the personalization message entered by the buyer. Will only change if is_personalizable is 'true'. Note: This field will be removed on Apr. 9th, 2026. See https://developers.etsy.com/documentation/tutorials/personalization-migration for migration details.", - "nullable": true - }, - "personalization_instructions": { - "type": "string", - "description": "[DEPRECATED] When true, this listing requires personalization. The default value is false. NOTE: This field will be removed on Apr. 9th, 2026. See https://developers.etsy.com/documentation/tutorials/personalization-migration for migration details.", - "nullable": true - }, "listing_type": { "type": "string", "description": "An enumerated type string that indicates whether the listing is physical or a digital download.", @@ -13240,20 +13240,6 @@ "type": "boolean", "description": "When true, this listing is personalizable. The default value is false." }, - "personalization_is_required": { - "type": "boolean", - "description": "[DEPRECATED] When true, this listing requires personalization. The default value is false. NOTE: This field will be removed on Apr. 9th, 2026. See https://developers.etsy.com/documentation/tutorials/personalization-migration for migration details." - }, - "personalization_char_count_max": { - "type": "integer", - "description": "[DEPRECATED] This is an integer value representing the maximum length for the personalization message entered by the buyer. Will only change if is_personalizable is 'true'. Note: This field will be removed on Apr. 9th, 2026. See https://developers.etsy.com/documentation/tutorials/personalization-migration for migration details.", - "nullable": true - }, - "personalization_instructions": { - "type": "string", - "description": "[DEPRECATED] When true, this listing requires personalization. The default value is false. NOTE: This field will be removed on Apr. 9th, 2026. See https://developers.etsy.com/documentation/tutorials/personalization-migration for migration details.", - "nullable": true - }, "listing_type": { "type": "string", "description": "An enumerated type string that indicates whether the listing is physical or a digital download.", @@ -14246,28 +14232,28 @@ }, "price_on_property": { "type": "array", - "description": "An array of unique [listing property](/documentation/reference#operation/getListingProperties) ID integers for the properties that change product prices, if any. For example, if you charge specific prices for different sized products in the same listing, then this array contains the property ID for size.", + "description": "An array of unique [listing property](/documentation/reference#operation/getListingInventory) ID integers for the properties that change product prices, if any. For example, if you charge specific prices for different sized products in the same listing, then this array contains the property ID for size.", "items": { "type": "integer" } }, "quantity_on_property": { "type": "array", - "description": "An array of unique [listing property](/documentation/reference#operation/getListingProperties) ID integers for the properties that change the quantity of the products, if any. For example, if you stock specific quantities of different colored products in the same listing, then this array contains the property ID for color.", + "description": "An array of unique [listing property](/documentation/reference#operation/getListingInventory) ID integers for the properties that change the quantity of the products, if any. For example, if you stock specific quantities of different colored products in the same listing, then this array contains the property ID for color.", "items": { "type": "integer" } }, "sku_on_property": { "type": "array", - "description": "An array of unique [listing property](/documentation/reference#operation/getListingProperties) ID integers for the properties that change the product SKU, if any. For example, if you use specific skus for different colored products in the same listing, then this array contains the property ID for color.", + "description": "An array of unique [listing property](/documentation/reference#operation/getListingInventory) ID integers for the properties that change the product SKU, if any. For example, if you use specific skus for different colored products in the same listing, then this array contains the property ID for color.", "items": { "type": "integer" } }, "readiness_state_on_property": { "type": "array", - "description": "An array of unique [listing property](/documentation/reference#operation/getListingProperties) ID integers for the properties that change processing profile, if any. For example, if you need specific processing profiles for different colored products in the same listing, then this array contains the property ID for color.", + "description": "An array of unique [listing property](/documentation/reference#operation/getListingInventory) ID integers for the properties that change processing profile, if any. For example, if you need specific processing profiles for different colored products in the same listing, then this array contains the property ID for color.", "items": { "type": "integer", "format": "int64", @@ -14655,7 +14641,7 @@ ] }, "shipping_cost": { - "description": "The shipping cost with VAT applied. Only present for UK buyers.", + "description": "The shipping cost to the buyer's country. Includes VAT where applicable. Null when shipping is free or unavailable — use is_free_shipping to distinguish.", "oneOf": [ { "$ref": "#/components/schemas/Money" @@ -14663,8 +14649,12 @@ ], "nullable": true }, + "is_free_shipping": { + "type": "boolean", + "description": "Whether shipping is free to the buyer's country." + }, "original_price": { - "description": "The all-in display price (base + shipping for UK). This is the price to show to the buyer.", + "description": "The display price. For UK buyers, includes base + shipping (DMCC). For others, base price only.", "oneOf": [ { "$ref": "#/components/schemas/Money" @@ -14672,7 +14662,7 @@ ] }, "discounted_price": { - "description": "The sale price (all-in). Null if no active promotion.", + "description": "The sale price. For UK buyers, includes base + shipping. For others, base price only. Null if no active promotion.", "oneOf": [ { "$ref": "#/components/schemas/Money" @@ -14753,28 +14743,28 @@ }, "price_on_property": { "type": "array", - "description": "An array of unique [listing property](/documentation/reference#operation/getListingProperties) ID integers for the properties that change product prices, if any. For example, if you charge specific prices for different sized products in the same listing, then this array contains the property ID for size.", + "description": "An array of unique [listing property](/documentation/reference#operation/getListingInventory) ID integers for the properties that change product prices, if any. For example, if you charge specific prices for different sized products in the same listing, then this array contains the property ID for size.", "items": { "type": "integer" } }, "quantity_on_property": { "type": "array", - "description": "An array of unique [listing property](/documentation/reference#operation/getListingProperties) ID integers for the properties that change the quantity of the products, if any. For example, if you stock specific quantities of different colored products in the same listing, then this array contains the property ID for color.", + "description": "An array of unique [listing property](/documentation/reference#operation/getListingInventory) ID integers for the properties that change the quantity of the products, if any. For example, if you stock specific quantities of different colored products in the same listing, then this array contains the property ID for color.", "items": { "type": "integer" } }, "sku_on_property": { "type": "array", - "description": "An array of unique [listing property](/documentation/reference#operation/getListingProperties) ID integers for the properties that change the product SKU, if any. For example, if you use specific skus for different colored products in the same listing, then this array contains the property ID for color.", + "description": "An array of unique [listing property](/documentation/reference#operation/getListingInventory) ID integers for the properties that change the product SKU, if any. For example, if you use specific skus for different colored products in the same listing, then this array contains the property ID for color.", "items": { "type": "integer" } }, "readiness_state_on_property": { "type": "array", - "description": "An array of unique [listing property](/documentation/reference#operation/getListingProperties) ID integers for the properties that change processing profile, if any. For example, if you need specific processing profiles for different colored products in the same listing, then this array contains the property ID for color.", + "description": "An array of unique [listing property](/documentation/reference#operation/getListingInventory) ID integers for the properties that change processing profile, if any. For example, if you need specific processing profiles for different colored products in the same listing, then this array contains the property ID for color.", "items": { "type": "integer", "format": "int64", @@ -16274,11 +16264,11 @@ }, "supports_attributes": { "type": "boolean", - "description": "When true, you can use this property in listing attributes." + "description": "When true, you can use this property in listing properties." }, "supports_variations": { "type": "boolean", - "description": "When true, you can use this property in listing variations." + "description": "When true, you can use this property in listing inventory." }, "is_multivalued": { "type": "boolean", diff --git a/tests/test_remaining_resources.py b/tests/test_remaining_resources.py index c50177e..10929e5 100644 --- a/tests/test_remaining_resources.py +++ b/tests/test_remaining_resources.py @@ -19,6 +19,7 @@ UpdateShopReturnPolicyRequest, ) from etsy_python.v3.enums.HolidayPreferences import US_HOLIDAYS +from etsy_python.v3.enums.ListingInventory import MaxVariationsSupported from etsy_python.v3.models.HolidayPreferences import UpdateHolidayPreferencesRequest from etsy_python.v3.resources.HolidayPreferences import HolidayPreferencesResource from etsy_python.v3.resources.ListingFile import ListingFileResource @@ -157,7 +158,26 @@ def test_update_listing_inventory(self, mock_session): f"/listings/{MOCK_LISTING_ID}/inventory", method=Method.PUT, payload=payload, - query_params={"legacy": None}, + query_params={"legacy": None, "max_variations_supported": None}, + ) + + def test_update_listing_inventory_with_max_variations(self, mock_session): + mock_session.make_request.return_value = Response( + 200, make_listing_inventory() + ) + resource = ListingInventoryResource(session=mock_session) + payload = MagicMock(spec=UpdateListingInventoryRequest) + resource.update_listing_inventory( + MOCK_LISTING_ID, + payload, + legacy=True, + max_variations_supported=MaxVariationsSupported.THREE, + ) + mock_session.make_request.assert_called_once_with( + f"/listings/{MOCK_LISTING_ID}/inventory", + method=Method.PUT, + payload=payload, + query_params={"legacy": True, "max_variations_supported": "3"}, ) From 129f1b2bd3e690adb51914d910eae7071640e066 Mon Sep 17 00:00:00 2001 From: Amit Ray <51674969+amitray007@users.noreply.github.com> Date: Sat, 25 Apr 2026 04:52:47 +0530 Subject: [PATCH 2/2] fix: update personalization deprecation warning to remove stale date The previous warning cited "April 9, 2026" as the API removal date, but that date has passed and Etsy has not yet removed the fields from the spec. Replace with a date-independent message linking to Etsy's personalization migration tutorial, so the warning stays accurate as the actual removal is rescheduled. Co-Authored-By: Claude Opus 4.7 (1M context) --- etsy_python/v3/models/Listing.py | 16 ++++++++++------ tests/test_listing_models.py | 3 ++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/etsy_python/v3/models/Listing.py b/etsy_python/v3/models/Listing.py index b2a5363..f178ee5 100644 --- a/etsy_python/v3/models/Listing.py +++ b/etsy_python/v3/models/Listing.py @@ -111,9 +111,11 @@ def __init__( ]): warnings.warn( "personalization_is_required, personalization_char_count_max, and " - "personalization_instructions are deprecated and will be removed " - "from the Etsy API on April 9, 2026. Use the personalization " - "endpoint (update_listing_personalization) instead.", + "personalization_instructions are deprecated by the Etsy API and " + "scheduled for removal. Use the personalization endpoint " + "(update_listing_personalization) instead. See " + "https://developers.etsy.com/documentation/tutorials/personalization-migration " + "for migration details.", DeprecationWarning, stacklevel=2, ) @@ -212,9 +214,11 @@ def __init__( ]): warnings.warn( "personalization_is_required, personalization_char_count_max, and " - "personalization_instructions are deprecated and will be removed " - "from the Etsy API on April 9, 2026. Use the personalization " - "endpoint (update_listing_personalization) instead.", + "personalization_instructions are deprecated by the Etsy API and " + "scheduled for removal. Use the personalization endpoint " + "(update_listing_personalization) instead. See " + "https://developers.etsy.com/documentation/tutorials/personalization-migration " + "for migration details.", DeprecationWarning, stacklevel=2, ) diff --git a/tests/test_listing_models.py b/tests/test_listing_models.py index 7709b9f..9e51f60 100644 --- a/tests/test_listing_models.py +++ b/tests/test_listing_models.py @@ -193,7 +193,8 @@ def test_create_warns_with_personalization_is_required(self): ) deprecation_warnings = [x for x in w if issubclass(x.category, DeprecationWarning)] assert len(deprecation_warnings) == 1 - assert "April 9, 2026" in str(deprecation_warnings[0].message) + assert "deprecated by the Etsy API" in str(deprecation_warnings[0].message) + assert "personalization-migration" in str(deprecation_warnings[0].message) def test_update_no_warning_without_personalization(self): with warnings.catch_warnings(record=True) as w: