diff --git a/etsy_python/v3/enums/Listing.py b/etsy_python/v3/enums/Listing.py index 71e35b4..d8e0d57 100644 --- a/etsy_python/v3/enums/Listing.py +++ b/etsy_python/v3/enums/Listing.py @@ -58,7 +58,7 @@ class State(Enum): SOLD_OUT = "sold_out" DRAFT = "draft" EXPIRED = "expired" - REMOVED = "removed" # Not in OAS spec; kept for backward compatibility, may be removed in next major version + REMOVED = "removed" class VideoState(Enum): diff --git a/etsy_python/v3/models/Listing.py b/etsy_python/v3/models/Listing.py index bd02b38..04a12e0 100644 --- a/etsy_python/v3/models/Listing.py +++ b/etsy_python/v3/models/Listing.py @@ -127,7 +127,53 @@ def _store_personalization( ) -class CreateDraftListingRequest(_PersonalizationFieldsMixin, Request): +#: EU commercial guarantee / GPSR fields (ECGT), shared by createDraftListing +#: and updateListing. All are optional and nullable. Etsy silently ignores them +#: for sellers who are not eligible EU traders. ``ecgt_software_update_details`` +#: is ignored for physical listings; the other six are ignored for digital ones. +#: ``ecgt_garan_brand``, ``ecgt_garan_model``, ``ecgt_garan_years`` and +#: ``ecgt_garan_guarantee_details`` are required *together* -- Etsy enforces +#: that server-side and rejects a partial set. +ECGT_FIELDS = ( + "ecgt_garan_brand", + "ecgt_garan_model", + "ecgt_garan_years", + "ecgt_garan_guarantee_details", + "ecgt_other_commercial_guarantee_details", + "ecgt_after_sales_service_info", + "ecgt_software_update_details", +) + + +class _ECGTFieldsMixin: + """Storage for the EU commercial guarantee (ECGT) listing fields. + + Both listing request models accept the same seven fields, so the + assignment lives here instead of being duplicated. See ``ECGT_FIELDS``. + """ + + def _store_ecgt( + self, + ecgt_garan_brand: Optional[str], + ecgt_garan_model: Optional[str], + ecgt_garan_years: Optional[int], + ecgt_garan_guarantee_details: Optional[str], + ecgt_other_commercial_guarantee_details: Optional[str], + ecgt_after_sales_service_info: Optional[str], + ecgt_software_update_details: Optional[str], + ) -> None: + self.ecgt_garan_brand = ecgt_garan_brand + self.ecgt_garan_model = ecgt_garan_model + self.ecgt_garan_years = ecgt_garan_years + self.ecgt_garan_guarantee_details = ecgt_garan_guarantee_details + self.ecgt_other_commercial_guarantee_details = ( + ecgt_other_commercial_guarantee_details + ) + self.ecgt_after_sales_service_info = ecgt_after_sales_service_info + self.ecgt_software_update_details = ecgt_software_update_details + + +class CreateDraftListingRequest(_PersonalizationFieldsMixin, _ECGTFieldsMixin, Request): nullable = [ "shipping_profile_id", "return_policy_id", @@ -146,6 +192,7 @@ class CreateDraftListingRequest(_PersonalizationFieldsMixin, Request): "production_partner_ids", "image_ids", "readiness_state_id", + *ECGT_FIELDS, ] mandatory = [ "quantity", @@ -192,6 +239,13 @@ def __init__( is_taxable: Optional[bool] = None, listing_type: Optional[Type] = None, readiness_state_id: Optional[int] = None, + ecgt_garan_brand: Optional[str] = None, + ecgt_garan_model: Optional[str] = None, + ecgt_garan_years: Optional[int] = None, + ecgt_garan_guarantee_details: Optional[str] = None, + ecgt_other_commercial_guarantee_details: Optional[str] = None, + ecgt_after_sales_service_info: Optional[str] = None, + ecgt_software_update_details: Optional[str] = None, ): self.quantity = quantity self.title = title @@ -228,13 +282,22 @@ def __init__( self.is_taxable = is_taxable self._type = listing_type self.readiness_state_id = readiness_state_id + self._store_ecgt( + ecgt_garan_brand, + ecgt_garan_model, + ecgt_garan_years, + ecgt_garan_guarantee_details, + ecgt_other_commercial_guarantee_details, + ecgt_after_sales_service_info, + ecgt_software_update_details, + ) super().__init__( nullable=CreateDraftListingRequest.nullable, mandatory=CreateDraftListingRequest.mandatory, ) -class UpdateListingRequest(_PersonalizationFieldsMixin, Request): +class UpdateListingRequest(_PersonalizationFieldsMixin, _ECGTFieldsMixin, Request): nullable: List[str] = [ "materials", "shipping_profile_id", @@ -250,6 +313,7 @@ class UpdateListingRequest(_PersonalizationFieldsMixin, Request): "featured_rank", "production_partner_ids", "_type", + *ECGT_FIELDS, ] mandatory: List[str] = [] @@ -284,6 +348,13 @@ def __init__( is_supply: Optional[bool] = None, production_partner_ids: Optional[List[int]] = None, listing_type: Optional[Type] = None, + ecgt_garan_brand: Optional[str] = None, + ecgt_garan_model: Optional[str] = None, + ecgt_garan_years: Optional[int] = None, + ecgt_garan_guarantee_details: Optional[str] = None, + ecgt_other_commercial_guarantee_details: Optional[str] = None, + ecgt_after_sales_service_info: Optional[str] = None, + ecgt_software_update_details: Optional[str] = None, ): self.image_ids = image_ids self.title = title @@ -315,6 +386,15 @@ def __init__( self.is_supply = is_supply self.production_partner_ids = production_partner_ids self._type = listing_type + self._store_ecgt( + ecgt_garan_brand, + ecgt_garan_model, + ecgt_garan_years, + ecgt_garan_guarantee_details, + ecgt_other_commercial_guarantee_details, + ecgt_after_sales_service_info, + ecgt_software_update_details, + ) super().__init__( nullable=UpdateListingRequest.nullable, mandatory=UpdateListingRequest.mandatory, diff --git a/etsy_python/v3/resources/ListingVideo.py b/etsy_python/v3/resources/ListingVideo.py index f6b1095..61724ce 100644 --- a/etsy_python/v3/resources/ListingVideo.py +++ b/etsy_python/v3/resources/ListingVideo.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import Union +from typing import Optional, Union from etsy_python.v3.exceptions.RequestException import RequestException from etsy_python.v3.models.Listing import UpdateListingVideoRequest @@ -29,9 +29,22 @@ def get_listing_videos(self, listing_id: int) -> Union[Response, RequestExceptio return self.session.make_request(endpoint) def upload_listing_video( - self, shop_id: int, listing_id: int, listing_video: UpdateListingVideoRequest + self, + shop_id: int, + listing_id: int, + listing_video: UpdateListingVideoRequest, + is_multi_video: Optional[bool] = None, ) -> Union[Response, RequestException]: + """Upload a video to a listing. + + Pass ``is_multi_video=True`` to keep existing videos on the listing. + Omitting it preserves the former single-video behaviour, where the + upload replaces any video already attached. + """ endpoint = f"/shops/{shop_id}/listings/{listing_id}/videos" return self.session.make_request( - endpoint, method=Method.POST, payload=listing_video + endpoint, + method=Method.POST, + payload=listing_video, + query_params={"is_multi_video": is_multi_video}, ) diff --git a/specs/audit-ignore.json b/specs/audit-ignore.json index fa1df39..fa85d21 100644 --- a/specs/audit-ignore.json +++ b/specs/audit-ignore.json @@ -113,26 +113,6 @@ "reason": "By design: the SDK names US_HOLIDAYS (1-11) and CA_HOLIDAYS (12-23) and documents passing integer IDs directly for other regions (24-105), so the full spec enum is intentionally not enumerated. See enums/HolidayPreferences.py.", "added": "2026-06-02" }, - { - "type": "enum_staleness", - "key": "ShopListing.state -> State", - "direction": "extra", - "values": [ - "removed" - ], - "reason": "State.REMOVED is kept for backward compatibility and is not in the OAS response schema. Documented inline in enums/Listing.py; may be removed in the next major version.", - "added": "2026-06-02" - }, - { - "type": "enum_staleness", - "key": "ShopListingWithAssociations.state -> State", - "direction": "extra", - "values": [ - "removed" - ], - "reason": "State.REMOVED is kept for backward compatibility and is not in the OAS response schema. Documented inline in enums/Listing.py; may be removed in the next major version.", - "added": "2026-06-02" - }, { "type": "enum_staleness", "key": "getListing.includes -> Includes", @@ -155,16 +135,6 @@ "reason": "Etsy removed 'Shipping' and 'Inventory' from the includes enum on getListing and getListingsByListingIds (still valid on getListingsByShop). The SDK Includes enum keeps both for backward compatibility and because they remain valid on getListingsByShop, which shares the enum; removing them would be a breaking change. Documented inline in enums/Listing.py.", "added": "2026-07-08" }, - { - "type": "enum_staleness", - "key": "getListingsByShop.state -> State", - "direction": "extra", - "values": [ - "removed" - ], - "reason": "Same State.REMOVED back-compat value as ShopListing.state; surfaced additionally as the state query parameter on getListingsByShop now that parameter-level enums are audited. Documented inline in enums/Listing.py.", - "added": "2026-07-08" - }, { "type": "enum_staleness", "key": "updateHolidayPreferences.holiday_id -> CA_HOLIDAYS", diff --git a/specs/baseline.json b/specs/baseline.json index 9747287..77925ba 100644 --- a/specs/baseline.json +++ b/specs/baseline.json @@ -510,6 +510,42 @@ "minimum": 1 } }, + "ecgt_garan_brand": { + "type": "string", + "description": "The brand or trademark name for the EU commercial guarantee (required under GPSR/ECGT for eligible EU traders). Maximum 25 characters. See the [Etsy Seller Handbook](https://help.etsy.com/hc/articles/43191692248343) for details. If any one of ecgt_garan_brand, ecgt_garan_model, ecgt_garan_years, or ecgt_garan_guarantee_details is provided and non-empty, all four are required. Silently ignored for digital listings and for sellers who are not eligible EU traders.", + "nullable": true + }, + "ecgt_garan_years": { + "type": "integer", + "description": "Duration of the EU commercial guarantee in whole years (minimum 3, maximum 99). Required together with ecgt_garan_brand, ecgt_garan_model, and ecgt_garan_guarantee_details. Silently ignored for digital listings and for sellers who are not eligible EU traders.", + "format": "int64", + "nullable": true + }, + "ecgt_garan_model": { + "type": "string", + "description": "The product model or reference number for the EU commercial guarantee label. Maximum 20 characters. Required together with ecgt_garan_brand, ecgt_garan_years, and ecgt_garan_guarantee_details. Silently ignored for digital listings and for sellers who are not eligible EU traders.", + "nullable": true + }, + "ecgt_garan_guarantee_details": { + "type": "string", + "description": "Free-text description of the EU commercial guarantee terms and coverage. Maximum 255 characters. Required together with ecgt_garan_brand, ecgt_garan_model, and ecgt_garan_years. Silently ignored for digital listings and for sellers who are not eligible EU traders.", + "nullable": true + }, + "ecgt_other_commercial_guarantee_details": { + "type": "string", + "description": "Free-text details of any additional commercial guarantee or warranty beyond the primary EU commercial guarantee. Maximum 255 characters. Silently ignored for digital listings and for sellers who are not eligible EU traders.", + "nullable": true + }, + "ecgt_after_sales_service_info": { + "type": "string", + "description": "After-sales service, repairability, or eco-friendly delivery information required under EU GPSR/ECGT regulations. Maximum 255 characters. Silently ignored for digital listings and for sellers who are not eligible EU traders.", + "nullable": true + }, + "ecgt_software_update_details": { + "type": "string", + "description": "Details of software update availability and the duration of such updates, as required under EU ECGT regulations for digital content. Maximum 255 characters. Silently ignored for physical listings and for sellers who are not eligible EU traders.", + "nullable": true + }, "is_supply": { "type": "boolean", "description": "When true, tags the listing as a supply product, else indicates that it's a finished product. Helps buyers locate the listing under the Supplies heading. Requires 'who_made' and 'when_made'." @@ -643,6 +679,7 @@ "inactive", "sold_out", "draft", + "removed", "expired" ], "default": "active" @@ -4769,6 +4806,42 @@ "download", "both" ] + }, + "ecgt_garan_brand": { + "type": "string", + "description": "The brand or trademark name for the EU commercial guarantee (required under GPSR/ECGT for eligible EU traders). Maximum 25 characters. See the [Etsy Seller Handbook](https://help.etsy.com/hc/articles/43191692248343) for details. If any one of ecgt_garan_brand, ecgt_garan_model, ecgt_garan_years, or ecgt_garan_guarantee_details is provided and non-empty, all four are required. Silently ignored for digital listings and for sellers who are not eligible EU traders.", + "nullable": true + }, + "ecgt_garan_years": { + "type": "integer", + "description": "Duration of the EU commercial guarantee in whole years (minimum 3, maximum 99). Required together with ecgt_garan_brand, ecgt_garan_model, and ecgt_garan_guarantee_details. Silently ignored for digital listings and for sellers who are not eligible EU traders.", + "format": "int64", + "nullable": true + }, + "ecgt_garan_model": { + "type": "string", + "description": "The product model or reference number for the EU commercial guarantee label. Maximum 20 characters. Required together with ecgt_garan_brand, ecgt_garan_years, and ecgt_garan_guarantee_details. Silently ignored for digital listings and for sellers who are not eligible EU traders.", + "nullable": true + }, + "ecgt_garan_guarantee_details": { + "type": "string", + "description": "Free-text description of the EU commercial guarantee terms and coverage. Maximum 255 characters. Required together with ecgt_garan_brand, ecgt_garan_model, and ecgt_garan_years. Silently ignored for digital listings and for sellers who are not eligible EU traders.", + "nullable": true + }, + "ecgt_other_commercial_guarantee_details": { + "type": "string", + "description": "Free-text details of any additional commercial guarantee or warranty beyond the primary EU commercial guarantee. Maximum 255 characters. Silently ignored for digital listings and for sellers who are not eligible EU traders.", + "nullable": true + }, + "ecgt_after_sales_service_info": { + "type": "string", + "description": "After-sales service, repairability, or eco-friendly delivery information required under EU GPSR/ECGT regulations. Maximum 255 characters. Silently ignored for digital listings and for sellers who are not eligible EU traders.", + "nullable": true + }, + "ecgt_software_update_details": { + "type": "string", + "description": "Details of software update availability and the duration of such updates, as required under EU ECGT regulations for digital content. Maximum 255 characters. Silently ignored for physical listings and for sellers who are not eligible EU traders.", + "nullable": true } } } @@ -5385,7 +5458,7 @@ "/v3/application/shops/{shop_id}/listings/{listing_id}/videos": { "post": { "operationId": "uploadListingVideo", - "description": "
This endpoint is ready for production use.
This endpoint is ready for production use.