Skip to content

Commit 8401f54

Browse files
authored
Merge pull request #147 from Enirsa/master
Bumped `creatorsapi-python-sdk` from 1.1.2 to 1.2.0 which introduces LWA endpoints (v3.x)
2 parents d9a407e + 13f4bcd commit 8401f54

File tree

15 files changed

+380
-43
lines changed

15 files changed

+380
-43
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [6.2.0] - 2026-03-12
9+
10+
### Added
11+
12+
- LWA support in the `amazon_creatorsapi.aio` API layer
13+
14+
### Changed
15+
16+
- Bumped `creatorsapi-python-sdk` from `1.1.2` to `1.2.0`
17+
- Updated bundled SDK support to include LWA endpoints for v3.x
18+
819
## [6.1.0] - 2026-02-09
920

1021
### Added

amazon_creatorsapi/aio/api.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,8 @@ class AsyncAmazonCreatorsApi:
106106
107107
Raises:
108108
InvalidArgumentError: If neither country nor marketplace is provided.
109-
ValueError: If version is not supported (valid versions: 2.1, 2.2, 2.3).
109+
ValueError: If version is not supported (valid versions: 2.1, 2.2, 2.3,
110+
3.1, 3.2, 3.3).
110111
111112
"""
112113

@@ -483,7 +484,7 @@ async def _make_request(
483484
token = await self._token_manager.get_token()
484485

485486
headers = {
486-
"Authorization": f"Bearer {token}, Version {self._version}",
487+
"Authorization": self._build_authorization_header(token),
487488
"Content-Type": "application/json; charset=utf-8",
488489
"x-marketplace": self.marketplace,
489490
}
@@ -501,6 +502,12 @@ async def _make_request(
501502

502503
return response.json()
503504

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+
504511
def _handle_error_response(self, status_code: int, body: str) -> None:
505512
"""Handle API error responses and raise appropriate exceptions.
506513

amazon_creatorsapi/aio/auth.py

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@
2121

2222

2323
# OAuth2 constants
24-
SCOPE = "creatorsapi/default"
24+
COGNITO_SCOPE = "creatorsapi/default"
25+
LWA_SCOPE = "creatorsapi::default"
26+
# Backward-compatible alias for existing v2.x users.
27+
SCOPE = COGNITO_SCOPE
2528
GRANT_TYPE = "client_credentials"
2629

2730
# Token expiration buffer in seconds (refresh 30s before actual expiration)
@@ -32,6 +35,9 @@
3235
"2.1": "https://creatorsapi.auth.us-east-1.amazoncognito.com/oauth2/token",
3336
"2.2": "https://creatorsapi.auth.eu-south-2.amazoncognito.com/oauth2/token",
3437
"2.3": "https://creatorsapi.auth.us-west-2.amazoncognito.com/oauth2/token",
38+
"3.1": "https://api.amazon.com/auth/o2/token",
39+
"3.2": "https://api.amazon.co.uk/auth/o2/token",
40+
"3.3": "https://api.amazon.co.jp/auth/o2/token",
3541
}
3642

3743

