Skip to content
Open
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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [6.4.0] - 2026-08-06

### Added

- `timeout` parameter in `AmazonCreatorsApi` and `AsyncAmazonCreatorsApi` to set the request timeout in seconds, or `None` to wait indefinitely

### Changed

- `AmazonCreatorsApi` requests now time out after 30 seconds instead of waiting indefinitely, matching the timeout already used by `AsyncAmazonCreatorsApi`. Pass `timeout=None` to restore the previous behavior

## [6.3.0] - 2026-05-15

### Added
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,15 @@ amazon = AmazonCreatorsApi(ID, SECRET, VERSION, TAG, COUNTRY, throttling=4) # M
amazon = AmazonCreatorsApi(ID, SECRET, VERSION, TAG, COUNTRY, throttling=0) # No wait time between requests
```

### Timeout

Timeout value represents the number of seconds to wait for a response before failing, being the default value 30 seconds. Use `None` to wait indefinitely.

```python
amazon = AmazonCreatorsApi(ID, SECRET, VERSION, TAG, COUNTRY, timeout=10) # Fails after 10 seconds
amazon = AmazonCreatorsApi(ID, SECRET, VERSION, TAG, COUNTRY, timeout=0.5) # Fails after half a second
```

### Async Support

For async/await applications, use the async version of the API with `httpx`:
Expand Down
10 changes: 7 additions & 3 deletions amazon_creatorsapi/aio/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from typing_extensions import Self

from amazon_creatorsapi.core.constants import DEFAULT_THROTTLING
from amazon_creatorsapi.core.constants import DEFAULT_THROTTLING, DEFAULT_TIMEOUT
from amazon_creatorsapi.core.error_handling import handle_api_error
from amazon_creatorsapi.core.parsers import get_asin, get_items_ids
from amazon_creatorsapi.core.resources import get_all_resources
Expand Down Expand Up @@ -104,6 +104,8 @@ class AsyncAmazonCreatorsApi:
country: Country code (e.g., "ES", "US"). Used to determine marketplace.
marketplace: Marketplace URL (e.g., "www.amazon.es"). Overrides country.
throttling: Wait time in seconds between API calls. Defaults to 1 second.
timeout: Request timeout in seconds, or None to wait indefinitely.
Defaults to 30 seconds.

Raises:
InvalidArgumentError: If neither country nor marketplace is provided.
Expand All @@ -121,6 +123,7 @@ def __init__(
country: CountryCode | None = None,
marketplace: str | None = None,
throttling: float = DEFAULT_THROTTLING,
timeout: float | None = DEFAULT_TIMEOUT,
) -> None:
"""Initialize the async Amazon Creators API client."""
# Validate version early to fail fast (before token manager initialization)
Expand All @@ -133,6 +136,7 @@ def __init__(
self._throttle_lock: asyncio.Lock | None = None
self.tag = tag
self.throttling = float(throttling)
self.timeout = timeout

# Determine marketplace from country or direct value
self.marketplace = validate_and_get_marketplace(country, marketplace)
Expand Down Expand Up @@ -163,7 +167,7 @@ def _validate_version(self, version: str) -> None:

async def __aenter__(self) -> Self:
"""Enter async context manager, creating a persistent HTTP client."""
self._http_client = AsyncHttpClient(host=API_HOST)
self._http_client = AsyncHttpClient(host=API_HOST, timeout=self.timeout)
await self._http_client.__aenter__()
self._owns_client = True
return self
Expand Down Expand Up @@ -498,7 +502,7 @@ async def _make_request(
if self._http_client is not None:
response = await self._http_client.post(endpoint, headers, body)
else:
async with AsyncHttpClient(host=API_HOST) as client:
async with AsyncHttpClient(host=API_HOST, timeout=self.timeout) as client:
response = await client.post(endpoint, headers, body)

# Handle errors
Expand Down
8 changes: 5 additions & 3 deletions amazon_creatorsapi/aio/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

from typing_extensions import Self

from amazon_creatorsapi.core.constants import DEFAULT_TIMEOUT

if TYPE_CHECKING:
from types import TracebackType

Expand All @@ -26,7 +28,6 @@


DEFAULT_HOST = "https://creatorsapi.amazon"
DEFAULT_TIMEOUT = 30.0
VERSION = version("python-amazon-paapi")
USER_AGENT = f"python-amazon-paapi/{VERSION} (async)"

Expand Down Expand Up @@ -64,14 +65,15 @@ class AsyncHttpClient:

Args:
host: Base URL for API requests. Defaults to Amazon Creators API.
timeout: Request timeout in seconds. Defaults to 30.
timeout: Request timeout in seconds, or None to wait indefinitely.
Defaults to 30.

"""

def __init__(
self,
host: str = DEFAULT_HOST,
timeout: float = DEFAULT_TIMEOUT,
timeout: float | None = DEFAULT_TIMEOUT,
) -> None:
"""Initialize the async HTTP client."""
self._host = host
Expand Down
10 changes: 9 additions & 1 deletion amazon_creatorsapi/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import time
from typing import TYPE_CHECKING, NoReturn

from amazon_creatorsapi.core.constants import DEFAULT_THROTTLING
from amazon_creatorsapi.core.constants import DEFAULT_THROTTLING, DEFAULT_TIMEOUT
from amazon_creatorsapi.core.error_handling import handle_api_error
from amazon_creatorsapi.core.parsers import get_asin, get_items_ids
from amazon_creatorsapi.core.resources import get_all_resources
Expand Down Expand Up @@ -58,6 +58,8 @@ class AmazonCreatorsApi:
country: Country code (e.g., "ES", "US"). Used to determine marketplace.
marketplace: Marketplace URL (e.g., "www.amazon.es"). Overrides country.
throttling: Wait time in seconds between API calls. Defaults to 1 second.
timeout: Request timeout in seconds, or None to wait indefinitely.
Defaults to 30 seconds.

Raises:
InvalidArgumentError: If neither country nor marketplace is provided.
Expand All @@ -83,6 +85,7 @@ def __init__(
country: CountryCode | None = None,
marketplace: str | None = None,
throttling: float = DEFAULT_THROTTLING,
timeout: float | None = DEFAULT_TIMEOUT,
) -> None:
"""Initialize the Amazon Creators API client."""
self._credential_id = credential_id
Expand All @@ -91,6 +94,7 @@ def __init__(
self._last_query_time = time.time() - throttling
self.tag = tag
self.throttling = float(throttling)
self.timeout = timeout

# Determine marketplace from country or direct value
self.marketplace = validate_and_get_marketplace(country, marketplace)
Expand Down Expand Up @@ -148,6 +152,7 @@ def get_items(
response = self._api.get_items(
x_marketplace=self.marketplace,
get_items_request_content=request,
_request_timeout=self.timeout,
)
except ApiException as exc:
self._handle_api_exception(exc)
Expand Down Expand Up @@ -248,6 +253,7 @@ def search_items(
response = self._api.search_items(
x_marketplace=self.marketplace,
search_items_request_content=request,
_request_timeout=self.timeout,
)
except ApiException as exc:
self._handle_api_exception(exc)
Expand Down Expand Up @@ -308,6 +314,7 @@ def get_variations(
response = self._api.get_variations(
x_marketplace=self.marketplace,
get_variations_request_content=request,
_request_timeout=self.timeout,
)
except ApiException as exc:
self._handle_api_exception(exc)
Expand Down Expand Up @@ -354,6 +361,7 @@ def get_browse_nodes(
response = self._api.get_browse_nodes(
x_marketplace=self.marketplace,
get_browse_nodes_request_content=request,
_request_timeout=self.timeout,
)
except ApiException as exc:
self._handle_api_exception(exc)
Expand Down
1 change: 1 addition & 0 deletions amazon_creatorsapi/core/constants.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Constants for the Amazon Creators API."""

DEFAULT_THROTTLING = 1
DEFAULT_TIMEOUT = 30.0

# HTTP status codes
HTTP_NOT_FOUND = 404
Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
author = "Sergio Abad"

# The full version, including alpha/beta/rc tags
release = "6.3.0"
release = "6.4.0"


# -- General configuration ---------------------------------------------------
Expand Down
9 changes: 9 additions & 0 deletions docs/pages/usage-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,15 @@ api = AmazonCreatorsApi(ID, SECRET, VERSION, TAG, COUNTRY, throttling=4) # Make
api = AmazonCreatorsApi(ID, SECRET, VERSION, TAG, COUNTRY, throttling=0) # No wait time between requests
```

