예제 #1
0
 def __init__(self, default_media_type='application/json', handlers=None):
     """The __init__ method documented in the class level."""
     self.default_media_type = default_media_type
     self.handlers = handlers or {
         'application/json': JSONHandler(),
         'application/json; charset=UTF-8': JSONHandler()
     }
     if handlers is not None:
         extra_handlers = {
             media_type: handler
             for handler in handlers.values()
             for media_type in handler.allowed_media_types
             if media_type not in self.handlers
         }
         self.handlers.update(extra_handlers)
     if self.default_media_type not in self.handlers:
         raise ValueError("no handler for default media type '{}'".format(
             default_media_type))
     super().__init__(extra_media_types=list(self.handlers))
예제 #2
0
def test_custom_media_handlers(default_media_type, req, resp, media, mocker):
    class FakeYAMLHandler(BaseMediaHandler):
        def deserialize(self, stream, content_type, content_length, **kwargs):
            try:
                return json.loads(stream.read(content_length or 0), **kwargs)
            except ValueError as err:
                raise falcon.HTTPBadRequest(
                    title='Invalid YAML',
                    description='Could not parse YAML body - {}'.format(err))

        def serialize(self, media, content_type, indent=0, **kwargs):
            return json.dumps(media, indent=indent, **kwargs)

        @property
        def media_type(self):
            return 'application/yaml'

    json_handler = JSONHandler()
    yaml_handler = FakeYAMLHandler()

    media_handlers = MediaHandlers(default_media_type=default_media_type,
                                   handlers={
                                       'application/json': json_handler,
                                       'application/yaml': yaml_handler
                                   })
    request_stream = copy.copy(req.stream)

    # testing YAML request handler
    assert media_handlers.media_type == default_media_type
    assert media_handlers.lookup_handler('application/yaml') is yaml_handler
    mocker.patch.object(yaml_handler, 'deserialize')
    req.stream = request_stream
    req.content_type = 'application/yaml'
    media_handlers.handle_request(req)
    yaml_handler.deserialize.assert_called_once()

    # testing JSON request handler
    assert media_handlers.lookup_handler('application/json') is json_handler
    mocker.patch.object(json_handler, 'deserialize')
    req.stream = request_stream
    req.content_type = 'application/json'
    media_handlers.handle_request(req)
    json_handler.deserialize.assert_called_once()

    # testing response handler
    default_handler = media_handlers.handlers[default_media_type]
    mocker.patch.object(default_handler, 'serialize')
    media_handlers.handle_response(resp, media=media)
    assert resp.content_type == media_handlers.media_type
    default_handler.serialize.assert_called_once()
예제 #3
0
def test_custom_extra_media_handlers():
    extra_media_types = ['application/json; charset=UTF-8']
    json_handler = JSONHandler(extra_media_types=extra_media_types)
    media_handlers = MediaHandlers(default_media_type='application/json',
                                   handlers={'application/json': json_handler})
    assert media_handlers.lookup_handler('application/json') is json_handler
    for extra_media_type in extra_media_types:
        media_handler = media_handlers.lookup_handler(extra_media_type)
        assert media_handler is media_handlers.handlers[extra_media_type]
        assert media_handler is json_handler

    media_handlers = MediaHandlers(default_media_type='application/json',
                                   handlers={
                                       'application/json':
                                       json_handler,
                                       'application/json; charset=UTF-8':
                                       JSONHandler()
                                   })

    assert media_handlers.lookup_handler('application/json') is json_handler
    for extra_media_type in extra_media_types:
        media_handler = media_handlers.lookup_handler(extra_media_type)
        assert media_handler is media_handlers.handlers[extra_media_type]
        assert media_handler is not json_handler
