namespace.py 13 KB

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