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