python-amazon-paapi 6.0.0__py3-none-any.whl → 6.2.0__py3-none-any.whl
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.
- amazon_creatorsapi/aio/__init__.py +14 -0
- amazon_creatorsapi/aio/api.py +551 -0
- amazon_creatorsapi/aio/auth.py +237 -0
- amazon_creatorsapi/aio/client.py +147 -0
- amazon_creatorsapi/api.py +15 -58
- amazon_creatorsapi/core/constants.py +4 -0
- amazon_creatorsapi/core/error_handling.py +55 -0
- amazon_creatorsapi/core/resources.py +22 -0
- amazon_creatorsapi/core/validation.py +40 -0
- amazon_creatorsapi/errors.py +5 -0
- creatorsapi_python_sdk/__init__.py +1 -0
- creatorsapi_python_sdk/api/default_api.py +15 -0
- creatorsapi_python_sdk/api_client.py +6 -3
- creatorsapi_python_sdk/auth/oauth2_config.py +20 -4
- creatorsapi_python_sdk/auth/oauth2_token_manager.py +29 -16
- creatorsapi_python_sdk/models/__init__.py +1 -0
- creatorsapi_python_sdk/models/variation_summary.py +7 -1
- creatorsapi_python_sdk/models/variation_summary_price.py +101 -0
- {python_amazon_paapi-6.0.0.dist-info → python_amazon_paapi-6.2.0.dist-info}/METADATA +40 -2
- {python_amazon_paapi-6.0.0.dist-info → python_amazon_paapi-6.2.0.dist-info}/RECORD +22 -14
- {python_amazon_paapi-6.0.0.dist-info → python_amazon_paapi-6.2.0.dist-info}/WHEEL +1 -1
- {python_amazon_paapi-6.0.0.dist-info → python_amazon_paapi-6.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Async support for Amazon Creators API."""
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
import httpx # noqa: F401
|
|
5
|
+
except ImportError as exc: # pragma: no cover
|
|
6
|
+
msg = (
|
|
7
|
+
"httpx is required for async support. "
|
|
8
|
+
"Install it with: pip install python-amazon-paapi[async]"
|
|
9
|
+
)
|
|
10
|
+
raise ImportError(msg) from exc
|
|
11
|
+
|
|
12
|
+
from amazon_creatorsapi.aio.api import AsyncAmazonCreatorsApi
|
|
13
|
+
|
|
14
|
+
__all__ = ["AsyncAmazonCreatorsApi"]
|
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
"""Async Amazon Creators API wrapper for Python.
|
|
2
|
+
|
|
3
|
+
Provides async methods to interact with the Amazon Creators API.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import time
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from typing import TYPE_CHECKING, Any, TypeVar
|
|
12
|
+
|
|
13
|
+
from typing_extensions import Self
|
|
14
|
+
|
|
15
|
+
from amazon_creatorsapi.core.constants import DEFAULT_THROTTLING
|
|
16
|
+
from amazon_creatorsapi.core.error_handling import handle_api_error
|
|
17
|
+
from amazon_creatorsapi.core.parsers import get_asin, get_items_ids
|
|
18
|
+
from amazon_creatorsapi.core.resources import get_all_resources
|
|
19
|
+
from amazon_creatorsapi.core.validation import validate_and_get_marketplace
|
|
20
|
+
from amazon_creatorsapi.errors import ItemsNotFoundError
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
from .auth import VERSION_ENDPOINTS, AsyncOAuth2TokenManager
|
|
24
|
+
from .client import AsyncHttpClient
|
|
25
|
+
except ImportError as exc: # pragma: no cover
|
|
26
|
+
msg = (
|
|
27
|
+
"httpx is required for async support. "
|
|
28
|
+
"Install it with: pip install python-amazon-paapi[async]"
|
|
29
|
+
)
|
|
30
|
+
raise ImportError(msg) from exc
|
|
31
|
+
|
|
32
|
+
from creatorsapi_python_sdk.models.get_browse_nodes_resource import (
|
|
33
|
+
GetBrowseNodesResource,
|
|
34
|
+
)
|
|
35
|
+
from creatorsapi_python_sdk.models.get_items_resource import GetItemsResource
|
|
36
|
+
from creatorsapi_python_sdk.models.get_variations_resource import GetVariationsResource
|
|
37
|
+
from creatorsapi_python_sdk.models.search_items_resource import SearchItemsResource
|
|
38
|
+
|
|
39
|
+
if TYPE_CHECKING:
|
|
40
|
+
from types import TracebackType
|
|
41
|
+
|
|
42
|
+
from amazon_creatorsapi.core.marketplaces import CountryCode
|
|
43
|
+
from creatorsapi_python_sdk.models.condition import Condition
|
|
44
|
+
from creatorsapi_python_sdk.models.sort_by import SortBy
|
|
45
|
+
|
|
46
|
+
from creatorsapi_python_sdk.models.browse_node import BrowseNode
|
|
47
|
+
from creatorsapi_python_sdk.models.item import Item
|
|
48
|
+
from creatorsapi_python_sdk.models.search_result import SearchResult
|
|
49
|
+
from creatorsapi_python_sdk.models.variations_result import VariationsResult
|
|
50
|
+
|
|
51
|
+
# API endpoints
|
|
52
|
+
API_HOST = "https://creatorsapi.amazon"
|
|
53
|
+
ENDPOINT_GET_ITEMS = "/catalog/v1/getItems"
|
|
54
|
+
ENDPOINT_SEARCH_ITEMS = "/catalog/v1/searchItems"
|
|
55
|
+
ENDPOINT_GET_VARIATIONS = "/catalog/v1/getVariations"
|
|
56
|
+
ENDPOINT_GET_BROWSE_NODES = "/catalog/v1/getBrowseNodes"
|
|
57
|
+
|
|
58
|
+
# TypeVar for generic resource handling
|
|
59
|
+
ResourceT = TypeVar("ResourceT", bound=Enum)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class AsyncAmazonCreatorsApi:
|
|
63
|
+
"""Async version of Amazon Creators API wrapper.
|
|
64
|
+
|
|
65
|
+
Provides async methods to get information from Amazon using the Creators API.
|
|
66
|
+
This class can be used with or without a context manager.
|
|
67
|
+
|
|
68
|
+
Basic usage (creates new HTTP connection per request):
|
|
69
|
+
>>> api = AsyncAmazonCreatorsApi(
|
|
70
|
+
... credential_id="your_id",
|
|
71
|
+
... credential_secret="your_secret",
|
|
72
|
+
... version="2.2",
|
|
73
|
+
... tag="your-tag",
|
|
74
|
+
... country="ES"
|
|
75
|
+
... )
|
|
76
|
+
>>> items = await api.get_items(["B0DLFMFBJW"])
|
|
77
|
+
|
|
78
|
+
Advanced usage with context manager (reuses HTTP connection):
|
|
79
|
+
>>> async with AsyncAmazonCreatorsApi(
|
|
80
|
+
... credential_id="your_id",
|
|
81
|
+
... credential_secret="your_secret",
|
|
82
|
+
... version="2.2",
|
|
83
|
+
... tag="your-tag",
|
|
84
|
+
... country="ES"
|
|
85
|
+
... ) as api:
|
|
86
|
+
... items = await api.get_items(["B0DLFMFBJW"])
|
|
87
|
+
|
|
88
|
+
The context manager approach is more efficient when making multiple
|
|
89
|
+
requests in quick succession due to HTTP connection pooling.
|
|
90
|
+
|
|
91
|
+
Note:
|
|
92
|
+
Using without context manager creates a new HTTP connection for each
|
|
93
|
+
request, which is less efficient and may impact performance. For
|
|
94
|
+
production code making multiple API calls, always use the context
|
|
95
|
+
manager (async with) to benefit from connection pooling and reduced
|
|
96
|
+
overhead.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
credential_id: Your Creators API credential ID.
|
|
100
|
+
credential_secret: Your Creators API credential secret.
|
|
101
|
+
version: API version for your region.
|
|
102
|
+
tag: Your affiliate tracking id (partner tag).
|
|
103
|
+
country: Country code (e.g., "ES", "US"). Used to determine marketplace.
|
|
104
|
+
marketplace: Marketplace URL (e.g., "www.amazon.es"). Overrides country.
|
|
105
|
+
throttling: Wait time in seconds between API calls. Defaults to 1 second.
|
|
106
|
+
|
|
107
|
+
Raises:
|
|
108
|
+
InvalidArgumentError: If neither country nor marketplace is provided.
|
|
109
|
+
ValueError: If version is not supported (valid versions: 2.1, 2.2, 2.3,
|
|
110
|
+
3.1, 3.2, 3.3).
|
|
111
|
+
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
def __init__(
|
|
115
|
+
self,
|
|
116
|
+
credential_id: str,
|
|
117
|
+
credential_secret: str,
|
|
118
|
+
version: str,
|
|
119
|
+
tag: str,
|
|
120
|
+
country: CountryCode | None = None,
|
|
121
|
+
marketplace: str | None = None,
|
|
122
|
+
throttling: float = DEFAULT_THROTTLING,
|
|
123
|
+
) -> None:
|
|
124
|
+
"""Initialize the async Amazon Creators API client."""
|
|
125
|
+
# Validate version early to fail fast (before token manager initialization)
|
|
126
|
+
self._validate_version(version)
|
|
127
|
+
|
|
128
|
+
self._credential_id = credential_id
|
|
129
|
+
self._credential_secret = credential_secret
|
|
130
|
+
self._version = version
|
|
131
|
+
self._last_query_time = time.time() - throttling
|
|
132
|
+
self._throttle_lock: asyncio.Lock | None = None
|
|
133
|
+
self.tag = tag
|
|
134
|
+
self.throttling = float(throttling)
|
|
135
|
+
|
|
136
|
+
# Determine marketplace from country or direct value
|
|
137
|
+
self.marketplace = validate_and_get_marketplace(country, marketplace)
|
|
138
|
+
|
|
139
|
+
# HTTP client and token manager (initialized lazily or via context manager)
|
|
140
|
+
self._http_client: AsyncHttpClient | None = None
|
|
141
|
+
self._token_manager = AsyncOAuth2TokenManager(
|
|
142
|
+
credential_id=credential_id,
|
|
143
|
+
credential_secret=credential_secret,
|
|
144
|
+
version=version,
|
|
145
|
+
)
|
|
146
|
+
self._owns_client = False
|
|
147
|
+
|
|
148
|
+
def _validate_version(self, version: str) -> None:
|
|
149
|
+
"""Validate that the API version is supported.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
version: API version to validate.
|
|
153
|
+
|
|
154
|
+
Raises:
|
|
155
|
+
ValueError: If version is not in the list of supported versions.
|
|
156
|
+
|
|
157
|
+
"""
|
|
158
|
+
if version not in VERSION_ENDPOINTS:
|
|
159
|
+
supported = ", ".join(VERSION_ENDPOINTS.keys())
|
|
160
|
+
msg = f"Unsupported version: {version}. Supported versions are: {supported}"
|
|
161
|
+
raise ValueError(msg)
|
|
162
|
+
|
|
163
|
+
async def __aenter__(self) -> Self:
|
|
164
|
+
"""Enter async context manager, creating a persistent HTTP client."""
|
|
165
|
+
self._http_client = AsyncHttpClient(host=API_HOST)
|
|
166
|
+
await self._http_client.__aenter__()
|
|
167
|
+
self._owns_client = True
|
|
168
|
+
return self
|
|
169
|
+
|
|
170
|
+
async def __aexit__(
|
|
171
|
+
self,
|
|
172
|
+
exc_type: type[BaseException] | None,
|
|
173
|
+
exc_val: BaseException | None,
|
|
174
|
+
exc_tb: TracebackType | None,
|
|
175
|
+
) -> None:
|
|
176
|
+
"""Exit async context manager, closing the HTTP client."""
|
|
177
|
+
if self._http_client is not None and self._owns_client:
|
|
178
|
+
await self._http_client.__aexit__(exc_type, exc_val, exc_tb)
|
|
179
|
+
self._http_client = None
|
|
180
|
+
self._owns_client = False
|
|
181
|
+
|
|
182
|
+
async def get_items(
|
|
183
|
+
self,
|
|
184
|
+
items: str | list[str],
|
|
185
|
+
condition: Condition | None = None,
|
|
186
|
+
currency_of_preference: str | None = None,
|
|
187
|
+
languages_of_preference: list[str] | None = None,
|
|
188
|
+
resources: list[GetItemsResource] | None = None,
|
|
189
|
+
) -> list[Item]:
|
|
190
|
+
"""Get items information from Amazon.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
items: One or more items, using ASIN or Amazon product URL.
|
|
194
|
+
Accepts a single string (comma-separated) or a list of strings.
|
|
195
|
+
condition: Filter offers by condition type.
|
|
196
|
+
currency_of_preference: ISO 4217 currency code for prices.
|
|
197
|
+
languages_of_preference: Languages in order of preference.
|
|
198
|
+
resources: List of resources to retrieve. Defaults to all.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
List of Item objects with Amazon information.
|
|
202
|
+
|
|
203
|
+
Raises:
|
|
204
|
+
ItemsNotFoundError: If no items are found.
|
|
205
|
+
InvalidArgumentError: If parameters are invalid.
|
|
206
|
+
|
|
207
|
+
"""
|
|
208
|
+
if resources is None:
|
|
209
|
+
resources = get_all_resources(GetItemsResource)
|
|
210
|
+
|
|
211
|
+
item_ids = get_items_ids(items)
|
|
212
|
+
|
|
213
|
+
request_body = {
|
|
214
|
+
"partnerTag": self.tag,
|
|
215
|
+
"itemIds": item_ids,
|
|
216
|
+
"resources": [r.value for r in resources],
|
|
217
|
+
}
|
|
218
|
+
if condition is not None:
|
|
219
|
+
request_body["condition"] = condition.value
|
|
220
|
+
if currency_of_preference is not None:
|
|
221
|
+
request_body["currencyOfPreference"] = currency_of_preference
|
|
222
|
+
if languages_of_preference is not None:
|
|
223
|
+
request_body["languagesOfPreference"] = languages_of_preference
|
|
224
|
+
|
|
225
|
+
response = await self._make_request(ENDPOINT_GET_ITEMS, request_body)
|
|
226
|
+
|
|
227
|
+
items_result = response.get("itemsResult")
|
|
228
|
+
if items_result is None or items_result.get("items") is None:
|
|
229
|
+
msg = "No items have been found"
|
|
230
|
+
raise ItemsNotFoundError(msg)
|
|
231
|
+
|
|
232
|
+
return self._deserialize_items(items_result["items"])
|
|
233
|
+
|
|
234
|
+
async def search_items( # noqa: PLR0912, C901
|
|
235
|
+
self,
|
|
236
|
+
keywords: str | None = None,
|
|
237
|
+
actor: str | None = None,
|
|
238
|
+
artist: str | None = None,
|
|
239
|
+
author: str | None = None,
|
|
240
|
+
brand: str | None = None,
|
|
241
|
+
title: str | None = None,
|
|
242
|
+
browse_node_id: str | None = None,
|
|
243
|
+
search_index: str | None = None,
|
|
244
|
+
item_count: int | None = None,
|
|
245
|
+
item_page: int | None = None,
|
|
246
|
+
condition: Condition | None = None,
|
|
247
|
+
currency_of_preference: str | None = None,
|
|
248
|
+
languages_of_preference: list[str] | None = None,
|
|
249
|
+
max_price: int | None = None,
|
|
250
|
+
min_price: int | None = None,
|
|
251
|
+
min_saving_percent: int | None = None,
|
|
252
|
+
min_reviews_rating: int | None = None,
|
|
253
|
+
sort_by: SortBy | None = None,
|
|
254
|
+
resources: list[SearchItemsResource] | None = None,
|
|
255
|
+
) -> SearchResult:
|
|
256
|
+
"""Search for items on Amazon based on a search query.
|
|
257
|
+
|
|
258
|
+
At least one of the following parameters should be specified: keywords,
|
|
259
|
+
actor, artist, author, brand, title, browse_node_id or search_index.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
keywords: A word or phrase that describes an item.
|
|
263
|
+
actor: Actor name associated with the item.
|
|
264
|
+
artist: Artist name associated with the item.
|
|
265
|
+
author: Author name associated with the item.
|
|
266
|
+
brand: Brand name associated with the item.
|
|
267
|
+
title: Title associated with the item.
|
|
268
|
+
browse_node_id: A unique ID for a product category.
|
|
269
|
+
search_index: Product category to search. Defaults to All.
|
|
270
|
+
item_count: Number of items returned (1-10). Defaults to 10.
|
|
271
|
+
item_page: Page of items to return (1-10). Defaults to 1.
|
|
272
|
+
condition: Filter offers by condition type.
|
|
273
|
+
currency_of_preference: ISO 4217 currency code for prices.
|
|
274
|
+
languages_of_preference: Languages in order of preference.
|
|
275
|
+
max_price: Max price in lowest currency denomination.
|
|
276
|
+
min_price: Min price in lowest currency denomination.
|
|
277
|
+
min_saving_percent: Min savings percentage (1-99).
|
|
278
|
+
min_reviews_rating: Min review rating (1-5).
|
|
279
|
+
sort_by: Sort method for results.
|
|
280
|
+
resources: List of resources to retrieve. Defaults to all.
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
SearchResult containing the list of items.
|
|
284
|
+
|
|
285
|
+
Raises:
|
|
286
|
+
ItemsNotFoundError: If no items are found.
|
|
287
|
+
|
|
288
|
+
"""
|
|
289
|
+
if resources is None:
|
|
290
|
+
resources = get_all_resources(SearchItemsResource)
|
|
291
|
+
|
|
292
|
+
request_body: dict[str, Any] = {
|
|
293
|
+
"partnerTag": self.tag,
|
|
294
|
+
"resources": [r.value for r in resources],
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
# Add optional parameters
|
|
298
|
+
if keywords is not None:
|
|
299
|
+
request_body["keywords"] = keywords
|
|
300
|
+
if actor is not None:
|
|
301
|
+
request_body["actor"] = actor
|
|
302
|
+
if artist is not None:
|
|
303
|
+
request_body["artist"] = artist
|
|
304
|
+
if author is not None:
|
|
305
|
+
request_body["author"] = author
|
|
306
|
+
if brand is not None:
|
|
307
|
+
request_body["brand"] = brand
|
|
308
|
+
if title is not None:
|
|
309
|
+
request_body["title"] = title
|
|
310
|
+
if browse_node_id is not None:
|
|
311
|
+
request_body["browseNodeId"] = browse_node_id
|
|
312
|
+
if search_index is not None:
|
|
313
|
+
request_body["searchIndex"] = search_index
|
|
314
|
+
if item_count is not None:
|
|
315
|
+
request_body["itemCount"] = item_count
|
|
316
|
+
if item_page is not None:
|
|
317
|
+
request_body["itemPage"] = item_page
|
|
318
|
+
if condition is not None:
|
|
319
|
+
request_body["condition"] = condition.value
|
|
320
|
+
if currency_of_preference is not None:
|
|
321
|
+
request_body["currencyOfPreference"] = currency_of_preference
|
|
322
|
+
if languages_of_preference is not None:
|
|
323
|
+
request_body["languagesOfPreference"] = languages_of_preference
|
|
324
|
+
if max_price is not None:
|
|
325
|
+
request_body["maxPrice"] = max_price
|
|
326
|
+
if min_price is not None:
|
|
327
|
+
request_body["minPrice"] = min_price
|
|
328
|
+
if min_saving_percent is not None:
|
|
329
|
+
request_body["minSavingPercent"] = min_saving_percent
|
|
330
|
+
if min_reviews_rating is not None:
|
|
331
|
+
request_body["minReviewsRating"] = min_reviews_rating
|
|
332
|
+
if sort_by is not None:
|
|
333
|
+
request_body["sortBy"] = sort_by.value
|
|
334
|
+
|
|
335
|
+
response = await self._make_request(ENDPOINT_SEARCH_ITEMS, request_body)
|
|
336
|
+
|
|
337
|
+
search_result = response.get("searchResult")
|
|
338
|
+
if search_result is None:
|
|
339
|
+
msg = "No items have been found"
|
|
340
|
+
raise ItemsNotFoundError(msg)
|
|
341
|
+
|
|
342
|
+
return self._deserialize_search_result(search_result)
|
|
343
|
+
|
|
344
|
+
async def get_variations(
|
|
345
|
+
self,
|
|
346
|
+
asin: str,
|
|
347
|
+
variation_count: int | None = None,
|
|
348
|
+
variation_page: int | None = None,
|
|
349
|
+
condition: Condition | None = None,
|
|
350
|
+
currency_of_preference: str | None = None,
|
|
351
|
+
languages_of_preference: list[str] | None = None,
|
|
352
|
+
resources: list[GetVariationsResource] | None = None,
|
|
353
|
+
) -> VariationsResult:
|
|
354
|
+
"""Return variations of a product (different sizes, colors, etc.).
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
asin: The ASIN or Amazon product URL of the product.
|
|
358
|
+
variation_count: Number of variations to return (1-10). Defaults to 10.
|
|
359
|
+
variation_page: Page of variations to return (1-10). Defaults to 1.
|
|
360
|
+
condition: Filter offers by condition type.
|
|
361
|
+
currency_of_preference: ISO 4217 currency code for prices.
|
|
362
|
+
languages_of_preference: Languages in order of preference.
|
|
363
|
+
resources: List of resources to retrieve. Defaults to all.
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
VariationsResult containing the list of variations.
|
|
367
|
+
|
|
368
|
+
Raises:
|
|
369
|
+
ItemsNotFoundError: If no variations are found.
|
|
370
|
+
|
|
371
|
+
"""
|
|
372
|
+
if resources is None:
|
|
373
|
+
resources = get_all_resources(GetVariationsResource)
|
|
374
|
+
|
|
375
|
+
asin = get_asin(asin)
|
|
376
|
+
|
|
377
|
+
request_body: dict[str, Any] = {
|
|
378
|
+
"partnerTag": self.tag,
|
|
379
|
+
"asin": asin,
|
|
380
|
+
"resources": [r.value for r in resources],
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if variation_count is not None:
|
|
384
|
+
request_body["variationCount"] = variation_count
|
|
385
|
+
if variation_page is not None:
|
|
386
|
+
request_body["variationPage"] = variation_page
|
|
387
|
+
if condition is not None:
|
|
388
|
+
request_body["condition"] = condition.value
|
|
389
|
+
if currency_of_preference is not None:
|
|
390
|
+
request_body["currencyOfPreference"] = currency_of_preference
|
|
391
|
+
if languages_of_preference is not None:
|
|
392
|
+
request_body["languagesOfPreference"] = languages_of_preference
|
|
393
|
+
|
|
394
|
+
response = await self._make_request(ENDPOINT_GET_VARIATIONS, request_body)
|
|
395
|
+
|
|
396
|
+
variations_result = response.get("variationsResult")
|
|
397
|
+
if variations_result is None:
|
|
398
|
+
msg = "No variations have been found"
|
|
399
|
+
raise ItemsNotFoundError(msg)
|
|
400
|
+
|
|
401
|
+
return self._deserialize_variations_result(variations_result)
|
|
402
|
+
|
|
403
|
+
async def get_browse_nodes(
|
|
404
|
+
self,
|
|
405
|
+
browse_node_ids: list[str],
|
|
406
|
+
languages_of_preference: list[str] | None = None,
|
|
407
|
+
resources: list[GetBrowseNodesResource] | None = None,
|
|
408
|
+
) -> list[BrowseNode]:
|
|
409
|
+
"""Return browse node information including name, children, and ancestors.
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
browse_node_ids: List of browse node IDs.
|
|
413
|
+
languages_of_preference: Languages in order of preference.
|
|
414
|
+
resources: List of resources to retrieve. Defaults to all.
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
List of BrowseNode objects.
|
|
418
|
+
|
|
419
|
+
Raises:
|
|
420
|
+
ItemsNotFoundError: If no browse nodes are found.
|
|
421
|
+
|
|
422
|
+
"""
|
|
423
|
+
if resources is None:
|
|
424
|
+
resources = get_all_resources(GetBrowseNodesResource)
|
|
425
|
+
|
|
426
|
+
request_body: dict[str, Any] = {
|
|
427
|
+
"partnerTag": self.tag,
|
|
428
|
+
"browseNodeIds": browse_node_ids,
|
|
429
|
+
"resources": [r.value for r in resources],
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if languages_of_preference is not None:
|
|
433
|
+
request_body["languagesOfPreference"] = languages_of_preference
|
|
434
|
+
|
|
435
|
+
response = await self._make_request(ENDPOINT_GET_BROWSE_NODES, request_body)
|
|
436
|
+
|
|
437
|
+
browse_nodes_result = response.get("browseNodesResult")
|
|
438
|
+
if (
|
|
439
|
+
browse_nodes_result is None
|
|
440
|
+
or browse_nodes_result.get("browseNodes") is None
|
|
441
|
+
):
|
|
442
|
+
msg = "No browse nodes have been found"
|
|
443
|
+
raise ItemsNotFoundError(msg)
|
|
444
|
+
|
|
445
|
+
return self._deserialize_browse_nodes(browse_nodes_result["browseNodes"])
|
|
446
|
+
|
|
447
|
+
async def _throttle(self) -> None:
|
|
448
|
+
"""Wait for the throttling interval to elapse since the last API call.
|
|
449
|
+
|
|
450
|
+
Uses asyncio.Lock to prevent race conditions when multiple coroutines
|
|
451
|
+
attempt to make concurrent requests.
|
|
452
|
+
"""
|
|
453
|
+
# Lazy initialization of the lock (ensures event loop is active)
|
|
454
|
+
if self._throttle_lock is None:
|
|
455
|
+
self._throttle_lock = asyncio.Lock()
|
|
456
|
+
|
|
457
|
+
async with self._throttle_lock:
|
|
458
|
+
wait_time = self.throttling - (time.time() - self._last_query_time)
|
|
459
|
+
if wait_time > 0:
|
|
460
|
+
await asyncio.sleep(wait_time)
|
|
461
|
+
self._last_query_time = time.time()
|
|
462
|
+
|
|
463
|
+
async def _make_request(
|
|
464
|
+
self,
|
|
465
|
+
endpoint: str,
|
|
466
|
+
body: dict[str, Any],
|
|
467
|
+
) -> dict[str, Any]:
|
|
468
|
+
"""Make an API request with authentication and throttling.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
endpoint: API endpoint path.
|
|
472
|
+
body: Request body.
|
|
473
|
+
|
|
474
|
+
Returns:
|
|
475
|
+
Parsed JSON response.
|
|
476
|
+
|
|
477
|
+
Raises:
|
|
478
|
+
Various exceptions based on API errors.
|
|
479
|
+
|
|
480
|
+
"""
|
|
481
|
+
await self._throttle()
|
|
482
|
+
|
|
483
|
+
# Get auth token
|
|
484
|
+
token = await self._token_manager.get_token()
|
|
485
|
+
|
|
486
|
+
headers = {
|
|
487
|
+
"Authorization": self._build_authorization_header(token),
|
|
488
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
489
|
+
"x-marketplace": self.marketplace,
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
# Use persistent client if available, otherwise create a new one
|
|
493
|
+
if self._http_client is not None:
|
|
494
|
+
response = await self._http_client.post(endpoint, headers, body)
|
|
495
|
+
else:
|
|
496
|
+
async with AsyncHttpClient(host=API_HOST) as client:
|
|
497
|
+
response = await client.post(endpoint, headers, body)
|
|
498
|
+
|
|
499
|
+
# Handle errors
|
|
500
|
+
if response.status_code != 200: # noqa: PLR2004
|
|
501
|
+
self._handle_error_response(response.status_code, response.text)
|
|
502
|
+
|
|
503
|
+
return response.json()
|
|
504
|
+
|
|
505
|
+
def _build_authorization_header(self, token: str) -> str:
|
|
506
|
+
"""Build the version-appropriate Authorization header."""
|
|
507
|
+
if self._version.startswith("3."):
|
|
508
|
+
return f"Bearer {token}"
|
|
509
|
+
return f"Bearer {token}, Version {self._version}"
|
|
510
|
+
|
|
511
|
+
def _handle_error_response(self, status_code: int, body: str) -> None:
|
|
512
|
+
"""Handle API error responses and raise appropriate exceptions.
|
|
513
|
+
|
|
514
|
+
Args:
|
|
515
|
+
status_code: HTTP status code.
|
|
516
|
+
body: Response body text.
|
|
517
|
+
|
|
518
|
+
Raises:
|
|
519
|
+
ItemsNotFoundError: For 404 errors.
|
|
520
|
+
TooManyRequestsError: For 429 errors.
|
|
521
|
+
InvalidArgumentError: For validation errors.
|
|
522
|
+
AssociateValidationError: For invalid associate credentials.
|
|
523
|
+
RequestError: For other errors.
|
|
524
|
+
|
|
525
|
+
"""
|
|
526
|
+
handle_api_error(status_code, body)
|
|
527
|
+
|
|
528
|
+
def _deserialize_items(self, items_data: list[dict[str, Any]]) -> list[Item]:
|
|
529
|
+
"""Deserialize item data from API response to Item models."""
|
|
530
|
+
return [Item.model_validate(item) for item in items_data]
|
|
531
|
+
|
|
532
|
+
def _deserialize_search_result(
|
|
533
|
+
self,
|
|
534
|
+
search_result_data: dict[str, Any],
|
|
535
|
+
) -> SearchResult:
|
|
536
|
+
"""Deserialize search result data from API response to SearchResult model."""
|
|
537
|
+
return SearchResult.model_validate(search_result_data)
|
|
538
|
+
|
|
539
|
+
def _deserialize_variations_result(
|
|
540
|
+
self,
|
|
541
|
+
variations_result_data: dict[str, Any],
|
|
542
|
+
) -> VariationsResult:
|
|
543
|
+
"""Deserialize variations data from API response to VariationsResult model."""
|
|
544
|
+
return VariationsResult.model_validate(variations_result_data)
|
|
545
|
+
|
|
546
|
+
def _deserialize_browse_nodes(
|
|
547
|
+
self,
|
|
548
|
+
browse_nodes_data: list[dict[str, Any]],
|
|
549
|
+
) -> list[BrowseNode]:
|
|
550
|
+
"""Deserialize browse nodes data from API response to BrowseNode models."""
|
|
551
|
+
return [BrowseNode.model_validate(node) for node in browse_nodes_data]
|