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