Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion channel_advisor_api/models/channel_advisor.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,17 @@ def all(cls, limit: int = None, filter: str = None, order_by: str = "Sku") -> Li

@classmethod
def by_sku(cls, sku: str) -> "MinProduct":
products = cls.all(filter=f"Sku eq '{sku}'")
escaped_sku = cls.escape_filter_value(sku)
products = cls.all(filter=f"Sku eq '{escaped_sku}'")
if not products:
raise ValueError(f"Product with SKU {sku} not found")
return products[0]

@classmethod
def escape_filter_value(cls, value: str) -> str:
"""Escapes special characters in filter values for the ChannelAdvisor API."""
return value.replace("'", "''")

@classmethod
def search_by_sku(cls, sku: str, limit: int = None, include_children: bool = False) -> List["MinProduct"]:
"""We have to check each page for the sku because the API doesn't support fuzzy search"""
Expand Down
1 change: 1 addition & 0 deletions notebooks/sku.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"cells":[{"cell_type":"code","execution_count":5,"metadata":{},"outputs":[{"data":{"text/plain":["True"]},"execution_count":5,"metadata":{},"output_type":"execute_result"}],"source":["from importlib import reload\n","from channel_advisor_api.models import channel_advisor\n","from dotenv import load_dotenv\n","import os\n","\n","os.environ[\"POWERTOOLS_LOG_LEVEL\"] = \"DEBUG\"\n","\n","load_dotenv()\n"]},{"cell_type":"code","execution_count":17,"metadata":{},"outputs":[{"name":"stdout","output_type":"stream","text":["{\"message\":\"Request: GET: https://api.channeladvisor.com/v1/Products?%24orderby=Sku&%24select=ID%2CSku%2CTitle%2CSubtitle%2CBrand%2CDescription%2CShortDescription%2CASIN%2CProductType%2CIsParent%2CIsInRelationship%2CParentProductID%2CParentSku%2CRelationshipName%2CVaryBy&%24filter=Sku+eq+%27Men%27%27s+Rise+Series+Monosuit%27\",\"service\":\"channel_advisor_api.models.channel_advisor_client\",\"location\":\"request:68\",\"level\":\"INFO\",\"timestamp\":\"2025-02-18 09:28:38,935-0600\",\"method\":\"get\",\"url\":\"https://api.channeladvisor.com/v1/Products?%24orderby=Sku&%24select=ID%2CSku%2CTitle%2CSubtitle%2CBrand%2CDescription%2CShortDescription%2CASIN%2CProductType%2CIsParent%2CIsInRelationship%2CParentProductID%2CParentSku%2CRelationshipName%2CVaryBy&%24filter=Sku+eq+%27Men%27%27s+Rise+Series+Monosuit%27\",\"kwargs\":{}}\n","{\"message\":\"get_all_pages('Products?%24orderby=Sku&%24select=ID%2CSku%2CTitle%2CSubtitle%2CBrand%2CDescription%2CShortDescription%2CASIN%2CProductType%2CIsParent%2CIsInRelationship%2CParentProductID%2CParentSku%2CRelationshipName%2CVaryBy&%24filter=Sku+eq+%27Men%27%27s+Rise+Series+Monosuit%27') returned 1 items from 1 pages in 0.49 seconds\",\"service\":\"channel_advisor_api.models.channel_advisor_client\",\"location\":\"get_all_pages:133\",\"level\":\"INFO\",\"timestamp\":\"2025-02-18 09:28:39,424-0600\",\"method\":\"get_all_pages\",\"url\":\"Products?%24orderby=Sku&%24select=ID%2CSku%2CTitle%2CSubtitle%2CBrand%2CDescription%2CShortDescription%2CASIN%2CProductType%2CIsParent%2CIsInRelationship%2CParentProductID%2CParentSku%2CRelationshipName%2CVaryBy&%24filter=Sku+eq+%27Men%27%27s+Rise+Series+Monosuit%27\",\"item_count\":1,\"page_count\":1,\"duration\":0.49}\n"]},{"data":{"text/plain":["MinProduct(id=10455152, sku=\"Men's Rise Series Monosuit\", title=\"Pure Adrenaline Men's Rise Series Snowmobile Monosuit\", subtitle='', brand='PureAdrenaline', description=\"Men's Rise Series Monosuit is designed for active riding. Nylon waterproof shell for extreme performance with extra durable oxford weave fabric on knees, bottom inner leg, and cuff. Active stretch panel for aggressive mobility. Durable Water Repellent (DWR) 4 and Technical Waterproof and Breathable PU Coating. Waterproofness: 17,000 mm; Breathability: 10,000g/m2/24hrs.<BR>\\n<BR>\\n• Taped waterproof seams<BR>\\n• YKK zippers<BR>\\n• Full-length side leg zippers for optimal access and versatile ventilation<BR>\\n• Internal suspension system (padded shoulder straps, adjustable and removable)<BR>\\n• Five external zippered pockets (2 chests and 2 hands w/soft brushed interior, 1 top forearm)<BR>\\n• Tethered Goggle chamois in the chest pocket<BR>\\n• 2 internal zippered pockets (one has a clear window with touch action ability and headphone port)<BR>\\n• Internal Snow Gaiters with boot loop attachment (elastic hem retention loop to prevent deep snow penetration)<BR>\", short_description=\"Men's Rise Series Monosuit is designed for active riding. Nylon waterproof shell for extreme performance with extra durable oxford weave fabric on knees, bottom inner leg, and cuff. Active stretch panel for aggressive mobility. Durable Water Repellent (DWR) 4 and Technical Waterproof and Breathable PU Coating. Waterproofness: 17,000 mm; Breathability: 10,000g/m2/24hrs.\", asin='B0CH4778C4', product_type='Parent', is_parent=True, is_in_relationship=True, parent_product_id=None, parent_sku=None, relationship_name='Size-Color', vary_by=None)"]},"execution_count":17,"metadata":{},"output_type":"execute_result"}],"source":["reload(channel_advisor)\n","channel_advisor.MinProduct.by_sku(\"Men's Rise Series Monosuit\")"]},{"cell_type":"code","execution_count":7,"metadata":{},"outputs":[{"name":"stdout","output_type":"stream","text":["{\"message\":\"Searching for product with sku: Men and include_children: False\",\"service\":\"channel_advisor_api.models.channel_advisor\",\"location\":\"search_by_sku:76\",\"level\":\"INFO\",\"timestamp\":\"2025-02-18 09:10:31,762-0600\"}\n","{\"message\":\"Request: GET: https://api.channeladvisor.com/v1/Products?%24orderby=Sku&%24filter=Sku+ge+%27Men%27+and+%28IsParent+eq+true+or+IsInRelationship+eq+false%29&%24select=ID%2CSku%2CTitle%2CSubtitle%2CBrand%2CDescription%2CShortDescription%2CASIN%2CProductType%2CIsParent%2CIsInRelationship%2CParentProductID%2CParentSku%2CRelationshipName%2CVaryBy\",\"service\":\"channel_advisor_api.models.channel_advisor_client\",\"location\":\"request:68\",\"level\":\"INFO\",\"timestamp\":\"2025-02-18 09:10:31,763-0600\",\"method\":\"get\",\"url\":\"https://api.channeladvisor.com/v1/Products?%24orderby=Sku&%24filter=Sku+ge+%27Men%27+and+%28IsParent+eq+true+or+IsInRelationship+eq+false%29&%24select=ID%2CSku%2CTitle%2CSubtitle%2CBrand%2CDescription%2CShortDescription%2CASIN%2CProductType%2CIsParent%2CIsInRelationship%2CParentProductID%2CParentSku%2CRelationshipName%2CVaryBy\",\"kwargs\":{}}\n","{\"message\":\"Request: GET: https://api.channeladvisor.com/v1/https://api.channeladvisor.com/v1/Products?%24orderby=Sku&%24filter=Sku+ge+%27Men%27+and+%28IsParent+eq+true+or+IsInRelationship+eq+false%29&%24select=ID%2CSku%2CTitle%2CSubtitle%2CBrand%2CDescription%2CShortDescription%2CASIN%2CProductType%2CIsParent%2CIsInRelationship%2CParentProductID%2CParentSku%2CRelationshipName%2CVaryBy&$skip=100\",\"service\":\"channel_advisor_api.models.channel_advisor_client\",\"location\":\"request:68\",\"level\":\"INFO\",\"timestamp\":\"2025-02-18 09:10:34,358-0600\",\"method\":\"get\",\"url\":\"https://api.channeladvisor.com/v1/https://api.channeladvisor.com/v1/Products?%24orderby=Sku&%24filter=Sku+ge+%27Men%27+and+%28IsParent+eq+true+or+IsInRelationship+eq+false%29&%24select=ID%2CSku%2CTitle%2CSubtitle%2CBrand%2CDescription%2CShortDescription%2CASIN%2CProductType%2CIsParent%2CIsInRelationship%2CParentProductID%2CParentSku%2CRelationshipName%2CVaryBy&$skip=100\",\"kwargs\":{}}\n","{\"message\":\"Rate Limit: 429 for GET https://api.channeladvisor.com/v1/Products?%24orderby=Sku&%24filter=Sku+ge+%27Men%27+and+%28IsParent+eq+true+or+IsInRelationship+eq+false%29&%24select=ID%2CSku%2CTitle%2CSubtitle%2CBrand%2CDescription%2CShortDescription%2CASIN%2CProductType%2CIsParent%2CIsInRelationship%2CParentProductID%2CParentSku%2CRelationshipName%2CVaryBy&$skip=100 attempt 1 in 5 seconds. Error: ''\",\"service\":\"channel_advisor_api.models.channel_advisor_client\",\"location\":\"request:96\",\"level\":\"INFO\",\"timestamp\":\"2025-02-18 09:10:34,592-0600\"}\n"]},{"ename":"KeyboardInterrupt","evalue":"","output_type":"error","traceback":["\u001b[0;31m---------------------------------------------------------------------------\u001b[0m","\u001b[0;31mKeyboardInterrupt\u001b[0m Traceback (most recent call last)","Cell \u001b[0;32mIn[7], line 2\u001b[0m\n\u001b[1;32m 1\u001b[0m reload(channel_advisor)\n\u001b[0;32m----> 2\u001b[0m \u001b[43mchannel_advisor\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mMinProduct\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43msearch_by_sku\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mMen\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n","File \u001b[0;32m~/Projects/bhmoto/channel_advisor_api_python/channel_advisor_api/models/channel_advisor.py:90\u001b[0m, in \u001b[0;36mBaseProduct.search_by_sku\u001b[0;34m(cls, sku, limit, include_children)\u001b[0m\n\u001b[1;32m 88\u001b[0m client \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mcls\u001b[39m\u001b[38;5;241m.\u001b[39mget_client()\n\u001b[1;32m 89\u001b[0m \u001b[38;5;28;01mwhile\u001b[39;00m next_link:\n\u001b[0;32m---> 90\u001b[0m response \u001b[38;5;241m=\u001b[39m \u001b[43mclient\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrequest\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mget\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mnext_link\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 91\u001b[0m content \u001b[38;5;241m=\u001b[39m json\u001b[38;5;241m.\u001b[39mloads(response\u001b[38;5;241m.\u001b[39mcontent)\n\u001b[1;32m 92\u001b[0m next_link \u001b[38;5;241m=\u001b[39m content\u001b[38;5;241m.\u001b[39mget(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m@odata.nextLink\u001b[39m\u001b[38;5;124m\"\u001b[39m, \u001b[38;5;28;01mNone\u001b[39;00m)\n","File \u001b[0;32m~/Projects/bhmoto/channel_advisor_api_python/channel_advisor_api/models/channel_advisor_client.py:100\u001b[0m, in \u001b[0;36mChannelAdvisorClient.request\u001b[0;34m(self, method, uri, data, attempts, **kwargs)\u001b[0m\n\u001b[1;32m 97\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m attempts \u001b[38;5;241m>\u001b[39m\u001b[38;5;241m=\u001b[39m \u001b[38;5;241m5\u001b[39m:\n\u001b[1;32m 98\u001b[0m \u001b[38;5;66;03m# eventually give up\u001b[39;00m\n\u001b[1;32m 99\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m RateLimitExceeded(msg)\n\u001b[0;32m--> 100\u001b[0m \u001b[43mtime\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43msleep\u001b[49m\u001b[43m(\u001b[49m\u001b[43msleep_time\u001b[49m\u001b[43m)\u001b[49m \u001b[38;5;66;03m# TODO make this async\u001b[39;00m\n\u001b[1;32m 101\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mrequest(method, uri, attempts\u001b[38;5;241m=\u001b[39mattempts, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs)\n\u001b[1;32m 102\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mException\u001b[39;00m(\n\u001b[1;32m 103\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mRequest \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mmethod\u001b[38;5;241m.\u001b[39mupper()\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m \u001b[39m\u001b[38;5;132;01m{\u001b[39;00murl\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m failed. Status: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mresponse\u001b[38;5;241m.\u001b[39mstatus_code\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m. Response: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mresponse\u001b[38;5;241m.\u001b[39mtext\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 104\u001b[0m )\n","\u001b[0;31mKeyboardInterrupt\u001b[0m: "]}],"source":["reload(channel_advisor)\n","channel_advisor.MinProduct.search_by_sku(\"Men\")"]}],"metadata":{"kernelspec":{"display_name":".venv","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.11.1"}},"nbformat":4,"nbformat_minor":2}