Source code for invenio_rest.views

# -*- coding: utf-8 -*-
#
# This file is part of Invenio.
# Copyright (C) 2015-2018 CERN.
#
# Invenio is free software; you can redistribute it and/or modify it
# under the terms of the MIT License; see LICENSE file for more details.

"""REST API module for Invenio."""

from __future__ import absolute_import, print_function

from flask import Response, abort, current_app, g, jsonify, make_response, \
    request
from flask.views import MethodView
from werkzeug.exceptions import HTTPException

from .errors import RESTException, SameContentException


[docs]def create_api_errorhandler(**kwargs): r"""Create an API error handler. E.g. register a 404 error: .. code-block:: python app.errorhandler(404)(create_api_errorhandler( status=404, message='Not Found')) :param \*\*kwargs: It contains the ``'status'`` and the ``'message'`` to describe the error. """ def api_errorhandler(e): if isinstance(e, RESTException): return e.get_response() elif isinstance(e, HTTPException) and e.description: kwargs['message'] = e.description if kwargs.get('status', 400) >= 500 and hasattr(g, 'sentry_event_id'): kwargs['error_id'] = str(g.sentry_event_id) return make_response(jsonify(kwargs), kwargs['status']) return api_errorhandler
[docs]class ContentNegotiatedMethodView(MethodView): """MethodView with content negotiation. Dispatch HTTP requests as MethodView does and build responses using the registered serializers. It chooses the right serializer using the request's accept type. It also provides a helper method for handling ETags. """ def __init__(self, serializers=None, method_serializers=None, serializers_query_aliases=None, default_media_type=None, default_method_media_type=None, *args, **kwargs): """Register the serializing functions. Serializing functions will receive all named and non named arguments provided to ``make_response`` or returned by request handling methods. Recommended prototype is: ``serializer(data, code=200, headers=None)`` and it should return :class:`flask.Response` instances. Serializing functions can also be overridden by setting ``self.serializers``. :param serializers: A mapping from mediatype to a serializer function. :param method_serializers: A mapping of HTTP method name (GET, PUT, PATCH, POST, DELETE) -> dict(mediatype -> serializer function). If set, it overrides the serializers dict. :param serializers_query_aliases: A mapping of values of the defined query arg (see `config.REST_MIMETYPE_QUERY_ARG_NAME`) to valid mimetypes: dict(alias -> mimetype). :param default_media_type: Default media type used if no accept type has been provided and global serializers are used for the request. Can be None if there is only one global serializer or None. This media type is used for method serializers too if ``default_method_media_type`` is not set. :param default_method_media_type: Default media type used if no accept type has been provided and a specific method serializers are used for the request. Can be ``None`` if the method has only one serializer or ``None``. """ super(ContentNegotiatedMethodView, self).__init__(*args, **kwargs) self.serializers = serializers or None self.default_media_type = default_media_type self.default_method_media_type = default_method_media_type or {} # set default default media_types if none has been given if self.serializers and not self.default_media_type: if len(self.serializers) == 1: self.default_media_type = next(iter(self.serializers.keys())) elif len(self.serializers) > 1: raise ValueError('Multiple serializers with no default media' ' type') # set method serializers self.method_serializers = ({key.upper(): func for key, func in method_serializers.items()} if method_serializers else {}) # set serializer aliases self.serializers_query_aliases = serializers_query_aliases or {} # create default method media_types dict if none has been given if self.method_serializers and not self.default_method_media_type: self.default_method_media_type = {} for http_method, meth_serial in self.method_serializers.items(): if len(self.method_serializers[http_method]) == 1: self.default_method_media_type[http_method] = \ next(iter(self.method_serializers[http_method].keys())) elif len(self.method_serializers[http_method]) > 1: # try to use global default media type if default_media_type in \ self.method_serializers[http_method]: self.default_method_media_type[http_method] = \ default_media_type else: raise ValueError('Multiple serializers for method {0}' 'with no default media type'.format( http_method))
[docs] def get_method_serializers(self, http_method): """Get request method serializers + default media type. Grab serializers from ``method_serializers`` if defined, otherwise returns the default serializers. Uses GET serializers for HEAD requests if no HEAD serializers were specified. The method also determines the default media type. :param http_method: HTTP method as a string. :returns: Tuple of serializers and default media type. """ if http_method == 'HEAD' and 'HEAD' not in self.method_serializers: http_method = 'GET' return ( self.method_serializers.get(http_method, self.serializers), self.default_method_media_type.get( http_method, self.default_media_type) )
def _match_serializers_by_query_arg(self, serializers): """Match serializer by query arg.""" # if the format query argument is present, match the serializer arg_name = current_app.config.get('REST_MIMETYPE_QUERY_ARG_NAME') if arg_name: arg_value = request.args.get(arg_name, None) if arg_value is None: return None # Search for the serializer matching the format try: return serializers[ self.serializers_query_aliases[arg_value]] except KeyError: # either no serializer for this format return None return None def _match_serializers_by_accept_headers(self, serializers, default_media_type): """Match serializer by `Accept` headers.""" # Bail out fast if no accept headers were given. if len(request.accept_mimetypes) == 0: return serializers[default_media_type] # Determine best match based on quality. best_quality = -1 best = None has_wildcard = False for client_accept, quality in request.accept_mimetypes: if quality <= best_quality: continue if client_accept == '*/*': has_wildcard = True for s in serializers: if s in ['*/*', client_accept] and quality > 0: best_quality = quality best = s # If no match found, but wildcard exists, them use default media # type. if best is None and has_wildcard: best = default_media_type if best is not None: return serializers[best] return None
[docs] def match_serializers(self, serializers, default_media_type): """Choose serializer for a given request based on query arg or headers. Checks if query arg `format` (by default) is present and tries to match the serializer based on the arg value, by resolving the mimetype mapped to the arg value. Otherwise, chooses the serializer by retrieving the best quality `Accept` headers and matching its value (mimetype). :param serializers: Dictionary of serializers. :param default_media_type: The default media type. :returns: Best matching serializer based on `format` query arg first, then client `Accept` headers or None if no matching serializer. """ return self._match_serializers_by_query_arg(serializers) or self.\ _match_serializers_by_accept_headers(serializers, default_media_type)
[docs] def make_response(self, *args, **kwargs): """Create a Flask Response. Dispatch the given arguments to the serializer best matching the current request's Accept header. :return: The response created by the serializing function. :rtype: :class:`flask.Response` :raises werkzeug.exceptions.NotAcceptable: If no media type matches current Accept header. """ serializer = self.match_serializers( *self.get_method_serializers(request.method)) if serializer: return serializer(*args, **kwargs) abort(406)
[docs] def dispatch_request(self, *args, **kwargs): """Dispatch current request. Dispatch the current request using :class:`flask.views.MethodView` `dispatch_request()` then, if the result is not already a :py:class:`flask.Response`, search for the serializing function which matches the best the current request's Accept header and use it to build the :py:class:`flask.Response`. :rtype: :class:`flask.Response` :raises werkzeug.exceptions.NotAcceptable: If no media type matches current Accept header. :returns: The response returned by the request handler or created by the serializing function. """ result = super(ContentNegotiatedMethodView, self).dispatch_request( *args, **kwargs ) if isinstance(result, Response): return result elif isinstance(result, (list, tuple)): return self.make_response(*result) else: return self.make_response(result)
[docs] def check_etag(self, etag, weak=False): """Validate the given ETag with current request conditions. Compare the given ETag to the ones in the request header If-Match and If-None-Match conditions. The result is unspecified for requests having If-Match and If-None-Match being both set. :param str etag: The ETag of the current resource. For PUT and PATCH it is the one before any modification of the resource. This ETag will be tested with the Accept header conditions. The given ETag should not be quoted. :raises werkzeug.exceptions.PreconditionFailed: If the condition is not met. :raises invenio_rest.errors.SameContentException: If the the request is GET or HEAD and the If-None-Match condition is not met. """ # bool(:py:class:`werkzeug.datastructures.ETags`) is not consistent # in Python 3. bool(Etags()) == True even though it is empty. if len(request.if_match.as_set(include_weak=weak)) > 0 or \ request.if_match.star_tag: contains_etag = (request.if_match.contains_weak(etag) if weak else request.if_match.contains(etag)) if not contains_etag and '*' not in request.if_match: abort(412) if len(request.if_none_match.as_set(include_weak=weak)) > 0 or \ request.if_none_match.star_tag: contains_etag = (request.if_none_match.contains_weak(etag) if weak else request.if_none_match.contains(etag)) if contains_etag or '*' in request.if_none_match: if request.method in ('GET', 'HEAD'): raise SameContentException(etag) else: abort(412)
[docs] def check_if_modified_since(self, dt, etag=None): """Validate If-Modified-Since with current request conditions.""" dt = dt.replace(microsecond=0) if request.if_modified_since and dt <= request.if_modified_since: raise SameContentException(etag, last_modified=dt)