Skip to content

Commit 0aaa213

Browse files
committed
refactor: Centralize API error handling, resource extraction, and marketplace validation into new core modules.
1 parent c8911ad commit 0aaa213

File tree

6 files changed

+151
-120
lines changed

6 files changed

+151
-120
lines changed

amazon_creatorsapi/aio/api.py

Lines changed: 15 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,11 @@
1313
from typing_extensions import Self
1414

1515
from amazon_creatorsapi.core.constants import DEFAULT_THROTTLING
16-
from amazon_creatorsapi.core.marketplaces import MARKETPLACES
16+
from amazon_creatorsapi.core.error_handling import handle_api_error
1717
from amazon_creatorsapi.core.parsers import get_asin, get_items_ids
18-
from amazon_creatorsapi.errors import (
19-
AssociateValidationError,
20-
InvalidArgumentError,
21-
ItemsNotFoundError,
22-
RequestError,
23-
TooManyRequestsError,
24-
)
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
2521

2622
try:
2723
from .auth import VERSION_ENDPOINTS, AsyncOAuth2TokenManager
@@ -132,21 +128,12 @@ def __init__(
132128
self._credential_secret = credential_secret
133129
self._version = version
134130
self._last_query_time = time.time() - throttling
135-
self._throttle_lock = asyncio.Lock()
131+
self._throttle_lock: asyncio.Lock | None = None
136132
self.tag = tag
137133
self.throttling = float(throttling)
138134

139135
# Determine marketplace from country or direct value
140-
if marketplace:
141-
self.marketplace = marketplace
142-
elif country:
143-
if country not in MARKETPLACES:
144-
msg = f"Country code '{country}' is not valid"
145-
raise InvalidArgumentError(msg)
146-
self.marketplace = MARKETPLACES[country]
147-
else:
148-
msg = "Either 'country' or 'marketplace' must be provided"
149-
raise InvalidArgumentError(msg)
136+
self.marketplace = validate_and_get_marketplace(country, marketplace)
150137

151138
# HTTP client and token manager (initialized lazily or via context manager)
152139
self._http_client: AsyncHttpClient | None = None
@@ -218,7 +205,7 @@ async def get_items(
218205
219206
"""
220207
if resources is None:
221-
resources = self._get_all_resources(GetItemsResource)
208+
resources = get_all_resources(GetItemsResource)
222209

223210
item_ids = get_items_ids(items)
224211

@@ -299,7 +286,7 @@ async def search_items( # noqa: PLR0912, C901
299286
300287
"""
301288
if resources is None:
302-
resources = self._get_all_resources(SearchItemsResource)
289+
resources = get_all_resources(SearchItemsResource)
303290

304291
request_body: dict[str, Any] = {
305292
"partnerTag": self.tag,
@@ -382,7 +369,7 @@ async def get_variations(
382369
383370
"""
384371
if resources is None:
385-
resources = self._get_all_resources(GetVariationsResource)
372+
resources = get_all_resources(GetVariationsResource)
386373

387374
asin = get_asin(asin)
388375

@@ -433,7 +420,7 @@ async def get_browse_nodes(
433420
434421
"""
435422
if resources is None:
436-
resources = self._get_all_resources(GetBrowseNodesResource)
423+
resources = get_all_resources(GetBrowseNodesResource)
437424

438425
request_body: dict[str, Any] = {
439426
"partnerTag": self.tag,
@@ -462,6 +449,10 @@ async def _throttle(self) -> None:
462449
Uses asyncio.Lock to prevent race conditions when multiple coroutines
463450
attempt to make concurrent requests.
464451
"""
452+
# Lazy initialization of the lock (ensures event loop is active)
453+
if self._throttle_lock is None:
454+
self._throttle_lock = asyncio.Lock()
455+
465456
async with self._throttle_lock:
466457
wait_time = self.throttling - (time.time() - self._last_query_time)
467458
if wait_time > 0:
@@ -525,45 +516,7 @@ def _handle_error_response(self, status_code: int, body: str) -> None:
525516
RequestError: For other errors.
526517
527518
"""
528-
http_not_found = 404
529-
http_too_many_requests = 429
530-
531-
if status_code == http_not_found:
532-
msg = "No items found for the request"
533-
raise ItemsNotFoundError(msg)
534-
535-
if status_code == http_too_many_requests:
536-
msg = "Rate limit exceeded, try increasing throttling"
537-
raise TooManyRequestsError(msg)
538-
539-
if "InvalidParameterValue" in body:
540-
msg = "Invalid parameter value provided in the request"
541-
raise InvalidArgumentError(msg)
542-
543-
if "InvalidPartnerTag" in body:
544-
msg = "The partner tag is invalid or not present"
545-
raise InvalidArgumentError(msg)
546-
547-
if "InvalidAssociate" in body:
548-
msg = "Credentials are not valid for the selected marketplace"
549-
raise AssociateValidationError(msg)
550-
551-
# Generic error
552-
body_info = f" - {body[:200]}" if body else ""
553-
msg = f"Request failed with status {status_code}{body_info}"
554-
raise RequestError(msg)
555-
556-
def _get_all_resources(self, resource_class: type[ResourceT]) -> list[ResourceT]:
557-
"""Extract all resource values from a resource enum class.
558-
559-
Args:
560-
resource_class: Enum class containing resource definitions.
561-
562-
Returns:
563-
List of all enum members from the resource class.
564-
565-
"""
566-
return list(resource_class)
519+
handle_api_error(status_code, body)
567520

568521
def _deserialize_items(self, items_data: list[dict[str, Any]]) -> list[Item]:
569522
"""Deserialize item data from API response to Item models."""

amazon_creatorsapi/api.py

Lines changed: 15 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,14 @@
66
from __future__ import annotations
77

88
import time
9-
from typing import TYPE_CHECKING, Any, NoReturn
9+
from typing import TYPE_CHECKING, NoReturn
1010

1111
from amazon_creatorsapi.core.constants import DEFAULT_THROTTLING
12-
from amazon_creatorsapi.core.marketplaces import MARKETPLACES
12+
from amazon_creatorsapi.core.error_handling import handle_api_error
1313
from amazon_creatorsapi.core.parsers import get_asin, get_items_ids
14-
from amazon_creatorsapi.errors import (
15-
AssociateValidationError,
16-
InvalidArgumentError,
17-
ItemsNotFoundError,
18-
RequestError,
19-
TooManyRequestsError,
20-
)
14+
from amazon_creatorsapi.core.resources import get_all_resources
15+
from amazon_creatorsapi.core.validation import validate_and_get_marketplace
16+
from amazon_creatorsapi.errors import ItemsNotFoundError
2117
from creatorsapi_python_sdk.api.default_api import DefaultApi
2218
from creatorsapi_python_sdk.api_client import ApiClient
2319
from creatorsapi_python_sdk.exceptions import ApiException
@@ -41,8 +37,6 @@
4137
from creatorsapi_python_sdk.models.search_items_resource import SearchItemsResource
4238

4339
if TYPE_CHECKING:
44-
from enum import Enum
45-
4640
from amazon_creatorsapi.core.marketplaces import CountryCode
4741
from creatorsapi_python_sdk.models.browse_node import BrowseNode
4842
from creatorsapi_python_sdk.models.condition import Condition
@@ -98,16 +92,7 @@ def __init__(
9892
self.throttling = float(throttling)
9993

10094
# Determine marketplace from country or direct value
101-
if marketplace:
102-
self.marketplace = marketplace
103-
elif country:
104-
if country not in MARKETPLACES:
105-
msg = f"Country code '{country}' is not valid"
106-
raise InvalidArgumentError(msg)
107-
self.marketplace = MARKETPLACES[country]
108-
else:
109-
msg = "Either 'country' or 'marketplace' must be provided"
110-
raise InvalidArgumentError(msg)
95+
self.marketplace = validate_and_get_marketplace(country, marketplace)
11196

11297
self._api_client = ApiClient(
11398
credential_id=credential_id,
@@ -143,7 +128,7 @@ def get_items(
143128
144129
"""
145130
if resources is None:
146-
resources = self._get_all_resources(GetItemsResource)
131+
resources = get_all_resources(GetItemsResource)
147132

148133
item_ids = get_items_ids(items)
149134

@@ -228,7 +213,7 @@ def search_items(
228213
229214
"""
230215
if resources is None:
231-
resources = self._get_all_resources(SearchItemsResource)
216+
resources = get_all_resources(SearchItemsResource)
232217

233218
request = SearchItemsRequestContent(
234219
partnerTag=self.tag,
@@ -298,7 +283,7 @@ def get_variations(
298283
299284
"""
300285
if resources is None:
301-
resources = self._get_all_resources(GetVariationsResource)
286+
resources = get_all_resources(GetVariationsResource)
302287

303288
asin = get_asin(asin)
304289

@@ -350,7 +335,7 @@ def get_browse_nodes(
350335
351336
"""
352337
if resources is None:
353-
resources = self._get_all_resources(GetBrowseNodesResource)
338+
resources = get_all_resources(GetBrowseNodesResource)
354339

355340
request = GetBrowseNodesRequestContent(
356341
partnerTag=self.tag,
@@ -385,39 +370,11 @@ def _throttle(self) -> None:
385370
time.sleep(wait_time)
386371
self._last_query_time = time.time()
387372

388-
def _get_all_resources(self, resource_class: type[Enum]) -> list[Any]:
389-
"""Extract all resource values from a resource enum class."""
390-
return [member.value for member in resource_class]
391-
392373
def _handle_api_exception(self, error: ApiException) -> NoReturn:
393374
"""Handle API exceptions and raise appropriate custom exceptions."""
394-
http_not_found = 404
395-
http_too_many_requests = 429
396-
397-
if error.status == http_not_found:
398-
msg = "No items found for the request"
399-
raise ItemsNotFoundError(msg) from error
400-
401-
if error.status == http_too_many_requests:
402-
msg = "Rate limit exceeded, try increasing throttling"
403-
raise TooManyRequestsError(msg) from error
404-
405375
error_body = str(error.body) if error.body else ""
406-
407-
if "InvalidParameterValue" in error_body:
408-
msg = "Invalid parameter value provided in the request"
409-
raise InvalidArgumentError(msg) from error
410-
411-
if "InvalidPartnerTag" in error_body:
412-
msg = "The partner tag is invalid or not present"
413-
raise InvalidArgumentError(msg) from error
414-
415-
if "InvalidAssociate" in error_body:
416-
msg = "Credentials are not valid for the selected marketplace"
417-
raise AssociateValidationError(msg) from error
418-
419-
# Include error body in message for debugging
420-
reason = error.reason or "Unknown error"
421-
body_info = f" - {error_body[:200]}" if error_body else ""
422-
msg = f"Request failed: {reason}{body_info}"
423-
raise RequestError(msg) from error
376+
try:
377+
handle_api_error(error.status, error_body)
378+
except Exception as exc:
379+
# Re-raise with original exception as cause for better stack traces
380+
raise exc from error
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
"""Constants for the Amazon Creators API."""
22

33
DEFAULT_THROTTLING = 1
4+
5+
# HTTP status codes
6+
HTTP_NOT_FOUND = 404
7+
HTTP_TOO_MANY_REQUESTS = 429
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""Error handling utilities for the Amazon Creators API."""
2+
3+
from __future__ import annotations
4+
5+
from typing import NoReturn
6+
7+
from amazon_creatorsapi.core.constants import HTTP_NOT_FOUND, HTTP_TOO_MANY_REQUESTS
8+
from amazon_creatorsapi.errors import (
9+
AssociateValidationError,
10+
InvalidArgumentError,
11+
ItemsNotFoundError,
12+
RequestError,
13+
TooManyRequestsError,
14+
)
15+
16+
17+
def handle_api_error(status_code: int, body: str) -> NoReturn:
18+
"""Handle API error responses and raise appropriate exceptions.
19+
20+
Args:
21+
status_code: HTTP status code.
22+
body: Response body text.
23+
24+
Raises:
25+
ItemsNotFoundError: For 404 errors.
26+
TooManyRequestsError: For 429 errors.
27+
InvalidArgumentError: For validation errors.
28+
AssociateValidationError: For invalid associate credentials.
29+
RequestError: For other errors.
30+
31+
"""
32+
if status_code == HTTP_NOT_FOUND:
33+
msg = "No items found for the request"
34+
raise ItemsNotFoundError(msg)
35+
36+
if status_code == HTTP_TOO_MANY_REQUESTS:
37+
msg = "Rate limit exceeded, try increasing throttling"
38+
raise TooManyRequestsError(msg)
39+
40+
if "InvalidParameterValue" in body:
41+
msg = "Invalid parameter value provided in the request"
42+
raise InvalidArgumentError(msg)
43+
44+
if "InvalidPartnerTag" in body:
45+
msg = "The partner tag is invalid or not present"
46+
raise InvalidArgumentError(msg)
47+
48+
if "InvalidAssociate" in body:
49+
msg = "Credentials are not valid for the selected marketplace"
50+
raise AssociateValidationError(msg)
51+
52+
# Generic error
53+
body_info = f" - {body[:200]}" if body else ""
54+
msg = f"Request failed with status {status_code}{body_info}"
55+
raise RequestError(msg)
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
"""Resource utilities for the Amazon Creators API."""
2+
3+
from __future__ import annotations
4+
5+
from enum import Enum
6+
from typing import TypeVar
7+
8+
# TypeVar for generic resource handling
9+
ResourceT = TypeVar("ResourceT", bound=Enum)
10+
11+
12+
def get_all_resources(resource_class: type[ResourceT]) -> list[ResourceT]:
13+
"""Extract all resource values from a resource enum class.
14+
15+
Args:
16+
resource_class: Enum class containing resource definitions.
17+
18+
Returns:
19+
List of all enum members from the resource class.
20+
21+
"""
22+
return list(resource_class)
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""Validation utilities for the Amazon Creators API."""
2+
3+
from __future__ import annotations
4+
5+
from typing import TYPE_CHECKING
6+
7+
from amazon_creatorsapi.core.marketplaces import MARKETPLACES
8+
from amazon_creatorsapi.errors import InvalidArgumentError
9+
10+
if TYPE_CHECKING:
11+
from amazon_creatorsapi.core.marketplaces import CountryCode
12+
13+
14+
def validate_and_get_marketplace(
15+
country: CountryCode | None,
16+
marketplace: str | None,
17+
) -> str:
18+
"""Validate and determine marketplace from country or direct value.
19+
20+
Args:
21+
country: Country code (e.g., "ES", "US").
22+
marketplace: Marketplace URL (e.g., "www.amazon.es").
23+
24+
Returns:
25+
The marketplace URL.
26+
27+
Raises:
28+
InvalidArgumentError: If neither country nor marketplace is provided,
29+
or if the country code is invalid.
30+
31+
"""
32+
if marketplace:
33+
return marketplace
34+
if country:
35+
if country not in MARKETPLACES:
36+
msg = f"Country code '{country}' is not valid"
37+
raise InvalidArgumentError(msg)
38+
return MARKETPLACES[country]
39+
msg = "Either 'country' or 'marketplace' must be provided"
40+
raise InvalidArgumentError(msg)

0 commit comments

Comments
 (0)