[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/python-amazon-mws/python-amazon-mws/develop/mws/utils/params.py [Back]  [Original]

"""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

Web Proxy Viewer  |  New URL  |  Original Page