python-amazon-sp-api 1.9.18__py3-none-any.whl → 2.0.7__py3-none-any.whl
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.
- {python_amazon_sp_api-1.9.18.data → python_amazon_sp_api-2.0.7.data}/scripts/make_endpoint +2 -2
- {python_amazon_sp_api-1.9.18.dist-info → python_amazon_sp_api-2.0.7.dist-info}/METADATA +46 -23
- python_amazon_sp_api-2.0.7.dist-info/RECORD +251 -0
- {python_amazon_sp_api-1.9.18.dist-info → python_amazon_sp_api-2.0.7.dist-info}/WHEEL +1 -1
- {python_amazon_sp_api-1.9.18.dist-info → python_amazon_sp_api-2.0.7.dist-info}/top_level.txt +0 -1
- sp_api/__version__.py +1 -1
- sp_api/api/__init__.py +18 -1
- sp_api/api/application_integrations/application_integrations.py +118 -0
- sp_api/api/application_management/application_management.py +2 -1
- sp_api/api/catalog/catalog.py +3 -4
- sp_api/api/catalog_items/catalog_items.py +3 -6
- sp_api/api/customer_feedback/customer_feedback.py +110 -0
- sp_api/api/data_kiosk/data_kiosk.py +5 -6
- sp_api/api/easy_ship/easy_ship.py +190 -0
- sp_api/api/external_fulfillment/external_fulfillment.py +706 -0
- sp_api/api/feeds/feeds.py +11 -8
- sp_api/api/finances/finances.py +30 -4
- sp_api/api/fulfillment_inbound/fulfillment_inbound.py +35 -2
- sp_api/api/inventories/inventories.py +2 -7
- sp_api/api/listings_items/listings_items.py +3 -24
- sp_api/api/messaging/messaging.py +42 -0
- sp_api/api/orders/orders.py +7 -0
- sp_api/api/product_fees/product_fees.py +31 -74
- sp_api/api/products/products.py +80 -2
- sp_api/api/products/products_definitions.py +11 -85
- sp_api/api/reports/reports.py +65 -97
- sp_api/api/sales/sales.py +2 -2
- sp_api/asyncio/api/__init__.py +164 -0
- sp_api/asyncio/api/amazon_warehousing_and_distribu/__init__.py +9 -0
- sp_api/asyncio/api/amazon_warehousing_and_distribu/amazon_warehousing_and_distribu.py +130 -0
- sp_api/asyncio/api/aplus_content/__init__.py +5 -0
- sp_api/asyncio/api/aplus_content/aplus_content.py +330 -0
- sp_api/asyncio/api/application_integrations/__init__.py +5 -0
- sp_api/asyncio/api/application_integrations/application_integrations.py +119 -0
- sp_api/asyncio/api/application_management/__init__.py +5 -0
- sp_api/asyncio/api/application_management/application_management.py +36 -0
- sp_api/asyncio/api/authorization/__init__.py +5 -0
- sp_api/asyncio/api/authorization/authorization.py +54 -0
- sp_api/asyncio/api/catalog/__init__.py +5 -0
- sp_api/asyncio/api/catalog/catalog.py +111 -0
- sp_api/asyncio/api/catalog_items/__init__.py +6 -0
- sp_api/asyncio/api/catalog_items/catalog_items.py +93 -0
- sp_api/asyncio/api/clients/__init__.py +1 -0
- sp_api/asyncio/api/customer_feedback/__init__.py +5 -0
- sp_api/asyncio/api/customer_feedback/customer_feedback.py +111 -0
- sp_api/asyncio/api/data_kiosk/__init__.py +5 -0
- sp_api/asyncio/api/data_kiosk/data_kiosk.py +236 -0
- sp_api/asyncio/api/easy_ship/__init__.py +5 -0
- sp_api/asyncio/api/easy_ship/easy_ship.py +191 -0
- sp_api/asyncio/api/external_fulfillment/__init__.py +5 -0
- sp_api/asyncio/api/external_fulfillment/external_fulfillment.py +706 -0
- sp_api/asyncio/api/fba_inbound_eligibility/__init__.py +5 -0
- sp_api/asyncio/api/fba_inbound_eligibility/fba_inbound_eligibility.py +96 -0
- sp_api/asyncio/api/fba_small_and_light/__init__.py +5 -0
- sp_api/asyncio/api/fba_small_and_light/fba_small_and_light.py +213 -0
- sp_api/asyncio/api/feeds/feeds.py +260 -0
- sp_api/asyncio/api/finances/finances.py +100 -0
- sp_api/asyncio/api/fulfillment_inbound/fulfillment_inbound.py +1798 -0
- sp_api/asyncio/api/fulfillment_outbound/fulfillment_outbound.py +736 -0
- sp_api/asyncio/api/inventories/inventories.py +74 -0
- sp_api/asyncio/api/listings_items/__init__.py +0 -0
- sp_api/asyncio/api/listings_items/listings_items.py +170 -0
- sp_api/asyncio/api/listings_restrictions/__init__.py +0 -0
- sp_api/asyncio/api/listings_restrictions/listings_restrictions.py +36 -0
- sp_api/asyncio/api/merchant_fulfillment/__init__.py +0 -0
- sp_api/asyncio/api/merchant_fulfillment/merchant_fulfillment.py +384 -0
- sp_api/asyncio/api/messaging/__init__.py +0 -0
- sp_api/asyncio/api/messaging/messaging.py +511 -0
- sp_api/asyncio/api/models/__init__.py +4 -0
- sp_api/asyncio/api/notifications/__init__.py +0 -0
- sp_api/asyncio/api/notifications/notifications.py +295 -0
- sp_api/asyncio/api/orders/__init__.py +0 -0
- sp_api/asyncio/api/orders/orders.py +362 -0
- sp_api/asyncio/api/overrides/__init__.py +1 -0
- sp_api/asyncio/api/product_fees/__init__.py +0 -0
- sp_api/asyncio/api/product_fees/product_fees.py +194 -0
- sp_api/asyncio/api/product_type_definitions/__init__.py +0 -0
- sp_api/asyncio/api/product_type_definitions/product_type_definitions.py +75 -0
- sp_api/asyncio/api/products/__init__.py +0 -0
- sp_api/asyncio/api/products/products.py +405 -0
- sp_api/asyncio/api/products/products_definitions.py +11 -0
- sp_api/asyncio/api/replenishment/__init__.py +0 -0
- sp_api/asyncio/api/replenishment/replenishment.py +121 -0
- sp_api/asyncio/api/reports/__init__.py +0 -0
- sp_api/asyncio/api/reports/reports.py +439 -0
- sp_api/asyncio/api/sales/__init__.py +0 -0
- sp_api/asyncio/api/sales/sales.py +93 -0
- sp_api/asyncio/api/sellers/__init__.py +0 -0
- sp_api/asyncio/api/sellers/sellers.py +70 -0
- sp_api/asyncio/api/services/__init__.py +0 -0
- sp_api/asyncio/api/services/services.py +218 -0
- sp_api/asyncio/api/shipping/__init__.py +0 -0
- sp_api/asyncio/api/shipping/shipping.py +459 -0
- sp_api/asyncio/api/shipping/shippingV2.py +651 -0
- sp_api/asyncio/api/solicitations/__init__.py +0 -0
- sp_api/asyncio/api/solicitations/solicitations.py +78 -0
- sp_api/asyncio/api/supply_sources/__init__.py +0 -0
- sp_api/asyncio/api/supply_sources/supply_sources.py +138 -0
- sp_api/asyncio/api/tokens/__init__.py +0 -0
- sp_api/asyncio/api/tokens/tokens.py +65 -0
- sp_api/asyncio/api/upload/__init__.py +0 -0
- sp_api/asyncio/api/upload/upload.py +18 -0
- sp_api/asyncio/api/vendor_direct_fulfillment_inventory/__init__.py +0 -0
- sp_api/asyncio/api/vendor_direct_fulfillment_inventory/vendor_direct_fulfillment_inventory.py +64 -0
- sp_api/asyncio/api/vendor_direct_fulfillment_orders/__init__.py +0 -0
- sp_api/asyncio/api/vendor_direct_fulfillment_orders/vendor_direct_fulfillment_orders.py +196 -0
- sp_api/asyncio/api/vendor_direct_fulfillment_payments/__init__.py +0 -0
- sp_api/asyncio/api/vendor_direct_fulfillment_payments/vendor_direct_fulfillment_payments.py +254 -0
- sp_api/asyncio/api/vendor_direct_fulfillment_shipping/__init__.py +0 -0
- sp_api/asyncio/api/vendor_direct_fulfillment_shipping/vendor_direct_fulfillment_shipping.py +627 -0
- sp_api/asyncio/api/vendor_direct_fulfillment_transactions/__init__.py +0 -0
- sp_api/asyncio/api/vendor_direct_fulfillment_transactions/vendor_direct_fulfillment_transactions.py +43 -0
- sp_api/asyncio/api/vendor_invoices/__init__.py +0 -0
- sp_api/asyncio/api/vendor_invoices/vendor_invoices.py +295 -0
- sp_api/asyncio/api/vendor_orders/__init__.py +0 -0
- sp_api/asyncio/api/vendor_orders/vendor_orders.py +210 -0
- sp_api/asyncio/api/vendor_shipments/__init__.py +0 -0
- sp_api/asyncio/api/vendor_shipments/vendor_shipments.py +118 -0
- sp_api/asyncio/api/vendor_transaction_status/__init__.py +0 -0
- sp_api/asyncio/api/vendor_transaction_status/vendor_transaction_status.py +41 -0
- sp_api/asyncio/auth/__init__.py +12 -0
- sp_api/asyncio/auth/access_token_client.py +145 -0
- sp_api/asyncio/auth/exceptions.py +5 -0
- sp_api/asyncio/base/__init__.py +53 -0
- sp_api/asyncio/base/_transport_httpx.py +50 -0
- sp_api/asyncio/base/base_client.py +8 -0
- sp_api/asyncio/base/client.py +169 -0
- sp_api/asyncio/util/__init__.py +29 -0
- sp_api/asyncio/util/key_maker.py +5 -0
- sp_api/asyncio/util/load_all_pages.py +55 -0
- sp_api/asyncio/util/load_date_bound.py +53 -0
- sp_api/asyncio/util/retry.py +88 -0
- sp_api/auth/_core.py +39 -0
- sp_api/auth/access_token_client.py +18 -29
- sp_api/base/ApiResponse.py +3 -2
- sp_api/base/_core.py +110 -0
- sp_api/base/_transport_httpx.py +39 -0
- sp_api/base/client.py +40 -63
- sp_api/base/helpers.py +1 -1
- sp_api/base/reportTypes.py +3 -2
- sp_api/util/__init__.py +36 -0
- sp_api/util/load_all_pages.py +2 -1
- sp_api/util/params.py +57 -0
- sp_api/util/product_fees.py +40 -0
- sp_api/util/products_definitions.py +169 -0
- sp_api/util/report_document.py +154 -0
- python_amazon_sp_api-1.9.18.dist-info/RECORD +0 -144
- tests/api/finances/test_finances.py +0 -19
- tests/api/notifications/test_notifications.py +0 -26
- tests/api/orders/test_orders.py +0 -122
- tests/api/product_fees/product_fees.py +0 -49
- tests/api/reports/test_reports.py +0 -127
- tests/client/test_auth.py +0 -59
- tests/client/test_base.py +0 -163
- tests/client/test_credential_provider.py +0 -45
- tests/client/test_helpers.py +0 -142
- {python_amazon_sp_api-1.9.18.dist-info → python_amazon_sp_api-2.0.7.dist-info/licenses}/LICENSE +0 -0
- {tests → sp_api/api/application_integrations}/__init__.py +0 -0
- {tests/api → sp_api/api/customer_feedback}/__init__.py +0 -0
- {tests/api/finances → sp_api/api/easy_ship}/__init__.py +0 -0
- {tests/api/notifications → sp_api/api/external_fulfillment}/__init__.py +0 -0
- {tests/api/orders → sp_api/asyncio}/__init__.py +0 -0
- {tests/api/product_fees → sp_api/asyncio/api/feeds}/__init__.py +0 -0
- {tests/api/reports → sp_api/asyncio/api/finances}/__init__.py +0 -0
- {tests/api/sellers → sp_api/asyncio/api/fulfillment_inbound}/__init__.py +0 -0
- {tests/client → sp_api/asyncio/api/fulfillment_outbound}/__init__.py +0 -0
- /tests/api/sellers/test_sellers.py → /sp_api/asyncio/api/inventories/__init__.py +0 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import urllib.parse
|
|
2
|
+
|
|
3
|
+
from sp_api.base import Client, sp_endpoint, fill_query_params, ApiResponse
|
|
4
|
+
from sp_api.asyncio.base import AsyncBaseClient
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class VendorTransactionStatus(AsyncBaseClient):
|
|
8
|
+
"""
|
|
9
|
+
VendorTransactionStatus SP-API Client
|
|
10
|
+
:link:
|
|
11
|
+
|
|
12
|
+
The Selling Partner API for Retail Procurement Transaction Status provides programmatic access to status information on specific asynchronous POST transactions for vendors.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
@sp_endpoint("/vendor/transactions/v1/transactions/{}", method="GET")
|
|
16
|
+
async def get_transaction(self, transactionId, **kwargs) -> ApiResponse:
|
|
17
|
+
"""
|
|
18
|
+
get_transaction(self, transactionId, **kwargs) -> ApiResponse
|
|
19
|
+
|
|
20
|
+
Returns the status of the transaction that you specify.
|
|
21
|
+
|
|
22
|
+
**Usage Plans:**
|
|
23
|
+
|
|
24
|
+
====================================== ==============
|
|
25
|
+
Rate (requests per second) Burst
|
|
26
|
+
====================================== ==============
|
|
27
|
+
10 10
|
|
28
|
+
====================================== ==============
|
|
29
|
+
|
|
30
|
+
The x-amzn-RateLimit-Limit response header returns the usage plan rate limits that were applied to the requested operation. Rate limits for some selling partners will vary from the default rate and burst shown in the table above. For more information, see "Usage Plans and Rate Limits" in the Selling Partner API documentation.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
transactionId:string | * REQUIRED The GUID provided by Amazon in the 'transactionId' field in response to the post request of a specific transaction.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
ApiResponse:
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
return await self._request(
|
|
40
|
+
fill_query_params(kwargs.pop("path"), transactionId), params=kwargs
|
|
41
|
+
)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from sp_api.auth.access_token_response import AccessTokenResponse
|
|
2
|
+
from sp_api.auth.credentials import Credentials
|
|
3
|
+
from sp_api.auth.exceptions import AuthorizationError
|
|
4
|
+
|
|
5
|
+
from .access_token_client import AccessTokenClient
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"AccessTokenResponse",
|
|
9
|
+
"AccessTokenClient",
|
|
10
|
+
"AuthorizationError",
|
|
11
|
+
"Credentials",
|
|
12
|
+
]
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from cachetools import TTLCache
|
|
6
|
+
|
|
7
|
+
from sp_api.auth.access_token_response import AccessTokenResponse
|
|
8
|
+
from sp_api.auth.credentials import Credentials
|
|
9
|
+
from sp_api.auth.exceptions import AuthorizationError
|
|
10
|
+
from sp_api.auth._core import (
|
|
11
|
+
build_auth_code_request_body,
|
|
12
|
+
build_cache_key,
|
|
13
|
+
build_grantless_data,
|
|
14
|
+
build_headers,
|
|
15
|
+
build_refresh_token_data,
|
|
16
|
+
)
|
|
17
|
+
from sp_api.asyncio.base.base_client import BaseClient
|
|
18
|
+
from sp_api.asyncio.base._transport_httpx import AsyncHttpxTransport
|
|
19
|
+
|
|
20
|
+
cache = TTLCache(
|
|
21
|
+
maxsize=int(os.environ.get("SP_API_AUTH_CACHE_SIZE", 10)),
|
|
22
|
+
ttl=int(os.environ.get("SP_API_AUTH_CACHE_TTL", 3200)),
|
|
23
|
+
)
|
|
24
|
+
grantless_cache = TTLCache(
|
|
25
|
+
maxsize=int(os.environ.get("SP_API_AUTH_CACHE_SIZE", 10)),
|
|
26
|
+
ttl=int(os.environ.get("SP_API_AUTH_CACHE_TTL", 3200)),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AccessTokenClient(BaseClient):
|
|
33
|
+
host = "api.amazon.com"
|
|
34
|
+
grant_type = "refresh_token"
|
|
35
|
+
path = "/auth/o2/token"
|
|
36
|
+
|
|
37
|
+
def __init__(self, refresh_token=None, credentials=None, proxies=None, verify=True):
|
|
38
|
+
self.cred = Credentials(refresh_token, credentials)
|
|
39
|
+
self.proxies = proxies
|
|
40
|
+
self.verify = verify
|
|
41
|
+
self._lock = asyncio.Lock()
|
|
42
|
+
self._transport = AsyncHttpxTransport(
|
|
43
|
+
proxies=proxies,
|
|
44
|
+
verify=verify,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
async def _request(self, url, data, headers):
|
|
48
|
+
response = await self._transport.request(
|
|
49
|
+
"POST",
|
|
50
|
+
url,
|
|
51
|
+
data=data,
|
|
52
|
+
headers=headers,
|
|
53
|
+
)
|
|
54
|
+
response_data = response.json()
|
|
55
|
+
if response.status_code != 200:
|
|
56
|
+
error_message = response_data.get("error_description")
|
|
57
|
+
error_code = response_data.get("error")
|
|
58
|
+
raise AuthorizationError(error_code, error_message, response.status_code)
|
|
59
|
+
return response_data
|
|
60
|
+
|
|
61
|
+
async def get_auth(self) -> AccessTokenResponse:
|
|
62
|
+
"""
|
|
63
|
+
Get's the access token
|
|
64
|
+
:return:AccessTokenResponse
|
|
65
|
+
"""
|
|
66
|
+
cache_key = self._get_cache_key()
|
|
67
|
+
try:
|
|
68
|
+
access_token = cache[cache_key]
|
|
69
|
+
except KeyError:
|
|
70
|
+
async with self._lock:
|
|
71
|
+
try:
|
|
72
|
+
access_token = cache[cache_key]
|
|
73
|
+
except KeyError:
|
|
74
|
+
request_url = self.scheme + self.host + self.path
|
|
75
|
+
access_token = await self._request(
|
|
76
|
+
request_url,
|
|
77
|
+
self.data,
|
|
78
|
+
self.headers,
|
|
79
|
+
)
|
|
80
|
+
cache[cache_key] = access_token
|
|
81
|
+
return AccessTokenResponse(**access_token)
|
|
82
|
+
|
|
83
|
+
async def get_grantless_auth(self, scope="sellingpartnerapi::notifications"):
|
|
84
|
+
"""
|
|
85
|
+
:param scope: One of allowed scope for grantless operations:
|
|
86
|
+
sellingpartnerapi::notifications or sellingpartnerapi::migration
|
|
87
|
+
See: https://github.com/amzn/selling-partner-api-docs/blob/main/guides/en-US/developer-guide/SellingPartnerApiDeveloperGuide.md#step-3-configure-your-lwa-credentials
|
|
88
|
+
|
|
89
|
+
POST /auth/o2/token HTTP/l.l
|
|
90
|
+
Host: api.amazon.com
|
|
91
|
+
Content-Type: application/x-www-form-urlencoded;charset=UTF-8
|
|
92
|
+
grant_type=client_credentials
|
|
93
|
+
&scope=sellingpartnerapi::notifications
|
|
94
|
+
&client_id=foodev
|
|
95
|
+
&client_secret=Y76SDl2F
|
|
96
|
+
:return: AccessTokenResponse
|
|
97
|
+
"""
|
|
98
|
+
cache_key = self._get_cache_key(scope)
|
|
99
|
+
try:
|
|
100
|
+
access_token = grantless_cache[cache_key]
|
|
101
|
+
logger.debug("from_cache. scope: %s", scope)
|
|
102
|
+
except KeyError:
|
|
103
|
+
async with self._lock:
|
|
104
|
+
try:
|
|
105
|
+
access_token = grantless_cache[cache_key]
|
|
106
|
+
except KeyError:
|
|
107
|
+
request_url = self.scheme + self.host + self.path
|
|
108
|
+
access_token = await self._request(
|
|
109
|
+
request_url,
|
|
110
|
+
data=self.grantless_data(scope),
|
|
111
|
+
headers=self.headers,
|
|
112
|
+
)
|
|
113
|
+
logger.debug("token_refreshed")
|
|
114
|
+
grantless_cache.clear()
|
|
115
|
+
grantless_cache[cache_key] = access_token
|
|
116
|
+
|
|
117
|
+
return AccessTokenResponse(**access_token)
|
|
118
|
+
|
|
119
|
+
async def authorize_auth_code(self, auth_code):
|
|
120
|
+
request_url = self.scheme + self.host + self.path
|
|
121
|
+
return await self._request(
|
|
122
|
+
request_url,
|
|
123
|
+
data=self._auth_code_request_body(auth_code),
|
|
124
|
+
headers=self.headers,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def _auth_code_request_body(self, auth_code):
|
|
128
|
+
return build_auth_code_request_body(self.cred, auth_code)
|
|
129
|
+
|
|
130
|
+
def grantless_data(self, scope_value: str):
|
|
131
|
+
return build_grantless_data(self.cred, scope_value)
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def data(self):
|
|
135
|
+
return build_refresh_token_data(self.cred, self.grant_type)
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def headers(self):
|
|
139
|
+
return build_headers(self.user_agent, self.content_type)
|
|
140
|
+
|
|
141
|
+
def _get_cache_key(self, token_flavor=""):
|
|
142
|
+
return build_cache_key(self.cred.refresh_token, token_flavor)
|
|
143
|
+
|
|
144
|
+
async def aclose(self):
|
|
145
|
+
await self._transport.aclose()
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from .base_client import BaseClient
|
|
2
|
+
from .client import Client
|
|
3
|
+
|
|
4
|
+
AsyncBaseClient = Client
|
|
5
|
+
|
|
6
|
+
from sp_api.base import (
|
|
7
|
+
Marketplaces,
|
|
8
|
+
ReportType,
|
|
9
|
+
FeedType,
|
|
10
|
+
ProcessingStatus,
|
|
11
|
+
Schedules,
|
|
12
|
+
ReportStatus,
|
|
13
|
+
FirstDayOfWeek,
|
|
14
|
+
Granularity,
|
|
15
|
+
BuyerType,
|
|
16
|
+
FulfillmentChannel,
|
|
17
|
+
NotificationType,
|
|
18
|
+
IncludedData,
|
|
19
|
+
ListingItemsIncludedData,
|
|
20
|
+
CatalogItemsIncludedData,
|
|
21
|
+
IneligibilityReasonList,
|
|
22
|
+
AwsEnv,
|
|
23
|
+
)
|
|
24
|
+
from sp_api.base.credential_provider import (
|
|
25
|
+
FromCodeCredentialProvider,
|
|
26
|
+
FromSecretsCredentialProvider,
|
|
27
|
+
FromCachedSecretsCredentialProvider,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"Marketplaces",
|
|
32
|
+
"ReportType",
|
|
33
|
+
"FeedType",
|
|
34
|
+
"ProcessingStatus",
|
|
35
|
+
"Schedules",
|
|
36
|
+
"ReportStatus",
|
|
37
|
+
"FirstDayOfWeek",
|
|
38
|
+
"Granularity",
|
|
39
|
+
"BuyerType",
|
|
40
|
+
"FulfillmentChannel",
|
|
41
|
+
"NotificationType",
|
|
42
|
+
"IncludedData",
|
|
43
|
+
"ListingItemsIncludedData",
|
|
44
|
+
"CatalogItemsIncludedData",
|
|
45
|
+
"IneligibilityReasonList",
|
|
46
|
+
"AwsEnv",
|
|
47
|
+
"FromCodeCredentialProvider",
|
|
48
|
+
"FromSecretsCredentialProvider",
|
|
49
|
+
"FromCachedSecretsCredentialProvider",
|
|
50
|
+
"BaseClient",
|
|
51
|
+
"Client",
|
|
52
|
+
"AsyncBaseClient",
|
|
53
|
+
]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AsyncHttpxTransport:
|
|
5
|
+
def __init__(
|
|
6
|
+
self, *, timeout=None, proxies=None, proxy=None, verify=True, client=None
|
|
7
|
+
):
|
|
8
|
+
proxy_config = proxy if proxy is not None else proxies
|
|
9
|
+
self._client = client or httpx.AsyncClient(
|
|
10
|
+
timeout=timeout,
|
|
11
|
+
proxy=proxy_config,
|
|
12
|
+
verify=verify,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
async def request(
|
|
16
|
+
self,
|
|
17
|
+
method,
|
|
18
|
+
url,
|
|
19
|
+
*,
|
|
20
|
+
params=None,
|
|
21
|
+
data=None,
|
|
22
|
+
content=None,
|
|
23
|
+
headers=None,
|
|
24
|
+
timeout=None,
|
|
25
|
+
):
|
|
26
|
+
return await self._client.request(
|
|
27
|
+
method,
|
|
28
|
+
url,
|
|
29
|
+
params=params,
|
|
30
|
+
data=data,
|
|
31
|
+
content=content,
|
|
32
|
+
headers=headers,
|
|
33
|
+
timeout=timeout,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def stream(
|
|
37
|
+
self, method, url, *, params=None, data=None, content=None, headers=None, timeout=None
|
|
38
|
+
):
|
|
39
|
+
return self._client.stream(
|
|
40
|
+
method,
|
|
41
|
+
url,
|
|
42
|
+
params=params,
|
|
43
|
+
data=data,
|
|
44
|
+
content=content,
|
|
45
|
+
headers=headers,
|
|
46
|
+
timeout=timeout,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
async def aclose(self):
|
|
50
|
+
await self._client.aclose()
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from sp_api.auth import AccessTokenResponse
|
|
6
|
+
from sp_api.base.ApiResponse import ApiResponse
|
|
7
|
+
from sp_api.base.credential_provider import CredentialProvider
|
|
8
|
+
from sp_api.base.marketplaces import Marketplaces
|
|
9
|
+
from sp_api.base._core import parse_response, prepare_request, resolve_method
|
|
10
|
+
|
|
11
|
+
from ._transport_httpx import AsyncHttpxTransport
|
|
12
|
+
from .base_client import BaseClient
|
|
13
|
+
from ..auth import AccessTokenClient
|
|
14
|
+
from sp_api.base.exceptions import MissingScopeException
|
|
15
|
+
|
|
16
|
+
log = logging.getLogger(__name__)
|
|
17
|
+
log.setLevel(logging.INFO) # Set default to DEBUG; users can override externally
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Client(BaseClient):
|
|
21
|
+
grantless_scope: str = ""
|
|
22
|
+
keep_restricted_data_token: bool = False
|
|
23
|
+
version = None
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
marketplace: Marketplaces = Marketplaces[
|
|
28
|
+
os.environ.get("SP_API_DEFAULT_MARKETPLACE", Marketplaces.US.name)
|
|
29
|
+
],
|
|
30
|
+
*,
|
|
31
|
+
refresh_token=None,
|
|
32
|
+
account="default",
|
|
33
|
+
credentials=None,
|
|
34
|
+
restricted_data_token=None,
|
|
35
|
+
proxies=None,
|
|
36
|
+
verify=True,
|
|
37
|
+
timeout=None,
|
|
38
|
+
version=None,
|
|
39
|
+
credential_providers=None,
|
|
40
|
+
auth_token_client_class=AccessTokenClient,
|
|
41
|
+
):
|
|
42
|
+
if os.environ.get("SP_API_DEFAULT_MARKETPLACE", None):
|
|
43
|
+
marketplace = Marketplaces[os.environ.get("SP_API_DEFAULT_MARKETPLACE")]
|
|
44
|
+
self.credentials = CredentialProvider(
|
|
45
|
+
account,
|
|
46
|
+
credentials,
|
|
47
|
+
credential_providers=credential_providers,
|
|
48
|
+
).credentials
|
|
49
|
+
|
|
50
|
+
self.endpoint = marketplace.endpoint
|
|
51
|
+
self.marketplace_id = marketplace.marketplace_id
|
|
52
|
+
self.region = marketplace.region
|
|
53
|
+
self.restricted_data_token = restricted_data_token
|
|
54
|
+
self._auth = auth_token_client_class(
|
|
55
|
+
refresh_token=refresh_token,
|
|
56
|
+
credentials=self.credentials,
|
|
57
|
+
proxies=proxies,
|
|
58
|
+
verify=verify,
|
|
59
|
+
)
|
|
60
|
+
self.proxies = proxies
|
|
61
|
+
self.timeout = timeout
|
|
62
|
+
self.version = version
|
|
63
|
+
self.verify = verify
|
|
64
|
+
self._transport = AsyncHttpxTransport(
|
|
65
|
+
timeout=timeout,
|
|
66
|
+
proxies=proxies,
|
|
67
|
+
verify=verify,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
async def _get_headers(self, access_token=None):
|
|
71
|
+
token = access_token or self.restricted_data_token
|
|
72
|
+
if token is None:
|
|
73
|
+
token = (await self.auth).access_token
|
|
74
|
+
return {
|
|
75
|
+
"host": self.endpoint[8:],
|
|
76
|
+
"user-agent": self.user_agent,
|
|
77
|
+
"x-amz-access-token": token,
|
|
78
|
+
"x-amz-date": datetime.utcnow().strftime("%Y%m%dT%H%M%SZ"),
|
|
79
|
+
"content-type": "application/json",
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
async def auth(self) -> AccessTokenResponse:
|
|
84
|
+
return await self._auth.get_auth()
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
async def grantless_auth(self) -> AccessTokenResponse:
|
|
88
|
+
if not self.grantless_scope:
|
|
89
|
+
raise MissingScopeException("Grantless operations require scope")
|
|
90
|
+
return await self._auth.get_grantless_auth(self.grantless_scope)
|
|
91
|
+
|
|
92
|
+
async def _request(
|
|
93
|
+
self,
|
|
94
|
+
path: str,
|
|
95
|
+
*,
|
|
96
|
+
data: dict = None,
|
|
97
|
+
params: dict = None,
|
|
98
|
+
headers=None,
|
|
99
|
+
add_marketplace=True,
|
|
100
|
+
res_no_data: bool = False,
|
|
101
|
+
bulk: bool = False,
|
|
102
|
+
wrap_list: bool = False,
|
|
103
|
+
) -> ApiResponse:
|
|
104
|
+
method, params, data = resolve_method(params, data)
|
|
105
|
+
self.method = method
|
|
106
|
+
|
|
107
|
+
request_headers = headers or await self._get_headers()
|
|
108
|
+
|
|
109
|
+
log.debug("HTTP Method: %s", method)
|
|
110
|
+
log.debug("Making request to URL: %s", self.endpoint + self._check_version(path))
|
|
111
|
+
log.debug("Request Params: %s", params)
|
|
112
|
+
log.debug(
|
|
113
|
+
"Request Data: %s",
|
|
114
|
+
data if method in ("POST", "PUT", "PATCH") else None,
|
|
115
|
+
)
|
|
116
|
+
log.debug("Request Headers: %s", request_headers)
|
|
117
|
+
|
|
118
|
+
prepared = prepare_request(
|
|
119
|
+
method=method,
|
|
120
|
+
endpoint=self.endpoint,
|
|
121
|
+
path=path,
|
|
122
|
+
params=params,
|
|
123
|
+
data=data,
|
|
124
|
+
headers=request_headers,
|
|
125
|
+
add_marketplace=add_marketplace,
|
|
126
|
+
marketplace_id=self.marketplace_id,
|
|
127
|
+
version=self.version,
|
|
128
|
+
)
|
|
129
|
+
res = await self._transport.request(**prepared)
|
|
130
|
+
return parse_response(
|
|
131
|
+
res,
|
|
132
|
+
method=self.method,
|
|
133
|
+
res_no_data=res_no_data,
|
|
134
|
+
bulk=bulk,
|
|
135
|
+
wrap_list=wrap_list,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
async def _request_grantless_operation(
|
|
139
|
+
self, path: str, *, data: dict = None, params: dict = None
|
|
140
|
+
):
|
|
141
|
+
headers = await self._get_headers(
|
|
142
|
+
access_token=(await self.grantless_auth).access_token
|
|
143
|
+
)
|
|
144
|
+
log.debug("HTTP Method: %s", self.method)
|
|
145
|
+
log.debug("Making request to URL: %s", self.endpoint + self._check_version(path))
|
|
146
|
+
log.debug("Request Params: %s", params)
|
|
147
|
+
log.debug(
|
|
148
|
+
"Request Data: %s",
|
|
149
|
+
data if self.method in ("POST", "PUT", "PATCH") else None,
|
|
150
|
+
)
|
|
151
|
+
log.debug("Request Headers: %s", headers)
|
|
152
|
+
return await self._request(path, data=data, params=params, headers=headers)
|
|
153
|
+
|
|
154
|
+
def _check_version(self, path):
|
|
155
|
+
if "<version>" not in path:
|
|
156
|
+
return path
|
|
157
|
+
return path.replace("<version>", self.version)
|
|
158
|
+
|
|
159
|
+
async def __aenter__(self):
|
|
160
|
+
self.keep_restricted_data_token = True
|
|
161
|
+
return self
|
|
162
|
+
|
|
163
|
+
async def __aexit__(self, *args, **kwargs):
|
|
164
|
+
self.restricted_data_token = None
|
|
165
|
+
self.keep_restricted_data_token = False
|
|
166
|
+
await self.aclose()
|
|
167
|
+
|
|
168
|
+
async def aclose(self):
|
|
169
|
+
await self._transport.aclose()
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from .retry import retry, sp_retry, throttle_retry
|
|
2
|
+
from .load_all_pages import load_all_pages
|
|
3
|
+
from .key_maker import KeyMaker
|
|
4
|
+
from .load_date_bound import load_date_bound
|
|
5
|
+
from sp_api.util import (
|
|
6
|
+
normalize_csv_param,
|
|
7
|
+
normalize_included_data,
|
|
8
|
+
normalize_marketplace_ids,
|
|
9
|
+
normalize_datetime_kwargs,
|
|
10
|
+
encode_kwarg,
|
|
11
|
+
should_add_marketplace,
|
|
12
|
+
ensure_csv,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"retry",
|
|
17
|
+
"sp_retry",
|
|
18
|
+
"throttle_retry",
|
|
19
|
+
"load_all_pages",
|
|
20
|
+
"KeyMaker",
|
|
21
|
+
"load_date_bound",
|
|
22
|
+
"normalize_csv_param",
|
|
23
|
+
"normalize_included_data",
|
|
24
|
+
"normalize_marketplace_ids",
|
|
25
|
+
"normalize_datetime_kwargs",
|
|
26
|
+
"encode_kwarg",
|
|
27
|
+
"should_add_marketplace",
|
|
28
|
+
"ensure_csv",
|
|
29
|
+
]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def make_sleep_time(rate_limit, use_rate_limit_header, throttle_by_seconds):
|
|
5
|
+
if use_rate_limit_header and rate_limit:
|
|
6
|
+
return 1 / float(rate_limit)
|
|
7
|
+
return throttle_by_seconds
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def load_all_pages(
|
|
11
|
+
throttle_by_seconds: float = 2,
|
|
12
|
+
next_token_param="NextToken",
|
|
13
|
+
use_rate_limit_header: bool = False,
|
|
14
|
+
extras: dict = None,
|
|
15
|
+
next_token_only: bool = False,
|
|
16
|
+
):
|
|
17
|
+
"""
|
|
18
|
+
Load all pages if a next token is returned
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
throttle_by_seconds: float
|
|
22
|
+
next_token_param: str | The param amazon expects to hold the next token
|
|
23
|
+
use_rate_limit_header: if the function should try to use amazon's rate limit header
|
|
24
|
+
extras: additional data to be sent with NextToken, e.g `dict(QueryType='NEXT_TOKEN')` for `FulfillmentInbound`
|
|
25
|
+
next_token_only: remove all other params from kwargs, required for reports API
|
|
26
|
+
Returns:
|
|
27
|
+
Transforms the function in an async generator, returning all pages
|
|
28
|
+
"""
|
|
29
|
+
if not extras:
|
|
30
|
+
extras = {}
|
|
31
|
+
|
|
32
|
+
def decorator(function):
|
|
33
|
+
async def wrapper(*args, **kwargs):
|
|
34
|
+
done = False
|
|
35
|
+
while not done:
|
|
36
|
+
res = await function(*args, **kwargs)
|
|
37
|
+
|
|
38
|
+
yield res
|
|
39
|
+
if res.next_token:
|
|
40
|
+
sleep_time = make_sleep_time(
|
|
41
|
+
res.rate_limit, use_rate_limit_header, throttle_by_seconds
|
|
42
|
+
)
|
|
43
|
+
if sleep_time > 0:
|
|
44
|
+
await asyncio.sleep(sleep_time)
|
|
45
|
+
if next_token_only:
|
|
46
|
+
kwargs = {next_token_param: res.next_token}
|
|
47
|
+
else:
|
|
48
|
+
kwargs.update({next_token_param: res.next_token, **extras})
|
|
49
|
+
else:
|
|
50
|
+
done = True
|
|
51
|
+
|
|
52
|
+
wrapper.__doc__ = function.__doc__
|
|
53
|
+
return wrapper
|
|
54
|
+
|
|
55
|
+
return decorator
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def load_date_bound(interval_days: int = 30):
|
|
5
|
+
def make_end_date(s: datetime, e: datetime, i: int):
|
|
6
|
+
end_date = s + datetime.timedelta(days=i)
|
|
7
|
+
if end_date > e:
|
|
8
|
+
return e
|
|
9
|
+
return end_date
|
|
10
|
+
|
|
11
|
+
def parse_if_needed(dt: datetime or str):
|
|
12
|
+
if isinstance(dt, datetime.datetime):
|
|
13
|
+
return dt
|
|
14
|
+
return datetime.datetime.fromisoformat(dt)
|
|
15
|
+
|
|
16
|
+
date_range = {}
|
|
17
|
+
|
|
18
|
+
def decorator(function):
|
|
19
|
+
async def wrapper(*args, **kwargs):
|
|
20
|
+
date_range.update(
|
|
21
|
+
{
|
|
22
|
+
"dataStartTime": parse_if_needed(kwargs["dataStartTime"]),
|
|
23
|
+
"dataEndTime": parse_if_needed(
|
|
24
|
+
kwargs.get("dataEndTime", datetime.datetime.utcnow())
|
|
25
|
+
),
|
|
26
|
+
}
|
|
27
|
+
)
|
|
28
|
+
kwargs.update(
|
|
29
|
+
{
|
|
30
|
+
"dataEndTime": make_end_date(
|
|
31
|
+
date_range["dataStartTime"],
|
|
32
|
+
date_range["dataEndTime"],
|
|
33
|
+
interval_days,
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
while kwargs["dataStartTime"] < kwargs["dataEndTime"]:
|
|
38
|
+
yield await function(*args, **kwargs)
|
|
39
|
+
kwargs.update(
|
|
40
|
+
{
|
|
41
|
+
"dataStartTime": parse_if_needed(kwargs["dataEndTime"]),
|
|
42
|
+
"dataEndTime": make_end_date(
|
|
43
|
+
parse_if_needed(kwargs["dataEndTime"]),
|
|
44
|
+
date_range["dataEndTime"],
|
|
45
|
+
interval_days,
|
|
46
|
+
),
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
wrapper.__doc__ = function.__doc__
|
|
51
|
+
return wrapper
|
|
52
|
+
|
|
53
|
+
return decorator
|