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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,26 @@ 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-09-03

### Added

- `list_feeds`, `get_feed`, `list_reports` and `get_report` methods in `amazon_creatorsapi` and `amazon_creatorsapi.aio`
- `Feed`, `FeedType`, `ReportMetadata` and `ReportType` in `amazon_creatorsapi.models`
- `FeedType` and `ReportType` models in the bundled SDK
- `feedType` field in `Feed` and `GetFeedRequestContent` models
- `reportType` field in `ReportMetadata` and `GetReportRequestContent` models

### Changed

- Bumped `creatorsapi-python-sdk` from `1.2.0` to `1.3.0`
- `partnerTag` is now a required field in `SearchItemsRequestContent`
- Pinned `ruff` to the version run by pre-commit, so `make lint` matches CI

### Fixed

- Search integration tests no longer assume Amazon always returns a full page of items

## [6.3.0] - 2026-05-15

### Added
Expand Down
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ A Python wrapper for Amazon's product APIs. This package supports both the legac
- �🔍 **Product search** by keywords, categories, or browse nodes
- 📦 **Product details** via ASIN or Amazon URL
- 🔄 **Item variations** support (size, color, etc.)
- 📊 **Feeds and reports** listing and download URLs
- 💰 **OffersV2 support** for enhanced pricing and offer details
- 🌍 **20+ countries** supported
- 🛡️ **Built-in throttling** to avoid API rate limits
Expand Down Expand Up @@ -91,6 +92,29 @@ for node in nodes:
print(node.display_name)
```

### Feeds and Reports

Feeds and reports are listed per marketplace, and downloaded through the
temporary URL returned by the API:

```python
from amazon_creatorsapi.models import FeedType, ReportType

for feed in api.list_feeds():
print(feed.feed_name, feed.feed_type, feed.size)

url = api.get_feed("product-feed", feed_type=FeedType.PRODUCT_FEEDS)

for report in api.list_reports():
print(report.filename, report.report_type, report.last_modified)

