Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/connexion/utils.py: 22%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2This module provides general utility functions used within Connexion.
3"""
5import functools
6import importlib
7import inspect
8import os
9import sys
10import typing
11import typing as t
13import yaml
14from starlette.routing import compile_path
16from connexion.exceptions import TypeValidationError
18if t.TYPE_CHECKING:
19 from connexion.middleware.main import API
22def boolean(s):
23 """
24 Convert JSON/Swagger boolean value to Python, raise ValueError otherwise
26 >>> boolean('true')
27 True
29 >>> boolean('false')
30 False
31 """
32 if isinstance(s, bool):
33 return s
34 elif not hasattr(s, "lower"):
35 raise ValueError("Invalid boolean value")
36 elif s.lower() == "true":
37 return True
38 elif s.lower() == "false":
39 return False
40 else:
41 raise ValueError("Invalid boolean value")
44# https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#data-types
45TYPE_MAP: t.Dict[str, t.Any] = {
46 "integer": int,
47 "number": float,
48 "string": str,
49 "boolean": boolean,
50 "array": list,
51 "object": dict,
52 "file": lambda x: x, # Don't cast files
53} # map of swagger types to python types
56def make_type(value: t.Any, type_: str, format_: t.Optional[str]) -> t.Any:
57 """Cast a value to the type defined in the specification."""
58 # In OpenAPI, files are represented with string type and binary format
59 if type_ == "string" and format_ == "binary":
60 type_ = "file"
62 type_func = TYPE_MAP[type_]
63 return type_func(value)
66def deep_merge(a, b):
67 """merges b into a
68 in case of conflict the value from b is used
69 """
70 for key in b:
71 if key in a:
72 if isinstance(a[key], dict) and isinstance(b[key], dict):
73 deep_merge(a[key], b[key])
74 elif a[key] == b[key]:
75 pass
76 else:
77 # b overwrites a
78 a[key] = b[key]
79 else:
80 a[key] = b[key]
81 return a
84def deep_getattr(obj, attr):
85 """
86 Recurses through an attribute chain to get the ultimate value.
87 """
89 attrs = attr.split(".")
91 return functools.reduce(getattr, attrs, obj)
94def deep_get(obj, keys):
95 """
96 Recurses through a nested object get a leaf value.
98 There are cases where the use of inheritance or polymorphism-- the use of allOf or
99 oneOf keywords-- will cause the obj to be a list. In this case the keys will
100 contain one or more strings containing integers.
102 :type obj: list or dict
103 :type keys: list of strings
104 """
105 if not keys:
106 return obj
108 if isinstance(obj, list):
109 return deep_get(obj[int(keys[0])], keys[1:])
110 else:
111 return deep_get(obj[keys[0]], keys[1:])
114def get_function_from_name(function_name):
115 """
116 Tries to get function by fully qualified name (e.g. "mymodule.myobj.myfunc")
118 :type function_name: str
119 """
120 if function_name is None:
121 raise ValueError("Empty function name")
123 if "." in function_name:
124 module_name, attr_path = function_name.rsplit(".", 1)
125 else:
126 module_name = ""
127 attr_path = function_name
129 module = None
130 last_import_error = None
132 while not module:
133 try:
134 module = importlib.import_module(module_name)
135 except ImportError as import_error:
136 last_import_error = import_error
137 if "." in module_name:
138 module_name, attr_path1 = module_name.rsplit(".", 1)
139 attr_path = f"{attr_path1}.{attr_path}"
140 else:
141 raise
142 try:
143 function = deep_getattr(module, attr_path)
144 except AttributeError:
145 if last_import_error:
146 raise last_import_error
147 else:
148 raise
149 return function
152def is_json_mimetype(mimetype):
153 """
154 :type mimetype: str
155 :rtype: bool
156 """
157 if mimetype is None:
158 return False
160 maintype, subtype = mimetype.split("/") # type: str, str
161 if ";" in subtype:
162 subtype, parameter = subtype.split(";", maxsplit=1)
163 return maintype == "application" and (
164 subtype == "json" or subtype.endswith("+json")
165 )
168def all_json(mimetypes):
169 """
170 Returns True if all mimetypes are serialized with json
172 :type mimetypes: list
173 :rtype: bool
175 >>> all_json(['application/json'])
176 True
177 >>> all_json(['application/x.custom+json'])
178 True
179 >>> all_json([])
180 True
181 >>> all_json(['application/xml'])
182 False
183 >>> all_json(['text/json'])
184 False
185 >>> all_json(['application/json', 'other/type'])
186 False
187 >>> all_json(['application/json', 'application/x.custom+json'])
188 True
189 """
190 return all(is_json_mimetype(mimetype) for mimetype in mimetypes)
193def is_nullable(param_def):
194 return param_def.get("schema", param_def).get("nullable", False) or param_def.get(
195 "x-nullable", False
196 ) # swagger2
199def is_null(value):
200 if hasattr(value, "strip") and value.strip() in ["null", "None"]:
201 return True
203 if value is None:
204 return True
206 return False
209def has_coroutine(function, api=None):
210 """
211 Checks if function is a coroutine.
212 If ``function`` is a decorator (has a ``__wrapped__`` attribute)
213 this function will also look at the wrapped function.
214 """
216 def iscorofunc(func):
217 iscorofunc = inspect.iscoroutinefunction(func)
218 while not iscorofunc and hasattr(func, "__wrapped__"):
219 func = func.__wrapped__
220 iscorofunc = inspect.iscoroutinefunction(func)
221 return iscorofunc
223 if api is None:
224 return iscorofunc(function)
226 else:
227 return any(iscorofunc(func) for func in (function, api.get_response))
230def yamldumper(openapi):
231 """
232 Returns a nicely-formatted yaml spec.
233 :param openapi: a spec dictionary.
234 :return: a nicely-formatted, serialized yaml spec.
235 """
237 def should_use_block(value):
238 char_list = (
239 "\u000a" # line feed
240 "\u000d" # carriage return
241 "\u001c" # file separator
242 "\u001d" # group separator
243 "\u001e" # record separator
244 "\u0085" # next line
245 "\u2028" # line separator
246 "\u2029" # paragraph separator
247 )
248 for c in char_list:
249 if c in value:
250 return True
251 return False
253 def my_represent_scalar(self, tag, value, style=None):
254 if should_use_block(value):
255 style = "|"
256 else:
257 style = self.default_style
259 node = yaml.representer.ScalarNode(tag, value, style=style)
260 if self.alias_key is not None:
261 self.represented_objects[self.alias_key] = node
262 return node
264 class NoAnchorDumper(yaml.dumper.SafeDumper):
265 """A yaml Dumper that does not replace duplicate entries
266 with yaml anchors.
267 """
269 def ignore_aliases(self, *args):
270 return True
272 # Dump long lines as "|".
273 yaml.representer.SafeRepresenter.represent_scalar = my_represent_scalar
275 return yaml.dump(openapi, allow_unicode=True, Dumper=NoAnchorDumper)
278def not_installed_error(exc, *, msg=None): # pragma: no cover
279 """Raises the ImportError when the module/object is actually called with a custom message."""
281 def _delayed_error(*args, **kwargs):
282 if msg is not None:
283 raise type(exc)(msg).with_traceback(exc.__traceback__)
284 raise exc
286 return _delayed_error
289def extract_content_type(
290 headers: t.Union[t.List[t.Tuple[bytes, bytes]], t.Dict[str, str]],
291) -> t.Optional[str]:
292 """Extract the mime type and encoding from the content type headers.
294 :param headers: Headers from ASGI scope
296 :return: The content type if available in headers, otherwise None
297 """
298 content_type: t.Optional[str] = None
300 header_pairs_type = t.Collection[t.Tuple[t.Union[str, bytes], t.Union[str, bytes]]]
301 header_pairs: header_pairs_type = headers.items() if isinstance(headers, dict) else headers # type: ignore
302 for key, value in header_pairs:
303 # Headers can always be decoded using latin-1:
304 # https://stackoverflow.com/a/27357138/4098821
305 if isinstance(key, bytes):
306 decoded_key: str = key.decode("latin-1")
307 else:
308 decoded_key = key
310 if decoded_key.lower() == "content-type":
311 if isinstance(value, bytes):
312 content_type = value.decode("latin-1")
313 else:
314 content_type = value
315 break
317 return content_type
320def split_content_type(
321 content_type: t.Optional[str],
322) -> t.Tuple[t.Optional[str], t.Optional[str]]:
323 """Split the content type in mime_type and encoding. Other parameters are ignored."""
324 mime_type, encoding = None, None
326 if content_type is None:
327 return mime_type, encoding
329 # Check for parameters
330 if ";" in content_type:
331 mime_type, parameters = content_type.split(";", maxsplit=1)
333 # Find parameter describing the charset
334 prefix = "charset="
335 for parameter in parameters.split(";"):
336 if parameter.startswith(prefix):
337 encoding = parameter[len(prefix) :]
338 else:
339 mime_type = content_type
340 return mime_type, encoding
343def coerce_type(param, value, parameter_type, parameter_name=None):
344 # TODO: clean up
345 TYPE_MAP = {"integer": int, "number": float, "boolean": boolean, "object": dict}
347 def make_type(value, type_literal):
348 type_func = TYPE_MAP.get(type_literal)
349 return type_func(value)
351 param_schema = param.get("schema", param)
352 if is_nullable(param_schema) and is_null(value):
353 return None
355 param_type = param_schema.get("type")
356 parameter_name = parameter_name if parameter_name else param.get("name")
357 if param_type == "array":
358 converted_params = []
359 if parameter_type == "header":
360 value = value.split(",")
361 for v in value:
362 try:
363 converted = make_type(v, param_schema["items"]["type"])
364 except (ValueError, TypeError):
365 converted = v
366 converted_params.append(converted)
367 return converted_params
368 elif param_type == "object":
369 if param_schema.get("properties"):
371 def cast_leaves(d, schema):
372 if type(d) is not dict:
373 try:
374 return make_type(d, schema["type"])
375 except (ValueError, TypeError):
376 return d
377 for k, v in d.items():
378 if k in schema["properties"]:
379 d[k] = cast_leaves(v, schema["properties"][k])
380 return d
382 return cast_leaves(value, param_schema)
383 return value
384 else:
385 try:
386 return make_type(value, param_type)
387 except ValueError:
388 raise TypeValidationError(param_type, parameter_type, parameter_name)
389 except TypeError:
390 return value
393@typing.no_type_check
394def get_root_path(import_name: str) -> str:
395 """Copied from Flask:
396 https://github.com/pallets/flask/blob/836866dc19218832cf02f8b04911060ac92bfc0b/src/flask/helpers.py#L595
398 Find the root path of a package, or the path that contains a
399 module. If it cannot be found, returns the current working
400 directory.
402 :meta private:
403 """
404 # Module already imported and has a file attribute. Use that first.
405 mod = sys.modules.get(import_name)
407 if mod is not None and hasattr(mod, "__file__") and mod.__file__ is not None:
408 return os.path.dirname(os.path.abspath(mod.__file__))
410 # Next attempt: check the loader.
411 try:
412 spec = importlib.util.find_spec(import_name)
414 if spec is None:
415 raise ValueError
416 except (ImportError, ValueError):
417 loader = None
418 else:
419 loader = spec.loader
421 # Loader does not exist or we're referring to an unloaded main
422 # module or a main module without path (interactive sessions), go
423 # with the current working directory.
424 if loader is None:
425 return os.getcwd()
427 if hasattr(loader, "get_filename"):
428 filepath = loader.get_filename(import_name) # pyright: ignore
429 else:
430 # Fall back to imports.
431 __import__(import_name)
432 mod = sys.modules[import_name]
433 filepath = getattr(mod, "__file__", None)
435 # If we don't have a file path it might be because it is a
436 # namespace package. In this case pick the root path from the
437 # first module that is contained in the package.
438 if filepath is None:
439 raise RuntimeError(
440 "No root path can be found for the provided module"
441 f" {import_name!r}. This can happen because the module"
442 " came from an import hook that does not provide file"
443 " name information or because it's a namespace package."
444 " In this case the root path needs to be explicitly"
445 " provided."
446 )
448 # filepath is import_name.py for a module, or __init__.py for a package.
449 return os.path.dirname(os.path.abspath(filepath)) # type: ignore[no-any-return]
452def inspect_function_arguments(function: t.Callable) -> t.Tuple[t.List[str], bool]:
453 """
454 Returns the list of variables names of a function and if it
455 accepts keyword arguments.
456 """
457 parameters = inspect.signature(function).parameters
458 bound_arguments = [
459 name
460 for name, p in parameters.items()
461 if p.kind not in (p.VAR_POSITIONAL, p.VAR_KEYWORD)
462 ]
463 has_kwargs = any(p.kind == p.VAR_KEYWORD for p in parameters.values())
464 return list(bound_arguments), has_kwargs
467T = t.TypeVar("T")
470@t.overload
471def sort_routes(routes: t.List[str], *, key: None = None) -> t.List[str]: ...
474@t.overload
475def sort_routes(routes: t.List[T], *, key: t.Callable[[T], str]) -> t.List[T]: ...
478def sort_routes(routes, *, key=None):
479 """Sorts a list of routes from most specific to least specific.
481 See Starlette routing documentation and implementation as this function
482 is aimed to sort according to that logic.
483 - https://www.starlette.io/routing/#route-priority
485 The only difference is that a `path` component is appended to each route
486 such that `/` is less specific than `/basepath` while they are technically
487 not comparable.
488 This is because it is also done by the `Mount` class internally:
489 https://github.com/encode/starlette/blob/1c1043ca0ab7126419948b27f9d0a78270fd74e6/starlette/routing.py#L388
491 For example, from most to least specific:
492 - /users/me
493 - /users/{username}/projects/{project}
494 - /users/{username}
496 :param routes: List of routes to sort
497 :param key: Function to extract the path from a route if it is not a string
499 :return: List of routes sorted from most specific to least specific
500 """
502 class SortableRoute:
503 def __init__(self, path: str) -> None:
504 self.path = path.rstrip("/")
505 if not self.path.endswith("/{path:path}"):
506 self.path += "/{path:path}"
507 self.path_regex, _, _ = compile_path(self.path)
509 def __lt__(self, other: "SortableRoute") -> bool:
510 return bool(other.path_regex.match(self.path)) or self.path < other.path
512 return sorted(routes, key=lambda r: SortableRoute(key(r) if key else r))
515def sort_apis_by_basepath(apis: t.List["API"]) -> t.List["API"]:
516 """Sorts a list of APIs by basepath.
518 :param apis: List of APIs to sort
520 :return: List of APIs sorted by basepath
521 """
522 return sort_routes(apis, key=lambda api: api.base_path or "/")
525def build_example_from_schema(schema):
526 if "example" in schema:
527 return schema["example"]
529 if "properties" in schema:
530 # Recurse if schema is an object
531 return {
532 key: build_example_from_schema(value)
533 for (key, value) in schema["properties"].items()
534 }
536 if "items" in schema:
537 # Recurse if schema is an array
538 min_item_count = schema.get("minItems", 0)
539 max_item_count = schema.get("maxItems")
541 if max_item_count is None or max_item_count >= min_item_count + 1:
542 item_count = min_item_count + 1
543 else:
544 item_count = min_item_count
546 return [build_example_from_schema(schema["items"]) for n in range(item_count)]
548 try:
549 from jsf import JSF
550 except ImportError:
551 return None
553 try:
554 faker = JSF(schema)
555 return faker.generate()
556 except TypeError:
558 return None