Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/flask_restx/utils.py: 37%
71 statements
« prev ^ index » next coverage.py v7.4.0, created at 2024-01-22 06:29 +0000
« prev ^ index » next coverage.py v7.4.0, created at 2024-01-22 06:29 +0000
1import re
2import warnings
3import typing
5from collections import OrderedDict
6from copy import deepcopy
8from ._http import HTTPStatus
11FIRST_CAP_RE = re.compile("(.)([A-Z][a-z]+)")
12ALL_CAP_RE = re.compile("([a-z0-9])([A-Z])")
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)
27def import_werkzeug_response():
28 """Resolve `werkzeug` `Response` class import because
29 `BaseResponse` was renamed in version 2.* to `Response`"""
30 import importlib.metadata
32 werkzeug_major = int(importlib.metadata.version("werkzeug").split(".")[0])
33 if werkzeug_major < 2:
34 from werkzeug.wrappers import BaseResponse
36 return BaseResponse
38 from werkzeug.wrappers import Response
40 return Response
43BaseResponse = import_werkzeug_response()
46class FlaskCompatibilityWarning(DeprecationWarning):
47 pass
50def merge(first, second):
51 """
52 Recursively merges two dictionaries.
54 Second dictionary values will take precedence over those from the first one.
55 Nested dictionaries are merged too.
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
73def camel_to_dash(value):
74 """
75 Transform a CamelCase string into a low_dashed one
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()
85def default_id(resource, method):
86 """Default operation ID generator"""
87 return "{0}_{1}".format(method, camel_to_dash(resource))
90def not_none(data):
91 """
92 Remove all keys where value is None
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)
101def not_none_sorted(data):
102 """
103 Remove all keys where value is None
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)
112def unpack(response, default_code=HTTPStatus.OK):
113 """
114 Unpack a Flask standard response.
116 Flask response can be:
117 - a single value
118 - a 2-tuple ``(value, code)``
119 - a 3-tuple ``(value, code, headers)``
121 .. warning::
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.
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")
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.
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__
160def import_check_view_func():
161 """
162 Resolve import flask _endpoint_from_view_func.
164 Show warning if function cannot be found and provide copy of last known implementation.
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
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