changeset 1445:c78f6bf52e9c beta

Beginning of API implementation for rhodecode
author Marcin Kuzminski <marcin@python-works.com>
date Sat, 20 Aug 2011 18:13:54 +0300
parents d17aa79768f0
children eab0cf9ab8bf
files rhodecode/config/routing.py rhodecode/controllers/api/__init__.py rhodecode/controllers/api/api.py
diffstat 3 files changed, 278 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- a/rhodecode/config/routing.py	Mon Aug 15 19:53:43 2011 +0300
+++ b/rhodecode/config/routing.py	Sat Aug 20 18:13:54 2011 +0300
@@ -254,6 +254,7 @@
         m.connect("admin_settings_create_repository", "/create_repository",
                   action="create_repository", conditions=dict(method=["GET"]))
 
+
     #ADMIN MAIN PAGES
     with rmap.submapper(path_prefix=ADMIN_PREFIX,
                         controller='admin/admin') as m:
@@ -261,6 +262,14 @@
         m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}',
                   action='add_repo')
 
+    #==========================================================================
+    # API V1
+    #==========================================================================
+    with rmap.submapper(path_prefix=ADMIN_PREFIX,
+                        controller='api/api') as m:
+        m.connect('api', '/api')
+
+
     #USER JOURNAL
     rmap.connect('journal', '%s/journal' % ADMIN_PREFIX, controller='journal')
 
@@ -400,4 +409,5 @@
     rmap.connect('repo_forks_home', '/{repo_name:.*}/forks',
                  controller='forks', action='forks',
                  conditions=dict(function=check_repo))
