diff --git a/.hgignore b/.hgignore --- a/.hgignore +++ b/.hgignore @@ -19,3 +19,4 @@ syntax: regexp ^RhodeCode\.egg-info$ ^rc\.ini$ ^fabfile.py +^\.rhodecode$ diff --git a/docs/api/api.rst b/docs/api/api.rst --- a/docs/api/api.rst +++ b/docs/api/api.rst @@ -59,6 +59,47 @@ All responses from API will be `HTTP/1.0 calling api *error* key from response will contain failure description and result will be null. + +API CLIENT +++++++++++ + +From version 1.4 RhodeCode adds a binary script that allows to easily +communicate with API. After installing RhodeCode a `rhodecode-api` script +will be available. + +To get started quickly simply run:: + + rhodecode-api _create_config --apikey= --apihost= + +This will create a file named .config in the directory you executed it storing +json config file with credentials. You can skip this step and always provide +both of the arguments to be able to communicate with server + + +after that simply run any api command for example get_repo:: + + rhodecode-api get_repo + + calling {"api_key": "", "id": 75, "args": {}, "method": "get_repo"} to http://127.0.0.1:5000 + rhodecode said: + {'error': 'Missing non optional `repoid` arg in JSON DATA', + 'id': 75, + 'result': None} + +Ups looks like we forgot to add an argument + +Let's try again now giving the repoid as parameters:: + + rhodecode-api get_repo repoid:rhodecode + + calling {"api_key": "", "id": 39, "args": {"repoid": "rhodecode"}, "method": "get_repo"} to http://127.0.0.1:5000 + rhodecode said: + {'error': None, + 'id': 39, + 'result': } + + + API METHODS +++++++++++ diff --git a/docs/changelog.rst b/docs/changelog.rst --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -22,6 +22,7 @@ news - #465 mentions autocomplete inside comments boxes - #469 added --update-only option to whoosh to re-index only given list of repos in index +- rhodecode-api CLI client fixes +++++ diff --git a/rhodecode/bin/__init__.py b/rhodecode/bin/__init__.py new file mode 100644 diff --git a/rhodecode/bin/rhodecode_api.py b/rhodecode/bin/rhodecode_api.py new file mode 100755 --- /dev/null +++ b/rhodecode/bin/rhodecode_api.py @@ -0,0 +1,216 @@ +# -*- coding: utf-8 -*- +""" + rhodecode.bin.backup_manager + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Api CLI client for RhodeCode + + :created_on: Jun 3, 2012 + :author: marcink + :copyright: (C) 2010-2012 Marcin Kuzminski + :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, either version 3 of the License, or +# (at your option) any later version. +# +# 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, see . + +from __future__ import with_statement +import os +import sys +import random +import urllib2 +import pprint +import argparse + +try: + from rhodecode.lib.ext_json import json +except ImportError: + try: + import simplejson as json + except ImportError: + import json + + +CONFIG_NAME = '.rhodecode' + + +class RcConf(object): + """ + RhodeCode config for API + + conf = RcConf() + conf['key'] + + """ + + def __init__(self, autoload=True, autocreate=False, config=None): + self._conf_name = CONFIG_NAME + self._conf = {} + if autocreate: + self.make_config(config) + if autoload: + self._conf = self.load_config() + + def __getitem__(self, key): + return self._conf[key] + + def __nonzero__(self): + if self._conf: + return True + return False + + def __eq__(self): + return self._conf.__eq__() + + def __repr__(self): + return 'RcConf<%s>' % self._conf.__repr__() + + def make_config(self, config): + """ + Saves given config as a JSON dump in the _conf_name location + + :param config: + :type config: + """ + with open(self._conf_name, 'wb') as f: + json.dump(config, f, indent=4) + sys.stdout.write('Updated conf\n') + + def update_config(self, new_config): + """ + Reads the JSON config updates it's values with new_config and + saves it back as JSON dump + + :param new_config: + """ + config = {} + try: + with open(self._conf_name, 'rb') as conf: + config = json.load(conf) + except IOError, e: + sys.stderr.write(str(e) + '\n') + + config.update(new_config) + self.make_config(config) + + def load_config(self): + """ + Loads config from file and returns loaded JSON object + """ + try: + with open(self._conf_name, 'rb') as conf: + return json.load(conf) + except IOError, e: + #sys.stderr.write(str(e) + '\n') + pass + + +def api_call(apikey, apihost, method=None, **kw): + """ + Api_call wrapper for RhodeCode + + :param apikey: + :param apihost: + :param method: + """ + def _build_data(random_id): + """ + Builds API data with given random ID + + :param random_id: + :type random_id: + """ + return { + "id": random_id, + "api_key": apikey, + "method": method, + "args": kw + } + + if not method: + raise Exception('please specify method name !') + id_ = random.randrange(1, 200) + req = urllib2.Request('%s/_admin/api' % apihost, + data=json.dumps(_build_data(id_)), + headers={'content-type': 'text/plain'}) + print 'calling %s to %s' % (req.get_data(), apihost) + ret = urllib2.urlopen(req) + json_data = json.loads(ret.read()) + id_ret = json_data['id'] + _formatted_json = pprint.pformat(json_data) + if id_ret == id_: + print 'rhodecode said:\n%s' % (_formatted_json) + else: + raise Exception('something went wrong. ' + 'ID mismatch got %s, expected %s | %s' % ( + id_ret, id_, _formatted_json)) + + +def argparser(argv): + usage = ("rhodecode_api [-h] [--apikey APIKEY] [--apihost APIHOST] " + "_create_config or METHOD ...") + + parser = argparse.ArgumentParser(description='RhodeCode API cli', + usage=usage) + + ## config + group = parser.add_argument_group('config') + group.add_argument('--apikey', help='api access key') + group.add_argument('--apihost', help='api host') + + group = parser.add_argument_group('API') + group.add_argument('method', metavar='METHOD', type=str, + help='API method name to call followed by key:value attributes', + ) + + args, other = parser.parse_known_args() + return parser, args, other + + +def main(argv=None): + """ + Main execution function for cli + + :param argv: + :type argv: + """ + if argv is None: + argv = sys.argv + + conf = None + parser, args, other = argparser(argv) + + api_credentials_given = (args.apikey and args.apihost) + if args.method == '_create_config': + if not api_credentials_given: + raise parser.error('_create_config requires --apikey and --apihost') + conf = RcConf(autocreate=True, config={'apikey': args.apikey, + 'apihost': args.apihost}) + sys.stdout.write('Create new config in %s\n' % CONFIG_NAME) + + if not conf: + conf = RcConf(autoload=True) + if not conf: + if not api_credentials_given: + parser.error('Could not find config file and missing ' + '--apikey or --apihost in params') + + apikey = args.apikey or conf['apikey'] + host = args.apihost or conf['apihost'] + method = args.method + margs = dict(map(lambda s: s.split(':', 1), other)) + + api_call(apikey, host, method, **margs) + return 0 + +if __name__ == '__main__': + sys.exit(main(sys.argv)) diff --git a/rhodecode/lib/backup_manager.py b/rhodecode/bin/rhodecode_backup.py old mode 100644 new mode 100755 rename from rhodecode/lib/backup_manager.py rename to rhodecode/bin/rhodecode_backup.py --- a/rhodecode/lib/backup_manager.py +++ b/rhodecode/bin/rhodecode_backup.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- """ - rhodecode.lib.backup_manager + rhodecode.bin.backup_manager ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Mercurial repositories backup manager, it allows to backups all + Repositories backup manager, it allows to backups all repositories and send it to backup server using RSA key via ssh. :created_on: Feb 28, 2010 @@ -39,7 +39,7 @@ logging.basicConfig(level=logging.DEBUG, class BackupManager(object): def __init__(self, repos_location, rsa_key, backup_server): today = datetime.datetime.now().weekday() + 1 - self.backup_file_name = "mercurial_repos.%s.tar.gz" % today + self.backup_file_name = "rhodecode_repos.%s.tar.gz" % today self.id_rsa_path = self.get_id_rsa(rsa_key) self.repos_path = self.get_repos_path(repos_location) diff --git a/rhodecode/controllers/api/__init__.py b/rhodecode/controllers/api/__init__.py --- a/rhodecode/controllers/api/__init__.py +++ b/rhodecode/controllers/api/__init__.py @@ -57,15 +57,16 @@ class JSONRPCError(BaseException): return str(self.message) -def jsonrpc_error(message, code=None): +def jsonrpc_error(message, retid=None, code=None): """ Generate a Response object with a JSON-RPC error body """ from pylons.controllers.util import Response - resp = Response(body=json.dumps(dict(id=None, result=None, error=message)), - status=code, - content_type='application/json') - return resp + return Response( + body=json.dumps(dict(id=retid, result=None, error=message)), + status=code, + content_type='application/json' + ) class JSONRPCController(WSGIController): @@ -94,9 +95,11 @@ class JSONRPCController(WSGIController): Parse the request body as JSON, look up the method on the controller and if it exists, dispatch to it. """ + self._req_id = None if 'CONTENT_LENGTH' not in environ: log.debug("No Content-Length") - return jsonrpc_error(message="No Content-Length in request") + return jsonrpc_error(retid=self._req_id, + message="No Content-Length in request") else: length = environ['CONTENT_LENGTH'] or 0 length = int(environ['CONTENT_LENGTH']) @@ -104,7 +107,8 @@ class JSONRPCController(WSGIController): if length == 0: log.debug("Content-Length is 0") - return jsonrpc_error(message="Content-Length is 0") + return jsonrpc_error(retid=self._req_id, + message="Content-Length is 0") raw_body = environ['wsgi.input'].read(length) @@ -112,7 +116,8 @@ class JSONRPCController(WSGIController): json_body = json.loads(urllib.unquote_plus(raw_body)) except ValueError, e: # catch JSON errors Here - return jsonrpc_error(message="JSON parse error ERR:%s RAW:%r" \ + return jsonrpc_error(retid=self._req_id, + message="JSON parse error ERR:%s RAW:%r" \ % (e, urllib.unquote_plus(raw_body))) # check AUTH based on API KEY @@ -126,22 +131,26 @@ class JSONRPCController(WSGIController): self._request_params) ) except KeyError, e: - return jsonrpc_error(message='Incorrect JSON query missing %s' % e) + return jsonrpc_error(retid=self._req_id, + 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) if u is None: - return jsonrpc_error(message='Invalid API KEY') + return jsonrpc_error(retid=self._req_id, + message='Invalid API KEY') auth_u = AuthUser(u.user_id, self._req_api_key) except Exception, e: - return jsonrpc_error(message='Invalid API KEY') + return jsonrpc_error(retid=self._req_id, + message='Invalid API KEY') self._error = None try: self._func = self._find_method() except AttributeError, e: - return jsonrpc_error(message=str(e)) + return jsonrpc_error(retid=self._req_id, + message=str(e)) # now that we have a method, add self._req_params to # self.kargs and dispatch control to WGIController @@ -164,9 +173,12 @@ class JSONRPCController(WSGIController): USER_SESSION_ATTR = 'apiuser' if USER_SESSION_ATTR not in arglist: - return jsonrpc_error(message='This method [%s] does not support ' - 'authentication (missing %s param)' % - (self._func.__name__, USER_SESSION_ATTR)) + return jsonrpc_error( + retid=self._req_id, + message='This method [%s] does not support ' + 'authentication (missing %s param)' % ( + self._func.__name__, USER_SESSION_ATTR) + ) # get our arglist and check if we provided them as args for arg, default in func_kwargs.iteritems(): @@ -179,6 +191,7 @@ class JSONRPCController(WSGIController): # NotImplementedType (default_empty) if (default == default_empty and arg not in self._request_params): return jsonrpc_error( + retid=self._req_id, message=( 'Missing non optional `%s` arg in JSON DATA' % arg ) diff --git a/rhodecode/controllers/api/api.py b/rhodecode/controllers/api/api.py --- a/rhodecode/controllers/api/api.py +++ b/rhodecode/controllers/api/api.py @@ -389,7 +389,7 @@ class ApiController(JSONRPCController): repo = RepoModel().get_repo(repoid) if repo is None: - raise JSONRPCError('unknown repository %s' % repo) + raise JSONRPCError('unknown repository "%s"' % (repo or repoid)) members = [] for user in repo.repo_to_perm: diff --git a/rhodecode/templates/admin/admin_log.html b/rhodecode/templates/admin/admin_log.html --- a/rhodecode/templates/admin/admin_log.html +++ b/rhodecode/templates/admin/admin_log.html @@ -14,7 +14,7 @@ ${h.link_to(l.user.username,h.url('edit_user', id=l.user.user_id))} ${h.action_parser(l)[0]()}
- ${h.literal(h.action_parser(l)[1]())} + ${h.literal(h.action_parser(l)[1]())}
diff --git a/setup.py b/setup.py --- a/setup.py +++ b/setup.py @@ -87,6 +87,9 @@ setup( zip_safe=False, paster_plugins=['PasteScript', 'Pylons'], entry_points=""" + [console_scripts] + rhodecode-api = rhodecode.bin.rhodecode_api:main + [paste.app_factory] main = rhodecode.config.middleware:make_app