url = api.get_report("earnings.csv", report_type=ReportType.CREATOR_CONNECTIONS)
```

The type is only needed to disambiguate a name available in more than one
program, such as a report present in both Creator Central and Creator
Connections.

### Get the ASIN from URL

```python
Expand Down Expand Up @@ -145,6 +169,8 @@ async with AsyncAmazonCreatorsApi(
results = await api.search_items(keywords="laptop")
variations = await api.get_variations("B01N5IB20Q")
nodes = await api.get_browse_nodes(["667049031"])
feeds = await api.list_feeds()
reports = await api.list_reports()

# Or use without context manager (creates new connection per request)
api = AsyncAmazonCreatorsApi(ID, SECRET, VERSION, TAG, COUNTRY)
Expand Down
113 changes: 111 additions & 2 deletions amazon_creatorsapi/aio/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,20 @@
from amazon_creatorsapi.core.marketplaces import CountryCode
from creatorsapi_python_sdk.models.condition import Condition
from creatorsapi_python_sdk.models.delivery_flag import DeliveryFlag
from creatorsapi_python_sdk.models.feed_type import FeedType
from creatorsapi_python_sdk.models.report_type import ReportType
from creatorsapi_python_sdk.models.sort_by import SortBy

from creatorsapi_python_sdk.models.browse_node import BrowseNode
from creatorsapi_python_sdk.models.feed import Feed
from creatorsapi_python_sdk.models.get_feed_response_content import (
GetFeedResponseContent,
)
from creatorsapi_python_sdk.models.get_report_response_content import (
GetReportResponseContent,
)
from creatorsapi_python_sdk.models.item import Item
from creatorsapi_python_sdk.models.report_metadata import ReportMetadata
from creatorsapi_python_sdk.models.search_result import SearchResult
from creatorsapi_python_sdk.models.variations_result import VariationsResult

Expand All @@ -55,6 +65,10 @@
ENDPOINT_SEARCH_ITEMS = "/catalog/v1/searchItems"
ENDPOINT_GET_VARIATIONS = "/catalog/v1/getVariations"
ENDPOINT_GET_BROWSE_NODES = "/catalog/v1/getBrowseNodes"
ENDPOINT_LIST_FEEDS = "/catalog/v1/listFeeds"
ENDPOINT_GET_FEED = "/catalog/v1/getFeed"
ENDPOINT_LIST_REPORTS = "/reports/v1/listReports"
ENDPOINT_GET_REPORT = "/reports/v1/getReport"

# TypeVar for generic resource handling
ResourceT = TypeVar("ResourceT", bound=Enum)
Expand Down Expand Up @@ -449,6 +463,90 @@ async def get_browse_nodes(

return self._deserialize_browse_nodes(browse_nodes_result["browseNodes"])

async def list_feeds(self) -> list[Feed]:
"""Return the feeds available for your account.

Returns:
List of Feed objects, empty if no feeds are available. Each feed
carries its type, so the same name can exist in more than one
feed program.

Raises:
RequestError: If the API request fails.

"""
response = await self._make_request(ENDPOINT_LIST_FEEDS)

return self._deserialize_feeds(response.get("feeds") or [])

async def get_feed(self, feed_name: str, feed_type: FeedType | None = None) -> str:
"""Return a temporary download URL for a feed.

Args:
feed_name: Name of the feed, as returned by list_feeds.
feed_type: Feed program the name belongs to. Needed to disambiguate
a name available in more than one program.

Returns:
URL to download the feed contents from.

Raises:
RequestError: If the API request fails.

"""
request_body: dict[str, Any] = {"feedName": feed_name}

if feed_type is not None:
request_body["feedType"] = feed_type.value

response = await self._make_request(ENDPOINT_GET_FEED, request_body)

return GetFeedResponseContent.model_validate(response).url

async def list_reports(self) -> list[ReportMetadata]:
"""Return the reports available for your account.

Returns:
List of ReportMetadata objects, empty if no reports are available.
Each report carries its type, telling Creator Central reports apart
from Creator Connections ones.

Raises:
RequestError: If the API request fails.

"""
response = await self._make_request(ENDPOINT_LIST_REPORTS)

return self._deserialize_reports(response.get("reports") or [])

async def get_report(
self,
filename: str,
report_type: ReportType | None = None,
) -> str:
"""Return a temporary download URL for a report.

Args:
filename: Name of the report, as returned by list_reports.
report_type: Program the report belongs to. Needed to disambiguate
a filename available in more than one program.

Returns:
URL to download the report contents from.

Raises:
RequestError: If the API request fails.

"""
request_body: dict[str, Any] = {"filename": filename}

if report_type is not None:
request_body["reportType"] = report_type.value

response = await self._make_request(ENDPOINT_GET_REPORT, request_body)

return GetReportResponseContent.model_validate(response).url

async def _throttle(self) -> None:
"""Wait for the throttling interval to elapse since the last API call.

Expand All @@ -468,13 +566,13 @@ async def _throttle(self) -> None:
async def _make_request(
self,
endpoint: str,
body: dict[str, Any],
body: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Make an API request with authentication and throttling.

Args:
endpoint: API endpoint path.
body: Request body.
body: Request body, omitted for operations that take no payload.

Returns:
Parsed JSON response.
Expand Down Expand Up @@ -554,3 +652,14 @@ def _deserialize_browse_nodes(
) -> list[BrowseNode]:
"""Deserialize browse nodes data from API response to BrowseNode models."""
return [BrowseNode.model_validate(node) for node in browse_nodes_data]

def _deserialize_feeds(self, feeds_data: list[dict[str, Any]]) -> list[Feed]:
"""Deserialize feed data from API response to Feed models."""
return [Feed.model_validate(feed) for feed in feeds_data]

def _deserialize_reports(
self,
reports_data: list[dict[str, Any]],
) -> list[ReportMetadata]:
"""Deserialize report data from API response to ReportMetadata models."""
return [ReportMetadata.model_validate(report) for report in reports_data]
5 changes: 3 additions & 2 deletions amazon_creatorsapi/aio/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,14 +105,15 @@ async def post(
self,
path: str,
headers: dict[str, str],
body: dict[str, Any],
body: dict[str, Any] | None = None,
) -> AsyncHttpResponse:
"""Make a POST request to the API.

Args:
path: API endpoint path (e.g., "/catalog/v1/getItems").
headers: Request headers.
body: Request body as a dictionary.
body: Request body as a dictionary. When omitted, the request is
sent without a payload, as operations like listFeeds expect.

Returns:
AsyncHttpResponse with status, headers, and body.
Expand Down
110 changes: 110 additions & 0 deletions amazon_creatorsapi/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,16 @@
from creatorsapi_python_sdk.models.get_browse_nodes_resource import (
GetBrowseNodesResource,
)
from creatorsapi_python_sdk.models.get_feed_request_content import (
GetFeedRequestContent,
)
from creatorsapi_python_sdk.models.get_items_request_content import (
GetItemsRequestContent,
)
from creatorsapi_python_sdk.models.get_items_resource import GetItemsResource
from creatorsapi_python_sdk.models.get_report_request_content import (
GetReportRequestContent,
)
from creatorsapi_python_sdk.models.get_variations_request_content import (
GetVariationsRequestContent,
)
Expand All @@ -41,7 +47,11 @@
from creatorsapi_python_sdk.models.browse_node import BrowseNode
from creatorsapi_python_sdk.models.condition import Condition
from creatorsapi_python_sdk.models.delivery_flag import DeliveryFlag
from creatorsapi_python_sdk.models.feed import Feed
from creatorsapi_python_sdk.models.feed_type import FeedType
from creatorsapi_python_sdk.models.item import Item
from creatorsapi_python_sdk.models.report_metadata import ReportMetadata
from creatorsapi_python_sdk.models.report_type import ReportType
from creatorsapi_python_sdk.models.search_result import SearchResult
from creatorsapi_python_sdk.models.sort_by import SortBy
from creatorsapi_python_sdk.models.variations_result import VariationsResult
Expand Down Expand Up @@ -367,6 +377,106 @@ def get_browse_nodes(

return response.browse_nodes_result.browse_nodes

def list_feeds(self) -> list[Feed]:
"""Return the feeds available for your account.

Returns:
List of Feed objects, empty if no feeds are available. Each feed
carries its type, so the same name can exist in more than one
feed program.

Raises:
RequestError: If the API request fails.

"""
self._throttle()

try:
response = self._api.list_feeds(x_marketplace=self.marketplace)
except ApiException as exc:
self._handle_api_exception(exc)

return response.feeds or []

def get_feed(self, feed_name: str, feed_type: FeedType | None = None) -> str:
"""Return a temporary download URL for a feed.

Args:
feed_name: Name of the feed, as returned by list_feeds.
feed_type: Feed program the name belongs to. Needed to disambiguate
a name available in more than one program.

Returns:
URL to download the feed contents from.

Raises:
RequestError: If the API request fails.

"""
request = GetFeedRequestContent(feedName=feed_name, feedType=feed_type)

self._throttle()

try:
response = self._api.get_feed(
x_marketplace=self.marketplace,
get_feed_request_content=request,
)
except ApiException as exc:
self._handle_api_exception(exc)

return response.url

def list_reports(self) -> list[ReportMetadata]:
"""Return the reports available for your account.

Returns:
List of ReportMetadata objects, empty if no reports are available.
Each report carries its type, telling Creator Central reports apart
from Creator Connections ones.

Raises:
RequestError: If the API request fails.

"""
self._throttle()

try:
response = self._api.list_reports(x_marketplace=self.marketplace)
except ApiException as exc:
self._handle_api_exception(exc)

return response.reports

def get_report(self, filename: str, report_type: ReportType | None = None) -> str:
"""Return a temporary download URL for a report.

Args:
filename: Name of the report, as returned by list_reports.
report_type: Program the report belongs to. Needed to disambiguate
a filename available in more than one program.

Returns:
URL to download the report contents from.

Raises:
RequestError: If the API request fails.

"""
request = GetReportRequestContent(filename=filename, reportType=report_type)

self._throttle()

try:
response = self._api.get_report(
x_marketplace=self.marketplace,
get_report_request_content=request,
)
except ApiException as exc:
self._handle_api_exception(exc)

return response.url

def _throttle(self) -> None:
"""Wait for the throttling interval to elapse since the last API call."""
wait_time = self.throttling - (time.time() - self._last_query_time)
Expand Down
Loading
Loading