+
     return rmap
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rhodecode/controllers/api/__init__.py	Sat Aug 20 18:13:54 2011 +0300
@@ -0,0 +1,228 @@
+# -*- coding: utf-8 -*-
+"""
+    rhodecode.controllers.api
+    ~~~~~~~~~~~~~~~~~~~~~~~~~
+
+    JSON RPC controller
+    
+    :created_on: Aug 20, 2011
+    :author: marcink
+    :copyright: (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>    
+    :license: GPLv3, see COPYING for more details.
+"""
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; version 2
+# of the License or (at your opinion) any later version of the license.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA  02110-1301, USA.
+
+import inspect
+import json
+import logging
+import types
+import urllib
+
+from paste.response import replace_header
+
+from pylons.controllers import WSGIController
+from pylons.controllers.util import Response
+
+from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError, \
+HTTPBadRequest, HTTPError
+
+from rhodecode.model.user import User
+from rhodecode.lib.auth import AuthUser
+
+log = logging.getLogger('JSONRPC')
+
+class JSONRPCError(BaseException):
+
+    def __init__(self, message):
+        self.message = message
+
+    def __str__(self):
+        return str(self.message)
+
+
+def jsonrpc_error(message, code=None):
+    """Generate a Response object with a JSON-RPC error body"""
+    return Response(body=json.dumps(dict(result=None,
+                                         error=message)))
+
+
+class JSONRPCController(WSGIController):
+    """
+     A WSGI-speaking JSON-RPC controller class
+    
+     See the specification:
+     <http://json-rpc.org/wiki/specification>`.
+   
+     Valid controller return values should be json-serializable objects.
+    
+     Sub-classes should catch their exceptions and raise JSONRPCError
+     if they want to pass meaningful errors to the client.
+    
+     """
+
+    def _get_method_args(self):
+        """
+        Return `self._rpc_args` to dispatched controller method
+        chosen by __call__
+        """
+        return self._rpc_args
+
+    def __call__(self, environ, start_response):
+        """
+        Parse the request body as JSON, look up the method on the
+        controller and if it exists, dispatch to it.
+        """
+
+        if 'CONTENT_LENGTH' not in environ:
+            log.debug("No Content-Length")
+            return jsonrpc_error(0, "No Content-Length")
+        else:
+            length = environ['CONTENT_LENGTH'] or 0
+            length = int(environ['CONTENT_LENGTH'])
+            log.debug('Content-Length: %s', length)
+
+        if length == 0:
+            log.debug("Content-Length is 0")
+            return jsonrpc_error(0, "Content-Length is 0")
+
+        raw_body = environ['wsgi.input'].read(length)
+
+        try:
+            json_body = json.loads(urllib.unquote_plus(raw_body))
+        except ValueError as e:
+            #catch JSON errors Here
+            return jsonrpc_error("JSON parse error ERR:%s RAW:%r" \
+                                 % (e, urllib.unquote_plus(raw_body)))
+
+
+        #check AUTH based on API KEY
+
+        try:
+            self._req_api_key = json_body['api_key']
+            self._req_method = json_body['method']
+            self._req_params = json_body['args']
+            log.debug('method: %s, params: %s',
+                      self._req_method,
+                      self._req_params)
+        except KeyError as e:
+            return jsonrpc_error(message='Incorrect JSON query missing %s' % e)
+
+        #check if we can find this session using api_key
+        try:
+            u = User.get_by_api_key(self._req_api_key)
+            auth_u = AuthUser(u.user_id, self._req_api_key)
+        except Exception as e:
+            return jsonrpc_error(message='Invalid API KEY')
+
+        self._error = None
+        try:
+            self._func = self._find_method()
+        except AttributeError, e:
+            return jsonrpc_error(str(e))
+
+        # now that we have a method, add self._req_params to
+        # self.kargs and dispatch control to WGIController
+        arglist = inspect.getargspec(self._func)[0][1:]
+
+        # this is little trick to inject logged in user for 
+        # perms decorators to work they expect the controller class to have
+        # rhodecode_user set
+        self.rhodecode_user = auth_u
+
+        if 'user' not in arglist:
+            return jsonrpc_error('This method [%s] does not support '
+                                 'authentication (missing user param)' %
+                                 self._func.__name__)
+
+        # get our arglist and check if we provided them as args
+        for arg in arglist:
+            if arg == 'user':
+                # user is something translated from api key and this is
+                # checked before
+                continue
+
+            if not self._req_params or arg not in self._req_params:
+                return jsonrpc_error('Missing %s arg in JSON DATA' % arg)
+
+        self._rpc_args = dict(user=u)
+        self._rpc_args.update(self._req_params)
+
+        self._rpc_args['action'] = self._req_method
+        self._rpc_args['environ'] = environ
+        self._rpc_args['start_response'] = start_response
+
+        status = []
+        headers = []
+        exc_info = []
+        def change_content(new_status, new_headers, new_exc_info=None):
+            status.append(new_status)
+            headers.extend(new_headers)
+            exc_info.append(new_exc_info)
+
+        output = WSGIController.__call__(self, environ, change_content)
+        output = list(output)
+        headers.append(('Content-Length', str(len(output[0]))))
+        replace_header(headers, 'Content-Type', 'application/json')
+        start_response(status[0], headers, exc_info[0])
+
+        return output
+
+    def _dispatch_call(self):
+        """
+        Implement dispatch interface specified by WSGIController
+        """
+        try:
+            raw_response = self._inspect_call(self._func)
+            print raw_response
+            if isinstance(raw_response, HTTPError):
+                self._error = str(raw_response)
+        except JSONRPCError as e:
+            self._error = str(e)
+        except Exception as e:
+            log.debug('Encountered unhandled exception: %s', repr(e))
+            json_exc = JSONRPCError('Internal server error')
+            self._error = str(json_exc)
+
+        if self._error is not None:
+            raw_response = None
+
+        response = dict(result=raw_response, error=self._error)
+
+        try:
+            return json.dumps(response)
+        except TypeError, e:
+            log.debug('Error encoding response: %s', e)
+            return json.dumps(dict(result=None,
+                                   error="Error encoding response"))
+
+    def _find_method(self):
+        """
+        Return method named by `self._req_method` in controller if able
+        """
+        log.debug('Trying to find JSON-RPC method: %s', self._req_method)
+        if self._req_method.startswith('_'):
+            raise AttributeError("Method not allowed")
+
+        try:
+            func = getattr(self, self._req_method, None)
+        except UnicodeEncodeError:
+            raise AttributeError("Problem decoding unicode in requested "
+                                 "method name.")
+
+        if isinstance(func, types.MethodType):
+            return func
+        else:
+            raise AttributeError("No such method: %s" % self._req_method)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rhodecode/controllers/api/api.py	Sat Aug 20 18:13:54 2011 +0300
@@ -0,0 +1,40 @@
+from rhodecode.controllers.api import JSONRPCController, JSONRPCError
+from rhodecode.lib.auth import HasPermissionAllDecorator
+from rhodecode.model.scm import ScmModel
+
+
+class ApiController(JSONRPCController):
+    """
+    API Controller
+    
+    
+    Each method needs to have USER as argument this is then based on given
+    API_KEY propagated as instance of user object
+    
+    Preferably this should be first argument also
+    
+    
+    Each function should also **raise** JSONRPCError for any 
+    errors that happens
+    
+    """
+
+    @HasPermissionAllDecorator('hg.admin')
+    def pull(self, user, repo):
+        """
+        Dispatch pull action on given repo
+        
+        
+        param user:
+        param repo:
+        """
+
+        try:
+            ScmModel().pull_changes(repo, self.rhodecode_user.username)
+            return 'Pulled from %s' % repo
+        except Exception:
+            raise JSONRPCError('Unable to pull changes from "%s"' % repo)
+
+
+
+