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))
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()
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
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]
def json_handler(): return JSONHandler()
def test_media_handlers_unknown_default_media_type(): with pytest.raises(ValueError): handlers = {'application/json': JSONHandler()} MediaHandlers(default_media_type='nope/json', handlers=handlers)
@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():