"""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
[docs]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