python-amazon-sp-api 1.9.60__py3-none-any.whl → 2.0.3__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.60.dist-info → python_amazon_sp_api-2.0.3.dist-info}/METADATA +41 -14
- python_amazon_sp_api-2.0.3.dist-info/RECORD +249 -0
- sp_api/__version__.py +1 -1
- sp_api/api/catalog/catalog.py +3 -4
- sp_api/api/catalog_items/catalog_items.py +3 -6
- sp_api/api/data_kiosk/data_kiosk.py +5 -6
- sp_api/api/feeds/feeds.py +11 -8
- sp_api/api/fulfillment_inbound/fulfillment_inbound.py +2 -2
- sp_api/api/inventories/inventories.py +2 -7
- sp_api/api/listings_items/listings_items.py +3 -24
- sp_api/api/products/products.py +3 -1
- sp_api/api/reports/reports.py +61 -97
- sp_api/api/sales/sales.py +2 -2
- sp_api/asyncio/__init__.py +0 -0
- 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/__init__.py +0 -0
- sp_api/asyncio/api/feeds/feeds.py +260 -0
- sp_api/asyncio/api/finances/__init__.py +0 -0
- sp_api/asyncio/api/finances/finances.py +100 -0
- sp_api/asyncio/api/fulfillment_inbound/__init__.py +0 -0
- sp_api/asyncio/api/fulfillment_inbound/fulfillment_inbound.py +1798 -0
- sp_api/asyncio/api/fulfillment_outbound/__init__.py +0 -0
- sp_api/asyncio/api/fulfillment_outbound/fulfillment_outbound.py +736 -0
- sp_api/asyncio/api/inventories/__init__.py +0 -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 +356 -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 +237 -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 +170 -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 -30
- sp_api/base/_core.py +110 -0
- sp_api/base/_transport_httpx.py +39 -0
- sp_api/base/client.py +40 -63
- sp_api/util/__init__.py +16 -0
- sp_api/util/params.py +57 -0
- sp_api/util/report_document.py +154 -0
- python_amazon_sp_api-1.9.60.dist-info/RECORD +0 -133
- {python_amazon_sp_api-1.9.60.data → python_amazon_sp_api-2.0.3.data}/scripts/make_endpoint +0 -0
- {python_amazon_sp_api-1.9.60.dist-info → python_amazon_sp_api-2.0.3.dist-info}/WHEEL +0 -0
- {python_amazon_sp_api-1.9.60.dist-info → python_amazon_sp_api-2.0.3.dist-info}/licenses/LICENSE +0 -0
- {python_amazon_sp_api-1.9.60.dist-info → python_amazon_sp_api-2.0.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def retry(exception_classes=None, tries=10, delay=5, rate=1.3):
|
|
5
|
+
"""
|
|
6
|
+
retry(exception_classes=None, tries=10, delay=5, rate=1.3)
|
|
7
|
+
|
|
8
|
+
Retry a call against an endpoint <tries> time
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
exception_classes: tuple | The Exceptions to be caught
|
|
12
|
+
tries: int | How often the call should be retried
|
|
13
|
+
delay: float | The delay after an error was caught
|
|
14
|
+
rate: float | The rate to increment delay by
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
|
|
18
|
+
"""
|
|
19
|
+
if exception_classes is None:
|
|
20
|
+
exception_classes = (Exception,)
|
|
21
|
+
|
|
22
|
+
tries_counter = {"count": 1, "last_delay": delay}
|
|
23
|
+
|
|
24
|
+
def decorator(function):
|
|
25
|
+
async def wrapper(*args, **kwargs):
|
|
26
|
+
try:
|
|
27
|
+
return await function(*args, **kwargs)
|
|
28
|
+
except exception_classes as e:
|
|
29
|
+
if tries_counter.get("count") + 1 > tries:
|
|
30
|
+
raise e
|
|
31
|
+
|
|
32
|
+
delay_now = (
|
|
33
|
+
delay
|
|
34
|
+
if tries_counter.get("count") == 1
|
|
35
|
+
else tries_counter.get("last_delay") * rate
|
|
36
|
+
)
|
|
37
|
+
tries_counter.update(
|
|
38
|
+
{"count": tries_counter.get("count") + 1, "last_delay": delay_now}
|
|
39
|
+
)
|
|
40
|
+
await asyncio.sleep(delay_now)
|
|
41
|
+
return await wrapper(*args, **kwargs)
|
|
42
|
+
finally:
|
|
43
|
+
tries_counter.update({"count": 1, "last_delay": delay})
|
|
44
|
+
|
|
45
|
+
wrapper.__doc__ = function.__doc__
|
|
46
|
+
return wrapper
|
|
47
|
+
|
|
48
|
+
return decorator
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def sp_retry(exception_classes=(), tries=10, delay=5, rate=1.3):
|
|
52
|
+
"""
|
|
53
|
+
This is a shorthand for retry that catches all exceptions thrown by this library
|
|
54
|
+
|
|
55
|
+
Retry a call against an endpoint <tries> time
|
|
56
|
+
Args:
|
|
57
|
+
exception_classes:
|
|
58
|
+
tries:
|
|
59
|
+
delay:
|
|
60
|
+
rate:
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
|
|
64
|
+
"""
|
|
65
|
+
from sp_api.base import SellingApiException
|
|
66
|
+
|
|
67
|
+
return retry((SellingApiException,) + exception_classes, tries, delay, rate)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def throttle_retry(exception_classes=(), tries=10, delay=5, rate=1.3):
|
|
71
|
+
"""
|
|
72
|
+
This is a shorthand for retry that catches SellingApiRequestThrottledException
|
|
73
|
+
|
|
74
|
+
Retry a call against an endpoint <tries> time
|
|
75
|
+
Args:
|
|
76
|
+
exception_classes:
|
|
77
|
+
tries:
|
|
78
|
+
delay:
|
|
79
|
+
rate:
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
|
|
83
|
+
"""
|
|
84
|
+
from sp_api.base import SellingApiRequestThrottledException
|
|
85
|
+
|
|
86
|
+
return retry(
|
|
87
|
+
(SellingApiRequestThrottledException,) + exception_classes, tries, delay, rate
|
|
88
|
+
)
|
sp_api/auth/_core.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def build_cache_key(refresh_token, token_flavor=""):
|
|
5
|
+
return "access_token_" + hashlib.md5(
|
|
6
|
+
(token_flavor + (refresh_token or "__grantless__")).encode("utf-8"),
|
|
7
|
+
usedforsecurity=False,
|
|
8
|
+
).hexdigest()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def build_refresh_token_data(credentials, grant_type):
|
|
12
|
+
return {
|
|
13
|
+
"grant_type": grant_type,
|
|
14
|
+
"client_id": credentials.client_id,
|
|
15
|
+
"refresh_token": credentials.refresh_token,
|
|
16
|
+
"client_secret": credentials.client_secret,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def build_grantless_data(credentials, scope_value):
|
|
21
|
+
return {
|
|
22
|
+
"grant_type": "client_credentials",
|
|
23
|
+
"client_id": credentials.client_id,
|
|
24
|
+
"scope": scope_value,
|
|
25
|
+
"client_secret": credentials.client_secret,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def build_auth_code_request_body(credentials, auth_code):
|
|
30
|
+
return {
|
|
31
|
+
"grant_type": "authorization_code",
|
|
32
|
+
"code": auth_code,
|
|
33
|
+
"client_id": credentials.client_id,
|
|
34
|
+
"client_secret": credentials.client_secret,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def build_headers(user_agent, content_type):
|
|
39
|
+
return {"User-Agent": user_agent, "content-type": content_type}
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import os
|
|
2
2
|
|
|
3
|
-
import requests
|
|
4
|
-
import hashlib
|
|
5
3
|
import logging
|
|
6
4
|
from cachetools import TTLCache
|
|
7
5
|
from sp_api.base import BaseClient
|
|
6
|
+
from sp_api.base._transport_httpx import HttpxTransport
|
|
8
7
|
|
|
9
8
|
from .credentials import Credentials
|
|
10
9
|
from .access_token_response import AccessTokenResponse
|
|
11
10
|
from .exceptions import AuthorizationError
|
|
11
|
+
from ._core import (
|
|
12
|
+
build_auth_code_request_body,
|
|
13
|
+
build_cache_key,
|
|
14
|
+
build_grantless_data,
|
|
15
|
+
build_headers,
|
|
16
|
+
build_refresh_token_data,
|
|
17
|
+
)
|
|
12
18
|
|
|
13
19
|
cache = TTLCache(maxsize=int(os.environ.get('SP_API_AUTH_CACHE_SIZE', 10)), ttl=int(os.environ.get('SP_API_AUTH_CACHE_TTL', 3200)))
|
|
14
20
|
grantless_cache = TTLCache(maxsize=int(os.environ.get('SP_API_AUTH_CACHE_SIZE', 10)), ttl=int(os.environ.get('SP_API_AUTH_CACHE_TTL', 3200)))
|
|
@@ -25,9 +31,13 @@ class AccessTokenClient(BaseClient):
|
|
|
25
31
|
self.cred = Credentials(refresh_token, credentials)
|
|
26
32
|
self.proxies = proxies
|
|
27
33
|
self.verify = verify
|
|
34
|
+
self._transport = HttpxTransport(
|
|
35
|
+
proxies=proxies,
|
|
36
|
+
verify=verify,
|
|
37
|
+
)
|
|
28
38
|
|
|
29
39
|
def _request(self, url, data, headers):
|
|
30
|
-
response =
|
|
40
|
+
response = self._transport.request("POST", url, data=data, headers=headers)
|
|
31
41
|
response_data = response.json()
|
|
32
42
|
if response.status_code != 200:
|
|
33
43
|
error_message = response_data.get('error_description')
|
|
@@ -93,40 +103,18 @@ class AccessTokenClient(BaseClient):
|
|
|
93
103
|
return res
|
|
94
104
|
|
|
95
105
|
def _auth_code_request_body(self, auth_code):
|
|
96
|
-
return
|
|
97
|
-
'grant_type': 'authorization_code',
|
|
98
|
-
'code': auth_code,
|
|
99
|
-
'client_id': self.cred.client_id,
|
|
100
|
-
'client_secret': self.cred.client_secret
|
|
101
|
-
}
|
|
106
|
+
return build_auth_code_request_body(self.cred, auth_code)
|
|
102
107
|
|
|
103
108
|
def grantless_data(self, scope_value: str):
|
|
104
|
-
return
|
|
105
|
-
'grant_type': 'client_credentials',
|
|
106
|
-
'client_id': self.cred.client_id,
|
|
107
|
-
'scope': scope_value,
|
|
108
|
-
'client_secret': self.cred.client_secret
|
|
109
|
-
}
|
|
109
|
+
return build_grantless_data(self.cred, scope_value)
|
|
110
110
|
|
|
111
111
|
@property
|
|
112
112
|
def data(self):
|
|
113
|
-
return
|
|
114
|
-
'grant_type': self.grant_type,
|
|
115
|
-
'client_id': self.cred.client_id,
|
|
116
|
-
'refresh_token': self.cred.refresh_token,
|
|
117
|
-
'client_secret': self.cred.client_secret
|
|
118
|
-
}
|
|
113
|
+
return build_refresh_token_data(self.cred, self.grant_type)
|
|
119
114
|
|
|
120
115
|
@property
|
|
121
116
|
def headers(self):
|
|
122
|
-
return
|
|
123
|
-
'User-Agent': self.user_agent,
|
|
124
|
-
'content-type': self.content_type
|
|
125
|
-
}
|
|
117
|
+
return build_headers(self.user_agent, self.content_type)
|
|
126
118
|
|
|
127
119
|
def _get_cache_key(self, token_flavor=''):
|
|
128
|
-
return
|
|
129
|
-
(token_flavor + (self.cred.refresh_token or '__grantless__')).encode('utf-8'),
|
|
130
|
-
usedforsecurity=False
|
|
131
|
-
).hexdigest()
|
|
132
|
-
|
|
120
|
+
return build_cache_key(self.cred.refresh_token, token_flavor)
|
sp_api/base/_core.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from json import JSONDecodeError
|
|
4
|
+
|
|
5
|
+
from .ApiResponse import ApiResponse
|
|
6
|
+
from .exceptions import get_exception_for_code
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def resolve_method(params, data):
|
|
10
|
+
if params is None:
|
|
11
|
+
params = {}
|
|
12
|
+
if data is None:
|
|
13
|
+
data = {}
|
|
14
|
+
method = params.pop(
|
|
15
|
+
"method", data.pop("method", "GET") if isinstance(data, dict) else "GET"
|
|
16
|
+
)
|
|
17
|
+
return method, params, data
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def check_version(path, version):
|
|
21
|
+
if "<version>" not in path:
|
|
22
|
+
return path
|
|
23
|
+
return path.replace("<version>", version)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def add_marketplaces(method, marketplace_id, data):
|
|
27
|
+
post_keys = ["marketplaceIds", "MarketplaceIds"]
|
|
28
|
+
get_keys = ["MarketplaceId", "MarketplaceIds", "marketplace_ids", "marketplaceIds"]
|
|
29
|
+
|
|
30
|
+
if method == "POST":
|
|
31
|
+
if any(key in data.keys() for key in post_keys):
|
|
32
|
+
return
|
|
33
|
+
data.update(
|
|
34
|
+
{
|
|
35
|
+
key: marketplace_id if not key.endswith("s") else [marketplace_id]
|
|
36
|
+
for key in post_keys
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
return
|
|
40
|
+
if any(key in data.keys() for key in get_keys):
|
|
41
|
+
return
|
|
42
|
+
data.update(
|
|
43
|
+
{
|
|
44
|
+
key: marketplace_id if not key.endswith("s") else [marketplace_id]
|
|
45
|
+
for key in get_keys
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def build_headers(endpoint, user_agent, access_token, content_type="application/json"):
|
|
51
|
+
return {
|
|
52
|
+
"host": endpoint[8:],
|
|
53
|
+
"user-agent": user_agent,
|
|
54
|
+
"x-amz-access-token": access_token,
|
|
55
|
+
"x-amz-date": datetime.utcnow().strftime("%Y%m%dT%H%M%SZ"),
|
|
56
|
+
"content-type": content_type,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def prepare_request(
|
|
61
|
+
*,
|
|
62
|
+
method,
|
|
63
|
+
endpoint,
|
|
64
|
+
path,
|
|
65
|
+
params,
|
|
66
|
+
data,
|
|
67
|
+
headers,
|
|
68
|
+
add_marketplace,
|
|
69
|
+
marketplace_id,
|
|
70
|
+
version,
|
|
71
|
+
):
|
|
72
|
+
if add_marketplace:
|
|
73
|
+
add_marketplaces(method, marketplace_id, data if method in ("POST", "PUT") else params)
|
|
74
|
+
url = endpoint + check_version(path, version)
|
|
75
|
+
content = (
|
|
76
|
+
json.dumps(data) if data and method in ("POST", "PUT", "PATCH") else None
|
|
77
|
+
)
|
|
78
|
+
return {
|
|
79
|
+
"method": method,
|
|
80
|
+
"url": url,
|
|
81
|
+
"params": params,
|
|
82
|
+
"content": content,
|
|
83
|
+
"headers": headers,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def parse_response(res, *, method, res_no_data=False, bulk=False, wrap_list=False):
|
|
88
|
+
if (method == "DELETE" or res_no_data) and 200 <= res.status_code < 300:
|
|
89
|
+
try:
|
|
90
|
+
js = res.json() or {}
|
|
91
|
+
except JSONDecodeError:
|
|
92
|
+
js = {"status_code": res.status_code}
|
|
93
|
+
else:
|
|
94
|
+
try:
|
|
95
|
+
js = res.json() or {}
|
|
96
|
+
except JSONDecodeError:
|
|
97
|
+
js = {}
|
|
98
|
+
|
|
99
|
+
if isinstance(js, list):
|
|
100
|
+
if wrap_list:
|
|
101
|
+
js = dict(payload=js)
|
|
102
|
+
else:
|
|
103
|
+
js = js[0]
|
|
104
|
+
|
|
105
|
+
error = js.get("errors", None)
|
|
106
|
+
if error:
|
|
107
|
+
exception = get_exception_for_code(res.status_code)
|
|
108
|
+
raise exception(error, headers=res.headers)
|
|
109
|
+
|
|
110
|
+
return ApiResponse(**js, headers=res.headers)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class HttpxTransport:
|
|
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.Client(
|
|
10
|
+
timeout=timeout,
|
|
11
|
+
proxy=proxy_config,
|
|
12
|
+
verify=verify,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
def request(self, method, url, *, params=None, data=None, content=None, headers=None):
|
|
16
|
+
return self._client.request(
|
|
17
|
+
method,
|
|
18
|
+
url,
|
|
19
|
+
params=params,
|
|
20
|
+
data=data,
|
|
21
|
+
content=content,
|
|
22
|
+
headers=headers,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def stream(
|
|
26
|
+
self, method, url, *, params=None, data=None, content=None, headers=None, timeout=None
|
|
27
|
+
):
|
|
28
|
+
return self._client.stream(
|
|
29
|
+
method,
|
|
30
|
+
url,
|
|
31
|
+
params=params,
|
|
32
|
+
data=data,
|
|
33
|
+
content=content,
|
|
34
|
+
headers=headers,
|
|
35
|
+
timeout=timeout,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def close(self):
|
|
39
|
+
self._client.close()
|
sp_api/base/client.py
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
|
-
import hashlib
|
|
2
|
-
import json
|
|
3
1
|
from datetime import datetime
|
|
4
2
|
import logging
|
|
5
3
|
import os
|
|
6
4
|
|
|
7
|
-
from requests import request
|
|
8
|
-
from requests.exceptions import JSONDecodeError
|
|
9
|
-
|
|
10
5
|
from sp_api.auth import AccessTokenClient, AccessTokenResponse
|
|
11
6
|
from .ApiResponse import ApiResponse
|
|
12
7
|
from .base_client import BaseClient
|
|
13
|
-
from .exceptions import
|
|
8
|
+
from .exceptions import MissingScopeException
|
|
14
9
|
from .marketplaces import Marketplaces
|
|
15
10
|
from sp_api.base.credential_provider import CredentialProvider
|
|
11
|
+
from ._core import prepare_request, parse_response, resolve_method
|
|
12
|
+
from ._transport_httpx import HttpxTransport
|
|
16
13
|
|
|
17
14
|
log = logging.getLogger(__name__)
|
|
18
15
|
log.setLevel(logging.INFO) # Set default to DEBUG; users can override externally
|
|
@@ -63,6 +60,11 @@ class Client(BaseClient):
|
|
|
63
60
|
self.timeout = timeout
|
|
64
61
|
self.version = version
|
|
65
62
|
self.verify = verify
|
|
63
|
+
self._transport = HttpxTransport(
|
|
64
|
+
timeout=timeout,
|
|
65
|
+
proxies=proxies,
|
|
66
|
+
verify=verify,
|
|
67
|
+
)
|
|
66
68
|
|
|
67
69
|
@property
|
|
68
70
|
def headers(self):
|
|
@@ -96,40 +98,32 @@ class Client(BaseClient):
|
|
|
96
98
|
bulk: bool = False,
|
|
97
99
|
wrap_list: bool = False,
|
|
98
100
|
) -> ApiResponse:
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if data is None:
|
|
102
|
-
data = {}
|
|
103
|
-
|
|
104
|
-
# Note: The use of isinstance here is to support request schemas that are an array at the
|
|
105
|
-
# top level, eg get_product_fees_estimate
|
|
106
|
-
self.method = params.pop(
|
|
107
|
-
"method", data.pop("method", "GET") if isinstance(data, dict) else "GET"
|
|
108
|
-
)
|
|
101
|
+
method, params, data = resolve_method(params, data)
|
|
102
|
+
self.method = method
|
|
109
103
|
|
|
110
|
-
|
|
111
|
-
self._add_marketplaces(data if self.method in ("POST", "PUT") else params)
|
|
104
|
+
request_headers = headers or self.headers
|
|
112
105
|
|
|
113
|
-
log.debug("HTTP Method: %s",
|
|
106
|
+
log.debug("HTTP Method: %s", method)
|
|
114
107
|
log.debug("Making request to URL: %s", self.endpoint + self._check_version(path))
|
|
115
108
|
log.debug("Request Params: %s", params)
|
|
116
|
-
log.debug(
|
|
117
|
-
|
|
109
|
+
log.debug(
|
|
110
|
+
"Request Data: %s",
|
|
111
|
+
data if method in ("POST", "PUT", "PATCH") else None,
|
|
112
|
+
)
|
|
113
|
+
log.debug("Request Headers: %s", request_headers)
|
|
118
114
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
self.endpoint
|
|
115
|
+
prepared = prepare_request(
|
|
116
|
+
method=method,
|
|
117
|
+
endpoint=self.endpoint,
|
|
118
|
+
path=path,
|
|
122
119
|
params=params,
|
|
123
|
-
data=
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
headers=headers or self.headers,
|
|
129
|
-
timeout=self.timeout,
|
|
130
|
-
proxies=self.proxies,
|
|
131
|
-
verify=self.verify,
|
|
120
|
+
data=data,
|
|
121
|
+
headers=request_headers,
|
|
122
|
+
add_marketplace=add_marketplace,
|
|
123
|
+
marketplace_id=self.marketplace_id,
|
|
124
|
+
version=self.version,
|
|
132
125
|
)
|
|
126
|
+
res = self._transport.request(**prepared)
|
|
133
127
|
return self._check_response(res, res_no_data, bulk, wrap_list)
|
|
134
128
|
|
|
135
129
|
def _check_response(
|
|
@@ -139,36 +133,15 @@ class Client(BaseClient):
|
|
|
139
133
|
bulk: bool = False,
|
|
140
134
|
wrap_list: bool = False,
|
|
141
135
|
) -> ApiResponse:
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
js = {}
|
|
152
|
-
|
|
153
|
-
log.debug("Response before list handling: %s", js)
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if isinstance(js, list):
|
|
157
|
-
if wrap_list:
|
|
158
|
-
# Support responses that are an array at the top level, eg get_product_fees_estimate
|
|
159
|
-
js = dict(payload=js)
|
|
160
|
-
else:
|
|
161
|
-
js = js[0]
|
|
162
|
-
|
|
163
|
-
error = js.get("errors", None)
|
|
164
|
-
|
|
165
|
-
if error:
|
|
166
|
-
log.error("Error Response: %s", error)
|
|
167
|
-
exception = get_exception_for_code(res.status_code)
|
|
168
|
-
raise exception(error, headers=res.headers)
|
|
169
|
-
|
|
170
|
-
log.debug("Response: %s", js)
|
|
171
|
-
return ApiResponse(**js, headers=res.headers)
|
|
136
|
+
response = parse_response(
|
|
137
|
+
res,
|
|
138
|
+
method=self.method,
|
|
139
|
+
res_no_data=res_no_data,
|
|
140
|
+
bulk=bulk,
|
|
141
|
+
wrap_list=wrap_list,
|
|
142
|
+
)
|
|
143
|
+
log.debug("Response: %s", response)
|
|
144
|
+
return response
|
|
172
145
|
|
|
173
146
|
def _add_marketplaces(self, data):
|
|
174
147
|
POST = ["marketplaceIds", "MarketplaceIds"]
|
|
@@ -196,6 +169,9 @@ class Client(BaseClient):
|
|
|
196
169
|
}
|
|
197
170
|
)
|
|
198
171
|
|
|
172
|
+
def close(self):
|
|
173
|
+
self._transport.close()
|
|
174
|
+
|
|
199
175
|
def _request_grantless_operation(
|
|
200
176
|
self, path: str, *, data: dict = None, params: dict = None
|
|
201
177
|
):
|
|
@@ -225,3 +201,4 @@ class Client(BaseClient):
|
|
|
225
201
|
def __exit__(self, *args, **kwargs):
|
|
226
202
|
self.restricted_data_token = None
|
|
227
203
|
self.keep_restricted_data_token = False
|
|
204
|
+
self.close()
|
sp_api/util/__init__.py
CHANGED
|
@@ -2,6 +2,15 @@ from .retry import retry, sp_retry, throttle_retry
|
|
|
2
2
|
from .load_all_pages import load_all_pages
|
|
3
3
|
from .key_maker import KeyMaker
|
|
4
4
|
from .load_date_bound import load_date_bound
|
|
5
|
+
from .params 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
|
+
)
|
|
5
14
|
|
|
6
15
|
__all__ = [
|
|
7
16
|
"retry",
|
|
@@ -10,4 +19,11 @@ __all__ = [
|
|
|
10
19
|
"load_all_pages",
|
|
11
20
|
"KeyMaker",
|
|
12
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",
|
|
13
29
|
]
|
sp_api/util/params.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from collections import abc
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def to_csv(value, mapper=None):
|
|
6
|
+
if value and isinstance(value, abc.Iterable) and not isinstance(value, str):
|
|
7
|
+
if mapper:
|
|
8
|
+
value = [mapper(item) for item in value]
|
|
9
|
+
return ",".join(value)
|
|
10
|
+
return value
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def normalize_csv_param(kwargs, key, mapper=None):
|
|
14
|
+
if key in kwargs:
|
|
15
|
+
value = to_csv(kwargs.get(key), mapper)
|
|
16
|
+
if value is not None:
|
|
17
|
+
kwargs[key] = value
|
|
18
|
+
return kwargs
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def normalize_included_data(kwargs, enum_cls=None, key="includedData"):
|
|
22
|
+
def mapper(value):
|
|
23
|
+
if enum_cls and isinstance(value, enum_cls):
|
|
24
|
+
return value.value
|
|
25
|
+
return value
|
|
26
|
+
|
|
27
|
+
return normalize_csv_param(kwargs, key, mapper)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def normalize_marketplace_ids(kwargs, marketplace_cls=None, key="marketplaceIds"):
|
|
31
|
+
def mapper(value):
|
|
32
|
+
if marketplace_cls and isinstance(value, marketplace_cls):
|
|
33
|
+
return value.marketplace_id
|
|
34
|
+
return value
|
|
35
|
+
|
|
36
|
+
return normalize_csv_param(kwargs, key, mapper)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def normalize_datetime_kwargs(kwargs, keys):
|
|
40
|
+
for key in keys:
|
|
41
|
+
if isinstance(kwargs.get(key), datetime):
|
|
42
|
+
kwargs[key] = kwargs.get(key).isoformat()
|
|
43
|
+
return kwargs
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def encode_kwarg(kwargs, key, encoder):
|
|
47
|
+
if key in kwargs:
|
|
48
|
+
kwargs[key] = encoder(kwargs.pop(key))
|
|
49
|
+
return kwargs
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def should_add_marketplace(kwargs, token_key="nextToken"):
|
|
53
|
+
return token_key not in kwargs
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def ensure_csv(value, mapper=None):
|
|
57
|
+
return to_csv(value, mapper)
|