1# Copyright 2021 Google LLC 
    2# 
    3# Licensed under the Apache License, Version 2.0 (the "License"); 
    4# you may not use this file except in compliance with the License. 
    5# You may obtain a copy of the License at 
    6# 
    7#     http://www.apache.org/licenses/LICENSE-2.0 
    8# 
    9# Unless required by applicable law or agreed to in writing, software 
    10# distributed under the License is distributed on an "AS IS" BASIS, 
    11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
    12# See the License for the specific language governing permissions and 
    13# limitations under the License. 
    14 
    15"""Helpers for rest transports.""" 
    16 
    17import functools 
    18import operator 
    19 
    20 
    21def flatten_query_params(obj, strict=False): 
    22    """Flatten a dict into a list of (name,value) tuples. 
    23 
    24    The result is suitable for setting query params on an http request. 
    25 
    26    .. code-block:: python 
    27 
    28        >>> obj = {'a': 
    29        ...         {'b': 
    30        ...           {'c': ['x', 'y', 'z']} }, 
    31        ...      'd': 'uvw', 
    32        ...      'e': True, } 
    33        >>> flatten_query_params(obj, strict=True) 
    34        [('a.b.c', 'x'), ('a.b.c', 'y'), ('a.b.c', 'z'), ('d', 'uvw'), ('e', 'true')] 
    35 
    36    Note that, as described in 
    37    https://github.com/googleapis/googleapis/blob/48d9fb8c8e287c472af500221c6450ecd45d7d39/google/api/http.proto#L117, 
    38    repeated fields (i.e. list-valued fields) may only contain primitive types (not lists or dicts). 
    39    This is enforced in this function. 
    40 
    41    Args: 
    42      obj: a possibly nested dictionary (from json), or None 
    43      strict: a bool, defaulting to False, to enforce that all values in the 
    44              result tuples be strings and, if boolean, lower-cased. 
    45 
    46    Returns: a list of tuples, with each tuple having a (possibly) multi-part name 
    47      and a scalar value. 
    48 
    49    Raises: 
    50      TypeError if obj is not a dict or None 
    51      ValueError if obj contains a list of non-primitive values. 
    52    """ 
    53 
    54    if obj is not None and not isinstance(obj, dict): 
    55        raise TypeError("flatten_query_params must be called with dict object") 
    56 
    57    return _flatten(obj, key_path=[], strict=strict) 
    58 
    59 
    60def _flatten(obj, key_path, strict=False): 
    61    if obj is None: 
    62        return [] 
    63    if isinstance(obj, dict): 
    64        return _flatten_dict(obj, key_path=key_path, strict=strict) 
    65    if isinstance(obj, list): 
    66        return _flatten_list(obj, key_path=key_path, strict=strict) 
    67    return _flatten_value(obj, key_path=key_path, strict=strict) 
    68 
    69 
    70def _is_primitive_value(obj): 
    71    if obj is None: 
    72        return False 
    73 
    74    if isinstance(obj, (list, dict)): 
    75        raise ValueError("query params may not contain repeated dicts or lists") 
    76 
    77    return True 
    78 
    79 
    80def _flatten_value(obj, key_path, strict=False): 
    81    return [(".".join(key_path), _canonicalize(obj, strict=strict))] 
    82 
    83 
    84def _flatten_dict(obj, key_path, strict=False): 
    85    items = ( 
    86        _flatten(value, key_path=key_path + [key], strict=strict) 
    87        for key, value in obj.items() 
    88    ) 
    89    return functools.reduce(operator.concat, items, []) 
    90 
    91 
    92def _flatten_list(elems, key_path, strict=False): 
    93    # Only lists of scalar values are supported. 
    94    # The name (key_path) is repeated for each value. 
    95    items = ( 
    96        _flatten_value(elem, key_path=key_path, strict=strict) 
    97        for elem in elems 
    98        if _is_primitive_value(elem) 
    99    ) 
    100    return functools.reduce(operator.concat, items, []) 
    101 
    102 
    103def _canonicalize(obj, strict=False): 
    104    if strict: 
    105        value = str(obj) 
    106        if isinstance(obj, bool): 
    107            value = value.lower() 
    108        return value 
    109    return obj