DRY code for Google Cloud Endpoints APIs
Question: I want to avoid boilerplate code for creating Google Cloud Endpoints APIs for
different models of my Google App Engine application. Suppose I have a `Post`,
`User` and `Category` model. The data is stored in the datastore. I want to
create a REST API with the resources `posts`, `users` and `categories`. I have
written the following code for the `posts` resource:

    
    
    import endpoints
    from protorpc import messages
    from protorpc import message_types
    from protorpc import remote
    from blog.models import Post
    from cloud_endpoints import WEB_CLIENT_ID, ANDROID_CLIENT_ID, IOS_CLIENT_ID, ANDROID_AUDIENCE
    
    
    class PostMessage(messages.Message):
        id = messages.StringField(1)
        title = messages.StringField(2)
        body = messages.StringField(3)
    
    
    class PostMessageCollection(messages.Message):
        post_messages = messages.MessageField(PostMessage, 1, repeated=True)
    
    
    def post_to_message(post):
        return PostMessage(
            id=str(post.key()),
            title=post.title,
            body=post.body)
    
    
    ID_RESOURCE = endpoints.ResourceContainer(
        message_types.VoidMessage,
        id=messages.StringField(1, variant=messages.Variant.STRING))
    
    PUT_RESOURCE = endpoints.ResourceContainer(
        PostMessage,
        id=messages.StringField(1, variant=messages.Variant.STRING))
    
    POST_RESOURCE = endpoints.ResourceContainer(Post)
    
    
    @endpoints.api(name='posts',
                   version='v1',
                   allowed_client_ids=[WEB_CLIENT_ID, ANDROID_CLIENT_ID, IOS_CLIENT_ID],
                   audiences=[ANDROID_AUDIENCE])
    class PostsApi(remote.Service):
        """List"""
        @endpoints.method(message_types.VoidMessage,
                          PostMessageCollection,
                          path='/posts',
                          http_method='GET',
                          name='posts.listPosts')
        def list(self, unused_request):
            post_messages = []
            for post in Post.all():
                post_messages.append(post_to_message(post))
    
            return PostCollection(post_messages=post_messages)
    
        """Get"""
        @endpoints.method(ID_RESOURCE,
                          PostMessage,
                          path='/posts/{id}',
                          http_method='GET',
                          name='posts.getPost')
        def get(self, request):
            try:
                return post_to_message(Post.get(request.id))
    
            except (IndexError, TypeError):
                raise endpoints.NotFoundException('Post %s not found.' % (request.id,))
    
        """Create"""
        @endpoints.method(POST_RESOURCE,
                          message_types.VoidMessage,
                          path='/posts',
                          http_method='POST',
                          name='posts.createPost')
        def create(self, request):
            post = Post(title=request.title, body=request.body)\
            post.put()
            return message_types.VoidMessage()
    
        """Update"""
        @endpoints.method(PUT_RESOURCE,
                          message_types.VoidMessage,
                          path='/posts/{id}',
                          http_method='POST',
                          name='posts.updatePost')
        def update(self, request):
            try:
                post = Post.get(request.id)
                post.title = request.title
                post.body = request.body
                return message_types.VoidMessage()
            except (IndexError, TypeError):
                raise endpoints.NotFoundException('Post %s not found.' % (request.id,))
    
        """Delete"""
        @endpoints.method(ID_RESOURCE,
                          message_types.VoidMessage,
                          path='/posts/{id}',
                          http_method='DELETE',
                          name='posts.deletePost')
        def delete(self, request):
            try:
                post = Post.get(request.id)
                post.delete()
                return message_types.VoidMessage()
    
            except (IndexError, TypeError):
                raise endpoints.NotFoundException('Post %s not found.' % (request.id,))
    

I could copy/paste this code and change "Post" to "Category" everywhere, and
edit `PostMessage`, `PostMessageCollection` and `post_to_message`, but that
seems bad practise. I would like not to repeat myself. Is it possible to
create an abstract API class and make subclasses for `PostAPI`, `CategoryAPI`
and `UserAPI`? Or is there a better way to parameterize `Post`, `PostMessage`,
`PostMessageCollection`, `post_to_message` and the path to the resource
("/posts", "/categories" and "/users") so that I don't have to copy/paste the
class for every resource? The classes would have the same methods with the
same decorators, and I would like not to repeat that for every resource. I use
Python 2.7.


Answer: I also have stumbled upon the same problem, and unfortunately this is not
possible with the `google cloud endpoints`. The method decorator needs the
request description (`PostMessageCollection` here). The request description
which subclasses the `message.Message` doesn't allow reuse through
inheritance, so all the message classes have to be completely defined without
any inheritance.

You can however achieve this to some extent (though I haven't tested it,
thought of it right now :) ) extent in the following way:

    
    
    # All the message and response definitions have to be here, complete.
    
    class PostMessage(messages.Message):
        id = messages.StringField(1)
        title = messages.StringField(2)
        body = messages.StringField(3)
    
    
    class PostMessageCollection(messages.Message):
        post_messages = messages.MessageField(PostMessage, 1, repeated=True)
    
    
    def post_to_message(post):
        return PostMessage(
            id=str(post.key()),
            title=post.title,
            body=post.body)
    
    
    ID_RESOURCE = endpoints.ResourceContainer(
        message_types.VoidMessage,
        id=messages.StringField(1, variant=messages.Variant.STRING))
    
    PUT_RESOURCE = endpoints.ResourceContainer(
        PostMessage,
        id=messages.StringField(1, variant=messages.Variant.STRING))
    
    POST_RESOURCE = endpoints.ResourceContainer(Post)
    
    # Now define all the 'Category' related messages here.
    
    
    @endpoints.api(name='posts_n_categories',  # The name can be a common one.
                   version='v1',
                   allowed_client_ids=[WEB_CLIENT_ID, ANDROID_CLIENT_ID, IOS_CLIENT_ID],
                   audiences=[ANDROID_AUDIENCE])
    class BaseAPI(remote.Service):
        """List"""
        # Common defs go here.
        MessageCollection = messages.Message
        PATH = '/'
        NAME = ''
    
        @staticmethod
        def converter(x):
            raise NotImplemented
    
        iterator = []
        collection = messages.Message
        @endpoints.method(message_types.VoidMessage,
                          MessageCollection,
                          path=PATH,
                          http_method='GET',
                          name=NAME)
        def list(self, unused_request):
            # Do the common work here. You can 
            _messages = []
            for post in self.__class__.iterator.all():
                _messages.append(self.__class__.converter(post))
    
            return self.__class__.collection(post_messages=_messages)
    
    
    
    
    @endpoints.api(name='posts',  # The name can be different.
                   version='v1',
                   allowed_client_ids=[WEB_CLIENT_ID, ANDROID_CLIENT_ID, IOS_CLIENT_ID],
                   audiences=[ANDROID_AUDIENCE])
    class PostAPI(Base):
        # Post specific defs go here.
        MessageCollection = PostMessageCollection
        PATH = '/posts'
        NAME = 'posts.listPosts'
        converter = post_to_message
        iterator = Post
        collection = PostCollection
    
    
    # Define the category class here.
    

Clearly, it doesn't save much time.

