Source

restontrac / trac-dev / restontrac / tracrest / api.py

Full commit
#!/usr/bin/env python
# -*- coding: UTF-8 -*-

# Copyright 2009-2011 Olemis Lang <olemis at gmail.com>
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.


r"""Publish Trac data via a REST-ful interface

RESTful interface built on top of Trac RpcPlugin

Copyright 2009-2011 Olemis Lang <olemis at gmail.com>
Licensed under the Apache License
"""
from trac.core import *
from trac.perm import IPermissionRequestor

__all__ = ['IRestResource', 'IFormatter', 'IRoutesResourceMapper', 'RestSystem', 'RespCodes']

class RespCodes():
    """ Container of Response Codes that migh
        arise when a request is processed
    """
    #TODO evaluate, maybe it is possible to use something already done within trac.web.api
    OK = ('OK', 200)
    CREATED = ('Created', 201)
    DELETED = ('', 204) # 204 says "Don't send a body!"
    BAD_REQUEST = ('Bad Request', 400)
    FORBIDDEN = ('Forbidden', 401)
    NOT_FOUND = ('Not Found', 404)
    DUPLICATE_ENTRY = ('Conflict/Duplicate', 409)
    NOT_HERE = ('Gone', 410)
    INTERNAL_ERROR = ('Internal Error', 500)
    NOT_IMPLEMENTED = ('Not Implemented', 501)
    THROTTLED = ('Throttled', 503)

class IFormatter(Interface):
    """ Specifies the contract of a formatter instance
        to be used during request parsing and response
        writing
    """
    def parse(self, request):
        """Used when a request is received
           In correspondance with the content-type specified
           the corresponding Formatter instance is used calling
           parse(), to decode the received data
        """
    def format(self, request):
        """Used when a response is received
           In correspondance with the content-type specified
           the corresponding Formatter instance is used calling
           parse(), to encode the returned data
        """
    def supported_ct(self):
        """Returns the supported content-type to wich the current
           Formatter responds
        """

class IRestResource(Interface):
    """Interface that defines the protocol of a REST
       resource inside trac's architecture
    """
    def get_name(self):
        """Indicating the name of the resource instance, for indexing
           purposes, possibly for delegating requests to the corresponding
           method
        """
    def supported_paths(self):
        """To indicate the paths that the current resource supports
           along with its provided http methods
           returns a dict of tuples (path, http_methods)
           where path is a route path to be used in
           routes.mapper.Mapper.connect(route_path)
           and http_methods is an iterable with some of the
           following values['GET','POST','PUT','DELETE']
           the OPTIONS http method doesn't need to be provided
           since its expected to be always present.
           Consecuently it will be handled by reusable mechanisms
        """

    def extract_extra_params(self, request):
        """Before the resource is used to process the concrete
           request, it is given the oportunity to parse any additional
           params that migh be relevant for the behaviour of the
           resource
           Returns a dictionary param name -> param value
        """

    def available_operations(self, request):
        """Method to return available operations
           to perform with the current resource
        """

    def read(self, request, *args, **kwargs):
        """Method to handle Http GET requests
           According to the type of resource this will have a different meaning
           If the resource is a path base resource, this will return
           the list of links to the nested resources.
           If the resource is a concrete one, this will return the resource's data
           as it would be done in a READ operation in CRUD terms
        """

    def create(self, request, *args, **kwargs):
        """Method to handle Http POST requests
           This will in general by translated into
           a CREATE routine in CRUD terms
        """

    def update(self, request, *args, **kwargs):
        """Method to handle Http POST requests
           This will in general by translated into
           an UPDATE routine in CRUD terms
        """

    def update_diff(self, request, *args, **kwargs):
        """Method to handle Http PATCH requests
           This will in general by translated into
           an UPDATE routine in CRUD terms
           but is expected to update only values that
           were received in the request, leaving all
           other values untouched
        """
        #TODO evaluate if this operation must be supported

    def delete(self, request, *args, **kwargs):
        """Method to handle Http DELETE requests
           This will in general by translated into
           a DELETE routine in CRUD terms
        """

    def get_formatter(self, content_type):
        """Giving the resource a chance to specify its
           own special formatter to be used for request
           input parsing and output formatting
           Returns an instance of IFormatter
        """

class IRoutesResourceMapper(Interface):
    """
       Extension point interface to externalize the mapping of
       resources to urls using a Routes Mapper
       There could be a possible use of this extension point
       to have a single component implementing the logic of
       resource mapping instead of having every resource
       handling this logic
    """
    def map_resources(self, mapper):
        """Performs the mapping logic using the routes.mapper.Mapper
           instance received as argument
        """

class RestSystem(Component):
    """ Core of the RPC system. """
    implements(IPermissionRequestor)

    #instances of resources to handle REST requests
    resources = ExtensionPoint(IRestResource)

    def __init__(self):
        super(RestSystem, self).__init__()

    def resource_names(self):
        return [r.get_name() for r in self.resources]

    def resource_path_dict(self):
        """Returns a map of every configured path with
           its corresponding resource handler instance
        """
        return dict([(path, resource) for resource in self.resources
                     for path in resource.supported_paths().iterkeys()])

    def getAPIVersion(self, req):
        #TODO unfinished work, do not review this
        pass

    def call_resource(self, req, resource, method, args, kwargs):
        #TODO unfinished work, do not review this
        if method in resource.supported_operations():
            callable = resource.__getattr__(method)
            result = callable(req, args, kwargs)
            #if isinstance(result, GeneratorType):
            #    result = list(result)