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