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