## Timeout

Timeout value represents the number of seconds to wait for a response before failing, being the default value 30 seconds. Use `None` to wait indefinitely.

```python
api = AmazonCreatorsApi(ID, SECRET, VERSION, TAG, COUNTRY, timeout=10) # Fails after 10 seconds
api = AmazonCreatorsApi(ID, SECRET, VERSION, TAG, COUNTRY, timeout=0.5) # Fails after half a second
```

## Async Support

For async/await applications, install with async support:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "python-amazon-paapi"
version = "6.3.0"
version = "6.4.0"
description = "Amazon Product Advertising API 5.0 wrapper for Python"
readme = "README.md"
requires-python = ">=3.9"
Expand Down
138 changes: 138 additions & 0 deletions tests/amazon_creatorsapi/aio/api_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from amazon_creatorsapi.aio import (
AsyncAmazonCreatorsApi,
)
from amazon_creatorsapi.aio.api import API_HOST
from amazon_creatorsapi.core.constants import DEFAULT_TIMEOUT
from amazon_creatorsapi.errors import (
AssociateValidationError,
InvalidArgumentError,
Expand Down Expand Up @@ -69,6 +71,33 @@ def test_with_custom_throttling(self, mock_token_manager: MagicMock) -> None:

self.assertEqual(api.throttling, 2.5)

@patch("amazon_creatorsapi.aio.api.AsyncOAuth2TokenManager")
def test_with_default_timeout(self, mock_token_manager: MagicMock) -> None:
"""Test initialization uses the default timeout value."""
api = AsyncAmazonCreatorsApi(
credential_id="test_id",
credential_secret="test_secret",
version="2.2",
tag="test-tag",
country="US",
)

self.assertEqual(api.timeout, DEFAULT_TIMEOUT)

@patch("amazon_creatorsapi.aio.api.AsyncOAuth2TokenManager")
def test_with_custom_timeout(self, mock_token_manager: MagicMock) -> None:
"""Test initialization with custom timeout value."""
api = AsyncAmazonCreatorsApi(
credential_id="test_id",
credential_secret="test_secret",
version="2.2",
tag="test-tag",
country="US",
timeout=5.0,
)

self.assertEqual(api.timeout, 5.0)

@patch("amazon_creatorsapi.aio.api.AsyncOAuth2TokenManager")
def test_accepts_lwa_version(self, mock_token_manager: MagicMock) -> None:
"""Test initialization accepts an LWA-backed 3.x version."""
Expand Down Expand Up @@ -1340,5 +1369,114 @@ async def test_request_uses_lwa_authorization_header(
self.assertEqual(headers["Authorization"], "Bearer test_token")


class TestAsyncAmazonCreatorsApiTimeout(unittest.IsolatedAsyncioTestCase):
"""Tests for the timeout given to the underlying HTTP client."""

@patch("amazon_creatorsapi.aio.api.AsyncOAuth2TokenManager")
@patch("amazon_creatorsapi.aio.api.AsyncHttpClient")
async def test_context_manager_client_uses_default_timeout(
self,
mock_http_client_class: MagicMock,
mock_token_manager: MagicMock,
) -> None:
"""Test the persistent client is created with the default timeout."""
mock_http_client_class.return_value = AsyncMock()

async with AsyncAmazonCreatorsApi(
credential_id="test_id",
credential_secret="test_secret",
version="2.2",
tag="test-tag",
country="ES",
):
pass

mock_http_client_class.assert_called_once_with(
host=API_HOST,
timeout=DEFAULT_TIMEOUT,
)

@patch("amazon_creatorsapi.aio.api.AsyncOAuth2TokenManager")
@patch("amazon_creatorsapi.aio.api.AsyncHttpClient")
async def test_context_manager_client_uses_custom_timeout(
self,
mock_http_client_class: MagicMock,
mock_token_manager: MagicMock,
) -> None:
"""Test the persistent client is created with a custom timeout."""
mock_http_client_class.return_value = AsyncMock()

async with AsyncAmazonCreatorsApi(
credential_id="test_id",
credential_secret="test_secret",
version="2.2",
tag="test-tag",
country="ES",
timeout=5.0,
):
pass

mock_http_client_class.assert_called_once_with(host=API_HOST, timeout=5.0)

@patch("amazon_creatorsapi.aio.api.AsyncOAuth2TokenManager")
@patch("amazon_creatorsapi.aio.api.AsyncHttpClient")
async def test_request_without_context_manager_uses_custom_timeout(
self,
mock_http_client_class: MagicMock,
mock_token_manager_class: MagicMock,
) -> None:
"""Test the temporary client is created with a custom timeout."""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {
"itemsResult": {"items": [{"ASIN": "B0DLFMFBJW"}]}
}

mock_client = AsyncMock()
mock_client.post.return_value = mock_response
mock_client.__aenter__.return_value = mock_client
mock_http_client_class.return_value = mock_client

mock_token_manager = AsyncMock()
mock_token_manager.get_token.return_value = "test_token"
mock_token_manager_class.return_value = mock_token_manager

api = AsyncAmazonCreatorsApi(
credential_id="test_id",
credential_secret="test_secret",
version="2.2",
tag="test-tag",
country="ES",
throttling=0,
timeout=5.0,
)

await api.get_items(["B0DLFMFBJW"])

mock_http_client_class.assert_called_once_with(host=API_HOST, timeout=5.0)

@patch("amazon_creatorsapi.aio.api.AsyncOAuth2TokenManager")
@patch("amazon_creatorsapi.aio.api.AsyncHttpClient")
async def test_client_receives_disabled_timeout(
self,
mock_http_client_class: MagicMock,
mock_token_manager: MagicMock,
) -> None:
"""Test a None timeout is passed to the HTTP client to disable it."""
mock_http_client_class.return_value = AsyncMock()

async with AsyncAmazonCreatorsApi(
credential_id="test_id",
credential_secret="test_secret",
version="2.2",
tag="test-tag",
country="ES",
timeout=None,
):
pass

mock_http_client_class.assert_called_once_with(host=API_HOST, timeout=None)


if __name__ == "__main__":
unittest.main()
5 changes: 5 additions & 0 deletions tests/amazon_creatorsapi/aio/client_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ async def test_init_custom(self) -> None:
self.assertEqual(client._host, host)
self.assertEqual(client._timeout, timeout)

async def test_init_timeout_disabled(self) -> None:
"""Test the timeout can be disabled with None."""
client = AsyncHttpClient(timeout=None)
self.assertIsNone(client._timeout)

@patch("amazon_creatorsapi.aio.client.httpx.AsyncClient")
async def test_context_manager(self, mock_client_cls: MagicMock) -> None:
"""Test context manager creates and closes client."""
Expand Down
Loading
Loading