1"""
2This module defines functionality specific to the Flask framework.
3"""
4
5import functools
6import random
7import re
8import string
9import typing as t
10
11import flask
12import werkzeug.routing
13
14from connexion import jsonifier
15from connexion.frameworks.abstract import Framework
16from connexion.lifecycle import WSGIRequest
17from connexion.uri_parsing import AbstractURIParser
18
19
20class Flask(Framework):
21 @staticmethod
22 def is_framework_response(response: t.Any) -> bool:
23 return isinstance(response, flask.Response) or isinstance(
24 response, werkzeug.wrappers.Response
25 )
26
27 @classmethod
28 def connexion_to_framework_response(cls, response):
29 return cls.build_response(
30 content_type=response.content_type,
31 headers=response.headers,
32 status_code=response.status_code,
33 data=response.body,
34 )
35
36 @classmethod
37 def build_response(
38 cls,
39 data: t.Any,
40 *,
41 content_type: t.Optional[str] = None,
42 headers: t.Optional[dict] = None,
43 status_code: t.Optional[int] = None,
44 ):
45 if cls.is_framework_response(data):
46 return flask.current_app.make_response((data, status_code, headers))
47
48 kwargs = {
49 "mimetype": content_type,
50 "headers": headers,
51 "response": data,
52 "status": status_code,
53 }
54 kwargs = {k: v for k, v in kwargs.items() if v is not None}
55 return flask.current_app.response_class(**kwargs)
56
57 @staticmethod
58 def get_request(*, uri_parser: AbstractURIParser, **kwargs) -> WSGIRequest: # type: ignore
59 return WSGIRequest(
60 flask.request, uri_parser=uri_parser, view_args=flask.request.view_args
61 )
62
63
64PATH_PARAMETER = re.compile(r"\{([^}]*)\}")
65
66# map Swagger type to flask path converter
67# see http://flask.pocoo.org/docs/0.10/api/#url-route-registrations
68PATH_PARAMETER_CONVERTERS = {"integer": "int", "number": "float", "path": "path"}
69
70
71def flaskify_endpoint(identifier, randomize=None):
72 """
73 Converts the provided identifier in a valid flask endpoint name
74
75 :type identifier: str
76 :param randomize: If specified, add this many random characters (upper case
77 and digits) to the endpoint name, separated by a pipe character.
78 :type randomize: int | None
79 :rtype: str
80
81 """
82 result = identifier.replace(".", "_")
83 if randomize is None:
84 return result
85
86 chars = string.ascii_uppercase + string.digits
87 return "{result}|{random_string}".format(
88 result=result,
89 random_string="".join(
90 random.SystemRandom().choice(chars) for _ in range(randomize)
91 ),
92 )
93
94
95def convert_path_parameter(match, types):
96 name = match.group(1)
97 swagger_type = types.get(name)
98 converter = PATH_PARAMETER_CONVERTERS.get(swagger_type)
99 return "<{}{}{}>".format(
100 converter or "", ":" if converter else "", name.replace("-", "_")
101 )
102
103
104def flaskify_path(swagger_path, types=None):
105 """
106 Convert swagger path templates to flask path templates
107
108 :type swagger_path: str
109 :type types: dict
110 :rtype: str
111
112 >>> flaskify_path('/foo-bar/{my-param}')
113 '/foo-bar/<my_param>'
114
115 >>> flaskify_path('/foo/{someint}', {'someint': 'int'})
116 '/foo/<int:someint>'
117 """
118 if types is None:
119 types = {}
120 convert_match = functools.partial(convert_path_parameter, types=types)
121 return PATH_PARAMETER.sub(convert_match, swagger_path)
122
123
124class FlaskJSONProvider(flask.json.provider.DefaultJSONProvider):
125 """Custom JSONProvider which adds connexion defaults on top of Flask's"""
126
127 @jsonifier.wrap_default
128 def default(self, o):
129 return super().default(o)
130
131
132class NumberConverter(werkzeug.routing.BaseConverter):
133 """Flask converter for OpenAPI number type"""
134
135 regex = r"[+-]?[0-9]*(?:\.[0-9]*)?"
136
137 def to_python(self, value):
138 return float(value)
139
140
141class IntegerConverter(werkzeug.routing.BaseConverter):
142 """Flask converter for OpenAPI integer type"""
143
144 regex = r"[+-]?[0-9]+"
145
146 def to_python(self, value):
147 return int(value)