This repository was archived by the owner on Jan 4, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 206
Expand file tree
/
Copy pathparams.py
More file actions
277 lines (224 loc) · 9.36 KB
/
params.py
File metadata and controls
277 lines (224 loc) · 9.36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
"""Parameter manipulation utilities."""
import datetime
import json
from collections.abc import Iterable, Mapping
from enum import Enum
from typing import Any, List, Union
from urllib.parse import quote
def enumerate_param(param: str, values: Union[list, set, tuple]) -> dict:
"""Builds a dictionary of an enumerated parameter, using the param string and some values.
If values is not a list, tuple, or set, it will be coerced to a list
with a single item.
Example:
enumerate_param('MarketplaceIdList.Id', (123, 345, 4343))
Returns:
{
MarketplaceIdList.Id.1: 123,
MarketplaceIdList.Id.2: 345,
MarketplaceIdList.Id.3: 4343
}
"""
if not isinstance(values, (list, set, tuple)):
# Coerces a single value to a list before continuing.
values = [values]
if not any(values):
# if not values -> returns ValueError
return {}
param = dot_appended_param(param)
# Return final output: dict comprehension of the enumerated param and values.
return {f"{param}{idx}": val for idx, val in enumerate(values, start=1)}
def enumerate_params(params: Mapping = None) -> dict:
"""For each param and values, runs enumerate_param,
returning a flat dict of all results
"""
if not params or not isinstance(params, Mapping):
return {}
params_output = {}
for param, values in params.items():
params_output.update(enumerate_param(param, values))
return params_output
def enumerate_keyed_param(param: str, values: List[Mapping]) -> dict:
"""Given a param string and a list of values dicts, returns a flat dict of keyed, enumerated params.
Each dict in the values list must pertain to a single item and its data points.
Example:
param = "InboundShipmentPlanRequestItems.member"
values = [
{'SellerSKU': 'Football2415',
'Quantity': 3},
{'SellerSKU': 'TeeballBall3251',
'Quantity': 5},
...
]
Returns:
{
'InboundShipmentPlanRequestItems.member.1.SellerSKU': 'Football2415',
'InboundShipmentPlanRequestItems.member.1.Quantity': 3,
'InboundShipmentPlanRequestItems.member.2.SellerSKU': 'TeeballBall3251',
'InboundShipmentPlanRequestItems.member.2.Quantity': 5,
...
}
"""
if not isinstance(values, (list, set, tuple)):
# If it's a single value, convert it to a list first
values = [values]
if not any(values):
# Shortcut for empty values
return {}
param = dot_appended_param(param)
for val in values:
# Every value in the list must be a dict.
if not isinstance(val, dict):
# Value is not a dict: can't work on it here.
raise ValueError(
(
"Non-dict value detected. "
"`values` must be a list, tuple, or set; containing only dicts."
)
)
params = {}
for idx, val_dict in enumerate(values, start=1):
# Build the final output.
params.update({f"{param}{idx}.{k}": v for k, v in val_dict.items()})
return params
def dict_keyed_param(param: str, dict_from: Mapping) -> dict:
"""Given a param string and a dict, returns a flat dict of keyed params without enumerate.
Example:
param = "ShipmentRequestDetails.PackageDimensions"
dict_from = {'Length': 5, 'Width': 5, 'Height': 5, 'Unit': 'inches'}
Returns:
{
'ShipmentRequestDetails.PackageDimensions.Length': 5,
'ShipmentRequestDetails.PackageDimensions.Width': 5,
'ShipmentRequestDetails.PackageDimensions.Height': 5,
'ShipmentRequestDetails.PackageDimensions.Unit': 'inches',
...
}
"""
params = {}
param = dot_appended_param(param)
for k, v in dict_from.items():
params.update({f"{param}{k}": v})
return params
def flat_param_dict(value: Union[str, Mapping, List], prefix: str = "") -> dict:
"""Returns a flattened params dictionary by collapsing nested dicts and
non-string iterables.
Any arbitrarily-nested dict or iterable will be expanded and flattened.
- Each key in a child dict will be concatenated to its parent key.
- Elements of a non-string iterable will be enumerated using a 1-based index,
with the index number concatenated to the parent key.
- In both cases, keys and sub-keys are joined by ``.``.
If ``prefix`` is set, all keys in the resulting output will begin with
``prefix + '.'``.
"""
prefix = "" if not prefix else str(prefix)
# Prefix is now either an empty string or a valid prefix string ending in '.'
# NOTE should ensure that a `None` value is changed to empty string, as well.
if isinstance(value, str) or not isinstance(value, (Mapping, Iterable)):
# Value is not one of the types we want to expand.
if prefix:
# Can return a single dict of the prefix and value as a base case
prefix = dot_appended_param(prefix, reverse=True)
return {prefix: value}
raise ValueError(
(
"Non-dict, non-iterable value requires a prefix "
"(would return a mapping of `prefix: value`)"
)
)
# Past here, the value is something that must be expanded.
# We'll build that output with recursive calls to `flat_param_dict`.
if prefix:
prefix = dot_appended_param(prefix)
output = {}
if isinstance(value, Mapping):
for key, val in value.items():
new_key = f"{prefix}{key}"
output.update(flat_param_dict(val, prefix=new_key))
else:
# value must be an Iterable
for idx, val in enumerate(value, start=1):
new_key = f"{prefix}{idx}"
output.update(flat_param_dict(val, prefix=new_key))
return output
def dot_appended_param(param_key: str, reverse: bool = False):
"""Returns ``param_key`` string, ensuring that it ends with ``'.'``.
Set ``reverse`` to ``True`` (default ``False``) to reverse this behavior,
ensuring that ``param_key`` *does not* end with ``'.'``.
"""
if not param_key.endswith("."):
# Ensure this enumerated param ends in '.'
param_key += "."
if reverse:
# Since param_key is guaranteed to end with '.' by this point,
# if `reverse` flag was set, now we just get rid of it.
param_key = param_key[:-1]
return param_key
BOOL_FALSE_STRINGS = ("no", "n", "none", "off", "false", "0")
def coerce_to_bool(val: Any) -> bool:
"""Coerces ``val`` to a boolean for use in MWS requests.
If ``val`` is a string, converts certain (case-insensitive) string values
to "False", such as:
- "no"
- "n"
- "none"
- "off"
- "false"
- "0"
Otherwise, ``val`` is simply cast using built-in ``bool()`` function.
"""
if isinstance(val, str) and val.lower() in BOOL_FALSE_STRINGS:
return False
return bool(val)
def remove_empty_param_keys(params: Mapping) -> dict:
"""Returns a copy of ``params`` dict where any key with a value of ``None``
or ``""`` (empty string) are removed.
"""
return {k: v for k, v in params.items() if v is not None and v != ""}
def clean_params_dict(params: Mapping, urlencode=False) -> dict:
"""Clean multiple param values in a dict, returning a new dict
containing the original keys and cleaned values.
"""
cleaned_params = dict()
for key, val in params.items():
newval = clean_value(val)
if urlencode:
newval = quote(val, safe="-_.~")
cleaned_params[key] = newval
return cleaned_params
def clean_value(val: Any) -> str:
"""Attempts to clean a value so that it can be sent in a request."""
if isinstance(val, (Mapping, list, set, tuple)):
raise ValueError("Cannot clean parameter value of type %s" % str(type(val)))
if isinstance(val, (datetime.datetime, datetime.date)):
return clean_date(val)
if isinstance(val, bool):
return clean_bool(val)
if isinstance(val, Enum):
return clean_enum(val)
# For all else, simply convert to a string value.
return str(val)
def clean_bool(val: bool) -> str:
"""Converts a boolean value to its JSON string equivalent."""
if val is not True and val is not False:
raise ValueError("Expected a boolean, got %s" % val)
return json.dumps(val)
def clean_date(val: Union[datetime.datetime, datetime.date]) -> str:
"""Converts a datetime.datetime or datetime.date to ISO 8601 string.
Further passes that string through `urllib.parse.quote`.
"""
return val.isoformat()
def clean_enum(val: Union[Enum, str]) -> str:
"""Simply put, converts an Enum to its ``.value`` attribute.
All known MWS Enum classes *should* return a proper value in this case.
"""
return val.value
def iterable_param(val) -> Iterable:
"""Wraps single values (that are *not* non-string iterables) as a list.
Used for methods that require some iterable value that should be enumerated
(without exploding string values into enumerated lists of their characters).
"""
# Special case: strings are iterables, too, but shouldn't be treated the same.
# TODO make the type check at the end more generic?
if isinstance(val, str) or not isinstance(val, Iterable):
return [val]
return val