diff --git a/AUTHORS b/AUTHORS index c6350f365..b086f42b0 100644 --- a/AUTHORS +++ b/AUTHORS @@ -12,3 +12,4 @@ Andrew Austin Koen Smets Mart Sõmermaa Diego Giovane Pasqualin +Crestez Dan Leonard diff --git a/ChangeLog b/ChangeLog index 29a8981c6..dc2633a9d 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,14 @@ +Version 0.7 + + * Fix license classifier in setup.py + * Fix encoding error when printing to redirected output + * Fix encoding error when updating with redirected output + * Add support for UserKey listing and deletion + * Add support for branches creation and deletion + * Support state_event in ProjectMilestone (#30) + * Support namespace/name for project id (#28) + * Fix handling of boolean values (#22) + Version 0.6 * IDs can be unicode (#15) diff --git a/gitlab b/gitlab index 4c8fb198c..18885006e 100755 --- a/gitlab +++ b/gitlab @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # -# Copyright (C) 2013 Gauvain Pocentek +# Copyright (C) 2013-2014 Gauvain Pocentek # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by @@ -32,26 +32,28 @@ import gitlab camel_re = re.compile('(.)([A-Z])') extra_actions = { - gitlab.ProjectBranch: { - 'protect': {'requiredAttrs': ['id', 'project-id']}, - 'unprotect': {'requiredAttrs': ['id', 'project-id']} - }, - gitlab.Project: { - 'search': {'requiredAttrs': ['query']}, - 'owned': {'requiredAttrs': []}, - 'all': {'requiredAttrs': []} - }, + gitlab.ProjectBranch: {'protect': {'requiredAttrs': ['id', 'project-id']}, + 'unprotect': {'requiredAttrs': ['id', 'project-id']} + }, + gitlab.Project: {'search': {'requiredAttrs': ['query']}, + 'owned': {'requiredAttrs': []}, + 'all': {'requiredAttrs': []} + }, } + def die(msg): sys.stderr.write(msg + "\n") sys.exit(1) + def whatToCls(what): return "".join([s.capitalize() for s in what.split("-")]) + def clsToWhat(cls): - return camel_re.sub(r'\1-\2', cls.__name__).lower() + return camel_re.sub(r'\1-\2', cls.__name__).lower() + def actionHelpList(cls): l = [] @@ -66,27 +68,33 @@ def actionHelpList(cls): detail = '' if action == 'list': - detail = " ".join(["--%s=ARG" % x.replace('_', '-') for x in cls.requiredListAttrs]) + detail = " ".join(["--%s=ARG" % x.replace('_', '-') + for x in cls.requiredListAttrs]) if detail: detail += " " detail += "--page=ARG --per-page=ARG" elif action in ['get', 'delete']: if cls not in [gitlab.CurrentUser]: detail = "--id=ARG " - detail += " ".join(["--%s=ARG" % x.replace('_', '-') for x in cls.requiredGetAttrs]) + detail += " ".join(["--%s=ARG" % x.replace('_', '-') + for x in cls.requiredGetAttrs]) elif action == 'create': - detail = " ".join(["--%s=ARG" % x.replace('_', '-') for x in cls.requiredCreateAttrs]) + detail = " ".join(["--%s=ARG" % x.replace('_', '-') + for x in cls.requiredCreateAttrs]) if detail: detail += " " - detail += " ".join(["[--%s=ARG]" % x.replace('_', '-') for x in cls.optionalCreateAttrs]) + detail += " ".join(["[--%s=ARG]" % x.replace('_', '-') + for x in cls.optionalCreateAttrs]) elif action == 'update': - detail = " ".join(["[--%s=ARG]" % x.replace('_', '-') for x in cls.requiredCreateAttrs]) + detail = " ".join(["[--%s=ARG]" % x.replace('_', '-') + for x in cls.requiredCreateAttrs]) if detail: detail += " " - detail += " ".join(["[--%s=ARG]" % x.replace('_', '-') for x in cls.optionalCreateAttrs]) + detail += " ".join(["[--%s=ARG]" % x.replace('_', '-') + for x in cls.optionalCreateAttrs]) l.append("%s %s" % (action, detail)) - if extra_actions.has_key(cls): + if cls in extra_actions: for action in sorted(extra_actions[cls]): d = extra_actions[cls][action] detail = " ".join(["--%s=ARG" % arg for arg in d['requiredAttrs']]) @@ -94,11 +102,14 @@ def actionHelpList(cls): return (l) + def usage(): - print("usage: gitlab [--help|-h] [--fancy|--verbose|-v] [--gitlab=GITLAB] WHAT ACTION [options]") + print("usage: gitlab [--help|-h] [--fancy|--verbose|-v] [--gitlab=GITLAB] " + "WHAT ACTION [options]") print("") print("--gitlab=GITLAB") - print(" Specifies which python-gitlab.cfg configuration section should be used.") + print(" Specifies which python-gitlab.cfg configuration section should " + "be used.") print(" If not defined, the default selection will be used.") print("") print("--fancy, --verbose, -v") @@ -108,7 +119,8 @@ def usage(): print(" Displays this message.") print("") print("Available `options` depend on which WHAT/ACTION couple is used.") - print("If `ACTION` is \"help\", available actions and options will be listed for `ACTION`.") + print("If `ACTION` is \"help\", available actions and options will be " + "listed for `ACTION`.") print("") print("Available `WHAT` values are:") @@ -129,15 +141,18 @@ def usage(): for cls in classes: print(" %s" % clsToWhat(cls)) + def do_auth(): try: - gl = gitlab.Gitlab(gitlab_url, private_token=gitlab_token, ssl_verify=ssl_verify) + gl = gitlab.Gitlab(gitlab_url, private_token=gitlab_token, + ssl_verify=ssl_verify) gl.auth() except: die("Could not connect to GitLab (%s)" % gitlab_url) return gl + def get_id(): try: id = d.pop('id') @@ -146,6 +161,7 @@ def get_id(): return id + def do_create(cls, d): if not cls.canCreate: die("%s objects can't be created" % what) @@ -158,6 +174,7 @@ def do_create(cls, d): return o + def do_list(cls, d): if not cls.canList: die("%s objects can't be listed" % what) @@ -169,6 +186,7 @@ def do_list(cls, d): return l + def do_get(cls, d): if not cls.canGet: die("%s objects can't be retrieved" % what) @@ -184,6 +202,7 @@ def do_get(cls, d): return o + def do_delete(cls, d): if not cls.canDelete: die("%s objects can't be deleted" % what) @@ -194,6 +213,7 @@ def do_delete(cls, d): except Exception as e: die("Impossible to destroy object (%s)" % str(e)) + def do_update(cls, d): if not cls.canUpdate: die("%s objects can't be updated" % what) @@ -208,22 +228,25 @@ def do_update(cls, d): return o + def do_project_search(d): try: return gl.search_projects(d['query']) - except: + except Exception as e: die("Impossible to search projects (%s)" % str(e)) + def do_project_all(): try: return gl.all_projects() except Exception as e: die("Impossible to list all projects (%s)" % str(e)) + def do_project_owned(): try: return gl.owned_projects() - except: + except Exception as e: die("Impossible to list owned projects (%s)" % str(e)) @@ -294,7 +317,8 @@ try: gitlab_url = config.get(gitlab_id, 'url') gitlab_token = config.get(gitlab_id, 'private_token') except: - die("Impossible to get gitlab informations from configuration (%s)" % gitlab_id) + die("Impossible to get gitlab informations from configuration (%s)" % + gitlab_id) try: ssl_verify = config.getboolean('global', 'ssl_verify') @@ -383,6 +407,7 @@ elif action == "all": o.display(verbose) else: - die("Unknown action: %s. Use \"gitlab %s help\" to get details." % (action, what)) + die("Unknown action: %s. Use \"gitlab %s help\" to get details." % + (action, what)) sys.exit(0) diff --git a/gitlab.py b/gitlab.py index a64fdcada..9ac179cea 100644 --- a/gitlab.py +++ b/gitlab.py @@ -1,7 +1,6 @@ -#!/usr/bin/python # -*- coding: utf-8 -*- # -# Copyright (C) 2013 Gauvain Pocentek +# Copyright (C) 2013-2014 Gauvain Pocentek # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by @@ -21,11 +20,11 @@ import sys __title__ = 'python-gitlab' -__version__ = '0.6' +__version__ = '0.7' __author__ = 'Gauvain Pocentek' __email__ = 'gauvain@pocentek.net' __license__ = 'LGPL3' -__copyright__ = 'Copyright 2013 Gauvain Pocentek' +__copyright__ = 'Copyright 2013-2014 Gauvain Pocentek' class jsonEncoder(json.JSONEncoder): @@ -89,6 +88,8 @@ def __init__(self, url, private_token=None, self.email = email self.password = password self.ssl_verify = ssl_verify + # Gitlab should handle UTF-8 + self.gitlab_encoding = 'UTF-8' def auth(self): """Performs an authentication using either the private token, or the @@ -186,11 +187,12 @@ def list(self, obj_class, **kwargs): raise GitlabListError('Missing attribute(s): %s' % ", ".join(missing)) - url = obj_class._url % kwargs + args = _sanitize_dict(kwargs) + url = obj_class._url % args url = '%s%s' % (self._url, url) - if kwargs: + if args: url += "?%s" % ("&".join( - ["%s=%s" % (k, v) for k, v in kwargs.items()])) + ["%s=%s" % (k, v) for k, v in args.items()])) try: r = requests.get(url, headers=self.headers, verify=self.ssl_verify) @@ -224,7 +226,7 @@ def get(self, obj_class, id=None, **kwargs): raise GitlabListError('Missing attribute(s): %s' % ", ".join(missing)) - url = obj_class._url % kwargs + url = obj_class._url % _sanitize_dict(kwargs) if id is not None: url = '%s%s/%s' % (self._url, url, str(id)) else: @@ -246,8 +248,9 @@ def get(self, obj_class, id=None, **kwargs): raise GitlabGetError('%d: %s' % (r.status_code, r.text)) def delete(self, obj): - url = obj._url % obj.__dict__ - url = '%s%s/%s' % (self._url, url, str(obj.id)) + args = _sanitize_dict(obj.__dict__) + url = obj._url % args + url = '%s%s/%s' % (self._url, url, args['id']) try: r = requests.delete(url, @@ -274,9 +277,14 @@ def create(self, obj): raise GitlabCreateError('Missing attribute(s): %s' % ", ".join(missing)) - url = obj._url % obj.__dict__ + args = _sanitize_dict(obj.__dict__) + url = obj._url % args url = '%s%s' % (self._url, url) + for k, v in obj.__dict__.items(): + if type(v) == bool: + obj.__dict__[k] = 1 if v else 0 + try: r = requests.post(url, obj.__dict__, headers=self.headers, @@ -293,16 +301,19 @@ def create(self, obj): raise GitlabCreateError('%d: %s' % (r.status_code, r.text)) def update(self, obj): - url = obj._url % obj.__dict__ + args = _sanitize_dict(obj.__dict__) + url = obj._url % args url = '%s%s/%s' % (self._url, url, str(obj.id)) # build a dict of data that can really be sent to server d = {} for k, v in obj.__dict__.items(): - if type(v) in (int, str, bool): + if type(v) in (int, str): d[k] = str(v) + elif type(v) == bool: + d[k] = 1 if v else 0 elif type(v) == unicode: - d[k] = str(v.encode(sys.stdout.encoding, "replace")) + d[k] = str(v.encode(self.gitlab_encoding, "replace")) try: r = requests.put(url, d, @@ -446,6 +457,20 @@ def Team(self, id=None, **kwargs): return self._getListOrObject(Team, id, **kwargs) +def _get_display_encoding(): + return sys.stdout.encoding or sys.getdefaultencoding() + + +def _sanitize(value): + if type(value) in (str, unicode): + return value.replace('/', '%2F') + return value + + +def _sanitize_dict(src): + return {k: _sanitize(v) for k, v in src.items()} + + class GitlabObject(object): _url = None _returnClass = None @@ -574,7 +599,7 @@ def _obj_to_str(obj): s = ", ".join([GitlabObject._obj_to_str(x) for x in obj]) return "[ %s ]" % s elif isinstance(obj, unicode): - return obj.encode(sys.stdout.encoding, "replace") + return obj.encode(_get_display_encoding(), "replace") else: return str(obj) @@ -585,8 +610,8 @@ def pretty_print(self, depth=0): if k == self.idAttr: continue v = self.__dict__[k] - pretty_k = k.replace('_', '-').encode(sys.stdout.encoding, - "replace") + pretty_k = k.replace('_', '-') + pretty_k = pretty_k.encode(_get_display_encoding(), "replace") if isinstance(v, GitlabObject): if depth == 0: print("%s:" % pretty_k) @@ -606,9 +631,9 @@ def json(self): class UserKey(GitlabObject): _url = '/users/%(user_id)s/keys' canGet = False - canList = False + canList = True canUpdate = False - canDelete = False + canDelete = True requiredCreateAttrs = ['user_id', 'title', 'key'] @@ -702,11 +727,11 @@ class Issue(GitlabObject): class ProjectBranch(GitlabObject): _url = '/projects/%(project_id)s/repository/branches' idAttr = 'name' - canDelete = False canUpdate = False - canCreate = False requiredGetAttrs = ['project_id'] requiredListAttrs = ['project_id'] + requiredCreateAttrs = ['project_id', 'branch_name', 'ref'] + requiredDeleteAttrs = ['project_id'] _constructorTypes = {'commit': 'ProjectCommit'} def protect(self, protect=True): @@ -736,8 +761,8 @@ class ProjectCommit(GitlabObject): shortPrintAttr = 'title' def diff(self): - url = '/projects/%(project_id)s/repository/commits/%(commit_id)s/diff' % \ - {'project_id': self.project_id, 'commit_id': self.id} + url = ('/projects/%(project_id)s/repository/commits/%(commit_id)s/diff' + % {'project_id': self.project_id, 'commit_id': self.id}) r = self.gitlab.rawGet(url) if r.status_code == 200: return r.json() @@ -872,7 +897,7 @@ class ProjectMilestone(GitlabObject): requiredListAttrs = ['project_id'] requiredGetAttrs = ['project_id'] requiredCreateAttrs = ['project_id', 'title'] - optionalCreateAttrs = ['description', 'due_date'] + optionalCreateAttrs = ['description', 'due_date', 'state_event'] shortPrintAttr = 'title' @@ -953,11 +978,6 @@ def Event(self, id=None, **kwargs): project_id=self.id, **kwargs) - def File(self, id=None, **kwargs): - return self._getListOrObject(ProjectFile, id, - project_id=self.id, - **kwargs) - def Hook(self, id=None, **kwargs): return self._getListOrObject(ProjectHook, id, project_id=self.id, diff --git a/setup.py b/setup.py index 4222b07e2..eeeeaf0d1 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ def get_version(): 'Development Status :: 5 - Production/Stable', 'Environment :: Console', 'Intended Audience :: System Administrators', - 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', + 'License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)', 'Natural Language :: English', 'Operating System :: POSIX', 'Operating System :: Microsoft :: Windows'