@@ -97,6 +103,14 @@ def _determine_auth_endpoint(
97103

98104
return VERSION_ENDPOINTS[version]
99105

106+
def is_lwa(self) -> bool:
107+
"""Return whether this token manager uses the LWA auth flow."""
108+
return self._version.startswith("3.")
109+
110+
def get_scope(self) -> str:
111+
"""Return the version-appropriate OAuth2 scope."""
112+
return LWA_SCOPE if self.is_lwa() else COGNITO_SCOPE
113+
100114
@property
101115
def lock(self) -> asyncio.Lock:
102116
"""Lazy initialization of the asyncio.Lock.
@@ -168,20 +182,23 @@ async def refresh_token(self) -> str:
168182
"grant_type": GRANT_TYPE,
169183
"client_id": self._credential_id,
170184
"client_secret": self._credential_secret,
171-
"scope": SCOPE,
172-
}
173-
174-
headers = {
175-
"Content-Type": "application/x-www-form-urlencoded",
185+
"scope": self.get_scope(),
176186
}
177187

178188
try:
179189
async with httpx.AsyncClient() as client:
180-
response = await client.post(
181-
self._auth_endpoint,
182-
data=request_data,
183-
headers=headers,
184-
)
190+
if self.is_lwa():
191+
response = await client.post(
192+
self._auth_endpoint,
193+
json=request_data,
194+
headers={"Content-Type": "application/json"},
195+
)
196+
else:
197+
response = await client.post(
198+
self._auth_endpoint,
199+
data=request_data,
200+
headers={"Content-Type": "application/x-www-form-urlencoded"},
201+
)
185202

186203
if response.status_code != 200: # noqa: PLR2004
187204
self.clear_token()

creatorsapi_python_sdk/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,5 +123,6 @@
123123
from creatorsapi_python_sdk.models.variation_attribute import VariationAttribute
124124
from creatorsapi_python_sdk.models.variation_dimension import VariationDimension
125125
from creatorsapi_python_sdk.models.variation_summary import VariationSummary
126+
from creatorsapi_python_sdk.models.variation_summary_price import VariationSummaryPrice
126127
from creatorsapi_python_sdk.models.variations_result import VariationsResult
127128
from creatorsapi_python_sdk.models.website_sales_rank import WebsiteSalesRank

creatorsapi_python_sdk/api/default_api.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,7 @@ def get_feed(
424424
'401': "UnauthorizedExceptionResponseContent",
425425
'403': "AccessDeniedExceptionResponseContent",
426426
'404': "ResourceNotFoundExceptionResponseContent",
427+
'429': "ThrottleExceptionResponseContent",
427428
'500': "InternalServerExceptionResponseContent",
428429
}
429430
response_data = self.api_client.call_api(
@@ -500,6 +501,7 @@ def get_feed_with_http_info(
500501
'401': "UnauthorizedExceptionResponseContent",
501502
'403': "AccessDeniedExceptionResponseContent",
502503
'404': "ResourceNotFoundExceptionResponseContent",
504+
'429': "ThrottleExceptionResponseContent",
503505
'500': "InternalServerExceptionResponseContent",
504506
}
505507
response_data = self.api_client.call_api(
@@ -576,6 +578,7 @@ def get_feed_without_preload_content(
576578
'401': "UnauthorizedExceptionResponseContent",
577579
'403': "AccessDeniedExceptionResponseContent",
578580
'404': "ResourceNotFoundExceptionResponseContent",
581+
'429': "ThrottleExceptionResponseContent",
579582
'500': "InternalServerExceptionResponseContent",
580583
}
581584
response_data = self.api_client.call_api(
@@ -1026,6 +1029,7 @@ def get_report(
10261029
'401': "UnauthorizedExceptionResponseContent",
10271030
'403': "AccessDeniedExceptionResponseContent",
10281031
'404': "ResourceNotFoundExceptionResponseContent",
1032+
'429': "ThrottleExceptionResponseContent",
10291033
'500': "InternalServerExceptionResponseContent",
10301034
}
10311035
response_data = self.api_client.call_api(
@@ -1102,6 +1106,7 @@ def get_report_with_http_info(
11021106
'401': "UnauthorizedExceptionResponseContent",
11031107
'403': "AccessDeniedExceptionResponseContent",
11041108
'404': "ResourceNotFoundExceptionResponseContent",
1109+
'429': "ThrottleExceptionResponseContent",
11051110
'500': "InternalServerExceptionResponseContent",
11061111
}
11071112
response_data = self.api_client.call_api(
@@ -1178,6 +1183,7 @@ def get_report_without_preload_content(
11781183
'401': "UnauthorizedExceptionResponseContent",
11791184
'403': "AccessDeniedExceptionResponseContent",
11801185
'404': "ResourceNotFoundExceptionResponseContent",
1186+
'429': "ThrottleExceptionResponseContent",
11811187
'500': "InternalServerExceptionResponseContent",
11821188
}
11831189
response_data = self.api_client.call_api(
@@ -1627,6 +1633,7 @@ def list_feeds(
16271633
'401': "UnauthorizedExceptionResponseContent",
16281634
'403': "AccessDeniedExceptionResponseContent",
16291635
'404': "ResourceNotFoundExceptionResponseContent",
1636+
'429': "ThrottleExceptionResponseContent",
16301637
'500': "InternalServerExceptionResponseContent",
16311638
}
16321639
response_data = self.api_client.call_api(
@@ -1699,6 +1706,7 @@ def list_feeds_with_http_info(
16991706
'401': "UnauthorizedExceptionResponseContent",
17001707
'403': "AccessDeniedExceptionResponseContent",
17011708
'404': "ResourceNotFoundExceptionResponseContent",
1709+
'429': "ThrottleExceptionResponseContent",
17021710
'500': "InternalServerExceptionResponseContent",
17031711
}
17041712
response_data = self.api_client.call_api(
@@ -1771,6 +1779,7 @@ def list_feeds_without_preload_content(
17711779
'401': "UnauthorizedExceptionResponseContent",
17721780
'403': "AccessDeniedExceptionResponseContent",
17731781
'404': "ResourceNotFoundExceptionResponseContent",
1782+
'429': "ThrottleExceptionResponseContent",
17741783
'500': "InternalServerExceptionResponseContent",
17751784
}
17761785
response_data = self.api_client.call_api(
@@ -1899,6 +1908,8 @@ def list_reports(
18991908
'400': "ValidationExceptionResponseContent",
19001909
'401': "UnauthorizedExceptionResponseContent",
19011910
'403': "AccessDeniedExceptionResponseContent",
1911+
'404': "ResourceNotFoundExceptionResponseContent",
1912+
'429': "ThrottleExceptionResponseContent",
19021913
'500': "InternalServerExceptionResponseContent",
19031914
}
19041915
response_data = self.api_client.call_api(
@@ -1970,6 +1981,8 @@ def list_reports_with_http_info(
19701981
'400': "ValidationExceptionResponseContent",
19711982
'401': "UnauthorizedExceptionResponseContent",
19721983
'403': "AccessDeniedExceptionResponseContent",
1984+
'404': "ResourceNotFoundExceptionResponseContent",
1985+
'429': "ThrottleExceptionResponseContent",
19731986
'500': "InternalServerExceptionResponseContent",
19741987
}
19751988
response_data = self.api_client.call_api(
@@ -2041,6 +2054,8 @@ def list_reports_without_preload_content(
20412054
'400': "ValidationExceptionResponseContent",
20422055
'401': "UnauthorizedExceptionResponseContent",
20432056
'403': "AccessDeniedExceptionResponseContent",
2057+
'404': "ResourceNotFoundExceptionResponseContent",
2058+
'429': "ThrottleExceptionResponseContent",
20442059
'500': "InternalServerExceptionResponseContent",
20452060
}
20462061
response_data = self.api_client.call_api(

creatorsapi_python_sdk/api_client.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ def __init__(
107107
self.default_headers[header_name] = header_value
108108
self.cookie = cookie
109109
# Set default User-Agent.
110-
self.user_agent = 'creatorsapi-python-sdk/1.1.2'
110+
self.user_agent = 'creatorsapi-python-sdk/1.2.0'
111111
self.client_side_validation = configuration.client_side_validation
112112

113113
# OAuth2 properties
@@ -387,8 +387,11 @@ def call_api(
387387
self._token_manager = OAuth2TokenManager(config)
388388
# Get token (will use cached token if valid)
389389
token = self._token_manager.get_token()
390-
# Add Authorization headers
391-
header_params['Authorization'] = 'Bearer {}, Version {}'.format(token, self.version)
390+
# Add Authorization headers - Version only for v2.x
391+
if self.version.startswith("3."):
392+
header_params['Authorization'] = 'Bearer {}'.format(token)
393+
else:
394+
header_params['Authorization'] = 'Bearer {}, Version {}'.format(token, self.version)
392395
except Exception as error:
393396
raise error
394397

creatorsapi_python_sdk/auth/oauth2_config.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ class OAuth2Config:
2424
"""OAuth2 configuration class that manages version-specific cognito endpoints"""
2525

2626
# Constants
27-
SCOPE = "creatorsapi/default"
27+
COGNITO_SCOPE = "creatorsapi/default"
28+
LWA_SCOPE = "creatorsapi::default"
2829
GRANT_TYPE = "client_credentials"
2930

3031
def __init__(self, credential_id, credential_secret, version, auth_endpoint):
@@ -54,15 +55,30 @@ def determine_token_endpoint(self, version, auth_endpoint):
5455
if auth_endpoint and auth_endpoint.strip():
5556
return auth_endpoint
5657

57-
# Fall back to version-based defaults
58+
# Cognito endpoints (v2.x)
5859
if version == "2.1":
5960
return "https://creatorsapi.auth.us-east-1.amazoncognito.com/oauth2/token"
6061
elif version == "2.2":
6162
return "https://creatorsapi.auth.eu-south-2.amazoncognito.com/oauth2/token"
6263
elif version == "2.3":
6364
return "https://creatorsapi.auth.us-west-2.amazoncognito.com/oauth2/token"
65+
# LWA endpoints (v3.x)
66+
elif version == "3.1":
67+
return "https://api.amazon.com/auth/o2/token"
68+
elif version == "3.2":
69+
return "https://api.amazon.co.uk/auth/o2/token"
70+
elif version == "3.3":
71+
return "https://api.amazon.co.jp/auth/o2/token"
6472
else:
65-
raise ValueError("Unsupported version: {}. Supported versions are: 2.1, 2.2, 2.3".format(version))
73+
raise ValueError("Unsupported version: {}. Supported versions are: 2.1, 2.2, 2.3, 3.1, 3.2, 3.3".format(version))
74+
75+
def is_lwa(self):
76+
"""
77+
Checks if this is an LWA (v3.x) configuration
78+
79+
:return: True if using LWA authentication
80+
"""
81+
return self.version.startswith("3.")
6682

6783
def get_token_endpoint(self, version):
6884
"""
@@ -112,7 +128,7 @@ def get_scope(self):
112128
113129
:return: The OAuth2 scope
114130
"""
115-
return OAuth2Config.SCOPE
131+
return OAuth2Config.LWA_SCOPE if self.is_lwa() else OAuth2Config.COGNITO_SCOPE
116132

117133
def get_grant_type(self):
118134
"""

creatorsapi_python_sdk/auth/oauth2_token_manager.py

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -67,22 +67,35 @@ def refresh_token(self):
6767
:raises Exception: If token refresh fails
6868
"""
6969
try:
70-
request_data = {
71-
'grant_type': self.config.get_grant_type(),
72-
'client_id': self.config.get_credential_id(),
73-
'client_secret': self.config.get_credential_secret(),
74-
'scope': self.config.get_scope()
75-
}
76-
77-
headers = {
78-
'Content-Type': 'application/x-www-form-urlencoded'
79-
}
80-
81-
response = requests.post(
82-
self.config.get_cognito_endpoint(),
83-
data=request_data,
84-
headers=headers
85-
)
70+
if self.config.is_lwa():
71+
# LWA (v3.x) uses JSON body
72+
request_data = {
73+
'grant_type': self.config.get_grant_type(),
74+
'client_id': self.config.get_credential_id(),
75+
'client_secret': self.config.get_credential_secret(),
76+
'scope': self.config.get_scope()
77+
}
78+
headers = {'Content-Type': 'application/json'}
79+
response = requests.post(
80+
self.config.get_cognito_endpoint(),
81+
json=request_data,
82+
headers=headers
83+
)
84+
else:
85+
# Cognito (v2.x) uses form-encoded
86+
request_data = {
87+
'grant_type': self.config.get_grant_type(),
88+
'client_id': self.config.get_credential_id(),
89+
'client_secret': self.config.get_credential_secret(),
90+
'scope': self.config.get_scope()
91+
}
92+
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
93+
response = requests.post(
94+
self.config.get_cognito_endpoint(),
95+
data=request_data,
96+
headers=headers
97+
)
98+
8699
if response.status_code != 200:
87100
raise Exception("OAuth2 token request failed with status {}: {}".format(response.status_code, response.text))
88101

creatorsapi_python_sdk/models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,5 +106,6 @@
106106
from creatorsapi_python_sdk.models.variation_attribute import VariationAttribute
107107
from creatorsapi_python_sdk.models.variation_dimension import VariationDimension
108108
from creatorsapi_python_sdk.models.variation_summary import VariationSummary
109+
from creatorsapi_python_sdk.models.variation_summary_price import VariationSummaryPrice
109110
from creatorsapi_python_sdk.models.variations_result import VariationsResult
110111
from creatorsapi_python_sdk.models.website_sales_rank import WebsiteSalesRank

creatorsapi_python_sdk/models/variation_summary.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from pydantic import BaseModel, ConfigDict, Field, StrictFloat, StrictInt
2727
from typing import Any, ClassVar, Dict, List, Optional, Union
2828
from creatorsapi_python_sdk.models.variation_dimension import VariationDimension
29+
from creatorsapi_python_sdk.models.variation_summary_price import VariationSummaryPrice
2930
from typing import Optional, Set
3031
from typing_extensions import Self
3132

@@ -34,9 +35,10 @@ class VariationSummary(BaseModel):
3435
The container for Variations Summary response. It consists of metadata of variations response like page numbers, number of variations, Price range and Variation Dimensions.
3536
""" # noqa: E501
3637
page_count: Optional[Union[StrictFloat, StrictInt]] = Field(default=None, description="Number of pages in the variation result set.", alias="pageCount")
38+
price: Optional[VariationSummaryPrice] = None
3739
variation_count: Optional[Union[StrictFloat, StrictInt]] = Field(default=None, description="Total number of variations available for the product. This represents the complete count of all child ASINs across all pages. Use this value along with pageCount to understand the full scope of available variations.", alias="variationCount")
3840
variation_dimensions: Optional[List[VariationDimension]] = Field(default=None, description="List of variation dimensions associated with the product. Variation dimensions define the attributes on which products vary (e.g., size, color). Each dimension includes: - Display name and locale for presentation - Dimension name (internal identifier) - List of all possible values for that dimension For example, a clothing item might have two dimensions: 'Size' with values ['S', 'M', 'L'] and 'Color' with values ['Red', 'Blue', 'Green']. These dimensions help users understand how variations differ from each other.", alias="variationDimensions")
39-
__properties: ClassVar[List[str]] = ["pageCount", "variationCount", "variationDimensions"]
41+
__properties: ClassVar[List[str]] = ["pageCount", "price", "variationCount", "variationDimensions"]
4042

4143
model_config = ConfigDict(
4244
populate_by_name=True,
@@ -76,6 +78,9 @@ def to_dict(self) -> Dict[str, Any]:
7678
exclude=excluded_fields,
7779
exclude_none=True,
7880
)
81+
# override the default output from pydantic by calling `to_dict()` of price
82+
if self.price:
83+
_dict['price'] = self.price.to_dict()
7984
# override the default output from pydantic by calling `to_dict()` of each item in variation_dimensions (list)
8085
_items = []
8186
if self.variation_dimensions:
@@ -96,6 +101,7 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
96101

97102
_obj = cls.model_validate({
98103
"pageCount": obj.get("pageCount"),
104+
"price": VariationSummaryPrice.from_dict(obj["price"]) if obj.get("price") is not None else None,
99105
"variationCount": obj.get("variationCount"),
100106
"variationDimensions": [VariationDimension.from_dict(_item) for _item in obj["variationDimensions"]] if obj.get("variationDimensions") is not None else None
101107
})

0 commit comments

Comments
 (0)