channel3-sdk 0.0.1__tar.gz → 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of channel3-sdk might be problematic. Click here for more details.

@@ -0,0 +1,235 @@
1
+ Metadata-Version: 2.1
2
+ Name: channel3-sdk
3
+ Version: 0.1.0
4
+ Summary: The official Python SDK for Channel3 AI Shopping API
5
+ Home-page: https://github.com/channel3/sdk-python
6
+ License: MIT
7
+ Keywords: ai,shopping,ecommerce,search,api
8
+ Author: Channel3
9
+ Author-email: alex@trychannel3.com
10
+ Requires-Python: >=3.8,<4.0
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Requires-Dist: aiohttp (>=3.9.0,<4.0.0)
20
+ Requires-Dist: pydantic (>=2.5.0,<3.0.0)
21
+ Requires-Dist: requests (>=2.31.0,<3.0.0)
22
+ Project-URL: Repository, https://github.com/channel3/sdk-python
23
+ Description-Content-Type: text/markdown
24
+
25
+ # Channel3 Python SDK
26
+
27
+ The official Python SDK for the [Channel3](https://trychannel3.com) AI Shopping API.
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ pip install channel3-sdk
33
+ ```
34
+
35
+ ## Quick Start
36
+
37
+ ### Synchronous Client
38
+
39
+ ```python
40
+ import os
41
+ from channel3_sdk import Channel3Client
42
+
43
+ # Initialize the client
44
+ client = Channel3Client(api_key="your_api_key_here")
45
+ # Or use environment variable: CHANNEL3_API_KEY
46
+
47
+ # Search for products
48
+ products = client.search(query="blue denim jacket")
49
+
50
+ for product in products:
51
+ print(f"Product: {product.title}")
52
+ print(f"Brand: {product.brand_name}")
53
+ print(f"Price: ${product.offers[0].price.price}")
54
+ print("---")
55
+
56
+ # Get detailed product information
57
+ product_detail = client.get_product("prod_123456")
58
+ print(f"Detailed info for: {product_detail.title}")
59
+ print(f"Materials: {product_detail.materials}")
60
+ print(f"Key features: {product_detail.key_features}")
61
+ ```
62
+
63
+ ### Asynchronous Client
64
+
65
+ ```python
66
+ import asyncio
67
+ from channel3_sdk import AsyncChannel3Client
68
+
69
+ async def main():
70
+ # Initialize the async client
71
+ client = AsyncChannel3Client(api_key="your_api_key_here")
72
+
73
+ # Search for products
74
+ products = await client.search(query="running shoes")
75
+
76
+ for product in products:
77
+ print(f"Product: {product.title}")
78
+ print(f"Score: {product.score}")
79
+
80
+ # Get detailed product information
81
+ if products:
82
+ product_detail = await client.get_product(products[0].id)
83
+ print(f"Gender: {product_detail.gender}")
84
+
85
+ # Run the async function
86
+ asyncio.run(main())
87
+ ```
88
+
89
+ ## Advanced Usage
90
+
91
+ ### Visual Search
92
+
93
+ ```python
94
+ # Search by image URL
95
+ products = client.search(image_url="https://example.com/image.jpg")
96
+
97
+ # Search by base64 image
98
+ with open("image.jpg", "rb") as f:
99
+ import base64
100
+ base64_image = base64.b64encode(f.read()).decode()
101
+ products = client.search(base64_image=base64_image)
102
+ ```
103
+
104
+ ### Multimodal Search
105
+
106
+ ```python
107
+ # Combine text and image search
108
+ products = client.search(
109
+ query="blue denim jacket",
110
+ image_url="https://example.com/jacket.jpg"
111
+ )
112
+ ```
113
+
114
+ ### Search with Filters
115
+
116
+ ```python
117
+ from channel3_sdk import SearchFilters
118
+
119
+ # Create search filters
120
+ filters = SearchFilters(
121
+ colors=["blue", "navy"],
122
+ materials=["cotton", "denim"],
123
+ min_price=50.0,
124
+ max_price=200.0
125
+ )
126
+
127
+ # Search with filters
128
+ products = client.search(
129
+ query="jacket",
130
+ filters=filters,
131
+ limit=10
132
+ )
133
+ ```
134
+
135
+ ## API Reference
136
+
137
+ ### Client Classes
138
+
139
+ #### `Channel3Client`
140
+ Synchronous client for the Channel3 API.
141
+
142
+ **Methods:**
143
+ - `search(query=None, image_url=None, base64_image=None, filters=None, limit=20)` → `List[Product]`
144
+ - `get_product(product_id)` → `ProductDetail`
145
+
146
+ #### `AsyncChannel3Client`
147
+ Asynchronous client for the Channel3 API.
148
+
149
+ **Methods:**
150
+ - `async search(query=None, image_url=None, base64_image=None, filters=None, limit=20)` → `List[Product]`
151
+ - `async get_product(product_id)` → `ProductDetail`
152
+
153
+ ### Models
154
+
155
+ #### `Product`
156
+ - `id: str` - Unique product identifier
157
+ - `score: float` - Search relevance score
158
+ - `brand_name: str` - Brand name
159
+ - `title: str` - Product title
160
+ - `description: str` - Product description
161
+ - `image_url: str` - Main product image URL
162
+ - `offers: List[MerchantOffering]` - Available purchase options
163
+ - `family: List[FamilyMember]` - Related products
164
+
165
+ #### `ProductDetail`
166
+ - `brand_id: str` - Brand identifier
167
+ - `brand_name: str` - Brand name
168
+ - `title: str` - Product title
169
+ - `description: str` - Product description
170
+ - `image_urls: List[str]` - Product image URLs
171
+ - `merchant_offerings: List[MerchantOffering]` - Purchase options
172
+ - `gender: Literal["na", "men", "women"]` - Target gender
173
+ - `materials: Optional[List[str]]` - Product materials
174
+ - `key_features: List[str]` - Key product features
175
+ - `family_members: List[FamilyMember]` - Related products
176
+
177
+ #### `SearchFilters`
178
+ - `colors: Optional[List[str]]` - Color filters
179
+ - `materials: Optional[List[str]]` - Material filters
180
+ - `min_price: Optional[float]` - Minimum price
181
+ - `max_price: Optional[float]` - Maximum price
182
+
183
+ #### `MerchantOffering`
184
+ - `url: str` - Purchase URL
185
+ - `merchant_name: str` - Merchant name
186
+ - `price: Price` - Price information
187
+ - `availability: AvailabilityStatus` - Availability status
188
+
189
+ #### `Price`
190
+ - `price: float` - Current price
191
+ - `compare_at_price: Optional[float]` - Original price (if discounted)
192
+ - `currency: str` - Currency code
193
+
194
+ ## Error Handling
195
+
196
+ The SDK provides specific exception types for different error conditions:
197
+
198
+ ```python
199
+ from channel3_sdk import (
200
+ Channel3AuthenticationError,
201
+ Channel3ValidationError,
202
+ Channel3NotFoundError,
203
+ Channel3ServerError,
204
+ Channel3ConnectionError
205
+ )
206
+
207
+ try:
208
+ products = client.search(query="shoes")
209
+ except Channel3AuthenticationError:
210
+ print("Invalid API key")
211
+ except Channel3ValidationError as e:
212
+ print(f"Invalid request: {e.message}")
213
+ except Channel3NotFoundError:
214
+ print("Resource not found")
215
+ except Channel3ServerError:
216
+ print("Server error - please try again later")
217
+ except Channel3ConnectionError:
218
+ print("Connection error - check your internet connection")
219
+ ```
220
+
221
+ ## Environment Variables
222
+
223
+ - `CHANNEL3_API_KEY` - Your Channel3 API key
224
+
225
+ ## Requirements
226
+
227
+ - Python 3.8+
228
+ - requests
229
+ - aiohttp
230
+ - pydantic
231
+
232
+ ## License
233
+
234
+ MIT License
235
+
@@ -0,0 +1,210 @@
1
+ # Channel3 Python SDK
2
+
3
+ The official Python SDK for the [Channel3](https://trychannel3.com) AI Shopping API.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install channel3-sdk
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ### Synchronous Client
14
+
15
+ ```python
16
+ import os
17
+ from channel3_sdk import Channel3Client
18
+
19
+ # Initialize the client
20
+ client = Channel3Client(api_key="your_api_key_here")
21
+ # Or use environment variable: CHANNEL3_API_KEY
22
+
23
+ # Search for products
24
+ products = client.search(query="blue denim jacket")
25
+
26
+ for product in products:
27
+ print(f"Product: {product.title}")
28
+ print(f"Brand: {product.brand_name}")
29
+ print(f"Price: ${product.offers[0].price.price}")
30
+ print("---")
31
+
32
+ # Get detailed product information
33
+ product_detail = client.get_product("prod_123456")
34
+ print(f"Detailed info for: {product_detail.title}")
35
+ print(f"Materials: {product_detail.materials}")
36
+ print(f"Key features: {product_detail.key_features}")
37
+ ```
38
+
39
+ ### Asynchronous Client
40
+
41
+ ```python
42
+ import asyncio
43
+ from channel3_sdk import AsyncChannel3Client
44
+
45
+ async def main():
46
+ # Initialize the async client
47
+ client = AsyncChannel3Client(api_key="your_api_key_here")
48
+
49
+ # Search for products
50
+ products = await client.search(query="running shoes")
51
+
52
+ for product in products:
53
+ print(f"Product: {product.title}")
54
+ print(f"Score: {product.score}")
55
+
56
+ # Get detailed product information
57
+ if products:
58
+ product_detail = await client.get_product(products[0].id)
59
+ print(f"Gender: {product_detail.gender}")
60
+
61
+ # Run the async function
62
+ asyncio.run(main())
63
+ ```
64
+
65
+ ## Advanced Usage
66
+
67
+ ### Visual Search
68
+
69
+ ```python
70
+ # Search by image URL
71
+ products = client.search(image_url="https://example.com/image.jpg")
72
+
73
+ # Search by base64 image
74
+ with open("image.jpg", "rb") as f:
75
+ import base64
76
+ base64_image = base64.b64encode(f.read()).decode()
77
+ products = client.search(base64_image=base64_image)
78
+ ```
79
+
80
+ ### Multimodal Search
81
+
82
+ ```python
83
+ # Combine text and image search
84
+ products = client.search(
85
+ query="blue denim jacket",
86
+ image_url="https://example.com/jacket.jpg"
87
+ )
88
+ ```
89
+
90
+ ### Search with Filters
91
+
92
+ ```python
93
+ from channel3_sdk import SearchFilters
94
+
95
+ # Create search filters
96
+ filters = SearchFilters(
97
+ colors=["blue", "navy"],
98
+ materials=["cotton", "denim"],
99
+ min_price=50.0,
100
+ max_price=200.0
101
+ )
102
+
103
+ # Search with filters
104
+ products = client.search(
105
+ query="jacket",
106
+ filters=filters,
107
+ limit=10
108
+ )
109
+ ```
110
+
111
+ ## API Reference
112
+
113
+ ### Client Classes
114
+
115
+ #### `Channel3Client`
116
+ Synchronous client for the Channel3 API.
117
+
118
+ **Methods:**
119
+ - `search(query=None, image_url=None, base64_image=None, filters=None, limit=20)` → `List[Product]`
120
+ - `get_product(product_id)` → `ProductDetail`
121
+
122
+ #### `AsyncChannel3Client`
123
+ Asynchronous client for the Channel3 API.
124
+
125
+ **Methods:**
126
+ - `async search(query=None, image_url=None, base64_image=None, filters=None, limit=20)` → `List[Product]`
127
+ - `async get_product(product_id)` → `ProductDetail`
128
+
129
+ ### Models
130
+
131
+ #### `Product`
132
+ - `id: str` - Unique product identifier
133
+ - `score: float` - Search relevance score
134
+ - `brand_name: str` - Brand name
135
+ - `title: str` - Product title
136
+ - `description: str` - Product description
137
+ - `image_url: str` - Main product image URL
138
+ - `offers: List[MerchantOffering]` - Available purchase options
139
+ - `family: List[FamilyMember]` - Related products
140
+
141
+ #### `ProductDetail`
142
+ - `brand_id: str` - Brand identifier
143
+ - `brand_name: str` - Brand name
144
+ - `title: str` - Product title
145
+ - `description: str` - Product description
146
+ - `image_urls: List[str]` - Product image URLs
147
+ - `merchant_offerings: List[MerchantOffering]` - Purchase options
148
+ - `gender: Literal["na", "men", "women"]` - Target gender
149
+ - `materials: Optional[List[str]]` - Product materials
150
+ - `key_features: List[str]` - Key product features
151
+ - `family_members: List[FamilyMember]` - Related products
152
+
153
+ #### `SearchFilters`
154
+ - `colors: Optional[List[str]]` - Color filters
155
+ - `materials: Optional[List[str]]` - Material filters
156
+ - `min_price: Optional[float]` - Minimum price
157
+ - `max_price: Optional[float]` - Maximum price
158
+
159
+ #### `MerchantOffering`
160
+ - `url: str` - Purchase URL
161
+ - `merchant_name: str` - Merchant name
162
+ - `price: Price` - Price information
163
+ - `availability: AvailabilityStatus` - Availability status
164
+
165
+ #### `Price`
166
+ - `price: float` - Current price
167
+ - `compare_at_price: Optional[float]` - Original price (if discounted)
168
+ - `currency: str` - Currency code
169
+
170
+ ## Error Handling
171
+
172
+ The SDK provides specific exception types for different error conditions:
173
+
174
+ ```python
175
+ from channel3_sdk import (
176
+ Channel3AuthenticationError,
177
+ Channel3ValidationError,
178
+ Channel3NotFoundError,
179
+ Channel3ServerError,
180
+ Channel3ConnectionError
181
+ )
182
+
183
+ try:
184
+ products = client.search(query="shoes")
185
+ except Channel3AuthenticationError:
186
+ print("Invalid API key")
187
+ except Channel3ValidationError as e:
188
+ print(f"Invalid request: {e.message}")
189
+ except Channel3NotFoundError:
190
+ print("Resource not found")
191
+ except Channel3ServerError:
192
+ print("Server error - please try again later")
193
+ except Channel3ConnectionError:
194
+ print("Connection error - check your internet connection")
195
+ ```
196
+
197
+ ## Environment Variables
198
+
199
+ - `CHANNEL3_API_KEY` - Your Channel3 API key
200
+
201
+ ## Requirements
202
+
203
+ - Python 3.8+
204
+ - requests
205
+ - aiohttp
206
+ - pydantic
207
+
208
+ ## License
209
+
210
+ MIT License
@@ -0,0 +1,44 @@
1
+ """Channel3 SDK for Python - Official SDK for the Channel3 AI Shopping API."""
2
+
3
+ from .client import Channel3Client, AsyncChannel3Client
4
+ from .models import (
5
+ Product,
6
+ ProductDetail,
7
+ SearchFilters,
8
+ SearchRequest,
9
+ MerchantOffering,
10
+ FamilyMember,
11
+ Price,
12
+ AvailabilityStatus,
13
+ )
14
+ from .exceptions import (
15
+ Channel3Error,
16
+ Channel3AuthenticationError,
17
+ Channel3ValidationError,
18
+ Channel3NotFoundError,
19
+ Channel3ServerError,
20
+ Channel3ConnectionError,
21
+ )
22
+
23
+ __version__ = "0.1.0"
24
+ __all__ = [
25
+ # Clients
26
+ "Channel3Client",
27
+ "AsyncChannel3Client",
28
+ # Models
29
+ "Product",
30
+ "ProductDetail",
31
+ "SearchFilters",
32
+ "SearchRequest",
33
+ "MerchantOffering",
34
+ "FamilyMember",
35
+ "Price",
36
+ "AvailabilityStatus",
37
+ # Exceptions
38
+ "Channel3Error",
39
+ "Channel3AuthenticationError",
40
+ "Channel3ValidationError",
41
+ "Channel3NotFoundError",
42
+ "Channel3ServerError",
43
+ "Channel3ConnectionError",
44
+ ]
@@ -0,0 +1,341 @@
1
+ # channel3_sdk/client.py
2
+ import os
3
+ import requests
4
+ from typing import List, Optional, Dict, Any, Union
5
+ import aiohttp
6
+ import asyncio
7
+ from pydantic import ValidationError
8
+
9
+ from .models import Product, ProductDetail, SearchFilters, SearchRequest
10
+ from .exceptions import (
11
+ Channel3Error,
12
+ Channel3AuthenticationError,
13
+ Channel3ValidationError,
14
+ Channel3NotFoundError,
15
+ Channel3ServerError,
16
+ Channel3ConnectionError,
17
+ )
18
+
19
+
20
+ class BaseChannel3Client:
21
+ """Base client with common functionality."""
22
+
23
+ def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None):
24
+ """
25
+ Initialize the base Channel3 client.
26
+
27
+ Args:
28
+ api_key: Your Channel3 API key. If not provided, will look for CHANNEL3_API_KEY in environment.
29
+ base_url: Base URL for the API. Defaults to https://api.trychannel3.com/v0
30
+
31
+ Raises:
32
+ ValueError: If no API key is provided and none is found in environment variables.
33
+ """
34
+ self.api_key = api_key or os.getenv("CHANNEL3_API_KEY")
35
+ if not self.api_key:
36
+ raise ValueError(
37
+ "No API key provided. Set CHANNEL3_API_KEY environment variable or pass api_key parameter."
38
+ )
39
+
40
+ self.base_url = base_url or "https://api.trychannel3.com/v0"
41
+ self.headers = {"x-api-key": self.api_key, "Content-Type": "application/json"}
42
+
43
+ def _handle_error_response(
44
+ self, status_code: int, response_data: Dict[str, Any], url: str
45
+ ) -> None:
46
+ """Handle error responses and raise appropriate exceptions."""
47
+ error_message = response_data.get(
48
+ "detail", f"Request failed with status {status_code}"
49
+ )
50
+
51
+ if status_code == 401:
52
+ raise Channel3AuthenticationError(
53
+ "Invalid or missing API key",
54
+ status_code=status_code,
55
+ response_data=response_data,
56
+ )
57
+ elif status_code == 404:
58
+ raise Channel3NotFoundError(
59
+ error_message, status_code=status_code, response_data=response_data
60
+ )
61
+ elif status_code == 422:
62
+ raise Channel3ValidationError(
63
+ f"Validation error: {error_message}",
64
+ status_code=status_code,
65
+ response_data=response_data,
66
+ )
67
+ elif status_code == 500:
68
+ raise Channel3ServerError(
69
+ "Internal server error",
70
+ status_code=status_code,
71
+ response_data=response_data,
72
+ )
73
+ else:
74
+ raise Channel3Error(
75
+ f"Request to {url} failed: {error_message}",
76
+ status_code=status_code,
77
+ response_data=response_data,
78
+ )
79
+
80
+
81
+ class Channel3Client(BaseChannel3Client):
82
+ """Synchronous Channel3 API client."""
83
+
84
+ def search(
85
+ self,
86
+ query: Optional[str] = None,
87
+ image_url: Optional[str] = None,
88
+ base64_image: Optional[str] = None,
89
+ filters: Optional[Union[SearchFilters, Dict[str, Any]]] = None,
90
+ limit: int = 20,
91
+ ) -> List[Product]:
92
+ """
93
+ Search for products using text query, image, or both with optional filters.
94
+
95
+ Args:
96
+ query: Text search query
97
+ image_url: URL to an image to use for visual search
98
+ base64_image: Base64-encoded image to use for visual search
99
+ filters: Search filters (SearchFilters object or dict)
100
+ limit: Maximum number of products to return (default: 20)
101
+
102
+ Returns:
103
+ List of Product objects
104
+
105
+ Raises:
106
+ Channel3AuthenticationError: If API key is invalid
107
+ Channel3ValidationError: If request parameters are invalid
108
+ Channel3ServerError: If server encounters an error
109
+ Channel3ConnectionError: If there are connection issues
110
+
111
+ Examples:
112
+ ```python
113
+ # Text search
114
+ products = client.search(query="blue denim jacket")
115
+
116
+ # Image search
117
+ products = client.search(image_url="https://example.com/image.jpg")
118
+
119
+ # Multimodal search with filters
120
+ from channel3_sdk.models import SearchFilters
121
+ filters = SearchFilters(colors=["blue"], min_price=50.0, max_price=150.0)
122
+ products = client.search(query="denim jacket", filters=filters)
123
+ ```
124
+ """
125
+ # Build request payload
126
+ search_request = SearchRequest(
127
+ query=query,
128
+ image_url=image_url,
129
+ base64_image=base64_image,
130
+ filters=filters,
131
+ limit=limit,
132
+ )
133
+
134
+ url = f"{self.base_url}/search"
135
+
136
+ try:
137
+ response = requests.post(
138
+ url,
139
+ json=search_request.model_dump(exclude_none=True),
140
+ headers=self.headers,
141
+ timeout=30,
142
+ )
143
+
144
+ response_data = response.json()
145
+
146
+ if response.status_code != 200:
147
+ self._handle_error_response(response.status_code, response_data, url)
148
+
149
+ # Parse and validate response
150
+ return [Product(**item) for item in response_data]
151
+
152
+ except requests.exceptions.ConnectionError as e:
153
+ raise Channel3ConnectionError(
154
+ f"Failed to connect to Channel3 API: {str(e)}"
155
+ )
156
+ except requests.exceptions.Timeout as e:
157
+ raise Channel3ConnectionError(f"Request timed out: {str(e)}")
158
+ except requests.exceptions.RequestException as e:
159
+ raise Channel3Error(f"Request failed: {str(e)}")
160
+ except ValidationError as e:
161
+ raise Channel3Error(f"Invalid response format: {str(e)}")
162
+
163
+ def get_product(self, product_id: str) -> ProductDetail:
164
+ """
165
+ Get detailed information about a specific product by its ID.
166
+
167
+ Args:
168
+ product_id: The unique identifier of the product
169
+
170
+ Returns:
171
+ ProductDetail object with detailed product information
172
+
173
+ Raises:
174
+ Channel3AuthenticationError: If API key is invalid
175
+ Channel3NotFoundError: If product is not found
176
+ Channel3ValidationError: If product_id is invalid
177
+ Channel3ServerError: If server encounters an error
178
+ Channel3ConnectionError: If there are connection issues
179
+
180
+ Example:
181
+ ```python
182
+ product_detail = client.get_product("prod_123456")
183
+ print(f"Product: {product_detail.title}")
184
+ print(f"Brand: {product_detail.brand_name}")
185
+ ```
186
+ """
187
+ if not product_id or not product_id.strip():
188
+ raise ValueError("product_id cannot be empty")
189
+
190
+ url = f"{self.base_url}/products/{product_id}"
191
+
192
+ try:
193
+ response = requests.get(url, headers=self.headers, timeout=30)
194
+ response_data = response.json()
195
+
196
+ if response.status_code != 200:
197
+ self._handle_error_response(response.status_code, response_data, url)
198
+
199
+ # Parse and validate response
200
+ return ProductDetail(**response_data)
201
+
202
+ except requests.exceptions.ConnectionError as e:
203
+ raise Channel3ConnectionError(
204
+ f"Failed to connect to Channel3 API: {str(e)}"
205
+ )
206
+ except requests.exceptions.Timeout as e:
207
+ raise Channel3ConnectionError(f"Request timed out: {str(e)}")
208
+ except requests.exceptions.RequestException as e:
209
+ raise Channel3Error(f"Request failed: {str(e)}")
210
+ except ValidationError as e:
211
+ raise Channel3Error(f"Invalid response format: {str(e)}")
212
+
213
+
214
+ class AsyncChannel3Client(BaseChannel3Client):
215
+ """Asynchronous Channel3 API client."""
216
+
217
+ async def search(
218
+ self,
219
+ query: Optional[str] = None,
220
+ image_url: Optional[str] = None,
221
+ base64_image: Optional[str] = None,
222
+ filters: Optional[Union[SearchFilters, Dict[str, Any]]] = None,
223
+ limit: int = 20,
224
+ ) -> List[Product]:
225
+ """
226
+ Search for products using text query, image, or both with optional filters.
227
+
228
+ Args:
229
+ query: Text search query
230
+ image_url: URL to an image to use for visual search
231
+ base64_image: Base64-encoded image to use for visual search
232
+ filters: Search filters (SearchFilters object or dict)
233
+ limit: Maximum number of products to return (default: 20)
234
+
235
+ Returns:
236
+ List of Product objects
237
+
238
+ Raises:
239
+ Channel3AuthenticationError: If API key is invalid
240
+ Channel3ValidationError: If request parameters are invalid
241
+ Channel3ServerError: If server encounters an error
242
+ Channel3ConnectionError: If there are connection issues
243
+
244
+ Examples:
245
+ ```python
246
+ # Text search
247
+ products = await async_client.search(query="blue denim jacket")
248
+
249
+ # Image search
250
+ products = await async_client.search(image_url="https://example.com/image.jpg")
251
+ ```
252
+ """
253
+ # Build request payload
254
+ search_request = SearchRequest(
255
+ query=query,
256
+ image_url=image_url,
257
+ base64_image=base64_image,
258
+ filters=filters,
259
+ limit=limit,
260
+ )
261
+
262
+ url = f"{self.base_url}/search"
263
+
264
+ try:
265
+ async with aiohttp.ClientSession() as session:
266
+ async with session.post(
267
+ url,
268
+ json=search_request.model_dump(exclude_none=True),
269
+ headers=self.headers,
270
+ timeout=aiohttp.ClientTimeout(total=30),
271
+ ) as response:
272
+ response_data = await response.json()
273
+
274
+ if response.status != 200:
275
+ self._handle_error_response(response.status, response_data, url)
276
+
277
+ # Parse and validate response
278
+ return [Product(**item) for item in response_data]
279
+
280
+ except aiohttp.ClientConnectionError as e:
281
+ raise Channel3ConnectionError(
282
+ f"Failed to connect to Channel3 API: {str(e)}"
283
+ )
284
+ except asyncio.TimeoutError as e:
285
+ raise Channel3ConnectionError(f"Request timed out: {str(e)}")
286
+ except aiohttp.ClientError as e:
287
+ raise Channel3Error(f"Request failed: {str(e)}")
288
+ except ValidationError as e:
289
+ raise Channel3Error(f"Invalid response format: {str(e)}")
290
+
291
+ async def get_product(self, product_id: str) -> ProductDetail:
292
+ """
293
+ Get detailed information about a specific product by its ID.
294
+
295
+ Args:
296
+ product_id: The unique identifier of the product
297
+
298
+ Returns:
299
+ ProductDetail object with detailed product information
300
+
301
+ Raises:
302
+ Channel3AuthenticationError: If API key is invalid
303
+ Channel3NotFoundError: If product is not found
304
+ Channel3ValidationError: If product_id is invalid
305
+ Channel3ServerError: If server encounters an error
306
+ Channel3ConnectionError: If there are connection issues
307
+
308
+ Example:
309
+ ```python
310
+ product_detail = await async_client.get_product("prod_123456")
311
+ print(f"Product: {product_detail.title}")
312
+ ```
313
+ """
314
+ if not product_id or not product_id.strip():
315
+ raise ValueError("product_id cannot be empty")
316
+
317
+ url = f"{self.base_url}/products/{product_id}"
318
+
319
+ try:
320
+ async with aiohttp.ClientSession() as session:
321
+ async with session.get(
322
+ url, headers=self.headers, timeout=aiohttp.ClientTimeout(total=30)
323
+ ) as response:
324
+ response_data = await response.json()
325
+
326
+ if response.status != 200:
327
+ self._handle_error_response(response.status, response_data, url)
328
+
329
+ # Parse and validate response
330
+ return ProductDetail(**response_data)
331
+
332
+ except aiohttp.ClientConnectionError as e:
333
+ raise Channel3ConnectionError(
334
+ f"Failed to connect to Channel3 API: {str(e)}"
335
+ )
336
+ except asyncio.TimeoutError as e:
337
+ raise Channel3ConnectionError(f"Request timed out: {str(e)}")
338
+ except aiohttp.ClientError as e:
339
+ raise Channel3Error(f"Request failed: {str(e)}")
340
+ except ValidationError as e:
341
+ raise Channel3Error(f"Invalid response format: {str(e)}")
@@ -0,0 +1,48 @@
1
+ """Custom exceptions for the Channel3 SDK."""
2
+
3
+ from typing import Optional, Dict, Any
4
+
5
+
6
+ class Channel3Error(Exception):
7
+ """Base exception for all Channel3 SDK errors."""
8
+
9
+ def __init__(
10
+ self,
11
+ message: str,
12
+ status_code: Optional[int] = None,
13
+ response_data: Optional[Dict[str, Any]] = None,
14
+ ):
15
+ super().__init__(message)
16
+ self.message = message
17
+ self.status_code = status_code
18
+ self.response_data = response_data or {}
19
+
20
+
21
+ class Channel3AuthenticationError(Channel3Error):
22
+ """Raised when authentication fails (401)."""
23
+
24
+ pass
25
+
26
+
27
+ class Channel3ValidationError(Channel3Error):
28
+ """Raised when request validation fails (422)."""
29
+
30
+ pass
31
+
32
+
33
+ class Channel3NotFoundError(Channel3Error):
34
+ """Raised when a resource is not found (404)."""
35
+
36
+ pass
37
+
38
+
39
+ class Channel3ServerError(Channel3Error):
40
+ """Raised when the server encounters an error (500)."""
41
+
42
+ pass
43
+
44
+
45
+ class Channel3ConnectionError(Channel3Error):
46
+ """Raised when there are connection issues."""
47
+
48
+ pass
@@ -0,0 +1,116 @@
1
+ """Pydantic models for the Channel3 API."""
2
+
3
+ from enum import Enum
4
+ from typing import List, Optional, Union, Literal
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class AvailabilityStatus(str, Enum):
9
+ """Availability status of a product."""
10
+
11
+ IN_STOCK = "InStock"
12
+ OUT_OF_STOCK = "OutOfStock"
13
+ PRE_ORDER = "PreOrder"
14
+ LIMITED_AVAILABILITY = "LimitedAvailability"
15
+ BACK_ORDER = "BackOrder"
16
+ DISCONTINUED = "Discontinued"
17
+ SOLD_OUT = "SoldOut"
18
+ UNKNOWN = "Unknown"
19
+
20
+
21
+ class Price(BaseModel):
22
+ """Price information for a product."""
23
+
24
+ price: float = Field(
25
+ ..., description="The current price of the product, including any discounts."
26
+ )
27
+ compare_at_price: Optional[float] = Field(
28
+ None, description="The original price of the product before any discounts."
29
+ )
30
+ currency: str = Field(..., description="The currency code of the product.")
31
+
32
+
33
+ class MerchantOffering(BaseModel):
34
+ """A merchant offering a product."""
35
+
36
+ url: str = Field(
37
+ default="https://buy.trychannel3.com", description="URL to purchase the product"
38
+ )
39
+ merchant_name: str = Field(..., description="Name of the merchant")
40
+ price: Price = Field(..., description="Price information")
41
+ availability: AvailabilityStatus = Field(
42
+ ..., description="Product availability status"
43
+ )
44
+
45
+
46
+ class FamilyMember(BaseModel):
47
+ """A family member product."""
48
+
49
+ id: str = Field(..., description="Unique identifier for the family member")
50
+ title: str = Field(..., description="Title of the family member product")
51
+ image_url: str = Field(..., description="Image URL for the family member product")
52
+
53
+
54
+ class Product(BaseModel):
55
+ """A product returned from search."""
56
+
57
+ id: str = Field(..., description="Unique identifier for the product")
58
+ score: float = Field(..., description="Relevance score for the search query")
59
+ brand_name: str = Field(..., description="Brand name of the product")
60
+ title: str = Field(..., description="Product title")
61
+ description: str = Field(..., description="Product description")
62
+ image_url: str = Field(..., description="Main product image URL")
63
+ offers: List[MerchantOffering] = Field(
64
+ ..., description="List of merchant offerings"
65
+ )
66
+ family: List[FamilyMember] = Field(
67
+ default_factory=list, description="Related family products"
68
+ )
69
+
70
+
71
+ class ProductDetail(BaseModel):
72
+ """Detailed information about a product."""
73
+
74
+ brand_id: str = Field(..., description="Unique identifier for the brand")
75
+ brand_name: str = Field(..., description="Brand name of the product")
76
+ title: str = Field(..., description="Product title")
77
+ description: str = Field(..., description="Product description")
78
+ image_urls: List[str] = Field(..., description="List of product image URLs")
79
+ merchant_offerings: List[MerchantOffering] = Field(
80
+ ..., description="List of merchant offerings"
81
+ )
82
+ gender: Literal["na", "men", "women"] = Field(
83
+ default="na", description="Target gender"
84
+ )
85
+ materials: Optional[List[str]] = Field(None, description="List of materials")
86
+ key_features: List[str] = Field(
87
+ default_factory=list, description="List of key product features"
88
+ )
89
+ family_members: List[FamilyMember] = Field(
90
+ default_factory=list, description="Related family products"
91
+ )
92
+
93
+
94
+ class SearchFilters(BaseModel):
95
+ """Search filters for product search."""
96
+
97
+ colors: Optional[List[str]] = Field(None, description="List of colors to filter by")
98
+ materials: Optional[List[str]] = Field(
99
+ None, description="List of materials to filter by"
100
+ )
101
+ min_price: Optional[float] = Field(None, description="Minimum price filter")
102
+ max_price: Optional[float] = Field(None, description="Maximum price filter")
103
+
104
+
105
+ class SearchRequest(BaseModel):
106
+ """Request model for product search."""
107
+
108
+ query: Optional[str] = Field(None, description="Text search query")
109
+ image_url: Optional[str] = Field(None, description="URL of image for visual search")
110
+ base64_image: Optional[str] = Field(
111
+ None, description="Base64-encoded image for visual search"
112
+ )
113
+ filters: Optional[SearchFilters] = Field(None, description="Search filters")
114
+ limit: Optional[int] = Field(
115
+ default=20, description="Maximum number of results to return"
116
+ )
@@ -0,0 +1,40 @@
1
+ [tool.poetry]
2
+ name = "channel3-sdk"
3
+ version = "0.1.0"
4
+ description = "The official Python SDK for Channel3 AI Shopping API"
5
+ authors = ["Channel3 <alex@trychannel3.com>"]
6
+ readme = "README.md"
7
+ license = "MIT"
8
+ packages = [{ include = "channel3_sdk" }]
9
+ repository = "https://github.com/channel3/sdk-python"
10
+ keywords = ["ai", "shopping", "ecommerce", "search", "api"]
11
+
12
+ [tool.poetry.dependencies]
13
+ python = "^3.8"
14
+ requests = "^2.31.0"
15
+ aiohttp = "^3.9.0"
16
+ pydantic = "^2.5.0"
17
+
18
+ [tool.poetry.group.dev.dependencies]
19
+ pytest = "^7.4.0"
20
+ pytest-asyncio = "^0.21.0"
21
+ black = "^23.0.0"
22
+ isort = "^5.12.0"
23
+ mypy = "^1.7.0"
24
+
25
+ [build-system]
26
+ requires = ["poetry-core"]
27
+ build-backend = "poetry.core.masonry.api"
28
+
29
+ [tool.black]
30
+ line-length = 88
31
+ target-version = ['py38']
32
+
33
+ [tool.isort]
34
+ profile = "black"
35
+
36
+ [tool.mypy]
37
+ python_version = "3.8"
38
+ warn_return_any = true
39
+ warn_unused_configs = true
40
+ disallow_untyped_defs = true
@@ -1,18 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: channel3-sdk
3
- Version: 0.0.1
4
- Summary: The API for AI Shopping
5
- License: MIT
6
- Author: Alex
7
- Author-email: alex@trychannel3.com
8
- Requires-Python: >=3.10,<4.0
9
- Classifier: License :: OSI Approved :: MIT License
10
- Classifier: Programming Language :: Python :: 3
11
- Classifier: Programming Language :: Python :: 3.10
12
- Classifier: Programming Language :: Python :: 3.11
13
- Classifier: Programming Language :: Python :: 3.12
14
- Classifier: Programming Language :: Python :: 3.13
15
- Requires-Dist: requests (>=2.31.0,<3.0.0)
16
- Description-Content-Type: text/markdown
17
-
18
-
File without changes
@@ -1 +0,0 @@
1
- from .client import Channel3Client
@@ -1,134 +0,0 @@
1
- # channel3_sdk/client.py
2
- import os
3
- import requests
4
- from typing import List, Optional, Dict, Any, Union
5
-
6
-
7
- class Channel3Client:
8
- def __init__(self, api_key: str = None):
9
- """
10
- Initialize a Channel3 API client.
11
-
12
- The client provides methods to interact with Channel3's product search and retrieval API.
13
-
14
- Args:
15
- api_key: Your Channel3 API key. If not provided, will look for CHANNEL3_API_KEY in environment.
16
-
17
- Raises:
18
- ValueError: If no API key is provided and none is found in environment variables.
19
-
20
- Example:
21
- ```python
22
- # Initialize with explicit API key
23
- client = Channel3Client(api_key="your_api_key")
24
-
25
- # Or use environment variable
26
- # os.environ["CHANNEL3_API_KEY"] = "your_api_key"
27
- # client = Channel3Client()
28
- ```
29
- """
30
- self.api_key = api_key or os.getenv("CHANNEL3_API_KEY")
31
- if not self.api_key:
32
- raise ValueError("No API key provided for Channel3Client")
33
- self.headers = {"x-api-key": self.api_key}
34
- self.api_version = "v0"
35
- self.base_url = f"https://api.channel3.com/{self.api_version}"
36
-
37
- def search(
38
- self,
39
- query: Optional[str] = None,
40
- image_url: Optional[str] = None,
41
- base64_image: Optional[str] = None,
42
- filters: Optional[Dict[str, Any]] = None,
43
- limit: int = 20,
44
- ) -> List[Dict[str, Any]]:
45
- """
46
- Search for products using text query, image, or both with optional filters.
47
-
48
- Args:
49
- query: Text search query
50
- image_url: URL to an image to use for visual search
51
- base64_image: Base64-encoded image to use for visual search
52
- filters: Dict containing optional filters with these possible keys:
53
- - colors: List of color strings
54
- - materials: List of material strings
55
- - min_price: Minimum price (float)
56
- - max_price: Maximum price (float)
57
- limit: Maximum number of products to return (default: 20)
58
-
59
- Returns:
60
- List of product dictionaries containing:
61
- id, url, score, price, brand_id, brand_name, title, description, image_url, variants
62
-
63
- Examples:
64
- ```python
65
- # Text search
66
- products = client.search(query="blue denim jacket")
67
-
68
- # Image search
69
- products = client.search(image_url="https://example.com/image.jpg")
70
-
71
- # Multimodal search
72
- products = client.search(
73
- query="blue denim jacket",
74
- base64_image="data:image/jpeg;base64,...",
75
- )
76
-
77
- # Search with filters
78
- products = client.search(
79
- query="running shoes",
80
- filters={
81
- "colors": ["black", "white"],
82
- "min_price": 50.0,
83
- "max_price": 150.0
84
- },
85
- limit=10
86
- )
87
- ```
88
- """
89
- payload = {
90
- "query": query,
91
- "image_url": image_url,
92
- "base64_image": base64_image,
93
- "limit": limit,
94
- }
95
-
96
- if filters:
97
- payload["filters"] = filters
98
-
99
- response = requests.post(
100
- f"{self.base_url}/search",
101
- json={k: v for k, v in payload.items() if v is not None},
102
- headers=self.headers,
103
- )
104
- response.raise_for_status()
105
- return response.json()
106
-
107
- def get_product(self, product_id: str) -> Dict[str, Any]:
108
- """
109
- Get detailed information about a specific product by its ID.
110
-
111
- Args:
112
- product_id: The unique identifier of the product
113
-
114
- Returns:
115
- Dictionary containing detailed product information:
116
- url, brand_id, brand_name, title, description, image_urls, variants,
117
- price, gender, materials, key_features
118
-
119
- Raises:
120
- requests.HTTPError: If the product does not exist or other API errors occur
121
-
122
- Example:
123
- ```python
124
- product_detail = client.get_product("prod_123456")
125
- print(product_detail["title"])
126
- print(product_detail["price"]["price"])
127
- ```
128
- """
129
- response = requests.get(
130
- f"{self.base_url}/products/{product_id}",
131
- headers=self.headers,
132
- )
133
- response.raise_for_status()
134
- return response.json()
@@ -1,17 +0,0 @@
1
- [tool.poetry]
2
- name = "channel3-sdk"
3
- version = "0.0.1"
4
- description = "The API for AI Shopping"
5
- authors = ["Alex <alex@trychannel3.com>"]
6
- readme = "README.md"
7
- license = "MIT"
8
- packages = [{ include = "channel3_sdk" }]
9
-
10
- [tool.poetry.dependencies]
11
- python = "^3.10"
12
- requests = "^2.31.0"
13
-
14
-
15
- [build-system]
16
- requires = ["poetry-core"]
17
- build-backend = "poetry.core.masonry.api"