Source code for feather.resource

"""Resource classes for creating a JSON restful API.
"""
import mimetypes
import falcon
import simplejson
from feather.hooks import validate_content_type
from feather import errors


[docs]def basic_error_handler(error_dict): """Handle an error dictionary returned by a marshmallow schema. This basic handler either returns a 409 conflict error if the error dictionary indicates a duplicate key, or a 400 bad request error with the error dictionary attached. """ # Duplicate keys indicate a conflict since the object already exists print(error_dict) if errors.DUPLICATE_KEY in error_dict: raise falcon.HTTPConflict('Duplicate Key', error_dict[errors.DUPLICATE_KEY]) elif error_dict: raise falcon.HTTPBadRequest('Validation Error', error_dict)
[docs]class FeatherResource(object): """Base class used for setting a uri_template, allowed content types and HTTP methods provided. By encapsulating the URI, we can provide factory methods for routing, allowing us to specify the resource and its' uri in one place HTTP handler methods (``on_<method>`` in falcon) are dynamically assigned in order to allow Resource instances to be created for with different sets of requiremets. (E.g. create a read-only collection by only passing ``('get',)`` when instantiating). This explains why the method handlers below are not named ``on_<method>`` but simple ``_<method>``. Allowed content types are passed for the same reason. A sub class could check these using the ``validated_content_type`` hooks. This is mostly useful for file uploads (see ``FileCollection`` or ``FileItem``) where you might wish to restrict content types (e.g. images only) Args: uri_template (str): A URI template for this resource which will be used when routing (using the ``feather.create_app`` factory function) and for setting ``Location`` headers. content_types (tuple, set or list): List of allowed content_types. This is not used by default. Instead, decorate desired handler methods with @falcon.before(validate_content_type). A ``set`` is reccomended as the validation performs an exclusion (``not in``) operation methods (tuple or list): List of HTTP methods to allow. error_handler (callable): A function which is responsible for handling validation errors returned by a marshmallow schema. Defaults to ``feather.resource.basic_error_handler`` """ def __init__( self, uri_template, content_types={'application/json',}, methods=('get', 'patch', 'put', 'delete', 'post'), error_handler=basic_error_handler ): self._uri = uri_template self._content_types = content_types self._error_handler = error_handler # We dynamically set attributes for the expected # falcon HTTP method handlers. # This allows the user of ``Resource`` to define a subset # of methods to use. E.g. if they dont want to support # 'on_post', this would be left off the method list for method in methods: method_name = 'on_{}'.format(method.lower()) handler = getattr(self, '_{}'.format(method.lower())) setattr(self, method_name, handler) @property def uri_template(self): """The URI template for this resource """ return self._uri @property def allowed_content_types(self): return self._content_types def _post(self, req, resp): """POST request handler. Override in child classes to provide POST handling Args: req (falcon.Request): The falcon Request object resp (falcon.Response): The falcon Response object """ raise falcon.HTTP_METHOD_NOT_ALLOWED def _get(self, req, resp): """GET request handler Args: req (falcon.Request): The falcon Request object resp (falcon.Response): The falcon Response object """ raise falcon.HTTP_METHOD_NOT_ALLOWED def _put(self, req, resp): """PUT request handler Args: req (falcon.Request): The falcon Request object resp (falcon.Response): The falcon Response object """ raise falcon.HTTP_METHOD_NOT_ALLOWED def _patch(self, req, resp): """PATCH request handler Args: req (falcon.Request): The falcon Request object resp (falcon.Response): The falcon Response object """ raise falcon.HTTP_METHOD_NOT_ALLOWED def _delete(self, req, resp): """DELETE request handler Args: req (falcon.Request): The falcon Request object resp (falcon.Response): The falcon Response object """ raise falcon.HTTP_METHOD_NOT_ALLOWED
[docs]class Collection(FeatherResource): """Generic class for listing/creating data via a schema Using falcons before/after decorators. ++++++++++++++++++++++++++++++++++++++ Remembering that the @ operator is just syntactic sugar, if we want to apply a decorator we could do it with minimal effort like this: resource = Collection(...) resource.on_post = falcon.before(my_function)(resource.on_post) Alternatively, we could create a subclass: class MyResource(Collection): on_post = falcon.before(my_function)(Collection.on_post.__func__) Also note that when overriding, you will need to manually add back the content type validation for the ``_post`` method if appropriate. Args: schema (feather.schema.MongoSchema): An instance of a ``MongoSchema`` child class on which the ``Collection`` instance should operate. uri_template (str): See ``feather.resource.FeatherResource`` content_types (tuple or list): See ``feather.resource.FeatherResource``. Defaults to ``'application/json'`` methods (str): See ``feather.resource.FeatherResource``. Defaults to ``('get', 'post')`` error_handler (callable): See ``feather.resource.FeatherResource``. """ def __init__( self, schema, uri_template, content_types=('application/json'), methods=('get', 'post'), error_handler=basic_error_handler ): super(Collection, self).__init__( uri_template, content_types, methods, error_handler ) self._schema = schema def _get(self, req, resp): """List all schmea objects in the database. This method will call ``get_filter`` on the ``MongoSchema`` instance that was passed in. The result of ``get_filter`` are then passed to the ``find`` method of ``MongoSchema`` in order to retrieve the final list of objects to return. """ # dump all schema objects cursor = self._schema.find(**self._schema.get_filter(req)) result = self._schema.dumps(cursor, many=True) resp.body = result.data resp.content_type = falcon.MEDIA_JSON resp.status = falcon.HTTP_OK @falcon.before(validate_content_type) def _post(self, req, resp): """Accepts data passes it to the schema for validation & creation. If overriding, note this function uses a decorator to validate the content types. """ data = req.bounded_stream.read() complete_data, error_dict = self._schema.post(data) self._error_handler(error_dict) resp.status = falcon.HTTP_CREATED
[docs]class Item(FeatherResource): """Generic class for getting/editing a single data item via a schema Args: schema (feather.schema.MongoSchema): An instance of a ``MongoSchema`` child class on which the ``Item`` instance should operate. uri_template (str): See ``feather.resource.FeatherResource`` content_types (tuple or list): See ``feather.resource.FeatherResource``. Defaults to ``'application/json'`` methods (str): See ``feather.resource.FeatherResource``. Defaults to ``('get', 'put', 'patch', 'delete')`` error_handler (callable): See ``feather.resource.FeatherResource``. """ def __init__( self, schema, uri_template, content_types=('application/json'), methods=('get', 'patch', 'put', 'delete') ): super(Item, self).__init__(uri_template, content_types, methods) self._schema = schema def _get(self, req, resp, **kwargs): """Get a representation of a single object in the schema. kwargs contains the lookup parameter specified in the uri template (as given by falcon). This will be used to update the result of ``get_filter`` """ filter_spec = self._schema.get_filter(req) try: kwargs.update(filter_spec['filter']) del filter_spec['filter'] except KeyError: pass document = self._schema.get(kwargs, **filter_spec) if document: result = self._schema.dumps(document) resp.body = result.data resp.content_type = falcon.MEDIA_JSON resp.status = falcon.HTTP_OK else: raise falcon.HTTPNotFound() @falcon.before(validate_content_type) def _put(self, req, resp, **kwargs): """Replace a schema object with the given data """ data = req.bounded_stream.read() validated, error_dict = self._schema.put(kwargs, data) self._error_handler(error_dict) resp.status = falcon.HTTP_NO_CONTENT resp.location = self.uri_template.format(**kwargs) @falcon.before(validate_content_type) def _patch(self, req, resp, **kwargs): """Update an existing schema object with the given data """ data = req.bounded_stream.read() validated, error_dict = self._schema.patch(kwargs, data) self._error_handler(error_dict) resp.status = falcon.HTTP_ACCEPTED resp.location = self.uri_template.format(**kwargs) def _delete(self, req, resp, **kwargs): """Delete an object """ self._schema.delete(kwargs) resp.status = falcon.HTTP_NO_CONTENT
[docs]class FileCollection(FeatherResource): """Collection for posting/listing file uploads. By default, all content types are allowed - usually you would want to limit this, e.g. just allow images by passing ``('image/png', 'image/jpeg')`` """ def __init__(self, store, uri_template='/files', content_types=None, methods=('get', 'post')): super(FileCollection, self).__init__(uri_template, content_types, methods) self._store = store def _get(self, req, resp): """Get a list of all file URL's available """ uploads = self._store.list() resp.body = simplejson.dumps(uploads) resp.status = falcon.HTTP_200 @falcon.before(validate_content_type) def _post(self, req, resp): """POST a new file (stream) If overriding, note this function uses a decorator to validate the content types. """ name = self._store.save(req.stream, req.content_type) resp.status = falcon.HTTP_CREATED resp.location = "{}/{}".format(self._uri, name)
[docs]class FileItem(FeatherResource): """Item resource for interacting with single files """ def __init__(self, store, uri_template='/files/{name}', content_types=None, methods=('get',)): super(FileItem, self).__init__(uri_template, content_types, methods) self._store = store self._uri_param = self._uri.split('{')[1].split('}')[0] def _get(self, req, resp, **kwargs): """Download a single file """ name = kwargs[self._uri_param] resp.content_type = mimetypes.guess_type(name)[0] resp.stream, resp.stream_len = self._store.open(name)