From db9bbf6528e792976e80f870b2013199569a0021 Mon Sep 17 00:00:00 2001 From: "James (d0c_s4vage) Johnson" Date: Thu, 4 Feb 2016 14:04:48 -0600 Subject: [PATCH 01/23] Adding new `ProjectHook` attributes: * `build_events` * `enable_ssl_verification` See the two links below: * https://github.com/gitlabhq/gitlabhq/blob/master/doc/api/projects.md#add-project-hook * https://github.com/pyapi-gitlab/pyapi-gitlab/pull/173 --- gitlab/objects.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gitlab/objects.py b/gitlab/objects.py index f8c102b00..c763791ac 100644 --- a/gitlab/objects.py +++ b/gitlab/objects.py @@ -851,7 +851,8 @@ class ProjectHook(GitlabObject): requiredUrlAttrs = ['project_id'] requiredCreateAttrs = ['url'] optionalCreateAttrs = ['push_events', 'issues_events', - 'merge_requests_events', 'tag_push_events'] + 'merge_requests_events', 'tag_push_events' + 'build_events', 'enable_ssl_verification'] shortPrintAttr = 'url' From 1f81c2d7a93cc7c719bf8bda627020946aa975d3 Mon Sep 17 00:00:00 2001 From: "James (d0c_s4vage) Johnson" Date: Thu, 4 Feb 2016 15:21:09 -0600 Subject: [PATCH 02/23] Added missing comma --- gitlab/objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitlab/objects.py b/gitlab/objects.py index c763791ac..9145622dd 100644 --- a/gitlab/objects.py +++ b/gitlab/objects.py @@ -851,7 +851,7 @@ class ProjectHook(GitlabObject): requiredUrlAttrs = ['project_id'] requiredCreateAttrs = ['url'] optionalCreateAttrs = ['push_events', 'issues_events', - 'merge_requests_events', 'tag_push_events' + 'merge_requests_events', 'tag_push_events', 'build_events', 'enable_ssl_verification'] shortPrintAttr = 'url' From e387de528ad21766747b91bb7e1cd91f6e4642b5 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Thu, 4 Feb 2016 22:39:21 +0100 Subject: [PATCH 03/23] Add support for user block/unblock --- gitlab/exceptions.py | 8 ++++++++ gitlab/objects.py | 14 ++++++++++++++ tools/python_test.py | 3 +++ 3 files changed, 25 insertions(+) diff --git a/gitlab/exceptions.py b/gitlab/exceptions.py index 74e6137cb..1b5ec6a56 100644 --- a/gitlab/exceptions.py +++ b/gitlab/exceptions.py @@ -83,6 +83,14 @@ class GitlabBuildRetryError(GitlabOperationError): pass +class GitlabBlockError(GitlabOperationError): + pass + + +class GitlabUnblockError(GitlabOperationError): + pass + + def raise_error_from_response(response, error, expected_code=200): """Tries to parse gitlab error message from response and raises error. diff --git a/gitlab/objects.py b/gitlab/objects.py index f8c102b00..a781cb1da 100644 --- a/gitlab/objects.py +++ b/gitlab/objects.py @@ -527,6 +527,20 @@ def Key(self, id=None, **kwargs): user_id=self.id, **kwargs) + def block(self, **kwargs): + """Blocks the user.""" + url = '/users/%s/block' % self.id + r = self.gitlab._raw_put(url, **kwargs) + raise_error_from_response(r, GitlabBlockError) + self.state = 'blocked' + + def unblock(self, **kwargs): + """Unblocks the user.""" + url = '/users/%s/unblock' % self.id + r = self.gitlab._raw_put(url, **kwargs) + raise_error_from_response(r, GitlabUnblockError) + self.state = 'active' + class UserManager(BaseManager): obj_cls = User diff --git a/tools/python_test.py b/tools/python_test.py index 8791da2c3..aa881b1b6 100644 --- a/tools/python_test.py +++ b/tools/python_test.py @@ -40,6 +40,9 @@ assert(new_user.username == user.username) assert(new_user.email == user.email) +new_user.block() +new_user.unblock() + # SSH keys key = new_user.keys.create({'title': 'testkey', 'key': SSH_KEY}) assert(len(new_user.keys.list()) == 1) From 293a9dc9b086568a043040f07fdf1aa574a52500 Mon Sep 17 00:00:00 2001 From: Mikhail Lopotkov Date: Fri, 5 Feb 2016 14:50:51 +0500 Subject: [PATCH 04/23] fix GitlabObject creation in _custom_list --- gitlab/objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitlab/objects.py b/gitlab/objects.py index a781cb1da..06297534a 100644 --- a/gitlab/objects.py +++ b/gitlab/objects.py @@ -163,7 +163,7 @@ def _custom_list(self, url, cls, **kwargs): l = [] for j in r.json(): - o = cls(self, j) + o = cls(self.gitlab, j) o._from_api = True l.append(o) From 8aa8d8cd054710e79d45c71c86eaf4358a152d7c Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Fri, 5 Feb 2016 18:50:08 +0100 Subject: [PATCH 05/23] Rework the CLI code Add support for more subcommands. --- gitlab/cli.py | 331 +++++++++++++++++++++++++++----------------------- 1 file changed, 177 insertions(+), 154 deletions(-) diff --git a/gitlab/cli.py b/gitlab/cli.py index cf1c6c045..4c205581e 100644 --- a/gitlab/cli.py +++ b/gitlab/cli.py @@ -25,29 +25,28 @@ import re import sys +import six + import gitlab camel_re = re.compile('(.)([A-Z])') -LIST = 'list' -GET = 'get' -CREATE = 'create' -UPDATE = 'update' -DELETE = 'delete' -PROTECT = 'protect' -UNPROTECT = 'unprotect' -SEARCH = 'search' -OWNED = 'owned' -ALL = 'all' -ACTIONS = [LIST, GET, CREATE, UPDATE, DELETE] -EXTRA_ACTION = [PROTECT, UNPROTECT, SEARCH, OWNED, ALL] - -extra_actions = { - gitlab.ProjectBranch: {PROTECT: {'requiredAttrs': ['id', 'project-id']}, - UNPROTECT: {'requiredAttrs': ['id', 'project-id']}}, - gitlab.Project: {SEARCH: {'requiredAttrs': ['query']}, - OWNED: {'requiredAttrs': []}, - ALL: {'requiredAttrs': []}}, - gitlab.Group: {SEARCH: {'requiredAttrs': ['query']}}, + +EXTRA_ACTIONS = { + gitlab.Group: {'search': {'required': ['query']}}, + gitlab.ProjectBranch: {'protect': {'required': ['id', 'project-id']}, + 'unprotect': {'required': ['id', 'project-id']}}, + gitlab.ProjectBuild: {'cancel': {'required': ['id', 'project-id']}, + 'retry': {'required': ['id', 'project-id']}}, + gitlab.ProjectCommit: {'diff': {'required': ['id', 'project-id']}, + 'blob': {'required': ['id', 'project-id', + 'filepath']}, + 'builds': {'required': ['id', 'project-id']}}, + gitlab.ProjectMilestone: {'issues': {'required': ['id', 'project-id']}}, + gitlab.Project: {'search': {'required': ['query']}, + 'owned': {}, + 'all': {}}, + gitlab.User: {'block': {'required': ['id']}, + 'unblock': {'required': ['id']}}, } @@ -65,7 +64,7 @@ def _cls_to_what(cls): def _populate_sub_parser_by_class(cls, sub_parser): - for action_name in ACTIONS: + for action_name in ['list', 'get', 'create', 'update', 'delete']: attr = 'can' + action_name.capitalize() if not getattr(cls, attr): continue @@ -75,14 +74,14 @@ def _populate_sub_parser_by_class(cls, sub_parser): for x in cls.requiredUrlAttrs] sub_parser_action.add_argument("--sudo", required=False) - if action_name == LIST: + if action_name == "list": [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), required=True) for x in cls.requiredListAttrs] sub_parser_action.add_argument("--page", required=False) sub_parser_action.add_argument("--per-page", required=False) - elif action_name in [GET, DELETE]: + elif action_name in ["get", "delete"]: if cls not in [gitlab.CurrentUser]: if cls.getRequiresId: id_attr = cls.idAttr.replace('_', '-') @@ -92,7 +91,7 @@ def _populate_sub_parser_by_class(cls, sub_parser): required=True) for x in cls.requiredGetAttrs if x != cls.idAttr] - elif action_name == CREATE: + elif action_name == "create": [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), required=True) for x in cls.requiredCreateAttrs] @@ -100,7 +99,7 @@ def _populate_sub_parser_by_class(cls, sub_parser): required=False) for x in cls.optionalCreateAttrs] - elif action_name == UPDATE: + elif action_name == "update": id_attr = cls.idAttr.replace('_', '-') sub_parser_action.add_argument("--%s" % id_attr, required=True) @@ -119,12 +118,12 @@ def _populate_sub_parser_by_class(cls, sub_parser): required=False) for x in attrs] - if cls in extra_actions: - for action_name in sorted(extra_actions[cls]): + if cls in EXTRA_ACTIONS: + for action_name in sorted(EXTRA_ACTIONS[cls]): sub_parser_action = sub_parser.add_parser(action_name) - d = extra_actions[cls][action_name] + d = EXTRA_ACTIONS[cls][action_name] [sub_parser_action.add_argument("--%s" % arg, required=True) - for arg in d['requiredAttrs']] + for arg in d.get('required', [])] def do_auth(gitlab_id, config_files): @@ -136,107 +135,155 @@ def do_auth(gitlab_id, config_files): _die(str(e)) -def _get_id(cls, args): - try: - id = args.pop(cls.idAttr) - except Exception: - _die("Missing --%s argument" % cls.idAttr.replace('_', '-')) +class GitlabCLI(object): + def _get_id(self, cls, args): + try: + id = args.pop(cls.idAttr) + except Exception: + _die("Missing --%s argument" % cls.idAttr.replace('_', '-')) - return id + return id + def do_create(self, cls, gl, what, args): + if not cls.canCreate: + _die("%s objects can't be created" % what) -def do_create(cls, gl, what, args): - if not cls.canCreate: - _die("%s objects can't be created" % what) + try: + o = cls.create(gl, args) + except Exception as e: + _die("Impossible to create object (%s)" % str(e)) - try: - o = cls.create(gl, args) - except Exception as e: - _die("Impossible to create object (%s)" % str(e)) + return o - return o + def do_list(self, cls, gl, what, args): + if not cls.canList: + _die("%s objects can't be listed" % what) + try: + l = cls.list(gl, **args) + except Exception as e: + _die("Impossible to list objects (%s)" % str(e)) -def do_list(cls, gl, what, args): - if not cls.canList: - _die("%s objects can't be listed" % what) + return l - try: - l = cls.list(gl, **args) - except Exception as e: - _die("Impossible to list objects (%s)" % str(e)) + def do_get(self, cls, gl, what, args): + if cls.canGet is False: + _die("%s objects can't be retrieved" % what) - return l + id = None + if cls not in [gitlab.CurrentUser] and cls.getRequiresId: + id = self._get_id(cls, args) + try: + o = cls.get(gl, id, **args) + except Exception as e: + _die("Impossible to get object (%s)" % str(e)) -def do_get(cls, gl, what, args): - if cls.canGet is False: - _die("%s objects can't be retrieved" % what) + return o - id = None - if cls not in [gitlab.CurrentUser] and cls.getRequiresId: - id = _get_id(cls, args) + def do_delete(self, cls, gl, what, args): + if not cls.canDelete: + _die("%s objects can't be deleted" % what) - try: - o = cls.get(gl, id, **args) - except Exception as e: - _die("Impossible to get object (%s)" % str(e)) - - return o + id = args.pop(cls.idAttr) + try: + gl.delete(cls, id, **args) + except Exception as e: + _die("Impossible to destroy object (%s)" % str(e)) + def do_update(self, cls, gl, what, args): + if not cls.canUpdate: + _die("%s objects can't be updated" % what) -def do_delete(cls, gl, what, args): - if not cls.canDelete: - _die("%s objects can't be deleted" % what) + o = self.do_get(cls, gl, what, args) + try: + for k, v in args.items(): + o.__dict__[k] = v + o.save() + except Exception as e: + _die("Impossible to update object (%s)" % str(e)) - id = args.pop(cls.idAttr) - try: - gl.delete(cls, id, **args) - except Exception as e: - _die("Impossible to destroy object (%s)" % str(e)) + return o + def do_group_search(self, cls, gl, what, args): + try: + return gl.groups.search(args['query']) + except Exception as e: + _die("Impossible to search projects (%s)" % str(e)) -def do_update(cls, gl, what, args): - if not cls.canUpdate: - _die("%s objects can't be updated" % what) + def do_project_search(self, cls, gl, what, args): + try: + return gl.projects.search(args['query']) + except Exception as e: + _die("Impossible to search projects (%s)" % str(e)) - o = do_get(cls, gl, what, args) - try: - for k, v in args.items(): - o.__dict__[k] = v - o.save() - except Exception as e: - _die("Impossible to update object (%s)" % str(e)) + def do_project_all(self, cls, gl, what, args): + try: + return gl.projects.all() + except Exception as e: + _die("Impossible to list all projects (%s)" % str(e)) - return o + def do_project_owned(self, cls, gl, what, args): + try: + return gl.projects.owned() + except Exception as e: + _die("Impossible to list owned projects (%s)" % str(e)) + def do_user_block(self, cls, gl, what, args): + try: + o = self.do_get(cls, gl, what, args) + o.block() + except Exception as e: + _die("Impossible to block user (%s)" % str(e)) -def do_group_search(gl, what, args): - try: - return gl.groups.search(args['query']) - except Exception as e: - _die("Impossible to search projects (%s)" % str(e)) + def do_user_unblock(self, cls, gl, what, args): + try: + o = self.do_get(cls, gl, what, args) + o.unblock() + except Exception as e: + _die("Impossible to block user (%s)" % str(e)) + def do_project_commit_diff(self, cls, gl, what, args): + try: + o = self.do_get(cls, gl, what, args) + return [x['diff'] for x in o.diff()] + except Exception as e: + _die("Impossible to get commit diff (%s)" % str(e)) -def do_project_search(gl, what, args): - try: - return gl.projects.search(args['query']) - except Exception as e: - _die("Impossible to search projects (%s)" % str(e)) + def do_project_commit_blob(self, cls, gl, what, args): + try: + o = self.do_get(cls, gl, what, args) + return o.blob(args['filepath']) + except Exception as e: + _die("Impossible to get commit blob (%s)" % str(e)) + def do_project_commit_builds(self, cls, gl, what, args): + try: + o = self.do_get(cls, gl, what, args) + return o.builds() + except Exception as e: + _die("Impossible to get commit builds (%s)" % str(e)) -def do_project_all(gl, what, args): - try: - return gl.projects.all() - except Exception as e: - _die("Impossible to list all projects (%s)" % str(e)) + def do_project_build_cancel(self, cls, gl, what, args): + try: + o = self.do_get(cls, gl, what, args) + return o.cancel() + except Exception as e: + _die("Impossible to cancel project build (%s)" % str(e)) + def do_project_build_retry(self, cls, gl, what, args): + try: + o = self.do_get(cls, gl, what, args) + return o.retry() + except Exception as e: + _die("Impossible to retry project build (%s)" % str(e)) -def do_project_owned(gl, what, args): - try: - return gl.projects.owned() - except Exception as e: - _die("Impossible to list owned projects (%s)" % str(e)) + def do_project_milestone_issues(self, cls, gl, what, args): + try: + o = self.do_get(cls, gl, what, args) + return o.issues() + except Exception as e: + _die("Impossible to get milestone issues (%s)" % str(e)) def main(): @@ -293,11 +340,8 @@ def main(): what = arg.what # Remove CLI behavior-related args - args.pop("gitlab") - args.pop("config_file") - args.pop("verbose") - args.pop("what") - args.pop("action") + for item in ("gitlab", "config_file", "verbose", "what", "action"): + args.pop(item) cls = None try: @@ -307,52 +351,31 @@ def main(): gl = do_auth(gitlab_id, config_files) - if action == CREATE or action == GET: - o = globals()['do_%s' % action.lower()](cls, gl, what, args) - o.display(verbose) - - elif action == LIST: - for o in do_list(cls, gl, what, args): - o.display(verbose) - print("") - - elif action == DELETE or action == UPDATE: - o = globals()['do_%s' % action.lower()](cls, gl, what, args) - - elif action == PROTECT or action == UNPROTECT: - if cls != gitlab.ProjectBranch: - _die("%s objects can't be protected" % what) - - o = do_get(cls, gl, what, args) - getattr(o, action)() - - elif action == SEARCH: - - if cls == gitlab.Project: - l = do_project_search(gl, what, args) - elif cls == gitlab.Group: - l = do_group_search(gl, what, args) - else: - _die("%s objects don't support this request" % what) - - for o in l: - o.display(verbose) - print("") - - elif action == OWNED: - if cls != gitlab.Project: - _die("%s objects don't support this request" % what) - - for o in do_project_owned(gl, what, args): - o.display(verbose) - print("") - - elif action == ALL: - if cls != gitlab.Project: - _die("%s objects don't support this request" % what) - - for o in do_project_all(gl, what, args): - o.display(verbose) - print("") + cli = GitlabCLI() + method = None + what = what.replace('-', '_') + action = action.lower() + for test in ["do_%s_%s" % (what, action), + "do_%s" % action]: + if hasattr(cli, test): + method = test + + if method is None: + sys.stderr.write("Don't know how to deal with this!\n") + sys.exit(1) + + ret_val = getattr(cli, method)(cls, gl, what, args) + + if isinstance(ret_val, list): + for o in ret_val: + if isinstance(o, gitlab.GitlabObject): + o.display(verbose) + print("") + else: + print(o) + elif isinstance(ret_val, gitlab.GitlabObject): + ret_val.display(verbose) + elif isinstance(ret_val, six.string_types): + print(ret_val) sys.exit(0) From d2e30da81cafcff4295b067425b2d03e3fdf2556 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Fri, 5 Feb 2016 22:11:31 +0100 Subject: [PATCH 06/23] Add some unit tests for CLI Reorganize the cli.py code to ease the testing. --- gitlab/cli.py | 145 ++++++++++++++++-------------- gitlab/tests/test_cli.py | 95 ++++++++++++++++++++ gitlab/tests/test_gitlabobject.py | 4 - 3 files changed, 172 insertions(+), 72 deletions(-) create mode 100644 gitlab/tests/test_cli.py diff --git a/gitlab/cli.py b/gitlab/cli.py index 4c205581e..fc4c029d0 100644 --- a/gitlab/cli.py +++ b/gitlab/cli.py @@ -63,69 +63,6 @@ def _cls_to_what(cls): return camel_re.sub(r'\1-\2', cls.__name__).lower() -def _populate_sub_parser_by_class(cls, sub_parser): - for action_name in ['list', 'get', 'create', 'update', 'delete']: - attr = 'can' + action_name.capitalize() - if not getattr(cls, attr): - continue - sub_parser_action = sub_parser.add_parser(action_name) - [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), - required=True) - for x in cls.requiredUrlAttrs] - sub_parser_action.add_argument("--sudo", required=False) - - if action_name == "list": - [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), - required=True) - for x in cls.requiredListAttrs] - sub_parser_action.add_argument("--page", required=False) - sub_parser_action.add_argument("--per-page", required=False) - - elif action_name in ["get", "delete"]: - if cls not in [gitlab.CurrentUser]: - if cls.getRequiresId: - id_attr = cls.idAttr.replace('_', '-') - sub_parser_action.add_argument("--%s" % id_attr, - required=True) - [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), - required=True) - for x in cls.requiredGetAttrs if x != cls.idAttr] - - elif action_name == "create": - [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), - required=True) - for x in cls.requiredCreateAttrs] - [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), - required=False) - for x in cls.optionalCreateAttrs] - - elif action_name == "update": - id_attr = cls.idAttr.replace('_', '-') - sub_parser_action.add_argument("--%s" % id_attr, - required=True) - - attrs = (cls.requiredUpdateAttrs - if (cls.requiredUpdateAttrs or cls.optionalUpdateAttrs) - else cls.requiredCreateAttrs) - [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), - required=True) - for x in attrs if x != cls.idAttr] - - attrs = (cls.optionalUpdateAttrs - if (cls.requiredUpdateAttrs or cls.optionalUpdateAttrs) - else cls.optionalCreateAttrs) - [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), - required=False) - for x in attrs] - - if cls in EXTRA_ACTIONS: - for action_name in sorted(EXTRA_ACTIONS[cls]): - sub_parser_action = sub_parser.add_parser(action_name) - d = EXTRA_ACTIONS[cls][action_name] - [sub_parser_action.add_argument("--%s" % arg, required=True) - for arg in d.get('required', [])] - - def do_auth(gitlab_id, config_files): try: gl = gitlab.Gitlab.from_config(gitlab_id, config_files) @@ -286,11 +223,70 @@ def do_project_milestone_issues(self, cls, gl, what, args): _die("Impossible to get milestone issues (%s)" % str(e)) -def main(): - if "--version" in sys.argv: - print(gitlab.__version__) - exit(0) +def _populate_sub_parser_by_class(cls, sub_parser): + for action_name in ['list', 'get', 'create', 'update', 'delete']: + attr = 'can' + action_name.capitalize() + if not getattr(cls, attr): + continue + sub_parser_action = sub_parser.add_parser(action_name) + [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), + required=True) + for x in cls.requiredUrlAttrs] + sub_parser_action.add_argument("--sudo", required=False) + + if action_name == "list": + [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), + required=True) + for x in cls.requiredListAttrs] + sub_parser_action.add_argument("--page", required=False) + sub_parser_action.add_argument("--per-page", required=False) + elif action_name in ["get", "delete"]: + if cls not in [gitlab.CurrentUser]: + if cls.getRequiresId: + id_attr = cls.idAttr.replace('_', '-') + sub_parser_action.add_argument("--%s" % id_attr, + required=True) + [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), + required=True) + for x in cls.requiredGetAttrs if x != cls.idAttr] + + elif action_name == "create": + [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), + required=True) + for x in cls.requiredCreateAttrs] + [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), + required=False) + for x in cls.optionalCreateAttrs] + + elif action_name == "update": + id_attr = cls.idAttr.replace('_', '-') + sub_parser_action.add_argument("--%s" % id_attr, + required=True) + + attrs = (cls.requiredUpdateAttrs + if (cls.requiredUpdateAttrs or cls.optionalUpdateAttrs) + else cls.requiredCreateAttrs) + [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), + required=True) + for x in attrs if x != cls.idAttr] + + attrs = (cls.optionalUpdateAttrs + if (cls.requiredUpdateAttrs or cls.optionalUpdateAttrs) + else cls.optionalCreateAttrs) + [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), + required=False) + for x in attrs] + + if cls in EXTRA_ACTIONS: + for action_name in sorted(EXTRA_ACTIONS[cls]): + sub_parser_action = sub_parser.add_parser(action_name) + d = EXTRA_ACTIONS[cls][action_name] + [sub_parser_action.add_argument("--%s" % arg, required=True) + for arg in d.get('required', [])] + + +def _build_parser(args=sys.argv[1:]): parser = argparse.ArgumentParser( description="GitLab API Command Line Interface") parser.add_argument("--version", help="Display the version.", @@ -330,7 +326,20 @@ def main(): _populate_sub_parser_by_class(cls, object_subparsers) object_subparsers.required = True - arg = parser.parse_args() + return parser + + +def _parse_args(args=sys.argv[1:]): + parser = _build_parser() + return parser.parse_args(args) + + +def main(): + if "--version" in sys.argv: + print(gitlab.__version__) + exit(0) + + arg = _parse_args() args = arg.__dict__ config_files = arg.config_file diff --git a/gitlab/tests/test_cli.py b/gitlab/tests/test_cli.py new file mode 100644 index 000000000..c32ad5018 --- /dev/null +++ b/gitlab/tests/test_cli.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (C) 2016 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 +# 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . + +from __future__ import print_function +from __future__ import absolute_import + +import argparse + +import six +try: + import unittest +except ImportError: + import unittest2 as unittest + +from gitlab import cli + + +class TestCLI(unittest.TestCase): + def test_what_to_cls(self): + self.assertEqual("Foo", cli._what_to_cls("foo")) + self.assertEqual("FooBar", cli._what_to_cls("foo-bar")) + + def test_cls_to_what(self): + class Class(object): + pass + + class TestClass(object): + pass + + self.assertEqual("test-class", cli._cls_to_what(TestClass)) + self.assertEqual("class", cli._cls_to_what(Class)) + + def test_die(self): + with self.assertRaises(SystemExit) as test: + cli._die("foobar") + + self.assertEqual(test.exception.code, 1) + + def test_extra_actions(self): + for cls, data in six.iteritems(cli.EXTRA_ACTIONS): + for key in data: + self.assertIsInstance(data[key], dict) + + def test_parsing(self): + args = cli._parse_args(['-v', '-g', 'gl_id', + '-c', 'foo.cfg', '-c', 'bar.cfg', + 'project', 'list']) + self.assertTrue(args.verbose) + self.assertEqual(args.gitlab, 'gl_id') + self.assertEqual(args.config_file, ['foo.cfg', 'bar.cfg']) + self.assertEqual(args.what, 'project') + self.assertEqual(args.action, 'list') + + def test_parser(self): + parser = cli._build_parser() + subparsers = None + for action in parser._actions: + if type(action) == argparse._SubParsersAction: + subparsers = action + break + self.assertIsNotNone(subparsers) + self.assertIn('user', subparsers.choices) + + user_subparsers = None + for action in subparsers.choices['user']._actions: + if type(action) == argparse._SubParsersAction: + user_subparsers = action + break + self.assertIsNotNone(user_subparsers) + self.assertIn('list', user_subparsers.choices) + self.assertIn('get', user_subparsers.choices) + self.assertIn('delete', user_subparsers.choices) + self.assertIn('update', user_subparsers.choices) + self.assertIn('create', user_subparsers.choices) + self.assertIn('block', user_subparsers.choices) + self.assertIn('unblock', user_subparsers.choices) + + actions = user_subparsers.choices['create']._option_string_actions + self.assertFalse(actions['--twitter'].required) + self.assertTrue(actions['--username'].required) diff --git a/gitlab/tests/test_gitlabobject.py b/gitlab/tests/test_gitlabobject.py index e001a8c80..aea80ca28 100644 --- a/gitlab/tests/test_gitlabobject.py +++ b/gitlab/tests/test_gitlabobject.py @@ -492,7 +492,3 @@ def test_content(self): def test_blob_fail(self): with HTTMock(self.resp_content_fail): self.assertRaises(GitlabGetError, self.obj.Content) - - -if __name__ == "__main__": - main() From 2e1f84ede56b73c5b6857515d24d061a60b509fb Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Fri, 5 Feb 2016 22:21:06 +0100 Subject: [PATCH 07/23] Add a coverage tox env --- test-requirements.txt | 1 + tox.ini | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/test-requirements.txt b/test-requirements.txt index fead9f9bb..25cb7dd5d 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,3 +1,4 @@ +coverage discover testrepository hacking>=0.9.2,<0.10 diff --git a/tox.ini b/tox.ini index 929de456e..9990c45af 100644 --- a/tox.ini +++ b/tox.ini @@ -26,3 +26,7 @@ ignore = H501 [testenv:docs] commands = python setup.py build_sphinx + +[testenv:cover] +commands = + python setup.py testr --slowest --coverage --testr-args="{posargs}" From f15a7cfd7edbbc55ff4fb5d41995dee033517963 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 11 Feb 2016 22:41:03 -0500 Subject: [PATCH 08/23] define GitlabObject.as_dict() to dump object as a dict --- gitlab/objects.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/gitlab/objects.py b/gitlab/objects.py index 57b8e14a8..c03e77e48 100644 --- a/gitlab/objects.py +++ b/gitlab/objects.py @@ -34,9 +34,7 @@ class jsonEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, GitlabObject): - return {k: v for k, v in six.iteritems(obj.__dict__) - if (not isinstance(v, BaseManager) - and not k[0] == '_')} + return obj.as_dict() elif isinstance(obj, gitlab.Gitlab): return {'url': obj._url} return json.JSONEncoder.default(self, obj) @@ -488,6 +486,11 @@ def json(self): """ return json.dumps(self, cls=jsonEncoder) + def as_dict(self): + """Dump the object as a dict.""" + return {k: v for k, v in six.iteritems(self.__dict__) + if (not isinstance(v, BaseManager) and not k[0] == '_')} + class UserKey(GitlabObject): _url = '/users/%(user_id)s/keys' From 01802c0ceb7c677ea0eb9c6a1b2382048b9fed86 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 11 Feb 2016 22:43:25 -0500 Subject: [PATCH 09/23] define GitlabObject.__eq__() and __ne__() equivalence methods --- gitlab/objects.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/gitlab/objects.py b/gitlab/objects.py index c03e77e48..9849179f9 100644 --- a/gitlab/objects.py +++ b/gitlab/objects.py @@ -491,6 +491,14 @@ def as_dict(self): return {k: v for k, v in six.iteritems(self.__dict__) if (not isinstance(v, BaseManager) and not k[0] == '_')} + def __eq__(self, other): + if type(other) is type(self): + return self.as_dict() == other.as_dict() + return False + + def __ne__(self, other): + return not self.__eq__(other) + class UserKey(GitlabObject): _url = '/users/%(user_id)s/keys' @@ -544,6 +552,15 @@ def unblock(self, **kwargs): raise_error_from_response(r, GitlabUnblockError) self.state = 'active' + def __eq__(self, other): + if type(other) is type(self): + selfdict = self.as_dict() + otherdict = other.as_dict() + selfdict.pop(u'password', None) + otherdict.pop(u'password', None) + return selfdict == otherdict + return False + class UserManager(BaseManager): obj_cls = User From 8f59516a4d7d5c6c654e8c2531092e217d13a4be Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 11 Feb 2016 22:50:28 -0500 Subject: [PATCH 10/23] define UserManager.search() to search for users --- gitlab/objects.py | 8 ++++++++ gitlab/tests/test_manager.py | 23 +++++++++++++++++++++++ tools/python_test.py | 12 ++++++++++++ 3 files changed, 43 insertions(+) diff --git a/gitlab/objects.py b/gitlab/objects.py index 9849179f9..7a679fc3e 100644 --- a/gitlab/objects.py +++ b/gitlab/objects.py @@ -565,6 +565,14 @@ def __eq__(self, other): class UserManager(BaseManager): obj_cls = User + def search(self, query, **kwargs): + """Search users. + + Returns a list of matching users. + """ + url = self.obj_cls._url + '?search=' + query + return self._custom_list(url, self.obj_cls, **kwargs) + class CurrentUserKey(GitlabObject): _url = '/user/keys' diff --git a/gitlab/tests/test_manager.py b/gitlab/tests/test_manager.py index f1286afa6..1b808a95b 100644 --- a/gitlab/tests/test_manager.py +++ b/gitlab/tests/test_manager.py @@ -235,6 +235,29 @@ def resp_get_all(url, request): self.assertEqual(data[0].id, 1) self.assertEqual(data[1].id, 2) + def test_user_manager_search(self): + mgr = UserManager(self.gitlab) + + @urlmatch(scheme="http", netloc="localhost", path="/api/v3/users", + query="search=foo", method="get") + def resp_get_search(url, request): + headers = {'content-type': 'application/json'} + content = ('[{"name": "foo1", "id": 1}, ' + '{"name": "foo2", "id": 2}]') + content = content.encode("utf-8") + return response(200, content, headers, None, 5, request) + + with HTTMock(resp_get_search): + data = mgr.search('foo') + self.assertEqual(type(data), list) + self.assertEqual(2, len(data)) + self.assertEqual(type(data[0]), User) + self.assertEqual(type(data[1]), User) + self.assertEqual(data[0].name, "foo1") + self.assertEqual(data[1].name, "foo2") + self.assertEqual(data[0].id, 1) + self.assertEqual(data[1].id, 2) + def test_group_manager_search(self): mgr = GroupManager(self.gitlab) diff --git a/tools/python_test.py b/tools/python_test.py index aa881b1b6..a2e0b154f 100644 --- a/tools/python_test.py +++ b/tools/python_test.py @@ -43,12 +43,24 @@ new_user.block() new_user.unblock() +foobar_user = gl.users.create( + {'email': 'foobar@example.com', 'username': 'foobar', + 'name': 'Foo Bar', 'password': 'foobar_password'}) + +assert gl.users.search('foobar') == [foobar_user] +usercmp = lambda x,y: cmp(x.id, y.id) +expected = sorted([new_user, foobar_user], cmp=usercmp) +actual = sorted(gl.users.search('foo'), cmp=usercmp) +assert expected == actual +assert gl.users.search('asdf') == [] + # SSH keys key = new_user.keys.create({'title': 'testkey', 'key': SSH_KEY}) assert(len(new_user.keys.list()) == 1) key.delete() new_user.delete() +foobar_user.delete() assert(len(gl.users.list()) == 1) # current user key From ac2e534fb811f3c1295c742e74dcb14a3c1ff0c1 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 11 Feb 2016 22:50:41 -0500 Subject: [PATCH 11/23] define UserManager.get_by_username() to get a user by username --- gitlab/objects.py | 14 ++++++++++++++ gitlab/tests/test_manager.py | 26 ++++++++++++++++++++++++++ tools/python_test.py | 9 +++++++++ 3 files changed, 49 insertions(+) diff --git a/gitlab/objects.py b/gitlab/objects.py index 7a679fc3e..8e94cb220 100644 --- a/gitlab/objects.py +++ b/gitlab/objects.py @@ -573,6 +573,20 @@ def search(self, query, **kwargs): url = self.obj_cls._url + '?search=' + query return self._custom_list(url, self.obj_cls, **kwargs) + def get_by_username(self, username, **kwargs): + """Get a user by its username. + + Returns a User object or None if the named user does not + exist. + """ + url = self.obj_cls._url + '?username=' + username + results = self._custom_list(url, self.obj_cls, **kwargs) + assert len(results) in (0, 1) + try: + return results[0] + except IndexError: + raise GitlabGetError('no such user: ' + username) + class CurrentUserKey(GitlabObject): _url = '/user/keys' diff --git a/gitlab/tests/test_manager.py b/gitlab/tests/test_manager.py index 1b808a95b..59987a7a8 100644 --- a/gitlab/tests/test_manager.py +++ b/gitlab/tests/test_manager.py @@ -258,6 +258,32 @@ def resp_get_search(url, request): self.assertEqual(data[0].id, 1) self.assertEqual(data[1].id, 2) + def test_user_manager_get_by_username(self): + mgr = UserManager(self.gitlab) + + @urlmatch(scheme="http", netloc="localhost", path="/api/v3/users", + query="username=foo", method="get") + def resp_get_username(url, request): + headers = {'content-type': 'application/json'} + content = '[{"name": "foo", "id": 1}]'.encode("utf-8") + return response(200, content, headers, None, 5, request) + + with HTTMock(resp_get_username): + data = mgr.get_by_username('foo') + self.assertEqual(type(data), User) + self.assertEqual(data.name, "foo") + self.assertEqual(data.id, 1) + + @urlmatch(scheme="http", netloc="localhost", path="/api/v3/users", + query="username=foo", method="get") + def resp_get_username_nomatch(url, request): + headers = {'content-type': 'application/json'} + content = '[]'.encode("utf-8") + return response(200, content, headers, None, 5, request) + + with HTTMock(resp_get_username_nomatch): + self.assertRaises(GitlabGetError, mgr.get_by_username, 'foo') + def test_group_manager_search(self): mgr = GroupManager(self.gitlab) diff --git a/tools/python_test.py b/tools/python_test.py index a2e0b154f..d32dccd36 100644 --- a/tools/python_test.py +++ b/tools/python_test.py @@ -54,6 +54,15 @@ assert expected == actual assert gl.users.search('asdf') == [] +assert gl.users.get_by_username('foobar') == foobar_user +assert gl.users.get_by_username('foo') == new_user +try: + gl.users.get_by_username('asdf') +except gitlab.GitlabGetError: + pass +else: + assert False + # SSH keys key = new_user.keys.create({'title': 'testkey', 'key': SSH_KEY}) assert(len(new_user.keys.list()) == 1) From 073d8d5d84efa64ad2a13f8dc405e51840f47585 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Fri, 12 Feb 2016 09:09:11 +0100 Subject: [PATCH 12/23] Implement "user search" CLI --- gitlab/cli.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/gitlab/cli.py b/gitlab/cli.py index fc4c029d0..cbd0d7c06 100644 --- a/gitlab/cli.py +++ b/gitlab/cli.py @@ -46,7 +46,8 @@ 'owned': {}, 'all': {}}, gitlab.User: {'block': {'required': ['id']}, - 'unblock': {'required': ['id']}}, + 'unblock': {'required': ['id']}, + 'search': {'required': ['query']}}, } @@ -222,6 +223,12 @@ def do_project_milestone_issues(self, cls, gl, what, args): except Exception as e: _die("Impossible to get milestone issues (%s)" % str(e)) + def do_user_search(self, cls, gl, what, args): + try: + return gl.users.search(args['query']) + except Exception as e: + _die("Impossible to search users (%s)" % str(e)) + def _populate_sub_parser_by_class(cls, sub_parser): for action_name in ['list', 'get', 'create', 'update', 'delete']: From b79af1d8a8515a419267a8f8e8937c9134bcea3a Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Fri, 12 Feb 2016 09:16:36 +0100 Subject: [PATCH 13/23] Improve the doc for UserManager Describe parameters, return values and exceptions for search() and get_by_username(). --- gitlab/objects.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/gitlab/objects.py b/gitlab/objects.py index 8e94cb220..5dfc80a1d 100644 --- a/gitlab/objects.py +++ b/gitlab/objects.py @@ -568,7 +568,16 @@ class UserManager(BaseManager): def search(self, query, **kwargs): """Search users. - Returns a list of matching users. + Args: + query (str): The query string to send to GitLab for the search. + **kwargs: Additional arguments to send to GitLab. + + Returns: + list(User): A list of matching users. + + Raises: + GitlabConnectionError: If the server cannot be reached. + GitlabListError: If the server fails to perform the request. """ url = self.obj_cls._url + '?search=' + query return self._custom_list(url, self.obj_cls, **kwargs) @@ -576,8 +585,16 @@ def search(self, query, **kwargs): def get_by_username(self, username, **kwargs): """Get a user by its username. - Returns a User object or None if the named user does not - exist. + Args: + username (str): The name of the user. + **kwargs: Additional arguments to send to GitLab. + + Returns: + User: The matching user. + + Raises: + GitlabConnectionError: If the server cannot be reached. + GitlabGetError: If the server fails to perform the request. """ url = self.obj_cls._url + '?username=' + username results = self._custom_list(url, self.obj_cls, **kwargs) From 7260684d11e8ffe02461f761b6677d039b703a8d Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sun, 14 Feb 2016 07:55:09 +0100 Subject: [PATCH 14/23] CLI: fix discovery of method to execute --- gitlab/cli.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gitlab/cli.py b/gitlab/cli.py index cbd0d7c06..0e7d5ef57 100644 --- a/gitlab/cli.py +++ b/gitlab/cli.py @@ -375,6 +375,7 @@ def main(): "do_%s" % action]: if hasattr(cli, test): method = test + break if method is None: sys.stderr.write("Don't know how to deal with this!\n") From 58433d2b1854eb4e112c499d52d8dd0fd6dba094 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sun, 14 Feb 2016 10:40:12 +0100 Subject: [PATCH 15/23] CI: implement user get-by-username fixes #95 --- docs/cli.rst | 7 +++++++ gitlab/cli.py | 11 +++++++++-- tools/functional_tests.sh | 8 ++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/docs/cli.rst b/docs/cli.rst index 6ab795772..81d308d96 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -150,6 +150,13 @@ Get a specific project (id 2): $ gitlab project get --id 2 +Get a specific user by id or by username: + +.. code-block:: console + + $ gitlab user get --id 3 + $ gitlab user get-by-username --query jdoe + Get a list of snippets for this project: .. code-block:: console diff --git a/gitlab/cli.py b/gitlab/cli.py index 0e7d5ef57..090978b4d 100644 --- a/gitlab/cli.py +++ b/gitlab/cli.py @@ -47,7 +47,8 @@ 'all': {}}, gitlab.User: {'block': {'required': ['id']}, 'unblock': {'required': ['id']}, - 'search': {'required': ['query']}}, + 'search': {'required': ['query']}, + 'get-by-username': {'required': ['query']}}, } @@ -229,6 +230,12 @@ def do_user_search(self, cls, gl, what, args): except Exception as e: _die("Impossible to search users (%s)" % str(e)) + def do_user_getbyusername(self, cls, gl, what, args): + try: + return gl.users.search(args['query']) + except Exception as e: + _die("Impossible to get user %s (%s)" % (args['query'], str(e))) + def _populate_sub_parser_by_class(cls, sub_parser): for action_name in ['list', 'get', 'create', 'update', 'delete']: @@ -370,7 +377,7 @@ def main(): cli = GitlabCLI() method = None what = what.replace('-', '_') - action = action.lower() + action = action.lower().replace('-', '') for test in ["do_%s_%s" % (what, action), "do_%s" % action]: if hasattr(cli, test): diff --git a/tools/functional_tests.sh b/tools/functional_tests.sh index fefb5afec..84339e30f 100755 --- a/tools/functional_tests.sh +++ b/tools/functional_tests.sh @@ -35,6 +35,14 @@ testcase "user creation" ' ' USER_ID=$(pecho "${OUTPUT}" | grep ^id: | cut -d' ' -f2) +testcase "user get (by id)" ' + GITLAB user get --id $USER_ID >/dev/null 2>&1 +' + +testcase "user get (by username)" ' + GITLAB user get-by-username --query user1 >/dev/null 2>&1 +' + testcase "verbose output" ' OUTPUT=$(try GITLAB -v user list) || exit 1 pecho "${OUTPUT}" | grep -q avatar-url From 44d0dc54fb7edf7de4e50ca34d430fe734c0e8cc Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sun, 14 Feb 2016 10:57:13 +0100 Subject: [PATCH 16/23] remove unused _returnClass attribute --- gitlab/__init__.py | 3 --- gitlab/objects.py | 1 - 2 files changed, 4 deletions(-) diff --git a/gitlab/__init__.py b/gitlab/__init__.py index 1bb148ee0..a94708436 100644 --- a/gitlab/__init__.py +++ b/gitlab/__init__.py @@ -351,9 +351,6 @@ def list(self, obj_class, **kwargs): raise_error_from_response(r, GitlabListError) cls = obj_class - if obj_class._returnClass: - cls = obj_class._returnClass - cls_kwargs = kwargs.copy() # Add _from_api manually, because we are not creating objects diff --git a/gitlab/objects.py b/gitlab/objects.py index 5dfc80a1d..8146816dd 100644 --- a/gitlab/objects.py +++ b/gitlab/objects.py @@ -177,7 +177,6 @@ class GitlabObject(object): _urlPlural = None _id_in_delete_url = True _id_in_update_url = True - _returnClass = None _constructorTypes = None #: Whether _get_list_or_object should return list or object when id is None From 453224aaf64c37196b7211de8dd4a60061954987 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Thu, 18 Feb 2016 18:17:06 +0000 Subject: [PATCH 17/23] Re-implement _custom_list in the Gitlab class Rename the method _raw_list. This adds support for the ``all=True`` option to enable automatic recursion and avoid pagination if requested by the user. Fixes #93 --- docs/api-usage.rst | 3 ++- gitlab/__init__.py | 25 +++++++++++++++++++++++++ gitlab/objects.py | 37 +++++++++++++++++-------------------- 3 files changed, 44 insertions(+), 21 deletions(-) diff --git a/docs/api-usage.rst b/docs/api-usage.rst index b6a498dba..ca85fbdd8 100644 --- a/docs/api-usage.rst +++ b/docs/api-usage.rst @@ -112,11 +112,12 @@ You can use pagination to go throught long lists: ten_first_groups = gl.groups.list(page=0, per_page=10) -Use the ``all`` parameter to get all the items: +Use the ``all`` parameter to get all the items when using listing methods: .. code-block:: python all_groups = gl.groups.list(all=True) + all_owned_projects = gl.projects.owned(all=True) Sudo ==== diff --git a/gitlab/__init__.py b/gitlab/__init__.py index a94708436..ef39cd59e 100644 --- a/gitlab/__init__.py +++ b/gitlab/__init__.py @@ -269,6 +269,31 @@ def _raw_get(self, path, content_type=None, **kwargs): raise GitlabConnectionError( "Can't connect to GitLab server (%s)" % e) + def _raw_list(self, path, cls, **kwargs): + r = self._raw_get(path, **kwargs) + raise_error_from_response(r, GitlabListError) + + cls_kwargs = kwargs.copy() + + # Add _from_api manually, because we are not creating objects + # through normal path + cls_kwargs['_from_api'] = True + get_all_results = kwargs.get('all', False) + + # Remove parameters from kwargs before passing it to constructor + for key in ['all', 'page', 'per_page', 'sudo']: + if key in cls_kwargs: + del cls_kwargs[key] + + results = [cls(self, item, **cls_kwargs) for item in r.json() + if item is not None] + if ('next' in r.links and 'url' in r.links['next'] + and get_all_results is True): + args = kwargs.copy() + args['next_url'] = r.links['next']['url'] + results.extend(self.list(cls, **args)) + return results + def _raw_post(self, path, data=None, content_type=None, **kwargs): url = '%s%s' % (self._url, path) headers = self._create_headers(content_type) diff --git a/gitlab/objects.py b/gitlab/objects.py index 8146816dd..c453daf57 100644 --- a/gitlab/objects.py +++ b/gitlab/objects.py @@ -155,18 +155,6 @@ def delete(self, id, **kwargs): raise NotImplementedError self.gitlab.delete(self.obj_cls, id, **args) - def _custom_list(self, url, cls, **kwargs): - r = self.gitlab._raw_get(url, **kwargs) - raise_error_from_response(r, GitlabListError) - - l = [] - for j in r.json(): - o = cls(self.gitlab, j) - o._from_api = True - l.append(o) - - return l - class GitlabObject(object): """Base class for all classes that interface with GitLab.""" @@ -569,6 +557,7 @@ def search(self, query, **kwargs): Args: query (str): The query string to send to GitLab for the search. + all (bool): If True, return all the items, without pagination **kwargs: Additional arguments to send to GitLab. Returns: @@ -579,7 +568,7 @@ def search(self, query, **kwargs): GitlabListError: If the server fails to perform the request. """ url = self.obj_cls._url + '?search=' + query - return self._custom_list(url, self.obj_cls, **kwargs) + return self.gitlab._raw_list(url, self.obj_cls, **kwargs) def get_by_username(self, username, **kwargs): """Get a user by its username. @@ -596,7 +585,7 @@ def get_by_username(self, username, **kwargs): GitlabGetError: If the server fails to perform the request. """ url = self.obj_cls._url + '?username=' + username - results = self._custom_list(url, self.obj_cls, **kwargs) + results = self.gitlab._raw_list(url, self.obj_cls, **kwargs) assert len(results) in (0, 1) try: return results[0] @@ -712,10 +701,15 @@ class GroupManager(BaseManager): def search(self, query, **kwargs): """Searches groups by name. - Returns a list of matching groups. + Args: + query (str): The search string + all (bool): If True, return all the items, without pagination + + Returns: + list(Group): a list of matching groups. """ url = '/groups?search=' + query - return self._custom_list(url, Group, **kwargs) + return self.gitlab._raw_list(url, self.obj_cls, **kwargs) class Hook(GitlabObject): @@ -1524,35 +1518,38 @@ def search(self, query, **kwargs): Args: query (str): The query string to send to GitLab for the search. + all (bool): If True, return all the items, without pagination **kwargs: Additional arguments to send to GitLab. Returns: list(Project): A list of matching projects. """ - return self._custom_list("/projects/search/" + query, Project, - **kwargs) + return self.gitlab._raw_list("/projects/search/" + query, Project, + **kwargs) def all(self, **kwargs): """List all the projects (need admin rights). Args: + all (bool): If True, return all the items, without pagination **kwargs: Additional arguments to send to GitLab. Returns: list(Project): The list of projects. """ - return self._custom_list("/projects/all", Project, **kwargs) + return self.gitlab._raw_list("/projects/all", Project, **kwargs) def owned(self, **kwargs): """List owned projects. Args: + all (bool): If True, return all the items, without pagination **kwargs: Additional arguments to send to GitLab. Returns: list(Project): The list of owned projects. """ - return self._custom_list("/projects/owned", Project, **kwargs) + return self.gitlab._raw_list("/projects/owned", Project, **kwargs) class UserProjectManager(BaseManager): From 7ed84a7b4ca73d1b0cc6be7db0c43958ff9f4c47 Mon Sep 17 00:00:00 2001 From: Asher256 Date: Wed, 2 Mar 2016 13:55:59 -0500 Subject: [PATCH 18/23] Fix the 'invalid syntax' on Python 3.2, because of u'password' More informations regarding this issue: Operating system: Debian Wheezy, with Python 3.2 and the last version of python-gitlab. The gitlab module raised this exception, because of the 'u' (Unicode): Traceback (most recent call last): File "push_settings.py", line 14, in from helper import ROOT_EMAIL, ADMINS, git, old_git File "/opt/scripts/gitlab/helpers/helper.py", line 25, in from gitlab import Gitlab File "/opt/scripts/gitlab/helpers/gitlab/__init__.py", line 32, in from gitlab.objects import * # noqa File "/opt/scripts/gitlab/helpers/gitlab/objects.py", line 546 selfdict.pop(u'password', None) ^ SyntaxError: invalid syntax It is a recent change: 01802c0 (Richard Hansen 2016-02-11 22:43:25 -0500 546) selfdict.pop(u'password', None) 01802c0 (Richard Hansen 2016-02-11 22:43:25 -0500 547) otherdict.pop(u'password', None) To solve the issue, 'u' was removed. --- gitlab/objects.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gitlab/objects.py b/gitlab/objects.py index c453daf57..34a50e6da 100644 --- a/gitlab/objects.py +++ b/gitlab/objects.py @@ -543,8 +543,8 @@ def __eq__(self, other): if type(other) is type(self): selfdict = self.as_dict() otherdict = other.as_dict() - selfdict.pop(u'password', None) - otherdict.pop(u'password', None) + selfdict.pop('password', None) + otherdict.pop('password', None) return selfdict == otherdict return False From 86ade4ac78fd14cc8f12be39c74ff60688a2fcf7 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sat, 5 Mar 2016 14:23:54 +0100 Subject: [PATCH 19/23] pep8 ignore H803 errors (git messages) --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 9990c45af..b7e0d2fbd 100644 --- a/tox.ini +++ b/tox.ini @@ -22,7 +22,7 @@ commands = {posargs} [flake8] exclude = .git,.venv,.tox,dist,doc,*egg,build, -ignore = H501 +ignore = H501,H803 [testenv:docs] commands = python setup.py build_sphinx From e48e4aca9650b241d1f1e038fdcab125b7c95656 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sat, 12 Mar 2016 11:06:04 +0100 Subject: [PATCH 20/23] add a note about project search API --- gitlab/objects.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/gitlab/objects.py b/gitlab/objects.py index 34a50e6da..c5a47a039 100644 --- a/gitlab/objects.py +++ b/gitlab/objects.py @@ -1516,6 +1516,16 @@ class ProjectManager(BaseManager): def search(self, query, **kwargs): """Search projects by name. + .. note:: + + The search is only performed on the project name (not on the + namespace or the description). To perform a smarter search, use the + ``search`` argument of the ``list()`` method: + + .. code-block:: python + + gl.projects.list(search=your_search_string) + Args: query (str): The query string to send to GitLab for the search. all (bool): If True, return all the items, without pagination From f8528cce8d79b13e90e1f87b56c8cdbe30b2067e Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sat, 19 Mar 2016 15:28:06 +0100 Subject: [PATCH 21/23] Gitlab.update(): use the proper attributes if defined --- gitlab/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/gitlab/__init__.py b/gitlab/__init__.py index ef39cd59e..693621c1a 100644 --- a/gitlab/__init__.py +++ b/gitlab/__init__.py @@ -567,8 +567,11 @@ def update(self, obj, **kwargs): params = obj.__dict__.copy() params.update(kwargs) missing = [] - for k in itertools.chain(obj.requiredUrlAttrs, - obj.requiredCreateAttrs): + if obj.requiredUpdateAttrs or obj.optionalUpdateAttrs: + required_attrs = obj.requiredUpdateAttrs + else: + required_attrs = obj.requiredCreateAttrs + for k in itertools.chain(obj.requiredUrlAttrs, required_attrs): if k not in params: missing.append(k) if missing: From 754d5e5f66ac86baba02e7d63157c263510ec593 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sat, 19 Mar 2016 15:30:44 +0100 Subject: [PATCH 22/23] version bump --- gitlab/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitlab/__init__.py b/gitlab/__init__.py index 693621c1a..6c7519537 100644 --- a/gitlab/__init__.py +++ b/gitlab/__init__.py @@ -32,7 +32,7 @@ from gitlab.objects import * # noqa __title__ = 'python-gitlab' -__version__ = '0.12.1' +__version__ = '0.12.2' __author__ = 'Gauvain Pocentek' __email__ = 'gauvain@pocentek.net' __license__ = 'LGPL3' From bb463ae4e0ed79e472c0d594f76dc8177a29fb5c Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sat, 19 Mar 2016 15:36:56 +0100 Subject: [PATCH 23/23] Update changelog and authors --- AUTHORS | 3 +++ ChangeLog | 23 +++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/AUTHORS b/AUTHORS index 31c91fceb..9a00c26cd 100644 --- a/AUTHORS +++ b/AUTHORS @@ -24,3 +24,6 @@ François Gouteroux Daniel Serodio Colin D Bennett Richard Hansen +James (d0c_s4vage) Johnson +Mikhail Lopotkov +Asher256 diff --git a/ChangeLog b/ChangeLog index deead576d..ac4b4778f 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,26 @@ +Version 0.12.2 + + * Add new `ProjectHook` attributes + * Add support for user block/unblock + * Fix GitlabObject creation in _custom_list + * Add support for more CLI subcommands + * Add some unit tests for CLI + * Add a coverage tox env + * Define GitlabObject.as_dict() to dump object as a dict + * Define GitlabObject.__eq__() and __ne__() equivalence methods + * Define UserManager.search() to search for users + * Define UserManager.get_by_username() to get a user by username + * Implement "user search" CLI + * Improve the doc for UserManager + * CLI: implement user get-by-username + * Re-implement _custom_list in the Gitlab class + * Fix the 'invalid syntax' error on Python 3.2 + * Gitlab.update(): use the proper attributes if defined + +Version 0.12.1 + + * Fix a broken upload to pypi + Version 0.12 * Improve documentation