1. Olemis Lang
  2. restontrac

Source

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

#!/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 implements
from trac.core import Component
#TODO absolute imports
from api import IRestResource, IRoutesResourceMapper

#default http methods supported by collections
def_coll_methods = ['GET','POST', 'OPTIONS']
#default http methods supported by members
def_member_methods = ['GET', 'PUT', 'DELETE', 'PATCH','OPTIONS']

class RestResourceBase(Component):
    """ Reusable implementation of IRestResource that provides
        canned implementation of the most commom behaviour of
        resources acting like collection/member pairs
        Handles auto-configuration of routes using provided data
        Specializations might be created to handle specific reusable
        implementations of CRUD operations or to directly implement
        a specific isolated resource just to reuse the url mapping
        infrastructure
    """
    abstract = True
    implements(IRestResource, IRoutesResourceMapper)

    def setup(self, collection_name, member_name, resource_name = None,
                 coll_meths=def_coll_methods, member_meths = def_member_methods,
                 parent_collection=None, parent_resource=None):
        """
           To configure the resource's data that migh be used for the configuration
           of url mappings.
           Operation to be called within the init method of classes extending
           this base class. See the test cases for a reference.
        """
        if resource_name:
            self.name = resource_name
        elif collection_name:
            self.name = collection_name
        assert self.name, "Resource name or collection name must be provided"
        self.collection_name = collection_name
        self.member_name = member_name
        self.collection_methods = coll_meths
        self.member_methods = member_meths
        #If custom paths,methods are provided, auto_map is turned off
        self.auto_map = len(self.supported_paths()) == 0
        self.parent_collection = parent_collection
        self.parent_resource = parent_resource

    #Implementation of IRoutesResourceMapper
    def map_resources(self, mapper):
        """
            Implementation of IRoutesResourceMapper's logic
            By default is auto_config aware, meaning that if an explicit
            set of paths is returned by self.supported_paths() the present routine
            has no effect
            If overriden in specializations, this logic gets overriden as well
            so the routine will always have effect
            Specializations might want to override this method to perform
            a free numer of Mapper.resource() or Mapper.connect() calls
        """
        if self.auto_map:
            parent_res = None
            if self.parent_collection and self.parent_resource:
                parent_res = {'collection_name':self.parent_collection,
                               'member_name': self.parent_resource}
            mapper.resource(self.member_name, self.collection_name,
                            collection={'prototype':'GET'},
                            controller = self.name, member={'edit':'IGNORE'},
                            new='IGNORE', parent_resource = parent_res,
                            member_getter_action='read',
                            collection_getter_action='list')

    #Implementation of IRestResource
    def get_name(self):
        """Indicating the name of the resource instance, for indexing
           purposes
           defaults to the resource's collection_name
        """
        #TODO verify if the object will be created without the init method
        #thus causing self.name to be likely to have None value
        if not self.name:
            return self.collection_name
        return self.name

    def supported_paths(self):
        """To indicate the list of supported paths that the current resource
           will be handling
           returns a dict {path: http_methods}
        """
        return {}

    def get_custom_actions_map(self):
        """Returns an action map indicating
         for a pair path,HttpMethod which specific action
         should be mapped if there is one
         Used allong with supported_paths() to make customized
         configurations that do not respond to the standard
         expected ones
        """
        return {}

    def available_operations(self, request):
        """Method to return available operations
           to perform with the current resource
           by default assumes the request has been pre-processed
           and the rest request type has been published in the request scope
        """
        req_type = request.args.get('REST_REQUEST','COLLECTION')
        if 'COLLECTION' == req_type:
            return self.collection_methods
        elif 'MEMBER' == req_type:
            return self.member_methods
        else:
            return [] #nothing supported

    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
        """
        NotImplementedError()

    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
           Specializations must implement specific logic
           if not, unsupported opperation http error
           will be returned to the client
        """
        NotImplementedError()

    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
           Specializations must implement specific logic
           if not, unsupported opperation http error
           will be returned to the client
        """
        NotImplementedError()

    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
        """
        #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
           Specializations must implement specific logic
           if not, unsupported opperation http error
           will be returned to the client
        """
        NotImplementedError()

    def get_formatter(self, content_type):
        """Specializations willing to have a custom
           formatter used during received calls, shoud
           return here an instance of IFormatter
           by default no formatter is specified
        """
        return None