예제 #4
0
class BaseResource(metaclass=MetaResource):
    """Base resource class with core param and response functionality.

    This base class handles resource responses, parameter deserialization,
    and validation of request included representations if serializer is
    defined.

    All custom resource classes based on ``BaseResource`` accept additional
    ``with_context`` keyword argument:


    .. code-block:: python

        class MyResource(BaseResource, with_context=True):
            ...

    The ``with_context`` argument tells if resource modification methods
    (methods injected with mixins - list/create/update/etc.) should accept
    the ``context`` argument in their signatures. For more details
    see :ref:`guide-context-aware-resources` section of documentation. The
    default value for ``with_context`` class keyword argument is ``False``.

    .. versionchanged:: 0.3.0
        Added the ``with_context`` keyword argument.

    Note:
        The ``indent`` parameter may be used only on a supported media handler
        such as JSON or YAML, otherwise it should be ignored by the handler.

    """

    indent = IntParam("""
        JSON output indentation. Set to 0 if output should not be formatted.
        """,
                      default='0')

    #: Instance of serializer class used to serialize/deserialize and
    #: validate resource representations.
    serializer = None

    #: Instance of media handler class used to serialize response
    #: objects and to deserialize request objects.
    media_handler = JSONHandler()

    def __new__(cls, *args, **kwargs):
        """Do some sanity checks before resource instance initialization."""
        instance = super().__new__(cls)

        if not hasattr(instance, '_with_context'):
            # note: warnings is displayed only if user did not specify
            #       explicitly that he want's his resource to not accept
            #       the context keyword argument.
            # future: remove in 1.x
            warn(
                """
                Class {} was defined without the 'with_context' keyword
                argument. This means that its resource manipulation
                methods (list/retrieve/create etc.) won't receive context
                keyword argument.

                This behaviour will change in 1.x version of graceful
                (please refer to documentation).
                """.format(cls), FutureWarning)
        return instance

    @property
    def params(self):
        """Return dictionary of parameter definition objects."""
        return getattr(self, self.__class__._params_storage_key)

    def make_body(self, resp, params, meta, content):
        """Construct response body/data in ``resp`` object using media handler.

        Args:
            resp (falcon.Response): response object where to include
                serialized body
            params (dict): dictionary of parsed parameters
            meta (dict): dictionary of metadata to be included in 'meta'
                section of response
            content (dict): dictionary of response content (resource
                representation) to be included in 'content' section of response

        Returns:
            None

        """
        response = {'meta': meta, 'content': content}

        self.media_handler.handle_response(resp,
                                           media=response,
                                           indent=params.get('indent', 0))

    def allowed_methods(self):
        """Return list of allowed HTTP methods on this resource.

        This is only for purpose of making resource description.

        Returns:
            list: list of allowed HTTP method names (uppercase)

        """
        return [
            method for method, allowed in (
                ('GET', hasattr(self, 'on_get')),
                ('POST', hasattr(self, 'on_post')),
                ('PUT', hasattr(self, 'on_put')),
                ('PATCH', hasattr(self, 'on_patch')),
                ('DELETE', hasattr(self, 'on_delete')),
                ('HEAD', hasattr(self, 'on_head')),
                ('OPTIONS', hasattr(self, 'on_options')),
            ) if allowed
        ]

    def describe(self, req=None, resp=None, **kwargs):
        """Describe API resource using resource introspection.

        Additional description on derrived resource class can be added using
        keyword arguments and calling ``super().decribe()`` method call
        like following:

        .. code-block:: python

             class SomeResource(BaseResource):
                 def describe(req, resp, **kwargs):
                     return super().describe(
                         req, resp, type='list', **kwargs
                      )

        Args:
            req (falcon.Request): request object
            resp (falcon.Response): response object
            kwargs (dict): dictionary of values created from resource url
                template

        Returns:
            dict: dictionary with resource descritpion information

        .. versionchanged:: 0.2.0
           The `req` and `resp` parameters became optional to ease the
           implementation of application-level documentation generators.
        """
        description = {
            'params':
            OrderedDict([(name, param.describe())
                         for name, param in self.params.items()]),
            'details':
            inspect.cleandoc(self.__class__.__doc__
                             or "This resource does not have description yet"),
            'name':
            self.__class__.__name__,
            'methods':
            self.allowed_methods()
        }
        # note: add path to resource description only if request object was
        #       provided in order to make auto-documentation engines simpler
        if req:
            description['path'] = req.path

        description.update(**kwargs)
        return description

    def on_options(self, req, resp, **kwargs):
        """Respond with media formatted resource description on OPTIONS request.

        Args:
            req (falcon.Request): Optional request object. Defaults to None.
            resp (falcon.Response): Optional response object. Defaults to None.
            kwargs (dict): Dictionary of values created by falcon from
                resource uri template.

        Returns:
            None


        .. versionchanged:: 0.2.0
           Default ``OPTIONS`` responses include ``Allow`` header with list of
           allowed HTTP methods.
        """
        resp.set_header('Allow', ', '.join(self.allowed_methods()))
        self.media_handler.handle_response(resp,
                                           media=self.describe(req, resp))

    def require_params(self, req):
        """Require all defined parameters from request query string.

        Raises ``falcon.errors.HTTPMissingParam`` exception if any of required
        parameters is missing and ``falcon.errors.HTTPInvalidParam`` if any
        of parameters could not be understood (wrong format).

        Args:
            req (falcon.Request): request object

        """
        params = {}

        for name, param in self.params.items():
            if name not in req.params and param.required:
                # we could simply raise with this single param or use get_param
                # with required=True parameter but for client convenience
                # we prefer to list all missing params that are required
                missing = set(
                    p for p in self.params if self.params[p].required) - set(
                        req.params.keys())

                raise errors.HTTPMissingParam(", ".join(missing))

            elif name in req.params or param.default:
                # Note: lack of key in req.params means it was not specified
                # so unless there is default value it will not be included in
                # output params dict.
                # This way we have explicit information that param was
                # not specified. Using None would not be as good because param
                # class can also return None from `.value()` method as a valid
                # translated value.
                try:
                    if param.many:
                        # params with "many" enabled need special care
                        values = req.get_param_as_list(
                            # note: falcon allows to pass value handler using
                            #       `transform` param so we do not need to
                            #       iterate through list manually
                            name,
                            param.validated_value) or [
                                param.default
                                and param.validated_value(param.default)
                            ]
                        params[name] = param.container(values)
                    else:
                        # note that if many==False and query parameter
                        # occurs multiple times in qs then it is
                        # **unspecified** which one will be used. See:
                        # http://falcon.readthedocs.org/en/latest/api/request_and_response.html#falcon.Request.get_param  # noqa
                        params[name] = param.validated_value(
                            req.get_param(name, default=param.default))

                except ValidationError as err:
                    # ValidationError allows to easily translate itself to
                    # to falcon's HTTPInvalidParam (Bad Request HTTP response)
                    raise err.as_invalid_param(name)

                except ValueError as err:
                    # Other parsing issues are expected to raise ValueError
                    raise errors.HTTPInvalidParam(str(err), name)

        return params

    def require_meta_and_content(self, content_handler, params, **kwargs):
        """Require 'meta' and 'content' dictionaries using proper hander.

        Args:
            content_handler (callable): function that accepts
                ``params, meta, **kwargs`` argument and returns dictionary
                for ``content`` response section
            params (dict): dictionary of parsed resource parameters
            kwargs (dict): dictionary of values created from resource url
                template

        Returns:
            tuple (meta, content): two-tuple with dictionaries of ``meta`` and
                ``content`` response sections

        """
        meta = {'params': params}
        content = content_handler(params, meta, **kwargs)
        meta['params'] = params
        return meta, content

    def require_representation(self, req):
        """Require raw representation dictionary from falcon request object.

        This does not perform any field parsing or validation but only uses
        allowed content-encoding handler to decode content body.

        Note:
            By default, only JSON is allowed as content type.

        Args:
            req (falcon.Request): request object

        Returns:
            dict: raw dictionary of representation supplied in request body

        """
        try:
            type_, subtype, _ = parse_mime_type(req.content_type)
            content_type = '/'.join((type_, subtype))
        except:
            raise falcon.HTTPUnsupportedMediaType(
                description="Invalid Content-Type header: {}".format(
                    req.content_type))
        return self.media_handler.handle_request(req,
                                                 content_type=content_type)

    def require_validated(self, req, partial=False, bulk=False):
        """Require fully validated internal object dictionary.

        Internal object dictionary creation is based on content-decoded
        representation retrieved from request body. Internal object validation
        is performed using resource serializer.

        Args:
            req (falcon.Request): request object
            partial (bool): set to True if partially complete representation
                is accepted (e.g. for patching instead of full update). Missing
                fields in representation will be skiped.
            bulk (bool): set to True if request payload represents multiple
                resources instead of single one.

        Returns:
            dict: dictionary of fields and values representing internal object.
                Each value is a result of ``field.from_representation`` call.

        """
        representations = [self.require_representation(req)
                           ] if not bulk else self.require_representation(req)

        if bulk and not isinstance(representations, list):
            raise ValidationError(
                "Request payload should represent a list of resources."
            ).as_bad_request()

        object_dicts = []

        try:
            for representation in representations:
                object_dict = self.serializer.from_representation(
                    representation)
                self.serializer.validate(object_dict, partial)
                object_dicts.append(object_dict)

        except DeserializationError as err:
            # when working on Resource we know that we can finally raise
            # bad request exceptions
            raise err.as_bad_request()

        except ValidationError as err:
            # ValidationError is a suggested way to validate whole resource
            # so we also are prepared to catch it
            raise err.as_bad_request()

        return object_dicts if bulk else object_dicts[0]
예제 #5
0
def json_handler():
    return JSONHandler()
예제 #6
0
def test_media_handlers_unknown_default_media_type():
    with pytest.raises(ValueError):
        handlers = {'application/json': JSONHandler()}
        MediaHandlers(default_media_type='nope/json', handlers=handlers)
예제 #7
0

@pytest.fixture
def media_json():
    return 'application/json'


@pytest.fixture
def req(media, media_json):
    headers = {'Content-Type': media_json}
    env = create_environ(body=json.dumps(media), headers=headers)
    return falcon.Request(env)


@pytest.fixture(params=[
    JSONHandler(),
    SimpleJSONHandler(),
    SimpleMediaHandler(),
    MediaHandlers()
])
def media_handler(request):
    return request.param


@pytest.fixture
def json_handler():
    return JSONHandler()


@pytest.fixture
def subclass_json_handler():