forked from python-restx/flask-restx
-
Notifications
You must be signed in to change notification settings - Fork 0
/
namespace.py
374 lines (303 loc) · 12.6 KB
/
namespace.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
import inspect
import warnings
import logging
from collections import namedtuple, OrderedDict
from flask import request
from flask.views import http_method_funcs
from ._http import HTTPStatus
from .errors import abort
from .marshalling import marshal, marshal_with
from .model import Model, OrderedModel, SchemaModel
from .reqparse import RequestParser
from .utils import merge
# Container for each route applied to a Resource using @ns.route decorator
ResourceRoute = namedtuple("ResourceRoute", "resource urls route_doc kwargs")
class Namespace(object):
"""
Group resources together.
Namespace is to API what :class:`flask:flask.Blueprint` is for :class:`flask:flask.Flask`.
:param str name: The namespace name
:param str description: An optional short description
:param str path: An optional prefix path. If not provided, prefix is ``/+name``
:param list decorators: A list of decorators to apply to each resources
:param bool validate: Whether or not to perform validation on this namespace
:param bool ordered: Whether or not to preserve order on models and marshalling
:param Api api: an optional API to attache to the namespace
"""
def __init__(
self,
name,
description=None,
path=None,
decorators=None,
validate=None,
authorizations=None,
ordered=False,
**kwargs
):
self.name = name
self.description = description
self._path = path
self._schema = None
self._validate = validate
self.models = {}
self.urls = {}
self.decorators = decorators if decorators else []
self.resources = [] # List[ResourceRoute]
self.error_handlers = OrderedDict()
self.default_error_handler = None
self.authorizations = authorizations
self.ordered = ordered
self.apis = []
if "api" in kwargs:
self.apis.append(kwargs["api"])
self.logger = logging.getLogger(__name__ + "." + self.name)
@property
def path(self):
return (self._path or ("/" + self.name)).rstrip("/")
def add_resource(self, resource, *urls, **kwargs):
"""
Register a Resource for a given API Namespace
:param Resource resource: the resource ro register
:param str urls: one or more url routes to match for the resource,
standard flask routing rules apply.
Any url variables will be passed to the resource method as args.
:param str endpoint: endpoint name (defaults to :meth:`Resource.__name__.lower`
Can be used to reference this route in :class:`fields.Url` fields
:param list|tuple resource_class_args: args to be forwarded to the constructor of the resource.
:param dict resource_class_kwargs: kwargs to be forwarded to the constructor of the resource.
Additional keyword arguments not specified above will be passed as-is
to :meth:`flask.Flask.add_url_rule`.
Examples::
namespace.add_resource(HelloWorld, '/', '/hello')
namespace.add_resource(Foo, '/foo', endpoint="foo")
namespace.add_resource(FooSpecial, '/special/foo', endpoint="foo")
"""
route_doc = kwargs.pop("route_doc", {})
self.resources.append(ResourceRoute(resource, urls, route_doc, kwargs))
for api in self.apis:
ns_urls = api.ns_urls(self, urls)
api.register_resource(self, resource, *ns_urls, **kwargs)
def route(self, *urls, **kwargs):
"""
A decorator to route resources.
"""
def wrapper(cls):
if (doc := kwargs.pop("doc", None)) is not None:
# build api doc intended only for this route
kwargs["route_doc"] = self._build_doc(cls, doc)
self.add_resource(cls, *urls, **kwargs)
return cls
return wrapper
def _build_doc(self, cls, doc):
if doc is False:
return False
unshortcut_params_description(doc)
handle_deprecations(doc)
for http_method in http_method_funcs:
if http_method in doc:
if doc[http_method] is False:
continue
unshortcut_params_description(doc[http_method])
handle_deprecations(doc[http_method])
if "expect" in doc[http_method] and not isinstance(
doc[http_method]["expect"], (list, tuple)
):
doc[http_method]["expect"] = [doc[http_method]["expect"]]
return merge(getattr(cls, "__apidoc__", {}), doc)
def doc(self, shortcut=None, **kwargs):
"""A decorator to add some api documentation to the decorated object"""
if isinstance(shortcut, str):
kwargs["id"] = shortcut
show = shortcut if isinstance(shortcut, bool) else True
def wrapper(documented):
documented.__apidoc__ = self._build_doc(
documented, kwargs if show else False
)
return documented
return wrapper
def hide(self, func):
"""A decorator to hide a resource or a method from specifications"""
return self.doc(False)(func)
def abort(self, *args, **kwargs):
"""
Properly abort the current request
See: :func:`~flask_restx.errors.abort`
"""
abort(*args, **kwargs)
def add_model(self, name, definition):
self.models[name] = definition
for api in self.apis:
api.models[name] = definition
return definition
def model(self, name=None, model=None, mask=None, strict=False, **kwargs):
"""
Register a model
:param bool strict - should model validation raise error when non-specified param
is provided?
.. seealso:: :class:`Model`
"""
cls = OrderedModel if self.ordered else Model
model = cls(name, model, mask=mask, strict=strict)
model.__apidoc__.update(kwargs)
return self.add_model(name, model)
def schema_model(self, name=None, schema=None):
"""
Register a model
.. seealso:: :class:`Model`
"""
model = SchemaModel(name, schema)
return self.add_model(name, model)
def extend(self, name, parent, fields):
"""
Extend a model (Duplicate all fields)
:deprecated: since 0.9. Use :meth:`clone` instead
"""
if isinstance(parent, list):
parents = parent + [fields]
model = Model.extend(name, *parents)
else:
model = Model.extend(name, parent, fields)
return self.add_model(name, model)
def clone(self, name, *specs):
"""
Clone a model (Duplicate all fields)
:param str name: the resulting model name
:param specs: a list of models from which to clone the fields
.. seealso:: :meth:`Model.clone`
"""
model = Model.clone(name, *specs)
return self.add_model(name, model)
def inherit(self, name, *specs):
"""
Inherit a model (use the Swagger composition pattern aka. allOf)
.. seealso:: :meth:`Model.inherit`
"""
model = Model.inherit(name, *specs)
return self.add_model(name, model)
def expect(self, *inputs, **kwargs):
"""
A decorator to Specify the expected input model
:param ModelBase|Parse inputs: An expect model or request parser
:param bool validate: whether to perform validation or not
"""
expect = []
params = {"validate": kwargs.get("validate", self._validate), "expect": expect}
for param in inputs:
expect.append(param)
return self.doc(**params)
def parser(self):
"""Instanciate a :class:`~RequestParser`"""
return RequestParser()
def as_list(self, field):
"""Allow to specify nested lists for documentation"""
field.__apidoc__ = merge(getattr(field, "__apidoc__", {}), {"as_list": True})
return field
def marshal_with(
self, fields, as_list=False, code=HTTPStatus.OK, description=None, **kwargs
):
"""
A decorator specifying the fields to use for serialization.
:param bool as_list: Indicate that the return type is a list (for the documentation)
:param int code: Optionally give the expected HTTP response code if its different from 200
"""
def wrapper(func):
doc = {
"responses": {
str(code): (description, [fields], kwargs)
if as_list
else (description, fields, kwargs)
},
"__mask__": kwargs.get(
"mask", True
), # Mask values can't be determined outside app context
}
func.__apidoc__ = merge(getattr(func, "__apidoc__", {}), doc)
return marshal_with(fields, ordered=self.ordered, **kwargs)(func)
return wrapper
def marshal_list_with(self, fields, **kwargs):
"""A shortcut decorator for :meth:`~Api.marshal_with` with ``as_list=True``"""
return self.marshal_with(fields, True, **kwargs)
def marshal(self, *args, **kwargs):
"""A shortcut to the :func:`marshal` helper"""
return marshal(*args, **kwargs)
def errorhandler(self, exception):
"""A decorator to register an error handler for a given exception"""
if inspect.isclass(exception) and issubclass(exception, Exception):
# Register an error handler for a given exception
def wrapper(func):
self.error_handlers[exception] = func
return func
return wrapper
else:
# Register the default error handler
self.default_error_handler = exception
return exception
def param(self, name, description=None, _in="query", **kwargs):
"""
A decorator to specify one of the expected parameters
:param str name: the parameter name
:param str description: a small description
:param str _in: the parameter location `(query|header|formData|body|cookie)`
"""
param = kwargs
param["in"] = _in
param["description"] = description
return self.doc(params={name: param})
def response(self, code, description, model=None, **kwargs):
"""
A decorator to specify one of the expected responses
:param int code: the HTTP status code
:param str description: a small description about the response
:param ModelBase model: an optional response model
"""
return self.doc(responses={str(code): (description, model, kwargs)})
def header(self, name, description=None, **kwargs):
"""
A decorator to specify one of the expected headers
:param str name: the HTTP header name
:param str description: a description about the header
"""
header = {"description": description}
header.update(kwargs)
return self.doc(headers={name: header})
def produces(self, mimetypes):
"""A decorator to specify the MIME types the API can produce"""
return self.doc(produces=mimetypes)
def deprecated(self, func):
"""A decorator to mark a resource or a method as deprecated"""
return self.doc(deprecated=True)(func)
def vendor(self, *args, **kwargs):
"""
A decorator to expose vendor extensions.
Extensions can be submitted as dict or kwargs.
The ``x-`` prefix is optionnal and will be added if missing.
See: http://swagger.io/specification/#specification-extensions-128
"""
for arg in args:
kwargs.update(arg)
return self.doc(vendor=kwargs)
@property
def payload(self):
"""Store the input payload in the current request context"""
return request.get_json()
def unshortcut_params_description(data):
if "params" in data:
for name, description in data["params"].items():
if isinstance(description, str):
data["params"][name] = {"description": description}
def handle_deprecations(doc):
if "parser" in doc:
warnings.warn(
"The parser attribute is deprecated, use expect instead",
DeprecationWarning,
stacklevel=2,
)
doc["expect"] = doc.get("expect", []) + [doc.pop("parser")]
if "body" in doc:
warnings.warn(
"The body attribute is deprecated, use expect instead",
DeprecationWarning,
stacklevel=2,
)
doc["expect"] = doc.get("expect", []) + [doc.pop("body")]