1import re
2import warnings
3import typing
4
5from collections import OrderedDict
6from copy import deepcopy
7
8from ._http import HTTPStatus
9
10
11FIRST_CAP_RE = re.compile("(.)([A-Z][a-z]+)")
12ALL_CAP_RE = re.compile("([a-z0-9])([A-Z])")
13
14
15__all__ = (
16 "merge",
17 "camel_to_dash",
18 "default_id",
19 "not_none",
20 "not_none_sorted",
21 "unpack",
22 "BaseResponse",
23 "import_check_view_func",
24)
25
26
27def import_werkzeug_response():
28 """Resolve `werkzeug` `Response` class import because
29 `BaseResponse` was renamed in version 2.* to `Response`"""
30 import importlib.metadata
31
32 werkzeug_major = int(importlib.metadata.version("werkzeug").split(".")[0])
33 if werkzeug_major < 2:
34 from werkzeug.wrappers import BaseResponse
35
36 return BaseResponse
37
38 from werkzeug.wrappers import Response
39
40 return Response
41
42
43BaseResponse = import_werkzeug_response()
44
45
46class FlaskCompatibilityWarning(DeprecationWarning):
47 pass
48
49
50def merge(first, second):
51 """
52 Recursively merges two dictionaries.
53
54 Second dictionary values will take precedence over those from the first one.
55 Nested dictionaries are merged too.
56
57 :param dict first: The first dictionary
58 :param dict second: The second dictionary
59 :return: the resulting merged dictionary
60 :rtype: dict
61 """
62 if not isinstance(second, dict):
63 return second
64 result = deepcopy(first)
65 for key, value in second.items():
66 if key in result and isinstance(result[key], dict):
67 result[key] = merge(result[key], value)
68 else:
69 result[key] = deepcopy(value)
70 return result
71
72
73def camel_to_dash(value):
74 """
75 Transform a CamelCase string into a low_dashed one
76
77 :param str value: a CamelCase string to transform
78 :return: the low_dashed string
79 :rtype: str
80 """
81 first_cap = FIRST_CAP_RE.sub(r"\1_\2", value)
82 return ALL_CAP_RE.sub(r"\1_\2", first_cap).lower()
83
84
85def default_id(resource, method):
86 """Default operation ID generator"""
87 return "{0}_{1}".format(method, camel_to_dash(resource))
88
89
90def not_none(data):
91 """
92 Remove all keys where value is None
93
94 :param dict data: A dictionary with potentially some values set to None
95 :return: The same dictionary without the keys with values to ``None``
96 :rtype: dict
97 """
98 return dict((k, v) for k, v in data.items() if v is not None)
99
100
101def not_none_sorted(data):
102 """
103 Remove all keys where value is None
104
105 :param OrderedDict data: A dictionary with potentially some values set to None
106 :return: The same dictionary without the keys with values to ``None``
107 :rtype: OrderedDict
108 """
109 return OrderedDict((k, v) for k, v in sorted(data.items()) if v is not None)
110
111
112def unpack(response, default_code=HTTPStatus.OK):
113 """
114 Unpack a Flask standard response.
115
116 Flask response can be:
117 - a single value
118 - a 2-tuple ``(value, code)``
119 - a 3-tuple ``(value, code, headers)``
120
121 .. warning::
122
123 When using this function, you must ensure that the tuple is not the response data.
124 To do so, prefer returning list instead of tuple for listings.
125
126 :param response: A Flask style response
127 :param int default_code: The HTTP code to use as default if none is provided
128 :return: a 3-tuple ``(data, code, headers)``
129 :rtype: tuple
130 :raise ValueError: if the response does not have one of the expected format
131 """
132 if not isinstance(response, tuple):
133 # data only
134 return response, default_code, {}
135 elif len(response) == 1:
136 # data only as tuple
137 return response[0], default_code, {}
138 elif len(response) == 2:
139 # data and code
140 data, code = response
141 return data, code, {}
142 elif len(response) == 3:
143 # data, code and headers
144 data, code, headers = response
145 return data, code or default_code, headers
146 else:
147 raise ValueError("Too many response values")
148
149
150def to_view_name(view_func: typing.Callable) -> str:
151 """Helper that returns the default endpoint for a given
152 function. This always is the function name.
153
154 Note: copy of simple flask internal helper
155 """
156 assert view_func is not None, "expected view func if endpoint is not provided."
157 return view_func.__name__
158
159
160def import_check_view_func():
161 """
162 Resolve import flask _endpoint_from_view_func.
163
164 Show warning if function cannot be found and provide copy of last known implementation.
165
166 Note: This helper method exists because reoccurring problem with flask function, but
167 actual method body remaining the same in each flask version.
168 """
169 import importlib.metadata
170
171 flask_version = importlib.metadata.version("flask").split(".")
172 try:
173 if flask_version[0] == "1":
174 from flask.helpers import _endpoint_from_view_func
175 elif flask_version[0] == "2":
176 from flask.scaffold import _endpoint_from_view_func
177 elif flask_version[0] == "3":
178 from flask.sansio.scaffold import _endpoint_from_view_func
179 else:
180 warnings.simplefilter("once", FlaskCompatibilityWarning)
181 _endpoint_from_view_func = None
182 except ImportError:
183 warnings.simplefilter("once", FlaskCompatibilityWarning)
184 _endpoint_from_view_func = None
185 if _endpoint_from_view_func is None:
186 _endpoint_from_view_func = to_view_name
187 return _endpoint_from_view_func