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.
Files changed (137) hide show
  1. {python_amazon_sp_api-1.9.60.dist-info → python_amazon_sp_api-2.0.3.dist-info}/METADATA +41 -14
  2. python_amazon_sp_api-2.0.3.dist-info/RECORD +249 -0
  3. sp_api/__version__.py +1 -1
  4. sp_api/api/catalog/catalog.py +3 -4
  5. sp_api/api/catalog_items/catalog_items.py +3 -6
  6. sp_api/api/data_kiosk/data_kiosk.py +5 -6
  7. sp_api/api/feeds/feeds.py +11 -8
  8. sp_api/api/fulfillment_inbound/fulfillment_inbound.py +2 -2
  9. sp_api/api/inventories/inventories.py +2 -7
  10. sp_api/api/listings_items/listings_items.py +3 -24
  11. sp_api/api/products/products.py +3 -1
  12. sp_api/api/reports/reports.py +61 -97
  13. sp_api/api/sales/sales.py +2 -2
  14. sp_api/asyncio/__init__.py +0 -0
  15. sp_api/asyncio/api/__init__.py +164 -0
  16. sp_api/asyncio/api/amazon_warehousing_and_distribu/__init__.py +9 -0
  17. sp_api/asyncio/api/amazon_warehousing_and_distribu/amazon_warehousing_and_distribu.py +130 -0
  18. sp_api/asyncio/api/aplus_content/__init__.py +5 -0
  19. sp_api/asyncio/api/aplus_content/aplus_content.py +330 -0
  20. sp_api/asyncio/api/application_integrations/__init__.py +5 -0
  21. sp_api/asyncio/api/application_integrations/application_integrations.py +119 -0
  22. sp_api/asyncio/api/application_management/__init__.py +5 -0
  23. sp_api/asyncio/api/application_management/application_management.py +36 -0
  24. sp_api/asyncio/api/authorization/__init__.py +5 -0
  25. sp_api/asyncio/api/authorization/authorization.py +54 -0
  26. sp_api/asyncio/api/catalog/__init__.py +5 -0
  27. sp_api/asyncio/api/catalog/catalog.py +111 -0
  28. sp_api/asyncio/api/catalog_items/__init__.py +6 -0
  29. sp_api/asyncio/api/catalog_items/catalog_items.py +93 -0
  30. sp_api/asyncio/api/clients/__init__.py +1 -0
  31. sp_api/asyncio/api/customer_feedback/__init__.py +5 -0
  32. sp_api/asyncio/api/customer_feedback/customer_feedback.py +111 -0
  33. sp_api/asyncio/api/data_kiosk/__init__.py +5 -0
  34. sp_api/asyncio/api/data_kiosk/data_kiosk.py +236 -0
  35. sp_api/asyncio/api/easy_ship/__init__.py +5 -0
  36. sp_api/asyncio/api/easy_ship/easy_ship.py +191 -0
  37. sp_api/asyncio/api/external_fulfillment/__init__.py +5 -0
  38. sp_api/asyncio/api/external_fulfillment/external_fulfillment.py +706 -0
  39. sp_api/asyncio/api/fba_inbound_eligibility/__init__.py +5 -0
  40. sp_api/asyncio/api/fba_inbound_eligibility/fba_inbound_eligibility.py +96 -0
  41. sp_api/asyncio/api/fba_small_and_light/__init__.py +5 -0
  42. sp_api/asyncio/api/fba_small_and_light/fba_small_and_light.py +213 -0
  43. sp_api/asyncio/api/feeds/__init__.py +0 -0
  44. sp_api/asyncio/api/feeds/feeds.py +260 -0
  45. sp_api/asyncio/api/finances/__init__.py +0 -0
  46. sp_api/asyncio/api/finances/finances.py +100 -0
  47. sp_api/asyncio/api/fulfillment_inbound/__init__.py +0 -0
  48. sp_api/asyncio/api/fulfillment_inbound/fulfillment_inbound.py +1798 -0
  49. sp_api/asyncio/api/fulfillment_outbound/__init__.py +0 -0
  50. sp_api/asyncio/api/fulfillment_outbound/fulfillment_outbound.py +736 -0
  51. sp_api/asyncio/api/inventories/__init__.py +0 -0
  52. sp_api/asyncio/api/inventories/inventories.py +74 -0
  53. sp_api/asyncio/api/listings_items/__init__.py +0 -0
  54. sp_api/asyncio/api/listings_items/listings_items.py +170 -0
  55. sp_api/asyncio/api/listings_restrictions/__init__.py +0 -0
  56. sp_api/asyncio/api/listings_restrictions/listings_restrictions.py +36 -0
  57. sp_api/asyncio/api/merchant_fulfillment/__init__.py +0 -0
  58. sp_api/asyncio/api/merchant_fulfillment/merchant_fulfillment.py +384 -0
  59. sp_api/asyncio/api/messaging/__init__.py +0 -0
  60. sp_api/asyncio/api/messaging/messaging.py +511 -0
  61. sp_api/asyncio/api/models/__init__.py +4 -0
  62. sp_api/asyncio/api/notifications/__init__.py +0 -0
  63. sp_api/asyncio/api/notifications/notifications.py +295 -0
  64. sp_api/asyncio/api/orders/__init__.py +0 -0
  65. sp_api/asyncio/api/orders/orders.py +356 -0
  66. sp_api/asyncio/api/overrides/__init__.py +1 -0
  67. sp_api/asyncio/api/product_fees/__init__.py +0 -0
  68. sp_api/asyncio/api/product_fees/product_fees.py +237 -0
  69. sp_api/asyncio/api/product_type_definitions/__init__.py +0 -0
  70. sp_api/asyncio/api/product_type_definitions/product_type_definitions.py +75 -0
  71. sp_api/asyncio/api/products/__init__.py +0 -0
  72. sp_api/asyncio/api/products/products.py +405 -0
  73. sp_api/asyncio/api/products/products_definitions.py +170 -0
  74. sp_api/asyncio/api/replenishment/__init__.py +0 -0
  75. sp_api/asyncio/api/replenishment/replenishment.py +121 -0
  76. sp_api/asyncio/api/reports/__init__.py +0 -0
  77. sp_api/asyncio/api/reports/reports.py +439 -0
  78. sp_api/asyncio/api/sales/__init__.py +0 -0
  79. sp_api/asyncio/api/sales/sales.py +93 -0
  80. sp_api/asyncio/api/sellers/__init__.py +0 -0
  81. sp_api/asyncio/api/sellers/sellers.py +70 -0
  82. sp_api/asyncio/api/services/__init__.py +0 -0
  83. sp_api/asyncio/api/services/services.py +218 -0
  84. sp_api/asyncio/api/shipping/__init__.py +0 -0
  85. sp_api/asyncio/api/shipping/shipping.py +459 -0
  86. sp_api/asyncio/api/shipping/shippingV2.py +651 -0
  87. sp_api/asyncio/api/solicitations/__init__.py +0 -0
  88. sp_api/asyncio/api/solicitations/solicitations.py +78 -0
  89. sp_api/asyncio/api/supply_sources/__init__.py +0 -0
  90. sp_api/asyncio/api/supply_sources/supply_sources.py +138 -0
  91. sp_api/asyncio/api/tokens/__init__.py +0 -0
  92. sp_api/asyncio/api/tokens/tokens.py +65 -0
  93. sp_api/asyncio/api/upload/__init__.py +0 -0
  94. sp_api/asyncio/api/upload/upload.py +18 -0
  95. sp_api/asyncio/api/vendor_direct_fulfillment_inventory/__init__.py +0 -0
  96. sp_api/asyncio/api/vendor_direct_fulfillment_inventory/vendor_direct_fulfillment_inventory.py +64 -0
  97. sp_api/asyncio/api/vendor_direct_fulfillment_orders/__init__.py +0 -0
  98. sp_api/asyncio/api/vendor_direct_fulfillment_orders/vendor_direct_fulfillment_orders.py +196 -0
  99. sp_api/asyncio/api/vendor_direct_fulfillment_payments/__init__.py +0 -0
  100. sp_api/asyncio/api/vendor_direct_fulfillment_payments/vendor_direct_fulfillment_payments.py +254 -0
  101. sp_api/asyncio/api/vendor_direct_fulfillment_shipping/__init__.py +0 -0
  102. sp_api/asyncio/api/vendor_direct_fulfillment_shipping/vendor_direct_fulfillment_shipping.py +627 -0
  103. sp_api/asyncio/api/vendor_direct_fulfillment_transactions/__init__.py +0 -0
  104. sp_api/asyncio/api/vendor_direct_fulfillment_transactions/vendor_direct_fulfillment_transactions.py +43 -0
  105. sp_api/asyncio/api/vendor_invoices/__init__.py +0 -0
  106. sp_api/asyncio/api/vendor_invoices/vendor_invoices.py +295 -0
  107. sp_api/asyncio/api/vendor_orders/__init__.py +0 -0
  108. sp_api/asyncio/api/vendor_orders/vendor_orders.py +210 -0
  109. sp_api/asyncio/api/vendor_shipments/__init__.py +0 -0
  110. sp_api/asyncio/api/vendor_shipments/vendor_shipments.py +118 -0
  111. sp_api/asyncio/api/vendor_transaction_status/__init__.py +0 -0
  112. sp_api/asyncio/api/vendor_transaction_status/vendor_transaction_status.py +41 -0
  113. sp_api/asyncio/auth/__init__.py +12 -0
  114. sp_api/asyncio/auth/access_token_client.py +145 -0
  115. sp_api/asyncio/auth/exceptions.py +5 -0
  116. sp_api/asyncio/base/__init__.py +53 -0
  117. sp_api/asyncio/base/_transport_httpx.py +50 -0
  118. sp_api/asyncio/base/base_client.py +8 -0
  119. sp_api/asyncio/base/client.py +169 -0
  120. sp_api/asyncio/util/__init__.py +29 -0
  121. sp_api/asyncio/util/key_maker.py +5 -0
  122. sp_api/asyncio/util/load_all_pages.py +55 -0
  123. sp_api/asyncio/util/load_date_bound.py +53 -0
  124. sp_api/asyncio/util/retry.py +88 -0
  125. sp_api/auth/_core.py +39 -0
  126. sp_api/auth/access_token_client.py +18 -30
  127. sp_api/base/_core.py +110 -0
  128. sp_api/base/_transport_httpx.py +39 -0
  129. sp_api/base/client.py +40 -63
  130. sp_api/util/__init__.py +16 -0
  131. sp_api/util/params.py +57 -0
  132. sp_api/util/report_document.py +154 -0
  133. python_amazon_sp_api-1.9.60.dist-info/RECORD +0 -133
  134. {python_amazon_sp_api-1.9.60.data → python_amazon_sp_api-2.0.3.data}/scripts/make_endpoint +0 -0
  135. {python_amazon_sp_api-1.9.60.dist-info → python_amazon_sp_api-2.0.3.dist-info}/WHEEL +0 -0
  136. {python_amazon_sp_api-1.9.60.dist-info → python_amazon_sp_api-2.0.3.dist-info}/licenses/LICENSE +0 -0
  137. {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 = requests.post(url, data=data, headers=headers, proxies=self.proxies, verify=self.verify)
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 'access_token_' + hashlib.md5(
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 get_exception_for_code, MissingScopeException
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
- if params is None:
100
- params = {}
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
- if add_marketplace:
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", self.method)
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("Request Data: %s", data if self.method in ("POST", "PUT", "PATCH") else None)
117
- log.debug("Request Headers: %s", headers or self.headers)
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
- res = request(
120
- self.method,
121
- self.endpoint + self._check_version(path),
115
+ prepared = prepare_request(
116
+ method=method,
117
+ endpoint=self.endpoint,
118
+ path=path,
122
119
  params=params,
123
- data=(
124
- json.dumps(data)
125
- if data and self.method in ("POST", "PUT", "PATCH")
126
- else None
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
- if (self.method == "DELETE" or res_no_data) and 200 <= res.status_code < 300:
143
- try:
144
- js = res.json() or {}
145
- except JSONDecodeError:
146
- js = {"status_code": res.status_code}
147
- else:
148
- try:
149
- js = res.json() or {}
150
- except JSONDecodeError:
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)