amazon-creators-async 0.1.0__tar.gz

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.
@@ -0,0 +1,34 @@
1
+ name: Upload Python Package to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ deploy:
12
+ runs-on: ubuntu-latest
13
+
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - name: Set up Python
18
+ uses: actions/setup-python@v4
19
+ with:
20
+ python-version: '3.12'
21
+
22
+ - name: Install dependencies
23
+ run: |
24
+ python -m pip install --upgrade pip
25
+ pip install build twine
26
+
27
+ - name: Build package
28
+ run: python -m build
29
+
30
+ - name: Publish package
31
+ uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
32
+ with:
33
+ user: __token__
34
+ password: ${{ secrets.PYPI_API_TOKEN }}
@@ -0,0 +1,41 @@
1
+ name: Python package test
2
+
3
+ on:
4
+ push:
5
+ branches: [ "master", "main" ]
6
+ pull_request:
7
+ branches: [ "master", "main" ]
8
+
9
+ jobs:
10
+ build:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
16
+
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ - name: Set up Python ${{ matrix.python-version }}
20
+ uses: actions/setup-python@v4
21
+ with:
22
+ python-version: ${{ matrix.python-version }}
23
+
24
+ - name: Install dependencies
25
+ run: |
26
+ python -m pip install --upgrade pip
27
+ python -m pip install flake8 pytest pytest-asyncio
28
+ if [ -f pyproject.toml ]; then pip install -e .; fi
29
+
30
+ - name: Lint with flake8
31
+ run: |
32
+ # stop the build if there are Python syntax errors or undefined names
33
+ flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
34
+ # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
35
+ flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
36
+
37
+ # We will skip direct pytest here as we need live credentials for integration testing,
38
+ # or you can implement mock-based unittests later.
39
+ - name: Project Install Verification
40
+ run: |
41
+ python -c "import amazon-creators-async; print(amazon-creators-async.__version__)"
@@ -0,0 +1,48 @@
1
+ # Environments
2
+ .env
3
+ venv/
4
+ .venv/
5
+ env/
6
+ ENV/
7
+
8
+ # Python
9
+ __pycache__/
10
+ *.py[cod]
11
+ *$py.class
12
+ *.so
13
+ .Python
14
+ build/
15
+ develop-eggs/
16
+ dist/
17
+ downloads/
18
+ eggs/
19
+ .eggs/
20
+ lib/
21
+ lib64/
22
+ parts/
23
+ sdist/
24
+ var/
25
+ wheels/
26
+ share/python-wheels/
27
+ *.egg-info/
28
+ .installed.cfg
29
+ *.egg
30
+ MANIFEST
31
+
32
+ # Test scripts (local usage only)
33
+ test_*.py
34
+
35
+ # IDEs and Editors
36
+ .vscode/
37
+ .idea/
38
+ *.swp
39
+ *.swo
40
+
41
+ # OS generated files
42
+ .DS_Store
43
+ .DS_Store?
44
+ ._*
45
+ .Spotlight-V100
46
+ .Trashes
47
+ ehthumbs.db
48
+ Thumbs.db
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2026-03-05
9
+ ### Added
10
+ - Initial release of the `amazon-creators-async` wrapper.
11
+ - Full OAuth 2.0 support (Cognito v2.x and LWA v3.x credentials).
12
+ - Native asynchronous core leveraging `httpx`.
13
+ - Configurable Rate Limiting defaulting to 1 TPS via `aiolimiter`.
14
+ - Complete Pydantic v2 models mapping Python `snake_case` to Amazon API `lowerCamelCase`.
15
+ - Endpoints: `search_items`, `get_items`, `get_browse_nodes`, `get_variations`.
16
+ - PyPI release metadata (`pyproject.toml`).
17
+ - Examples and documentation (README, QUICK_START, CONTRIBUTING).
@@ -0,0 +1,56 @@
1
+ # Contributing to amazon-creators-async
2
+
3
+ Thank you for your interest in contributing! This project follows the same open-source model as `aliexpress-async-api`.
4
+
5
+ ## 🛠 Prerequisites
6
+
7
+ 1. **Python 3.8+**
8
+ 2. **Amazon Associate & Creators API Credentials**: You need a `credential_id`, `credential_secret`, and `partner_tag` to run the live tests.
9
+
10
+ ## 🚀 Setting Up Your Development Environment
11
+
12
+ 1. **Fork** the repository and clone your fork:
13
+ ```bash
14
+ git clone https://github.com/yourusername/amazon-creators-async.git
15
+ cd amazon-creators-async
16
+ ```
17
+ 2. **Create a virtual environment**:
18
+ ```bash
19
+ python -m venv .venv
20
+ source .venv/bin/activate # Windows: .venv\Scripts\activate
21
+ ```
22
+ 3. **Install dependencies in editable mode**:
23
+ ```bash
24
+ pip install -e .
25
+ ```
26
+
27
+ ## 🧪 Testing
28
+
29
+ We use the provided `test_client.py` and `test_extra_endpoints.py` to validate API responses directly against Amazon's servers.
30
+
31
+ 1. Create a `.env` file in the root directory:
32
+ ```ini
33
+ AMAZON_CREDENTIAL_ID="amzn1.application-oa2-client..."
34
+ AMAZON_CREDENTIAL_SECRET="amzn1.oa2-cs.v1..."
35
+ AMAZON_PARTNER_TAG="yourtag-20"
36
+ AMAZON_VERSION="3.1"
37
+ ```
38
+ 2. Run the tests:
39
+ ```bash
40
+ python test_client.py
41
+ python test_extra_endpoints.py
42
+ ```
43
+ 3. Ensure your code doesn't produce `400 Invalid Request` or `429 TooManyRequests`.
44
+
45
+ ## 📌 Coding Guidelines
46
+
47
+ * **Asynchronous First**: All network operations must remain asynchronous using `httpx`.
48
+ * **Pydantic Consistency**: Always map native Amazon `lowerCamelCase` responses to Pythonic `snake_case` using Pydantic `ConfigDict` and `alias_generator`. Do not leak `CamelCase` variables into the public Python signatures.
49
+ * **Rate Limits**: Do not bypass or hardcode modifications to the `RateLimiter` unless introducing a documented generic feature (like supporting Amazon's tiered TPS).
50
+
51
+ ## 📄 Pull Request Process
52
+
53
+ 1. Create a feature branch (`git checkout -b feature/awesome-new-feature`).
54
+ 2. Commit your changes following [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
55
+ 3. Push your branch (`git push origin feature/awesome-new-feature`).
56
+ 4. Open a Pull Request on GitHub.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Igor
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,140 @@
1
+ Metadata-Version: 2.4
2
+ Name: amazon-creators-async
3
+ Version: 0.1.0
4
+ Summary: A modern, high-performance asynchronous Python wrapper for the new Amazon Creators API (OAuth 2.0 based).
5
+ Project-URL: Homepage, https://github.com/ils15/amazon-creators-async
6
+ Project-URL: Repository, https://github.com/ils15/amazon-creators-async.git
7
+ Project-URL: Bug Tracker, https://github.com/ils15/amazon-creators-async/issues
8
+ Author-email: Igor <igor@example.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: affiliate,amazon,api,async,creators,httpx,oauth2
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.8
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Topic :: Internet :: WWW/HTTP
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Requires-Python: >=3.8
25
+ Requires-Dist: aiolimiter>=1.1.0
26
+ Requires-Dist: httpx>=0.24.0
27
+ Requires-Dist: pydantic>=2.0.0
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
30
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # Async Amazon Creators API (OAuth 2.0)
34
+
35
+ [![PyPI version](https://badge.fury.io/py/amazon-creators-async.svg)](https://badge.fury.io/py/amazon-creators-async)
36
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
37
+ [![Python Versions](https://img.shields.io/pypi/pyversions/amazon-creators-async.svg)](https://pypi.org/project/amazon-creators-async/)
38
+
39
+ A modern, high-performance, asynchronous Python wrapper for the **Amazon Creators API** (the replacement for the old Product Advertising API 5.0).
40
+
41
+ It fully supports the new **OAuth 2.0** authentication flow, including `v3.1` (Login with Amazon - LWA) credentials, and handles the strict rate limitations automatically to protect your account.
42
+
43
+ ## Features
44
+
45
+ - ⚡️ **Fully Asynchronous**: Built directly on `httpx` and `asyncio` for maximum performance.
46
+ - 🔐 **OAuth 2.0 Native**: Automatic token fetching, caching, and renewal before expiration. Supports Cognito (v2.x) and LWA (v3.x).
47
+ - 🚦 **Built-in Rate Limiting**: Uses `aiolimiter` to ensure you never exceed Amazon's 1 TPS (Transactions Per Second) limit by default, preventing `429 TooManyRequests` bans.
48
+ - 📦 **Pydantic Validation**: Full request and response validation using Pydantic v2.
49
+ - 🐪 **Auto CamelCase mapping**: You write Pythonic `snake_case`, the library talks to Amazon in their required `lowerCamelCase`.
50
+ - 🛠️ **Full API Coverage**: `SearchItems`, `GetItems`, `GetBrowseNodes`, and `GetVariations`.
51
+
52
+ ## Installation
53
+
54
+ ```bash
55
+ pip install amazon-creators-async
56
+ ```
57
+
58
+ ## Quick Start
59
+
60
+ ```python
61
+ import asyncio
62
+ from amazon_creators_async import AmazonCreatorsAsyncClient, Region
63
+
64
+ async def main():
65
+ async with AmazonCreatorsAsyncClient(
66
+ credential_id="YOUR_APP_ID",
67
+ credential_secret="YOUR_APP_SECRET",
68
+ partner_tag="yourtag-20", # Your Amazon Associate tag
69
+ marketplace="www.amazon.com", # Target marketplace domain
70
+ region=Region.NORTH_AMERICA, # Standard region (includes Brazil)
71
+ version="3.1" # Version from your Creator Console
72
+ ) as client:
73
+
74
+ # 1. Search for products
75
+ search_res = await client.search_items(
76
+ keywords="Mechanical Keyboard",
77
+ item_count=5, # Number of items to return
78
+ resources=[
79
+ "itemInfo.title",
80
+ "offersV2.listings.price",
81
+ "images.primary.large"
82
+ ]
83
+ )
84
+
85
+ for item in search_res.search_result.items:
86
+ print(f"ASIN: {item.asin}")
87
+ if item.item_info and item.item_info.title:
88
+ print(f"Title: {item.item_info.title.get('displayValue')}")
89
+
90
+ print("-" * 20)
91
+
92
+ # 2. Get specific items by ASIN
93
+ get_res = await client.get_items(
94
+ item_ids=["B07XQXZXJC", "B08FJMVZX6"],
95
+ resources=["itemInfo.features"]
96
+ )
97
+
98
+ for item in get_res.items_result.items:
99
+ print(f"Features for {item.asin}:")
100
+ if item.item_info and item.item_info.features:
101
+ for feature in item.item_info.features.get('displayValues', []):
102
+ print(f" - {feature}")
103
+
104
+ if __name__ == "__main__":
105
+ asyncio.run(main())
106
+ ```
107
+
108
+ ## Available Resources
109
+ The Amazon API requires you to specify the `resources` you want returned to minimize payload size. We use the updated **lowerCamelCase** format required by v3.x of the API.
110
+ Common Examples:
111
+ - `itemInfo.title`
112
+ - `offersV2.listings.price`
113
+ - `images.primary.small`
114
+ - `images.variants.large`
115
+ - `itemInfo.features`
116
+ - `browseNodeInfo.browseNodes`
117
+
118
+ ## Rate Limiting (Throttling)
119
+
120
+ Amazon imposes strict API limitations. Exceeding them regularly can lead to your account being blocked.
121
+ * **Default Limit**: 1 request per second (TPS).
122
+ * **Library Behavior**: The `AmazonCreatorsAsyncClient` handles this automatically. If you fire 10 requests at once using `asyncio.gather()`, the built-in limiter will process exactly 1 per second.
123
+ * **Custom Limits**: If your account has a higher tier limit, you can adjust the TPS:
124
+ ```python
125
+ AmazonCreatorsAsyncClient(..., rate_limit_tps=5.0) # For accounts with 5 TPS
126
+ ```
127
+
128
+ ## Authentication Versions
129
+
130
+ The library automatically negotiates endpoints based on the `version` passed during initialization:
131
+ - **v2.x (e.g., "2.1")**: Uses Cognito endpoints with Basic Auth forms.
132
+ - **v3.x (e.g., "3.1")**: Uses Login with Amazon (LWA) endpoints with JSON bodies.
133
+
134
+ ## Documentation
135
+ - [Amazon Creators API Official Docs](https://affiliate-program.amazon.com/creatorsapi/docs/en-us/introduction)
136
+ - [Quick Start Guide](QUICK_START.md)
137
+ - [Contributing](CONTRIBUTING.md)
138
+
139
+ ## License
140
+ MIT License. See [LICENSE](LICENSE) for details.
@@ -0,0 +1,98 @@
1
+ # Quick Start: amazon-creators-async
2
+
3
+ Get up and running with the Amazon Creators API in 5 minutes!
4
+
5
+ ## Requirements
6
+ * An approved **Amazon Associates** account.
7
+ * A registered application in the **Amazon Creators Console**.
8
+ * Your `credential_id`, `credential_secret`, and `partner_tag`.
9
+
10
+ ## 1. Installation
11
+
12
+ ```bash
13
+ pip install amazon-creators-async
14
+ ```
15
+
16
+ ## 2. Basic Setup (FastAPI Example)
17
+
18
+ Here is how you would use this library inside a modern async framework like FastAPI to prevent thread-blocking while waiting for Amazon's API:
19
+
20
+ ```python
21
+ from fastapi import FastAPI
22
+ from amazon_creators_async import AmazonCreatorsAsyncClient, Region
23
+
24
+ app = FastAPI()
25
+
26
+ # Usually, you'd load these from environment variables (.env)
27
+ CREATOR_ID = "amzn1.application..."
28
+ CREATOR_SECRET = "amzn1.oa2-cs..."
29
+ PARTNER_TAG = "mywebsite-20"
30
+
31
+ # Note: In production, instantiate this client once on startup (Lifespan event)
32
+ # and share it across requests to reuse the connection pool and rate limiter.
33
+ client = AmazonCreatorsAsyncClient(
34
+ credential_id=CREATOR_ID,
35
+ credential_secret=CREATOR_SECRET,
36
+ partner_tag=PARTNER_TAG,
37
+ marketplace="www.amazon.com",
38
+ region=Region.NORTH_AMERICA,
39
+ version="3.1" # Find your version in the Creators Console
40
+ )
41
+
42
+ @app.get("/search/{keyword}")
43
+ async def search_amazon(keyword: str):
44
+ response = await client.search_items(
45
+ keywords=keyword,
46
+ item_count=5,
47
+ resources=[
48
+ "itemInfo.title",
49
+ "offersV2.listings.price",
50
+ "images.primary.large"
51
+ ]
52
+ )
53
+
54
+ # Map the response into a clean JSON output
55
+ results = []
56
+ for item in response.search_result.items:
57
+ title = item.item_info.title.get('displayValue', 'No title') if item.item_info and item.item_info.title else "No title"
58
+
59
+ price = "Out of Stock"
60
+ if item.offers_v2 and item.offers_v2.listings:
61
+ price = item.offers_v2.listings[0].price.display_amount
62
+
63
+ results.append({
64
+ "asin": item.asin,
65
+ "title": title,
66
+ "price": price
67
+ })
68
+
69
+ return {"results": results}
70
+
71
+ @app.on_event("shutdown")
72
+ async def shutdown_event():
73
+ # Always close the client cleanly
74
+ await client.close()
75
+ ```
76
+
77
+ ## 3. Parallel Execution (Safe Throttling)
78
+
79
+ If you need to query multiple distinct ASINs or categories at once, you can safely use `asyncio.gather`.
80
+ Our built-in `RateLimiter` ensures you don't violate the 1 TPS limit:
81
+
82
+ ```python
83
+ import asyncio
84
+
85
+ async def parallel_fetch():
86
+ # Assuming `client` is already initialized
87
+
88
+ # These three calls are fired immediately
89
+ tasks = [
90
+ client.get_items(item_ids=["B07XQXZXJC"], resources=["itemInfo.title"]),
91
+ client.get_items(item_ids=["B08FJMVZX6"], resources=["itemInfo.title"]),
92
+ client.get_items(item_ids=["B09B8VGCR8"], resources=["itemInfo.title"])
93
+ ]
94
+
95
+ # The client will space them out (1 request per second) automatically!
96
+ results = await asyncio.gather(*tasks)
97
+ return results
98
+ ```
@@ -0,0 +1,108 @@
1
+ # Async Amazon Creators API (OAuth 2.0)
2
+
3
+ [![PyPI version](https://badge.fury.io/py/amazon-creators-async.svg)](https://badge.fury.io/py/amazon-creators-async)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![Python Versions](https://img.shields.io/pypi/pyversions/amazon-creators-async.svg)](https://pypi.org/project/amazon-creators-async/)
6
+
7
+ A modern, high-performance, asynchronous Python wrapper for the **Amazon Creators API** (the replacement for the old Product Advertising API 5.0).
8
+
9
+ It fully supports the new **OAuth 2.0** authentication flow, including `v3.1` (Login with Amazon - LWA) credentials, and handles the strict rate limitations automatically to protect your account.
10
+
11
+ ## Features
12
+
13
+ - ⚡️ **Fully Asynchronous**: Built directly on `httpx` and `asyncio` for maximum performance.
14
+ - 🔐 **OAuth 2.0 Native**: Automatic token fetching, caching, and renewal before expiration. Supports Cognito (v2.x) and LWA (v3.x).
15
+ - 🚦 **Built-in Rate Limiting**: Uses `aiolimiter` to ensure you never exceed Amazon's 1 TPS (Transactions Per Second) limit by default, preventing `429 TooManyRequests` bans.
16
+ - 📦 **Pydantic Validation**: Full request and response validation using Pydantic v2.
17
+ - 🐪 **Auto CamelCase mapping**: You write Pythonic `snake_case`, the library talks to Amazon in their required `lowerCamelCase`.
18
+ - 🛠️ **Full API Coverage**: `SearchItems`, `GetItems`, `GetBrowseNodes`, and `GetVariations`.
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ pip install amazon-creators-async
24
+ ```
25
+
26
+ ## Quick Start
27
+
28
+ ```python
29
+ import asyncio
30
+ from amazon_creators_async import AmazonCreatorsAsyncClient, Region
31
+
32
+ async def main():
33
+ async with AmazonCreatorsAsyncClient(
34
+ credential_id="YOUR_APP_ID",
35
+ credential_secret="YOUR_APP_SECRET",
36
+ partner_tag="yourtag-20", # Your Amazon Associate tag
37
+ marketplace="www.amazon.com", # Target marketplace domain
38
+ region=Region.NORTH_AMERICA, # Standard region (includes Brazil)
39
+ version="3.1" # Version from your Creator Console
40
+ ) as client:
41
+
42
+ # 1. Search for products
43
+ search_res = await client.search_items(
44
+ keywords="Mechanical Keyboard",
45
+ item_count=5, # Number of items to return
46
+ resources=[
47
+ "itemInfo.title",
48
+ "offersV2.listings.price",
49
+ "images.primary.large"
50
+ ]
51
+ )
52
+
53
+ for item in search_res.search_result.items:
54
+ print(f"ASIN: {item.asin}")
55
+ if item.item_info and item.item_info.title:
56
+ print(f"Title: {item.item_info.title.get('displayValue')}")
57
+
58
+ print("-" * 20)
59
+
60
+ # 2. Get specific items by ASIN
61
+ get_res = await client.get_items(
62
+ item_ids=["B07XQXZXJC", "B08FJMVZX6"],
63
+ resources=["itemInfo.features"]
64
+ )
65
+
66
+ for item in get_res.items_result.items:
67
+ print(f"Features for {item.asin}:")
68
+ if item.item_info and item.item_info.features:
69
+ for feature in item.item_info.features.get('displayValues', []):
70
+ print(f" - {feature}")
71
+
72
+ if __name__ == "__main__":
73
+ asyncio.run(main())
74
+ ```
75
+
76
+ ## Available Resources
77
+ The Amazon API requires you to specify the `resources` you want returned to minimize payload size. We use the updated **lowerCamelCase** format required by v3.x of the API.
78
+ Common Examples:
79
+ - `itemInfo.title`
80
+ - `offersV2.listings.price`
81
+ - `images.primary.small`
82
+ - `images.variants.large`
83
+ - `itemInfo.features`
84
+ - `browseNodeInfo.browseNodes`
85
+
86
+ ## Rate Limiting (Throttling)
87
+
88
+ Amazon imposes strict API limitations. Exceeding them regularly can lead to your account being blocked.
89
+ * **Default Limit**: 1 request per second (TPS).
90
+ * **Library Behavior**: The `AmazonCreatorsAsyncClient` handles this automatically. If you fire 10 requests at once using `asyncio.gather()`, the built-in limiter will process exactly 1 per second.
91
+ * **Custom Limits**: If your account has a higher tier limit, you can adjust the TPS:
92
+ ```python
93
+ AmazonCreatorsAsyncClient(..., rate_limit_tps=5.0) # For accounts with 5 TPS
94
+ ```
95
+
96
+ ## Authentication Versions
97
+
98
+ The library automatically negotiates endpoints based on the `version` passed during initialization:
99
+ - **v2.x (e.g., "2.1")**: Uses Cognito endpoints with Basic Auth forms.
100
+ - **v3.x (e.g., "3.1")**: Uses Login with Amazon (LWA) endpoints with JSON bodies.
101
+
102
+ ## Documentation
103
+ - [Amazon Creators API Official Docs](https://affiliate-program.amazon.com/creatorsapi/docs/en-us/introduction)
104
+ - [Quick Start Guide](QUICK_START.md)
105
+ - [Contributing](CONTRIBUTING.md)
106
+
107
+ ## License
108
+ MIT License. See [LICENSE](LICENSE) for details.
@@ -0,0 +1,17 @@
1
+ from .client import AmazonCreatorsAsyncClient
2
+ from .exceptions import AmazonCreatorsException, RateLimitError, AuthenticationError
3
+ from .models.requests import SearchItemsRequest, GetItemsRequest
4
+ from .models.responses import SearchItemsResponse, GetItemsResponse
5
+ from .utils import Region
6
+
7
+ __all__ = [
8
+ "AmazonCreatorsAsyncClient",
9
+ "AmazonCreatorsException",
10
+ "RateLimitError",
11
+ "AuthenticationError",
12
+ "SearchItemsRequest",
13
+ "GetItemsRequest",
14
+ "SearchItemsResponse",
15
+ "GetItemsResponse",
16
+ "Region"
17
+ ]
@@ -0,0 +1,105 @@
1
+ import base64
2
+ import time
3
+ import asyncio
4
+ import httpx
5
+ from typing import Optional
6
+
7
+ from .exceptions import AuthenticationError
8
+ from .utils import get_auth_endpoint, get_scope
9
+
10
+ class AuthManager:
11
+ """
12
+ Manages OAuth 2.0 Client Credentials token fetching and caching
13
+ for the Amazon Creators API. Supports both Cognito (v2.x) and LWA (v3.x).
14
+ """
15
+
16
+ def __init__(
17
+ self,
18
+ credential_id: str,
19
+ credential_secret: str,
20
+ version: str,
21
+ client: Optional[httpx.AsyncClient] = None
22
+ ):
23
+ self.credential_id = credential_id
24
+ self.credential_secret = credential_secret
25
+ self.version = version
26
+
27
+ # Determine endpoints and scopes
28
+ self.auth_url = get_auth_endpoint(version)
29
+ self.scope = get_scope(version)
30
+
31
+ # Pass a client if you want to share the connection pool, otherwise we manage our own
32
+ self._client = client or httpx.AsyncClient(timeout=30.0)
33
+ self._owns_client = client is None
34
+ self._token_lock = asyncio.Lock()
35
+
36
+ # State for token caching
37
+ self._access_token: Optional[str] = None
38
+ self._token_expires_at: float = 0.0
39
+
40
+ async def get_valid_token(self) -> str:
41
+ """
42
+ Returns a valid OAuth 2.0 access token.
43
+ If the current token is missing or expired (with a 60s buffer), it fetches a new one.
44
+ """
45
+ if self._access_token and time.time() < (self._token_expires_at - 60):
46
+ return self._access_token
47
+
48
+ # Avoid parallel refreshes from concurrent API calls.
49
+ async with self._token_lock:
50
+ if self._access_token and time.time() < (self._token_expires_at - 60):
51
+ return self._access_token
52
+ return await self._fetch_new_token()
53
+
54
+ async def _fetch_new_token(self) -> str:
55
+ """
56
+ Performs the HTTP call to the auth endpoint to get the token.
57
+ Handles both Cognito (form-encoded) and LWA (JSON) versions.
58
+ """
59
+ is_lwa = self.version.startswith("3.")
60
+
61
+ try:
62
+ if is_lwa:
63
+ # LWA (v3.1+) uses JSON body with credentials inside
64
+ headers = {"Content-Type": "application/json"}
65
+ payload = {
66
+ "grant_type": "client_credentials",
67
+ "client_id": self.credential_id,
68
+ "client_secret": self.credential_secret,
69
+ "scope": self.scope
70
+ }
71
+ response = await self._client.post(self.auth_url, headers=headers, json=payload)
72
+ else:
73
+ # Cognito (v2.x) uses Basic Auth + form-encoded data
74
+ auth_string = f"{self.credential_id}:{self.credential_secret}"
75
+ encoded_auth = base64.b64encode(auth_string.encode("utf-8")).decode("utf-8")
76
+ headers = {
77
+ "Content-Type": "application/x-www-form-urlencoded",
78
+ "Authorization": f"Basic {encoded_auth}"
79
+ }
80
+ data = {
81
+ "grant_type": "client_credentials",
82
+ "scope": self.scope
83
+ }
84
+ response = await self._client.post(self.auth_url, headers=headers, data=data)
85
+
86
+ if response.status_code != 200:
87
+ raise AuthenticationError(
88
+ f"Failed to obtain token ({self.version}). Status: {response.status_code}. Response: {response.text}"
89
+ )
90
+
91
+ payload_data = response.json()
92
+ self._access_token = payload_data["access_token"]
93
+
94
+ # Usually expires in 3600 seconds (1 hour)
95
+ expires_in = payload_data.get("expires_in", 3600)
96
+ self._token_expires_at = time.time() + expires_in
97
+
98
+ return self._access_token
99
+ except httpx.RequestError as exc:
100
+ raise AuthenticationError(f"HTTP error occurred while requesting auth token: {exc}") from exc
101
+
102
+ async def close(self):
103
+ """Close internally managed HTTP client."""
104
+ if self._owns_client:
105
+ await self._client.aclose()
@@ -0,0 +1,195 @@
1
+ import asyncio
2
+ import httpx
3
+ from typing import Optional, Dict, Any, List
4
+
5
+ from .auth import AuthManager
6
+ from .limiter import RateLimiter
7
+ from .utils import Region, get_api_endpoint, get_version_for_region, validate_marketplace
8
+ from .exceptions import RateLimitError, InvalidRequestError, APIError
9
+ from .models.requests import SearchItemsRequest, GetItemsRequest, GetBrowseNodesRequest, GetVariationsRequest
10
+ from .models.responses import SearchItemsResponse, GetItemsResponse, GetBrowseNodesResponse, GetVariationsResponse
11
+
12
+ class AmazonCreatorsAsyncClient:
13
+ """
14
+ Asynchronous client for the Amazon Creators API.
15
+ Handles OAuth 2.0 authentication, rate limiting, and parameter serialization.
16
+ """
17
+
18
+ def __init__(
19
+ self,
20
+ credential_id: str,
21
+ credential_secret: str,
22
+ marketplace: str = "www.amazon.com.br",
23
+ partner_tag: str = "",
24
+ region: Region = Region.NORTH_AMERICA,
25
+ version: Optional[str] = None, # Allow explicit version (e.g. 3.1)
26
+ rate_limit_tps: float = 1.0,
27
+ max_retries: int = 2,
28
+ retry_backoff_seconds: float = 0.5,
29
+ client: Optional[httpx.AsyncClient] = None
30
+ ):
31
+ validate_marketplace(marketplace)
32
+ if not partner_tag:
33
+ raise ValueError("partner_tag is required.")
34
+
35
+ self.credential_id = credential_id
36
+ self.credential_secret = credential_secret
37
+ self.marketplace = marketplace
38
+ self.partner_tag = partner_tag
39
+ self.region = region
40
+ self.api_version = version or get_version_for_region(region)
41
+ self.endpoint_url = get_api_endpoint(region)
42
+ self.max_retries = max(0, int(max_retries))
43
+ self.retry_backoff_seconds = max(0.1, float(retry_backoff_seconds))
44
+
45
+ # Async HTTP client
46
+ self._client = client or httpx.AsyncClient(timeout=30.0)
47
+ self._owns_client = client is None
48
+
49
+ # Managers
50
+ self._auth_manager = AuthManager(
51
+ credential_id=self.credential_id,
52
+ credential_secret=self.credential_secret,
53
+ version=self.api_version,
54
+ client=self._client
55
+ )
56
+ self._limiter = RateLimiter(tps=rate_limit_tps)
57
+
58
+ async def _request(self, operation: str, payload: Dict[str, Any]) -> Dict[str, Any]:
59
+ """Core method to dispatch a request to the Amazon API with Auth & Limiter."""
60
+ url = f"{self.endpoint_url}/{operation}"
61
+
62
+ for attempt in range(self.max_retries + 1):
63
+ # 1. Wait for Rate Limiter Capacity.
64
+ await self._limiter.acquire()
65
+
66
+ # 2. Get a valid OAuth2 Token (cached or renewed).
67
+ token = await self._auth_manager.get_valid_token()
68
+
69
+ # 3. Prepare headers.
70
+ if self.api_version.startswith("3."):
71
+ auth_header = f"Bearer {token}"
72
+ else:
73
+ auth_header = f"Bearer {token}, Version {self.api_version}"
74
+
75
+ headers = {
76
+ "Content-Type": "application/json",
77
+ "Authorization": auth_header,
78
+ "x-marketplace": self.marketplace,
79
+ "User-Agent": "amazon_creators_async/0.1.0",
80
+ }
81
+
82
+ try:
83
+ response = await self._client.post(url, json=payload, headers=headers)
84
+ except httpx.RequestError as exc:
85
+ if attempt >= self.max_retries:
86
+ raise APIError(f"Network error: {exc}") from exc
87
+ await asyncio.sleep(self.retry_backoff_seconds * (2 ** attempt))
88
+ continue
89
+
90
+ if response.status_code == 200:
91
+ return response.json()
92
+
93
+ if response.status_code == 429:
94
+ if attempt >= self.max_retries:
95
+ raise RateLimitError(f"Rate limit exceeded: {response.text}")
96
+ retry_after = response.headers.get("Retry-After")
97
+ delay = self.retry_backoff_seconds * (2 ** attempt)
98
+ if retry_after:
99
+ try:
100
+ delay = max(0.0, float(retry_after))
101
+ except ValueError:
102
+ pass
103
+ await asyncio.sleep(delay)
104
+ continue
105
+
106
+ if response.status_code == 400:
107
+ raise InvalidRequestError(f"Invalid request: {response.text}")
108
+
109
+ if response.status_code in {500, 502, 503, 504} and attempt < self.max_retries:
110
+ await asyncio.sleep(self.retry_backoff_seconds * (2 ** attempt))
111
+ continue
112
+
113
+ raise APIError(
114
+ f"API Error ({response.status_code}): {response.text}",
115
+ status_code=response.status_code,
116
+ )
117
+
118
+ raise APIError("Unexpected request retry flow termination")
119
+
120
+ async def search_items(self, **kwargs) -> SearchItemsResponse:
121
+ """
122
+ Search for items on Amazon using keywords, browse_node_id, brand, etc.
123
+ """
124
+ # Inject defaults if missing
125
+ kwargs.setdefault("marketplace", self.marketplace)
126
+ kwargs.setdefault("partner_tag", self.partner_tag)
127
+
128
+ # Validate through Pydantic model
129
+ request_obj = SearchItemsRequest(**kwargs)
130
+
131
+ # Export forcing lowerCamelCase format and removing nulls
132
+ payload = request_obj.model_dump(by_alias=True, exclude_none=True)
133
+
134
+ data = await self._request("searchItems", payload)
135
+ return SearchItemsResponse(**data)
136
+
137
+ async def get_items(self, item_ids: List[str], **kwargs) -> GetItemsResponse:
138
+ """
139
+ Get detailed item information using a batch of ASINs (ItemIds).
140
+ """
141
+ if not item_ids:
142
+ raise ValueError("item_ids must contain at least one ASIN")
143
+
144
+ kwargs.setdefault("marketplace", self.marketplace)
145
+ kwargs.setdefault("partner_tag", self.partner_tag)
146
+ kwargs["item_ids"] = item_ids
147
+
148
+ # Validate through Pydantic model
149
+ request_obj = GetItemsRequest(**kwargs)
150
+
151
+ # Export forcing lowerCamelCase format and removing nulls
152
+ payload = request_obj.model_dump(by_alias=True, exclude_none=True)
153
+
154
+ data = await self._request("getItems", payload)
155
+ return GetItemsResponse(**data)
156
+
157
+ async def get_browse_nodes(self, browse_node_ids: List[str], **kwargs) -> GetBrowseNodesResponse:
158
+ """
159
+ Get Browse Node details (categories geometry) on Amazon using their IDs.
160
+ """
161
+ kwargs.setdefault("marketplace", self.marketplace)
162
+ kwargs.setdefault("partner_tag", self.partner_tag)
163
+ kwargs["browse_node_ids"] = browse_node_ids
164
+
165
+ request_obj = GetBrowseNodesRequest(**kwargs)
166
+ payload = request_obj.model_dump(by_alias=True, exclude_none=True)
167
+
168
+ data = await self._request("getBrowseNodes", payload)
169
+ return GetBrowseNodesResponse(**data)
170
+
171
+ async def get_variations(self, asin: str, **kwargs) -> GetVariationsResponse:
172
+ """
173
+ Get all variations (e.g. size, colors) for a given parent ASIN.
174
+ """
175
+ kwargs.setdefault("marketplace", self.marketplace)
176
+ kwargs.setdefault("partner_tag", self.partner_tag)
177
+ kwargs["asin"] = asin
178
+
179
+ request_obj = GetVariationsRequest(**kwargs)
180
+ payload = request_obj.model_dump(by_alias=True, exclude_none=True)
181
+
182
+ data = await self._request("getVariations", payload)
183
+ return GetVariationsResponse(**data)
184
+
185
+
186
+ async def close(self):
187
+ """Close the underlying httpx client if we own it."""
188
+ if self._owns_client:
189
+ await self._client.aclose()
190
+
191
+ async def __aenter__(self):
192
+ return self
193
+
194
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
195
+ await self.close()
@@ -0,0 +1,23 @@
1
+ class AmazonCreatorsException(Exception):
2
+ """Base exception for all Amazon Creators API errors."""
3
+ pass
4
+
5
+ class AuthenticationError(AmazonCreatorsException):
6
+ """Raised when there is an issue obtaining or refreshing the OAuth token."""
7
+ pass
8
+
9
+ class RateLimitError(AmazonCreatorsException):
10
+ """Raised when the API returns a 429 TooManyRequests."""
11
+ pass
12
+
13
+ class InvalidRequestError(AmazonCreatorsException):
14
+ """Raised when the API returns a 400 Bad Request."""
15
+ pass
16
+
17
+ class APIError(AmazonCreatorsException):
18
+ """Raised for general API errors (e.g. 500 Internal Server Error)."""
19
+ def __init__(self, message: str, status_code: int = None, type: str = None, code: str = None):
20
+ super().__init__(message)
21
+ self.status_code = status_code
22
+ self.type = type
23
+ self.code = code
@@ -0,0 +1,33 @@
1
+ from typing import Tuple
2
+ from aiolimiter import AsyncLimiter
3
+
4
+ class RateLimiter:
5
+ """
6
+ Ensures that API calls do not exceed the Amazon Creators API rate limits.
7
+ The default is 1 TPS (Transaction Per Second) for the initial 30 days.
8
+ """
9
+ def __init__(self, tps: float = 1.0):
10
+ if tps <= 0:
11
+ raise ValueError("tps must be greater than 0")
12
+
13
+ # aiolimiter takes max_rate and time_period.
14
+ # e.g., max_rate=1, time_period=1 => 1 request per 1 second
15
+ self.tps = tps
16
+ max_rate, time_period = limiter_config_from_tps(tps)
17
+ self._limiter = AsyncLimiter(max_rate=max_rate, time_period=time_period)
18
+
19
+ async def acquire(self):
20
+ """
21
+ Wait until we have capacity to make a request based on the allowed TPS.
22
+ """
23
+ await self._limiter.acquire()
24
+
25
+ def limiter_config_from_tps(tps: float) -> Tuple[int, float]:
26
+ """
27
+ Convert TPS to (max_rate, time_period) for aiolimiter.
28
+ For TPS < 1, enforce one request every 1/tps seconds.
29
+ For TPS >= 1, use max_rate requests per second.
30
+ """
31
+ if tps < 1.0:
32
+ return 1, 1.0 / tps
33
+ return int(tps), 1.0
@@ -0,0 +1,50 @@
1
+ from .requests import (
2
+ GetItemsRequest,
3
+ SearchItemsRequest,
4
+ GetBrowseNodesRequest,
5
+ GetVariationsRequest,
6
+ BaseAPIRequest
7
+ )
8
+ from .responses import (
9
+ GetItemsResponse,
10
+ SearchItemsResponse,
11
+ GetBrowseNodesResponse,
12
+ GetVariationsResponse,
13
+ Item,
14
+ SearchResult,
15
+ Image,
16
+ Images,
17
+ Price,
18
+ ItemInfo,
19
+ Listing,
20
+ OffersV2,
21
+ BrowseNode,
22
+ BrowseNodesResult,
23
+ VariationDimension,
24
+ VariationsResult
25
+ )
26
+
27
+ __all__ = [
28
+ "GetItemsRequest",
29
+ "SearchItemsRequest",
30
+ "GetBrowseNodesRequest",
31
+ "GetVariationsRequest",
32
+ "BaseAPIRequest",
33
+ "GetItemsResponse",
34
+ "SearchItemsResponse",
35
+ "GetBrowseNodesResponse",
36
+ "GetVariationsResponse",
37
+ "Item",
38
+ "SearchResult",
39
+ "Image",
40
+ "Images",
41
+ "Price",
42
+ "ItemInfo",
43
+ "Listing",
44
+ "OffersV2",
45
+ "BrowseNode",
46
+ "BrowseNodesResult",
47
+ "VariationDimension",
48
+ "VariationsResult"
49
+ ]
50
+
@@ -0,0 +1,90 @@
1
+ from typing import List, Optional
2
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
3
+
4
+ class BaseAPIRequest(BaseModel):
5
+ """
6
+ Base configuration for Amazon Creators API requests.
7
+ Forces lowerCamelCase as required by the new API.
8
+ """
9
+ model_config = ConfigDict(
10
+ populate_by_name=True,
11
+ alias_generator=lambda string: "".join(
12
+ word.capitalize() if i > 0 else word for i, word in enumerate(string.split("_"))
13
+ )
14
+ )
15
+
16
+ class GetItemsRequest(BaseAPIRequest):
17
+ item_ids: List[str] = Field(min_length=1, max_length=10)
18
+ partner_tag: str
19
+ partner_type: str = "Associates"
20
+ marketplace: str
21
+ resources: Optional[List[str]] = None
22
+ condition: Optional[str] = None
23
+ currency_of_preference: Optional[str] = None
24
+ languages_of_preference: Optional[List[str]] = None
25
+ merchant: Optional[str] = None
26
+
27
+ class SearchItemsRequest(BaseAPIRequest):
28
+ keywords: Optional[str] = None
29
+ actor: Optional[str] = None
30
+ artist: Optional[str] = None
31
+ author: Optional[str] = None
32
+ brand: Optional[str] = None
33
+ browse_node_id: Optional[str] = None
34
+ condition: Optional[str] = None
35
+ currency_of_preference: Optional[str] = None
36
+ delivery_flags: Optional[List[str]] = None
37
+ item_count: Optional[int] = Field(ge=1, le=10, default=10) # 10 items max per page usually
38
+ item_page: Optional[int] = Field(ge=1, le=10, default=1)
39
+ languages_of_preference: Optional[List[str]] = None
40
+ marketplace: str
41
+ merchant: Optional[str] = None
42
+ min_price: Optional[int] = None
43
+ max_price: Optional[int] = None
44
+ partner_tag: str
45
+ partner_type: str = "Associates"
46
+ resources: Optional[List[str]] = None
47
+ search_index: Optional[str] = "All"
48
+ sort_by: Optional[str] = None
49
+ title: Optional[str] = None
50
+
51
+ @model_validator(mode="after")
52
+ def validate_search_criteria(self):
53
+ if not any(
54
+ [
55
+ self.keywords,
56
+ self.actor,
57
+ self.artist,
58
+ self.author,
59
+ self.brand,
60
+ self.browse_node_id,
61
+ self.title,
62
+ ]
63
+ ):
64
+ raise ValueError(
65
+ "At least one search criterion must be provided: "
66
+ "keywords, actor, artist, author, brand, browse_node_id, or title."
67
+ )
68
+ return self
69
+
70
+ class GetBrowseNodesRequest(BaseAPIRequest):
71
+ browse_node_ids: List[str] = Field(min_length=1, max_length=10)
72
+ partner_tag: str
73
+ partner_type: str = "Associates"
74
+ marketplace: str
75
+ languages_of_preference: Optional[List[str]] = None
76
+ resources: Optional[List[str]] = None
77
+
78
+ class GetVariationsRequest(BaseAPIRequest):
79
+ asin: str
80
+ partner_tag: str
81
+ partner_type: str = "Associates"
82
+ marketplace: str
83
+ condition: Optional[str] = None
84
+ currency_of_preference: Optional[str] = None
85
+ languages_of_preference: Optional[List[str]] = None
86
+ merchant: Optional[str] = None
87
+ offer_count: Optional[int] = Field(ge=1, le=10, default=1)
88
+ resources: Optional[List[str]] = None
89
+ variation_count: Optional[int] = Field(ge=1, le=10, default=10)
90
+ variation_page: Optional[int] = Field(ge=1, le=10, default=1)
@@ -0,0 +1,98 @@
1
+ from typing import List, Optional, Dict, Any
2
+ from pydantic import BaseModel, ConfigDict, Field
3
+
4
+ class BaseAPIResponse(BaseModel):
5
+ """
6
+ Base configuration for Amazon Creators API responses.
7
+ Ensures lowerCamelCase mapping from the API JSON.
8
+ """
9
+ model_config = ConfigDict(
10
+ populate_by_name=True,
11
+ alias_generator=lambda string: "".join(
12
+ word.capitalize() if i > 0 else word for i, word in enumerate(string.split("_"))
13
+ )
14
+ )
15
+
16
+ class Image(BaseAPIResponse):
17
+ url: Optional[str] = Field(None, alias="url")
18
+ height: Optional[int] = None
19
+ width: Optional[int] = None
20
+
21
+ class Images(BaseAPIResponse):
22
+ primary: Optional[Dict[str, Image]] = None
23
+ variants: Optional[List[Dict[str, Image]]] = None
24
+
25
+ class Price(BaseAPIResponse):
26
+ amount: Optional[float] = None
27
+ currency: Optional[str] = None
28
+ display_amount: Optional[str] = None
29
+ savings: Optional[Dict[str, Any]] = None
30
+
31
+ class ItemInfo(BaseAPIResponse):
32
+ title: Optional[Dict[str, str]] = None
33
+ features: Optional[Dict[str, List[str]]] = None
34
+ by_line_info: Optional[Dict[str, Any]] = None
35
+
36
+ class Listing(BaseAPIResponse):
37
+ id: Optional[str] = None
38
+ price: Optional[Price] = None
39
+ delivery_info: Optional[Dict[str, Any]] = None
40
+ condition: Optional[Dict[str, str]] = None
41
+
42
+ class OffersV2(BaseAPIResponse):
43
+ listings: Optional[List[Listing]] = None
44
+ summaries: Optional[List[Dict[str, Any]]] = None
45
+
46
+ class Item(BaseAPIResponse):
47
+ asin: str
48
+ detail_page_url: Optional[str] = None
49
+ images: Optional[Images] = None
50
+ item_info: Optional[ItemInfo] = None
51
+ offers_v2: Optional[OffersV2] = None
52
+
53
+ class SearchResult(BaseAPIResponse):
54
+ total_result_count: Optional[int] = None
55
+ search_url: Optional[str] = None
56
+ items: List[Item] = Field(default_factory=list)
57
+
58
+ class SearchItemsResponse(BaseAPIResponse):
59
+ search_result: Optional[SearchResult] = None
60
+ errors: Optional[List[Dict[str, str]]] = None
61
+
62
+ class GetItemsResult(BaseAPIResponse):
63
+ items: List[Item] = Field(default_factory=list)
64
+
65
+ class GetItemsResponse(BaseAPIResponse):
66
+ items_result: Optional[GetItemsResult] = None
67
+ errors: Optional[List[Dict[str, str]]] = None
68
+
69
+ # Model for Traverse Tree of BrowseNodes
70
+ class BrowseNode(BaseAPIResponse):
71
+ id: Optional[str] = None
72
+ display_name: Optional[str] = None
73
+ context_free_name: Optional[str] = None
74
+ is_root: Optional[bool] = None
75
+ ancestor: Optional['BrowseNode'] = None
76
+ children: Optional[List['BrowseNode']] = None
77
+
78
+ class BrowseNodesResult(BaseAPIResponse):
79
+ browse_nodes: List[BrowseNode] = Field(default_factory=list)
80
+
81
+ class GetBrowseNodesResponse(BaseAPIResponse):
82
+ browse_nodes_result: Optional[BrowseNodesResult] = None
83
+ errors: Optional[List[Dict[str, str]]] = None
84
+
85
+ class VariationDimension(BaseAPIResponse):
86
+ display_name: str
87
+ name: str
88
+
89
+ class VariationsResult(BaseAPIResponse):
90
+ items: List[Item] = Field(default_factory=list)
91
+ variation_dimensions: Optional[List[VariationDimension]] = None
92
+
93
+ class GetVariationsResponse(BaseAPIResponse):
94
+ variations_result: Optional[VariationsResult] = None
95
+ errors: Optional[List[Dict[str, str]]] = None
96
+
97
+ # Deal with recursive model definition for Pydantic v2
98
+ BrowseNode.model_rebuild()
@@ -0,0 +1,51 @@
1
+ """
2
+ Utilities and constants for the Amazon Creators API.
3
+ """
4
+
5
+ from enum import Enum
6
+
7
+ class Region(str, Enum):
8
+ NORTH_AMERICA = "NA"
9
+ EUROPE = "EU"
10
+ FAR_EAST = "FE"
11
+
12
+ def get_api_endpoint(region: Region) -> str:
13
+ """Returns the endpoint for the respective region."""
14
+ return "https://creatorsapi.amazon/catalog/v1"
15
+
16
+ def get_version_for_region(region: Region) -> str:
17
+ """Returns the authorization version for a given Region."""
18
+ if region == Region.NORTH_AMERICA:
19
+ return "2.1"
20
+ elif region == Region.EUROPE:
21
+ return "2.2"
22
+ elif region == Region.FAR_EAST:
23
+ return "2.3"
24
+ raise ValueError(f"Unknown region: {region}")
25
+
26
+ def get_auth_endpoint(version: str) -> str:
27
+ """Returns the correct OAuth2 token endpoint based on version."""
28
+ if version == "2.1":
29
+ return "https://creatorsapi.auth.us-east-1.amazoncognito.com/oauth2/token"
30
+ elif version == "2.2":
31
+ return "https://creatorsapi.auth.eu-south-2.amazoncognito.com/oauth2/token"
32
+ elif version == "2.3":
33
+ return "https://creatorsapi.auth.us-west-2.amazoncognito.com/oauth2/token"
34
+ elif version == "3.1":
35
+ return "https://api.amazon.com/auth/o2/token"
36
+ elif version == "3.2":
37
+ return "https://api.amazon.co.uk/auth/o2/token"
38
+ elif version == "3.3":
39
+ return "https://api.amazon.co.jp/auth/o2/token"
40
+ raise ValueError(f"Unsupported version: {version}")
41
+
42
+ def get_scope(version: str) -> str:
43
+ """Returns the correct OAuth2 scope based on version."""
44
+ if version.startswith("3."):
45
+ return "creatorsapi::default" # LWA scope
46
+ return "creatorsapi/default" # Cognito scope
47
+
48
+ def validate_marketplace(marketplace: str):
49
+ """Simple check for typical marketplace domains."""
50
+ if not marketplace.startswith("www.amazon."):
51
+ raise ValueError("Marketplace must be a valid Amazon domain (e.g. 'www.amazon.com', 'www.amazon.com.br')")
@@ -0,0 +1,50 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "amazon-creators-async"
7
+ version = "0.1.0"
8
+ description = "A modern, high-performance asynchronous Python wrapper for the new Amazon Creators API (OAuth 2.0 based)."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "Igor", email = "igor@example.com" }
14
+ ]
15
+ keywords = ["amazon", "creators", "api", "affiliate", "async", "httpx", "oauth2"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Operating System :: OS Independent",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.8",
23
+ "Programming Language :: Python :: 3.9",
24
+ "Programming Language :: Python :: 3.10",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Topic :: Software Development :: Libraries :: Python Modules",
28
+ "Topic :: Internet :: WWW/HTTP",
29
+ ]
30
+ dependencies = [
31
+ "httpx>=0.24.0",
32
+ "pydantic>=2.0.0",
33
+ "aiolimiter>=1.1.0"
34
+ ]
35
+
36
+ [project.optional-dependencies]
37
+ dev = [
38
+ "pytest>=8.0.0",
39
+ "pytest-asyncio>=0.23.0",
40
+ ]
41
+
42
+ [project.urls]
43
+ Homepage = "https://github.com/ils15/amazon-creators-async"
44
+ Repository = "https://github.com/ils15/amazon-creators-async.git"
45
+ "Bug Tracker" = "https://github.com/ils15/amazon-creators-async/issues"
46
+
47
+ [tool.pytest.ini_options]
48
+ asyncio_mode = "auto"
49
+ testpaths = ["tests", "."]
50
+