Source code for txspinneret.resource

"""
A collection of higher-level Twisted Web resources, suitable for use with any
existing ``IResource`` implementations.

`SpinneretResource` adapts an `ISpinneretResource` to `IResource`.

`ContentTypeNegotiator` will negotiate a resource based on the ``Accept``
header.
"""
from twisted.internet.defer import Deferred, maybeDeferred, succeed
from twisted.python.compat import nativeString
from twisted.python.urlpath import URLPath
from twisted.web import http
from twisted.web.error import UnsupportedMethod
from twisted.web.iweb import IRenderable
from twisted.web.resource import (
    IResource, NoResource, Resource, _computeAllowedMethods)
from twisted.web.server import NOT_DONE_YET
from twisted.web.template import renderElement
from twisted.web.util import DeferredResource, Redirect

from txspinneret.interfaces import ISpinneretResource
from txspinneret.util import _parseAccept



def _renderResource(resource, request):
    """
    Render a given resource.

    See `IResource.render <twisted:twisted.web.resource.IResource.render>`.
    """
    meth = getattr(resource, 'render_' + nativeString(request.method), None)
    if meth is None:
        try:
            allowedMethods = resource.allowedMethods
        except AttributeError:
            allowedMethods = _computeAllowedMethods(resource)
        raise UnsupportedMethod(allowedMethods)
    return meth(request)



[docs]class NotAcceptable(Resource): """ Leaf resource that renders an empty body for ``406 Not Acceptable``. """ isLeaf = True def render(self, request): request.setResponseCode(http.NOT_ACCEPTABLE) return b''
[docs]class NotFound(NoResource): """ Leaf resource that renders a page for ``404 Not Found``. """ def __init__(self): NoResource.__init__(self, b'Resource not found')
class _RenderableResource(Resource): """ Adapter from `IRenderable` to `IResource`. """ isLeaf = True def __init__(self, renderable, doctype=b'<!DOCTYPE html>'): Resource.__init__(self) self._renderable = renderable self._doctype = doctype def render(self, request): request.setResponseCode(http.OK) return renderElement(request, self._renderable, self._doctype)
[docs]class SpinneretResource(Resource): """ Adapter from `ISpinneretResource` to `IResource`. """ def __init__(self, wrappedResource): """ :type wrappedResource: `ISpinneretResource` :param wrappedResource: Spinneret resource to wrap in an `IResource`. """ self._wrappedResource = wrappedResource Resource.__init__(self) def _adaptToResource(self, result): """ Adapt a result to `IResource`. Several adaptions are tried they are, in order: ``None``, `IRenderable <twisted:twisted.web.iweb.IRenderable>`, `IResource <twisted:twisted.web.resource.IResource>`, and `URLPath <twisted:twisted.python.urlpath.URLPath>`. Anything else is returned as is. A `URLPath <twisted:twisted.python.urlpath.URLPath>` is treated as a redirect. """ if result is None: return NotFound() spinneretResource = ISpinneretResource(result, None) if spinneretResource is not None: return SpinneretResource(spinneretResource) renderable = IRenderable(result, None) if renderable is not None: return _RenderableResource(renderable) resource = IResource(result, None) if resource is not None: return resource if isinstance(result, URLPath): return Redirect(str(result)) return result def getChildWithDefault(self, path, request): def _setSegments(result): result, segments = result request.postpath[:] = segments return result def _locateChild(request, segments): def _defaultLocateChild(request, segments): return NotFound(), [] locateChild = getattr( self._wrappedResource, 'locateChild', _defaultLocateChild) return locateChild(request, segments) d = maybeDeferred( _locateChild, request, request.prepath[-1:] + request.postpath) d.addCallback(_setSegments) d.addCallback(self._adaptToResource) return DeferredResource(d) def _handleRenderResult(self, request, result): """ Handle the result from `IResource.render`. If the result is a `Deferred` then return `NOT_DONE_YET` and add a callback to write the result to the request when it arrives. """ def _requestFinished(result, cancel): cancel() return result if not isinstance(result, Deferred): result = succeed(result) def _whenDone(result): render = getattr(result, 'render', lambda request: result) renderResult = render(request) if renderResult != NOT_DONE_YET: request.write(renderResult) request.finish() return result request.notifyFinish().addBoth(_requestFinished, result.cancel) result.addCallback(self._adaptToResource) result.addCallback(_whenDone) result.addErrback(request.processingFailed) return NOT_DONE_YET def render(self, request): # This is kind of terrible but we need `_RouterResource.render` to be # called to handle the null route. Finding a better way to achieve this # would be great. if hasattr(self._wrappedResource, 'render'): result = self._wrappedResource.render(request) else: result = _renderResource(self._wrappedResource, request) return self._handleRenderResult(request, result)
[docs]class ContentTypeNegotiator(Resource): """ Negotiate an appropriate representation based on the ``Accept`` header. Rendering this resource will negotiate a representation and render the matching handler. """ def __init__(self, handlers, fallback=False): """ :type handlers: ``iterable`` of `INegotiableResource` and either `IResource` or `ISpinneretResource`. :param handlers: Iterable of negotiable resources, either `ISpinneretResource` or `IResource`, to use as handlers for negotiation. :type fallback: `bool` :param fallback: Fall back to the first handler in the case where negotiation fails? """ Resource.__init__(self) self._handlers = list(handlers) self._fallback = fallback self._acceptHandlers = {} for handler in self._handlers: for acceptType in handler.acceptTypes: if acceptType in self._acceptHandlers: raise ValueError( 'Duplicate handler for %r' % (acceptType,)) self._acceptHandlers[acceptType] = handler def _negotiateHandler(self, request): """ Negotiate a handler based on the content types acceptable to the client. :rtype: 2-`tuple` of `twisted.web.iweb.IResource` and `bytes` :return: Pair of a resource and the content type. """ accept = _parseAccept(request.requestHeaders.getRawHeaders('Accept')) for contentType in accept.keys(): handler = self._acceptHandlers.get(contentType.lower()) if handler is not None: return handler, handler.contentType if self._fallback: handler = self._handlers[0] return handler, handler.contentType return NotAcceptable(), None def render(self, request): resource, contentType = self._negotiateHandler(request) if contentType is not None: request.setHeader(b'Content-Type', contentType) spinneretResource = ISpinneretResource(resource, None) if spinneretResource is not None: resource = SpinneretResource(spinneretResource) return resource.render(request)
__all__ = [ 'SpinneretResource', 'ContentTypeNegotiator', 'NotAcceptable', 'NotFound']