• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
110