m2m模型翻译
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

738 lines
28 KiB

7 months ago
  1. from __future__ import absolute_import
  2. from functools import wraps, partial
  3. from flask import request, url_for, current_app
  4. from flask import abort as original_flask_abort
  5. from flask import make_response as original_flask_make_response
  6. from flask.views import MethodView
  7. from flask.signals import got_request_exception
  8. from werkzeug.datastructures import Headers
  9. from werkzeug.exceptions import HTTPException, MethodNotAllowed, NotFound, NotAcceptable, InternalServerError
  10. from werkzeug.wrappers import Response as ResponseBase
  11. from flask_restful.utils import http_status_message, unpack, OrderedDict
  12. from flask_restful.representations.json import output_json
  13. import sys
  14. from types import MethodType
  15. import operator
  16. try:
  17. from collections.abc import Mapping
  18. except ImportError:
  19. from collections import Mapping
  20. _PROPAGATE_EXCEPTIONS = 'PROPAGATE_EXCEPTIONS'
  21. __all__ = ('Api', 'Resource', 'marshal', 'marshal_with', 'marshal_with_field', 'abort')
  22. def abort(http_status_code, **kwargs):
  23. """Raise a HTTPException for the given http_status_code. Attach any keyword
  24. arguments to the exception for later processing.
  25. """
  26. # noinspection PyUnresolvedReferences
  27. try:
  28. original_flask_abort(http_status_code)
  29. except HTTPException as e:
  30. if len(kwargs):
  31. e.data = kwargs
  32. raise
  33. def _get_propagate_exceptions_bool(app):
  34. """Handle Flask's propagate_exceptions.
  35. If propagate_exceptions is set to True then the exceptions are re-raised rather than being handled
  36. by the apps error handlers.
  37. The default value for Flask's app.config['PROPAGATE_EXCEPTIONS'] is None. In this case return a sensible
  38. value: self.testing or self.debug.
  39. """
  40. propagate_exceptions = app.config.get(_PROPAGATE_EXCEPTIONS, False)
  41. if propagate_exceptions is None:
  42. return app.testing or app.debug
  43. return propagate_exceptions
  44. def _handle_flask_propagate_exceptions_config(app, e):
  45. propagate_exceptions = _get_propagate_exceptions_bool(app)
  46. if not isinstance(e, HTTPException) and propagate_exceptions:
  47. exc_type, exc_value, tb = sys.exc_info()
  48. if exc_value is e:
  49. raise
  50. else:
  51. raise e
  52. DEFAULT_REPRESENTATIONS = [('application/json', output_json)]
  53. class Api(object):
  54. """
  55. The main entry point for the application.
  56. You need to initialize it with a Flask Application: ::
  57. >>> app = Flask(__name__)
  58. >>> api = restful.Api(app)
  59. Alternatively, you can use :meth:`init_app` to set the Flask application
  60. after it has been constructed.
  61. :param app: the Flask application object
  62. :type app: flask.Flask or flask.Blueprint
  63. :param prefix: Prefix all routes with a value, eg v1 or 2010-04-01
  64. :type prefix: str
  65. :param default_mediatype: The default media type to return
  66. :type default_mediatype: str
  67. :param decorators: Decorators to attach to every resource
  68. :type decorators: list
  69. :param catch_all_404s: Use :meth:`handle_error`
  70. to handle 404 errors throughout your app
  71. :param serve_challenge_on_401: Whether to serve a challenge response to
  72. clients on receiving 401. This usually leads to a username/password
  73. popup in web browsers.
  74. :param url_part_order: A string that controls the order that the pieces
  75. of the url are concatenated when the full url is constructed. 'b'
  76. is the blueprint (or blueprint registration) prefix, 'a' is the api
  77. prefix, and 'e' is the path component the endpoint is added with
  78. :type catch_all_404s: bool
  79. :param errors: A dictionary to define a custom response for each
  80. exception or error raised during a request
  81. :type errors: dict
  82. """
  83. def __init__(self, app=None, prefix='',
  84. default_mediatype='application/json', decorators=None,
  85. catch_all_404s=False, serve_challenge_on_401=False,
  86. url_part_order='bae', errors=None):
  87. self.representations = OrderedDict(DEFAULT_REPRESENTATIONS)
  88. self.urls = {}
  89. self.prefix = prefix
  90. self.default_mediatype = default_mediatype
  91. self.decorators = decorators if decorators else []
  92. self.catch_all_404s = catch_all_404s
  93. self.serve_challenge_on_401 = serve_challenge_on_401
  94. self.url_part_order = url_part_order
  95. self.errors = errors or {}
  96. self.blueprint_setup = None
  97. self.endpoints = set()
  98. self.resources = []
  99. self.app = None
  100. self.blueprint = None
  101. if app is not None:
  102. self.app = app
  103. self.init_app(app)
  104. def init_app(self, app):
  105. """Initialize this class with the given :class:`flask.Flask`
  106. application or :class:`flask.Blueprint` object.
  107. :param app: the Flask application or blueprint object
  108. :type app: flask.Flask
  109. :type app: flask.Blueprint
  110. Examples::
  111. api = Api()
  112. api.add_resource(...)
  113. api.init_app(app)
  114. """
  115. # If app is a blueprint, defer the initialization
  116. try:
  117. app.record(self._deferred_blueprint_init)
  118. # Flask.Blueprint has a 'record' attribute, Flask.Api does not
  119. except AttributeError:
  120. self._init_app(app)
  121. else:
  122. self.blueprint = app
  123. def _complete_url(self, url_part, registration_prefix):
  124. """This method is used to defer the construction of the final url in
  125. the case that the Api is created with a Blueprint.
  126. :param url_part: The part of the url the endpoint is registered with
  127. :param registration_prefix: The part of the url contributed by the
  128. blueprint. Generally speaking, BlueprintSetupState.url_prefix
  129. """
  130. parts = {
  131. 'b': registration_prefix,
  132. 'a': self.prefix,
  133. 'e': url_part
  134. }
  135. return ''.join(parts[key] for key in self.url_part_order if parts[key])
  136. @staticmethod
  137. def _blueprint_setup_add_url_rule_patch(blueprint_setup, rule, endpoint=None, view_func=None, **options):
  138. """Method used to patch BlueprintSetupState.add_url_rule for setup
  139. state instance corresponding to this Api instance. Exists primarily
  140. to enable _complete_url's function.
  141. :param blueprint_setup: The BlueprintSetupState instance (self)
  142. :param rule: A string or callable that takes a string and returns a
  143. string(_complete_url) that is the url rule for the endpoint
  144. being registered
  145. :param endpoint: See BlueprintSetupState.add_url_rule
  146. :param view_func: See BlueprintSetupState.add_url_rule
  147. :param **options: See BlueprintSetupState.add_url_rule
  148. """
  149. if callable(rule):
  150. rule = rule(blueprint_setup.url_prefix)
  151. elif blueprint_setup.url_prefix:
  152. rule = blueprint_setup.url_prefix + rule
  153. options.setdefault('subdomain', blueprint_setup.subdomain)
  154. if endpoint is None:
  155. endpoint = view_func.__name__
  156. defaults = blueprint_setup.url_defaults
  157. if 'defaults' in options:
  158. defaults = dict(defaults, **options.pop('defaults'))
  159. blueprint_setup.app.add_url_rule(rule, '%s.%s' % (blueprint_setup.blueprint.name, endpoint),
  160. view_func, defaults=defaults, **options)
  161. def _deferred_blueprint_init(self, setup_state):
  162. """Synchronize prefix between blueprint/api and registration options, then
  163. perform initialization with setup_state.app :class:`flask.Flask` object.
  164. When a :class:`flask_restful.Api` object is initialized with a blueprint,
  165. this method is recorded on the blueprint to be run when the blueprint is later
  166. registered to a :class:`flask.Flask` object. This method also monkeypatches
  167. BlueprintSetupState.add_url_rule with _blueprint_setup_add_url_rule_patch.
  168. :param setup_state: The setup state object passed to deferred functions
  169. during blueprint registration
  170. :type setup_state: flask.blueprints.BlueprintSetupState
  171. """
  172. self.blueprint_setup = setup_state
  173. if setup_state.add_url_rule.__name__ != '_blueprint_setup_add_url_rule_patch':
  174. setup_state._original_add_url_rule = setup_state.add_url_rule
  175. setup_state.add_url_rule = MethodType(Api._blueprint_setup_add_url_rule_patch,
  176. setup_state)
  177. if not setup_state.first_registration:
  178. raise ValueError('flask-restful blueprints can only be registered once.')
  179. self._init_app(setup_state.app)
  180. def _init_app(self, app):
  181. """Perform initialization actions with the given :class:`flask.Flask`
  182. object.
  183. :param app: The flask application object
  184. :type app: flask.Flask
  185. """
  186. app.handle_exception = partial(self.error_router, app.handle_exception)
  187. app.handle_user_exception = partial(self.error_router, app.handle_user_exception)
  188. if len(self.resources) > 0:
  189. for resource, urls, kwargs in self.resources:
  190. self._register_view(app, resource, *urls, **kwargs)
  191. def owns_endpoint(self, endpoint):
  192. """Tests if an endpoint name (not path) belongs to this Api. Takes
  193. in to account the Blueprint name part of the endpoint name.
  194. :param endpoint: The name of the endpoint being checked
  195. :return: bool
  196. """
  197. if self.blueprint:
  198. if endpoint.startswith(self.blueprint.name):
  199. endpoint = endpoint.split(self.blueprint.name + '.', 1)[-1]
  200. else:
  201. return False
  202. return endpoint in self.endpoints
  203. def _should_use_fr_error_handler(self):
  204. """ Determine if error should be handled with FR or default Flask
  205. The goal is to return Flask error handlers for non-FR-related routes,
  206. and FR errors (with the correct media type) for FR endpoints. This
  207. method currently handles 404 and 405 errors.
  208. :return: bool
  209. """
  210. adapter = current_app.create_url_adapter(request)
  211. try:
  212. adapter.match()
  213. except MethodNotAllowed as e:
  214. # Check if the other HTTP methods at this url would hit the Api
  215. valid_route_method = e.valid_methods[0]
  216. rule, _ = adapter.match(method=valid_route_method, return_rule=True)
  217. return self.owns_endpoint(rule.endpoint)
  218. except NotFound:
  219. return self.catch_all_404s
  220. except:
  221. # Werkzeug throws other kinds of exceptions, such as Redirect
  222. pass
  223. def _has_fr_route(self):
  224. """Encapsulating the rules for whether the request was to a Flask endpoint"""
  225. # 404's, 405's, which might not have a url_rule
  226. if self._should_use_fr_error_handler():
  227. return True
  228. # for all other errors, just check if FR dispatched the route
  229. if not request.url_rule:
  230. return False
  231. return self.owns_endpoint(request.url_rule.endpoint)
  232. def error_router(self, original_handler, e):
  233. """This function decides whether the error occured in a flask-restful
  234. endpoint or not. If it happened in a flask-restful endpoint, our
  235. handler will be dispatched. If it happened in an unrelated view, the
  236. app's original error handler will be dispatched.
  237. In the event that the error occurred in a flask-restful endpoint but
  238. the local handler can't resolve the situation, the router will fall
  239. back onto the original_handler as last resort.
  240. :param original_handler: the original Flask error handler for the app
  241. :type original_handler: function
  242. :param e: the exception raised while handling the request
  243. :type e: Exception
  244. """
  245. if self._has_fr_route():
  246. try:
  247. return self.handle_error(e)
  248. except Exception:
  249. pass # Fall through to original handler
  250. return original_handler(e)
  251. def handle_error(self, e):
  252. """Error handler for the API transforms a raised exception into a Flask
  253. response, with the appropriate HTTP status code and body.
  254. :param e: the raised Exception object
  255. :type e: Exception
  256. """
  257. got_request_exception.send(current_app._get_current_object(), exception=e)
  258. _handle_flask_propagate_exceptions_config(current_app, e)
  259. headers = Headers()
  260. if isinstance(e, HTTPException):
  261. if e.response is not None:
  262. # If HTTPException is initialized with a response, then return e.get_response().
  263. # This prevents specified error response from being overridden.
  264. # e.g., HTTPException(response=Response("Hello World"))
  265. resp = e.get_response()
  266. return resp
  267. code = e.code
  268. default_data = {
  269. 'message': getattr(e, 'description', http_status_message(code))
  270. }
  271. headers = e.get_response().headers
  272. else:
  273. code = 500
  274. default_data = {
  275. 'message': http_status_message(code),
  276. }
  277. # Werkzeug exceptions generate a content-length header which is added
  278. # to the response in addition to the actual content-length header
  279. # https://github.com/flask-restful/flask-restful/issues/534
  280. remove_headers = ('Content-Length',)
  281. for header in remove_headers:
  282. headers.pop(header, None)
  283. data = getattr(e, 'data', default_data)
  284. if code and code >= 500:
  285. exc_info = sys.exc_info()
  286. if exc_info[1] is None:
  287. exc_info = None
  288. current_app.log_exception(exc_info)
  289. error_cls_name = type(e).__name__
  290. if error_cls_name in self.errors:
  291. custom_data = self.errors.get(error_cls_name, {})
  292. code = custom_data.get('status', 500)
  293. data.update(custom_data)
  294. if code == 406 and self.default_mediatype is None:
  295. # if we are handling NotAcceptable (406), make sure that
  296. # make_response uses a representation we support as the
  297. # default mediatype (so that make_response doesn't throw
  298. # another NotAcceptable error).
  299. supported_mediatypes = list(self.representations.keys())
  300. fallback_mediatype = supported_mediatypes[0] if supported_mediatypes else "text/plain"
  301. resp = self.make_response(
  302. data,
  303. code,
  304. headers,
  305. fallback_mediatype = fallback_mediatype
  306. )
  307. else:
  308. resp = self.make_response(data, code, headers)
  309. if code == 401:
  310. resp = self.unauthorized(resp)
  311. return resp
  312. def mediatypes_method(self):
  313. """Return a method that returns a list of mediatypes
  314. """
  315. return lambda resource_cls: self.mediatypes() + [self.default_mediatype]
  316. def add_resource(self, resource, *urls, **kwargs):
  317. """Adds a resource to the api.
  318. :param resource: the class name of your resource
  319. :type resource: :class:`Type[Resource]`
  320. :param urls: one or more url routes to match for the resource, standard
  321. flask routing rules apply. Any url variables will be
  322. passed to the resource method as args.
  323. :type urls: str
  324. :param endpoint: endpoint name (defaults to :meth:`Resource.__name__.lower`
  325. Can be used to reference this route in :class:`fields.Url` fields
  326. :type endpoint: str
  327. :param resource_class_args: args to be forwarded to the constructor of
  328. the resource.
  329. :type resource_class_args: tuple
  330. :param resource_class_kwargs: kwargs to be forwarded to the constructor
  331. of the resource.
  332. :type resource_class_kwargs: dict
  333. Additional keyword arguments not specified above will be passed as-is
  334. to :meth:`flask.Flask.add_url_rule`.
  335. Examples::
  336. api.add_resource(HelloWorld, '/', '/hello')
  337. api.add_resource(Foo, '/foo', endpoint="foo")
  338. api.add_resource(FooSpecial, '/special/foo', endpoint="foo")
  339. """
  340. if self.app is not None:
  341. self._register_view(self.app, resource, *urls, **kwargs)
  342. else:
  343. self.resources.append((resource, urls, kwargs))
  344. def resource(self, *urls, **kwargs):
  345. """Wraps a :class:`~flask_restful.Resource` class, adding it to the
  346. api. Parameters are the same as :meth:`~flask_restful.Api.add_resource`.
  347. Example::
  348. app = Flask(__name__)
  349. api = restful.Api(app)
  350. @api.resource('/foo')
  351. class Foo(Resource):
  352. def get(self):
  353. return 'Hello, World!'
  354. """
  355. def decorator(cls):
  356. self.add_resource(cls, *urls, **kwargs)
  357. return cls
  358. return decorator
  359. def _register_view(self, app, resource, *urls, **kwargs):
  360. endpoint = kwargs.pop('endpoint', None) or resource.__name__.lower()
  361. self.endpoints.add(endpoint)
  362. resource_class_args = kwargs.pop('resource_class_args', ())
  363. resource_class_kwargs = kwargs.pop('resource_class_kwargs', {})
  364. # NOTE: 'view_functions' is cleaned up from Blueprint class in Flask 1.0
  365. if endpoint in getattr(app, 'view_functions', {}):
  366. previous_view_class = app.view_functions[endpoint].__dict__['view_class']
  367. # if you override the endpoint with a different class, avoid the collision by raising an exception
  368. if previous_view_class != resource:
  369. raise ValueError('This endpoint (%s) is already set to the class %s.' % (endpoint, previous_view_class.__name__))
  370. resource.mediatypes = self.mediatypes_method() # Hacky
  371. resource.endpoint = endpoint
  372. resource_func = self.output(resource.as_view(endpoint, *resource_class_args,
  373. **resource_class_kwargs))
  374. for decorator in self.decorators:
  375. resource_func = decorator(resource_func)
  376. for url in urls:
  377. # If this Api has a blueprint
  378. if self.blueprint:
  379. # And this Api has been setup
  380. if self.blueprint_setup:
  381. # Set the rule to a string directly, as the blueprint is already
  382. # set up.
  383. self.blueprint_setup.add_url_rule(url, view_func=resource_func, **kwargs)
  384. continue
  385. else:
  386. # Set the rule to a function that expects the blueprint prefix
  387. # to construct the final url. Allows deferment of url finalization
  388. # in the case that the associated Blueprint has not yet been
  389. # registered to an application, so we can wait for the registration
  390. # prefix
  391. rule = partial(self._complete_url, url)
  392. else:
  393. # If we've got no Blueprint, just build a url with no prefix
  394. rule = self._complete_url(url, '')
  395. # Add the url to the application or blueprint
  396. app.add_url_rule(rule, view_func=resource_func, **kwargs)
  397. def output(self, resource):
  398. """Wraps a resource (as a flask view function), for cases where the
  399. resource does not directly return a response object
  400. :param resource: The resource as a flask view function
  401. """
  402. @wraps(resource)
  403. def wrapper(*args, **kwargs):
  404. resp = resource(*args, **kwargs)
  405. if isinstance(resp, ResponseBase): # There may be a better way to test
  406. return resp
  407. data, code, headers = unpack(resp)
  408. return self.make_response(data, code, headers=headers)
  409. return wrapper
  410. def url_for(self, resource, **values):
  411. """Generates a URL to the given resource.
  412. Works like :func:`flask.url_for`."""
  413. endpoint = resource.endpoint
  414. if self.blueprint:
  415. endpoint = '{0}.{1}'.format(self.blueprint.name, endpoint)
  416. return url_for(endpoint, **values)
  417. def make_response(self, data, *args, **kwargs):
  418. """Looks up the representation transformer for the requested media
  419. type, invoking the transformer to create a response object. This
  420. defaults to default_mediatype if no transformer is found for the
  421. requested mediatype. If default_mediatype is None, a 406 Not
  422. Acceptable response will be sent as per RFC 2616 section 14.1
  423. :param data: Python object containing response data to be transformed
  424. """
  425. default_mediatype = kwargs.pop('fallback_mediatype', None) or self.default_mediatype
  426. mediatype = request.accept_mimetypes.best_match(
  427. self.representations,
  428. default=default_mediatype,
  429. )
  430. if mediatype is None:
  431. raise NotAcceptable()
  432. if mediatype in self.representations:
  433. resp = self.representations[mediatype](data, *args, **kwargs)
  434. resp.headers['Content-Type'] = mediatype
  435. return resp
  436. elif mediatype == 'text/plain':
  437. resp = original_flask_make_response(str(data), *args, **kwargs)
  438. resp.headers['Content-Type'] = 'text/plain'
  439. return resp
  440. else:
  441. raise InternalServerError()
  442. def mediatypes(self):
  443. """Returns a list of requested mediatypes sent in the Accept header"""
  444. return [h for h, q in sorted(request.accept_mimetypes,
  445. key=operator.itemgetter(1), reverse=True)]
  446. def representation(self, mediatype):
  447. """Allows additional representation transformers to be declared for the
  448. api. Transformers are functions that must be decorated with this
  449. method, passing the mediatype the transformer represents. Three
  450. arguments are passed to the transformer:
  451. * The data to be represented in the response body
  452. * The http status code
  453. * A dictionary of headers
  454. The transformer should convert the data appropriately for the mediatype
  455. and return a Flask response object.
  456. Ex::
  457. @api.representation('application/xml')
  458. def xml(data, code, headers):
  459. resp = make_response(convert_data_to_xml(data), code)
  460. resp.headers.extend(headers)
  461. return resp
  462. """
  463. def wrapper(func):
  464. self.representations[mediatype] = func
  465. return func
  466. return wrapper
  467. def unauthorized(self, response):
  468. """ Given a response, change it to ask for credentials """
  469. if self.serve_challenge_on_401:
  470. realm = current_app.config.get("HTTP_BASIC_AUTH_REALM", "flask-restful")
  471. challenge = u"{0} realm=\"{1}\"".format("Basic", realm)
  472. response.headers['WWW-Authenticate'] = challenge
  473. return response
  474. class Resource(MethodView):
  475. """
  476. Represents an abstract RESTful resource. Concrete resources should
  477. extend from this class and expose methods for each supported HTTP
  478. method. If a resource is invoked with an unsupported HTTP method,
  479. the API will return a response with status 405 Method Not Allowed.
  480. Otherwise the appropriate method is called and passed all arguments
  481. from the url rule used when adding the resource to an Api instance. See
  482. :meth:`~flask_restful.Api.add_resource` for details.
  483. """
  484. representations = None
  485. method_decorators = []
  486. def dispatch_request(self, *args, **kwargs):
  487. # Taken from flask
  488. #noinspection PyUnresolvedReferences
  489. meth = getattr(self, request.method.lower(), None)
  490. if meth is None and request.method == 'HEAD':
  491. meth = getattr(self, 'get', None)
  492. assert meth is not None, 'Unimplemented method %r' % request.method
  493. if isinstance(self.method_decorators, Mapping):
  494. decorators = self.method_decorators.get(request.method.lower(), [])
  495. else:
  496. decorators = self.method_decorators
  497. for decorator in decorators:
  498. meth = decorator(meth)
  499. resp = meth(*args, **kwargs)
  500. if isinstance(resp, ResponseBase): # There may be a better way to test
  501. return resp
  502. representations = self.representations or OrderedDict()
  503. #noinspection PyUnresolvedReferences
  504. mediatype = request.accept_mimetypes.best_match(representations, default=None)
  505. if mediatype in representations:
  506. data, code, headers = unpack(resp)
  507. resp = representations[mediatype](data, code, headers)
  508. resp.headers['Content-Type'] = mediatype
  509. return resp
  510. return resp
  511. def marshal(data, fields, envelope=None):
  512. """Takes raw data (in the form of a dict, list, object) and a dict of
  513. fields to output and filters the data based on those fields.
  514. :param data: the actual object(s) from which the fields are taken from
  515. :param fields: a dict of whose keys will make up the final serialized
  516. response output
  517. :param envelope: optional key that will be used to envelop the serialized
  518. response
  519. >>> from flask_restful import fields, marshal
  520. >>> data = { 'a': 100, 'b': 'foo' }
  521. >>> mfields = { 'a': fields.Raw }
  522. >>> marshal(data, mfields)
  523. OrderedDict([('a', 100)])
  524. >>> marshal(data, mfields, envelope='data')
  525. OrderedDict([('data', OrderedDict([('a', 100)]))])
  526. """
  527. def make(cls):
  528. if isinstance(cls, type):
  529. return cls()
  530. return cls
  531. if isinstance(data, (list, tuple)):
  532. return (OrderedDict([(envelope, [marshal(d, fields) for d in data])])
  533. if envelope else [marshal(d, fields) for d in data])
  534. items = ((k, marshal(data, v) if isinstance(v, dict)
  535. else make(v).output(k, data))
  536. for k, v in fields.items())
  537. return OrderedDict([(envelope, OrderedDict(items))]) if envelope else OrderedDict(items)
  538. class marshal_with(object):
  539. """A decorator that apply marshalling to the return values of your methods.
  540. >>> from flask_restful import fields, marshal_with
  541. >>> mfields = { 'a': fields.Raw }
  542. >>> @marshal_with(mfields)
  543. ... def get():
  544. ... return { 'a': 100, 'b': 'foo' }
  545. ...
  546. ...
  547. >>> get()
  548. OrderedDict([('a', 100)])
  549. >>> @marshal_with(mfields, envelope='data')
  550. ... def get():
  551. ... return { 'a': 100, 'b': 'foo' }
  552. ...
  553. ...
  554. >>> get()
  555. OrderedDict([('data', OrderedDict([('a', 100)]))])
  556. see :meth:`flask_restful.marshal`
  557. """
  558. def __init__(self, fields, envelope=None):
  559. """
  560. :param fields: a dict of whose keys will make up the final
  561. serialized response output
  562. :param envelope: optional key that will be used to envelop the serialized
  563. response
  564. """
  565. self.fields = fields
  566. self.envelope = envelope
  567. def __call__(self, f):
  568. @wraps(f)
  569. def wrapper(*args, **kwargs):
  570. resp = f(*args, **kwargs)
  571. if isinstance(resp, tuple):
  572. data, code, headers = unpack(resp)
  573. return marshal(data, self.fields, self.envelope), code, headers
  574. else:
  575. return marshal(resp, self.fields, self.envelope)
  576. return wrapper
  577. class marshal_with_field(object):
  578. """
  579. A decorator that formats the return values of your methods with a single field.
  580. >>> from flask_restful import marshal_with_field, fields
  581. >>> @marshal_with_field(fields.List(fields.Integer))
  582. ... def get():
  583. ... return ['1', 2, 3.0]
  584. ...
  585. >>> get()
  586. [1, 2, 3]
  587. see :meth:`flask_restful.marshal_with`
  588. """
  589. def __init__(self, field):
  590. """
  591. :param field: a single field with which to marshal the output.
  592. """
  593. if isinstance(field, type):
  594. self.field = field()
  595. else:
  596. self.field = field
  597. def __call__(self, f):
  598. @wraps(f)
  599. def wrapper(*args, **kwargs):
  600. resp = f(*args, **kwargs)
  601. if isinstance(resp, tuple):
  602. data, code, headers = unpack(resp)
  603. return self.field.format(data), code, headers
  604. return self.field.format(resp)
  605. return wrapper