From 81b0a0679cc3adf93af22c4580b2f67c934b290e Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Tue, 29 Dec 2015 16:03:23 +0100 Subject: [PATCH 01/37] Move request return_code tests in _raise_error_from_response --- gitlab/__init__.py | 149 +++++++++++++++++++-------------------------- 1 file changed, 62 insertions(+), 87 deletions(-) diff --git a/gitlab/__init__.py b/gitlab/__init__.py index e2a723b9e..39fea25b0 100644 --- a/gitlab/__init__.py +++ b/gitlab/__init__.py @@ -107,15 +107,20 @@ class GitlabTransferProjectError(GitlabOperationError): pass -def _raise_error_from_response(response, error): +def _raise_error_from_response(response, error, expected_code=200): """Tries to parse gitlab error message from response and raises error. + Do nothing if the response status is the expected one. + If response status code is 401, raises instead GitlabAuthenticationError. response: requests response object error: Error-class to raise. Should be inherited from GitLabError """ + if expected_code == response.status_code: + return + try: message = response.json()['message'] except (KeyError, ValueError): @@ -183,12 +188,8 @@ def credentials_auth(self): data = json.dumps({'email': self.email, 'password': self.password}) r = self._raw_post('/session', data, content_type='application/json') - - if r.status_code == 201: - self.user = CurrentUser(self, r.json()) - else: - _raise_error_from_response(r, GitlabAuthenticationError) - + _raise_error_from_response(r, GitlabAuthenticationError, 201) + self.user = CurrentUser(self, r.json()) self.set_token(self.user.private_token) def token_auth(self): @@ -342,34 +343,33 @@ def list(self, obj_class, **kwargs): raise GitlabConnectionError( "Can't connect to GitLab server (%s)" % self._url) - if r.status_code == 200: - cls = obj_class - if obj_class._returnClass: - cls = obj_class._returnClass + _raise_error_from_response(r, GitlabListError) - cls_kwargs = kwargs.copy() + cls = obj_class + if obj_class._returnClass: + cls = obj_class._returnClass - # Add _created manually, because we are not creating objects - # through normal path - cls_kwargs['_created'] = True + cls_kwargs = kwargs.copy() - get_all_results = params.get('all', False) + # Add _created manually, because we are not creating objects + # through normal path + cls_kwargs['_created'] = True - # 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] + get_all_results = params.get('all', False) - 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(obj_class, **args)) - return results - else: - _raise_error_from_response(r, GitlabListError) + # 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(obj_class, **args)) + return results def get(self, obj_class, id=None, **kwargs): missing = [] @@ -399,10 +399,8 @@ def get(self, obj_class, id=None, **kwargs): raise GitlabConnectionError( "Can't connect to GitLab server (%s)" % self._url) - if r.status_code == 200: - return r.json() - else: - _raise_error_from_response(r, GitlabGetError) + _raise_error_from_response(r, GitlabGetError) + return r.json() def delete(self, obj, **kwargs): params = obj.__dict__.copy() @@ -435,10 +433,8 @@ def delete(self, obj, **kwargs): raise GitlabConnectionError( "Can't connect to GitLab server (%s)" % self._url) - if r.status_code == 200: - return True - else: - _raise_error_from_response(r, GitlabDeleteError) + _raise_error_from_response(r, GitlabDeleteError) + return True def create(self, obj, **kwargs): params = obj.__dict__.copy() @@ -467,10 +463,8 @@ def create(self, obj, **kwargs): raise GitlabConnectionError( "Can't connect to GitLab server (%s)" % self._url) - if r.status_code == 201: - return r.json() - else: - _raise_error_from_response(r, GitlabCreateError) + _raise_error_from_response(r, GitlabCreateError, 201) + return r.json() def update(self, obj, **kwargs): params = obj.__dict__.copy() @@ -498,10 +492,8 @@ def update(self, obj, **kwargs): raise GitlabConnectionError( "Can't connect to GitLab server (%s)" % self._url) - if r.status_code == 200: - return r.json() - else: - _raise_error_from_response(r, GitlabUpdateError) + _raise_error_from_response(r, GitlabUpdateError) + return r.json() def Hook(self, id=None, **kwargs): """Creates/tests/lists system hook(s) known by the GitLab server. @@ -539,8 +531,7 @@ def UserProject(self, id=None, **kwargs): def _list_projects(self, url, **kwargs): r = self._raw_get(url, **kwargs) - if r.status_code != 200: - _raise_error_from_response(r, GitlabListError) + _raise_error_from_response(r, GitlabListError) l = [] for o in r.json(): @@ -923,8 +914,7 @@ def Member(self, id=None, **kwargs): def transfer_project(self, id, **kwargs): url = '/groups/%d/projects/%d' % (self.id, id) r = self.gitlab._raw_post(url, None, **kwargs) - if r.status_code != 201: - _raise_error_from_response(r, GitlabTransferProjectError) + _raise_error_from_response(r, GitlabTransferProjectError, 201) class Hook(GitlabObject): @@ -960,14 +950,12 @@ def protect(self, protect=True, **kwargs): action = 'protect' if protect else 'unprotect' url = "%s/%s/%s" % (url, self.name, action) r = self.gitlab._raw_put(url, data=None, content_type=None, **kwargs) + _raise_error_from_response(r, GitlabProtectError) - if r.status_code == 200: - if protect: - self.protected = protect - else: - del self.protected + if protect: + self.protected = protect else: - _raise_error_from_response(r, GitlabProtectError) + del self.protected def unprotect(self, **kwargs): self.protect(False, **kwargs) @@ -985,20 +973,19 @@ def diff(self, **kwargs): url = ('/projects/%(project_id)s/repository/commits/%(commit_id)s/diff' % {'project_id': self.project_id, 'commit_id': self.id}) r = self.gitlab._raw_get(url, **kwargs) - if r.status_code == 200: - return r.json() - else: - _raise_error_from_response(r, GitlabGetError) + _raise_error_from_response(r, GitlabGetError) + + return r.json() def blob(self, filepath, **kwargs): url = ('/projects/%(project_id)s/repository/blobs/%(commit_id)s' % {'project_id': self.project_id, 'commit_id': self.id}) url += '?filepath=%s' % filepath r = self.gitlab._raw_get(url, **kwargs) - if r.status_code == 200: - return r.content - else: - _raise_error_from_response(r, GitlabGetError) + + _raise_error_from_response(r, GitlabGetError) + + return r.content class ProjectKey(GitlabObject): @@ -1173,11 +1160,8 @@ def Content(self, **kwargs): url = ("/projects/%(project_id)s/snippets/%(snippet_id)s/raw" % {'project_id': self.project_id, 'snippet_id': self.id}) r = self.gitlab._raw_get(url, **kwargs) - - if r.status_code == 200: - return r.content - else: - _raise_error_from_response(r, GitlabGetError) + _raise_error_from_response(r, GitlabGetError) + return r.content def Note(self, id=None, **kwargs): return ProjectSnippetNote._get_list_or_object( @@ -1288,29 +1272,23 @@ def tree(self, path='', ref_name='', **kwargs): url = "%s/%s/repository/tree" % (self._url, self.id) url += '?path=%s&ref_name=%s' % (path, ref_name) r = self.gitlab._raw_get(url, **kwargs) - if r.status_code == 200: - return r.json() - else: - _raise_error_from_response(r, GitlabGetError) + _raise_error_from_response(r, GitlabGetError) + return r.json() def blob(self, sha, filepath, **kwargs): url = "%s/%s/repository/blobs/%s" % (self._url, self.id, sha) url += '?filepath=%s' % (filepath) r = self.gitlab._raw_get(url, **kwargs) - if r.status_code == 200: - return r.content - else: - _raise_error_from_response(r, GitlabGetError) + _raise_error_from_response(r, GitlabGetError) + return r.content def archive(self, sha=None, **kwargs): url = '/projects/%s/repository/archive' % self.id if sha: url += '?sha=%s' % sha r = self.gitlab._raw_get(url, **kwargs) - if r.status_code == 200: - return r.content - else: - _raise_error_from_response(r, GitlabGetError) + _raise_error_from_response(r, GitlabGetError) + return r.content def create_file(self, path, branch, content, message, **kwargs): """Creates file in project repository @@ -1330,24 +1308,21 @@ def create_file(self, path, branch, content, message, **kwargs): url += ("?file_path=%s&branch_name=%s&content=%s&commit_message=%s" % (path, branch, content, message)) r = self.gitlab._raw_post(url, data=None, content_type=None, **kwargs) - if r.status_code != 201: - _raise_error_from_response(r, GitlabCreateError) + _raise_error_from_response(r, GitlabCreateError, 201) def update_file(self, path, branch, content, message, **kwargs): url = "/projects/%s/repository/files" % self.id url += ("?file_path=%s&branch_name=%s&content=%s&commit_message=%s" % (path, branch, content, message)) r = self.gitlab._raw_put(url, data=None, content_type=None, **kwargs) - if r.status_code != 200: - _raise_error_from_response(r, GitlabUpdateError) + _raise_error_from_response(r, GitlabUpdateError) def delete_file(self, path, branch, message, **kwargs): url = "/projects/%s/repository/files" % self.id url += ("?file_path=%s&branch_name=%s&commit_message=%s" % (path, branch, message)) r = self.gitlab._raw_delete(url, **kwargs) - if r.status_code != 200: - _raise_error_from_response(r, GitlabDeleteError) + _raise_error_from_response(r, GitlabDeleteError) class TeamMember(GitlabObject): From c580b1e69868e038ef61080aa6c6b92f112b4891 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Tue, 29 Dec 2015 16:42:18 +0100 Subject: [PATCH 02/37] functional_tests.sh: support python 2 and 3 --- tools/functional_tests.sh | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/tools/functional_tests.sh b/tools/functional_tests.sh index 825d41ffe..6ea0b4425 100755 --- a/tools/functional_tests.sh +++ b/tools/functional_tests.sh @@ -23,6 +23,25 @@ cleanup() { } trap cleanup EXIT +PY_VER=2 +while getopts :p: opt "$@"; do + case $opt in + p) + PY_VER=$OPTARG;; + *) + echo "Unknown option: $opt" + exit 1;; + esac +done + +case $PY_VER in + 2) VENV_CMD=virtualenv;; + 3) VENV_CMD=pyvenv;; + *) + echo "Wrong python version (2 or 3)" + exit 1;; +esac + docker run --name gitlab-test --detach --publish 8080:80 --publish 2222:22 genezys/gitlab:latest >/dev/null 2>&1 LOGIN='root' @@ -31,7 +50,7 @@ CONFIG=/tmp/python-gitlab.cfg GITLAB="gitlab --config-file $CONFIG" VENV=$(pwd)/.venv -virtualenv $VENV +$VENV_CMD $VENV . $VENV/bin/activate pip install -rrequirements.txt pip install -e . @@ -55,7 +74,7 @@ $OK TOKEN=$(curl -s http://localhost:8080/api/v3/session \ -X POST \ --data "login=$LOGIN&password=$PASSWORD" \ - | python -c 'import sys, json; print json.load(sys.stdin)["private_token"]') + | python -c 'import sys, json; print(json.load(sys.stdin)["private_token"])') cat > $CONFIG << EOF [global] From 74dc2acc788fb6e2fdced0561d8959e2a9d0572f Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Wed, 30 Dec 2015 19:13:10 +0100 Subject: [PATCH 03/37] Add a get method for GitlabObject This change provides a way to implement GET for objects that don't support it, but support LIST. It is also a first step to a cleaner API. --- gitlab/__init__.py | 24 ++++++++++++++++++------ gitlab/cli.py | 4 ++-- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/gitlab/__init__.py b/gitlab/__init__.py index 39fea25b0..638c5eb92 100644 --- a/gitlab/__init__.py +++ b/gitlab/__init__.py @@ -695,12 +695,24 @@ def list(cls, gl, **kwargs): return gl.list(cls, **kwargs) + @classmethod + def get(cls, gl, id, **kwargs): + if cls.canGet is True: + return cls(gl, id, **kwargs) + elif cls.canGet == 'from_list': + for obj in cls.list(gl, **kwargs): + obj_id = getattr(obj, obj.idAttr) + if str(obj_id) == str(id): + return obj + + raise GitlabGetError("Object not found") + @classmethod def _get_list_or_object(cls, gl, id, **kwargs): if id is None and cls.getListWhenNoId: return cls.list(gl, **kwargs) else: - return cls(gl, id, **kwargs) + return cls.get(gl, id, **kwargs) def _get_object(self, k, v): if self._constructorTypes and k in self._constructorTypes: @@ -834,7 +846,7 @@ def json(self): class UserKey(GitlabObject): _url = '/users/%(user_id)s/keys' - canGet = False + canGet = 'from_list' canUpdate = False requiredUrlAttrs = ['user_id'] requiredCreateAttrs = ['title', 'key'] @@ -882,7 +894,7 @@ def Key(self, id=None, **kwargs): class GroupMember(GitlabObject): _url = '/groups/%(group_id)s/members' - canGet = False + canGet = 'from_list' requiredUrlAttrs = ['group_id'] requiredCreateAttrs = ['access_level', 'user_id'] requiredUpdateAttrs = ['access_level'] @@ -928,7 +940,7 @@ class Issue(GitlabObject): _url = '/issues' _constructorTypes = {'author': 'User', 'assignee': 'User', 'milestone': 'ProjectMilestone'} - canGet = False + canGet = 'from_list' canDelete = False canUpdate = False canCreate = False @@ -997,7 +1009,7 @@ class ProjectKey(GitlabObject): class ProjectEvent(GitlabObject): _url = '/projects/%(project_id)s/events' - canGet = False + canGet = 'from_list' canDelete = False canUpdate = False canCreate = False @@ -1073,7 +1085,7 @@ class ProjectNote(GitlabObject): class ProjectTag(GitlabObject): _url = '/projects/%(project_id)s/repository/tags' idAttr = 'name' - canGet = False + canGet = 'from_list' canDelete = False canUpdate = False requiredUrlAttrs = ['project_id'] diff --git a/gitlab/cli.py b/gitlab/cli.py index 1f824986e..648da2d80 100644 --- a/gitlab/cli.py +++ b/gitlab/cli.py @@ -170,7 +170,7 @@ def do_list(cls, gl, what, args): def do_get(cls, gl, what, args): - if not cls.canGet: + if cls.canGet is False: die("%s objects can't be retrieved" % what) id = None @@ -178,7 +178,7 @@ def do_get(cls, gl, what, args): id = get_id(cls, args) try: - o = cls(gl, id, **args) + o = cls.get(gl, id, **args) except Exception as e: die("Impossible to get object (%s)" % str(e)) From 7e61a28d74a8589bffcfb70e0f3622113f6442ae Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Wed, 30 Dec 2015 19:23:09 +0100 Subject: [PATCH 04/37] Add the CLI -g short option for --gitlab --- gitlab/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitlab/cli.py b/gitlab/cli.py index 648da2d80..c3289df9f 100644 --- a/gitlab/cli.py +++ b/gitlab/cli.py @@ -241,7 +241,7 @@ def main(): parser.add_argument("-c", "--config-file", action='append', help=("Configuration file to use. Can be used " "multiple times.")) - parser.add_argument("--gitlab", + parser.add_argument("-g", "--gitlab", help=("Which configuration section should " "be used. If not defined, the default selection " "will be used."), From a636d5ab25d2b248d89363ac86ecad7a0b90f100 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Thu, 31 Dec 2015 06:37:44 +0100 Subject: [PATCH 05/37] Provide a create method for GitlabObject's Instead of using the constructor to do everything (get, list and create), we now provide a class method for each action. This should make code easier to read. --- gitlab/__init__.py | 10 ++++++++++ gitlab/cli.py | 3 +-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/gitlab/__init__.py b/gitlab/__init__.py index 638c5eb92..ab4fc8786 100644 --- a/gitlab/__init__.py +++ b/gitlab/__init__.py @@ -761,6 +761,16 @@ def delete(self, **kwargs): return self.gitlab.delete(self, **kwargs) + @classmethod + def create(cls, gl, data, **kwargs): + if not cls.canCreate: + raise NotImplementedError + + obj = cls(gl, data, **kwargs) + obj.save() + + return obj + def __init__(self, gl, data=None, **kwargs): self._created = False self.gitlab = gl diff --git a/gitlab/cli.py b/gitlab/cli.py index c3289df9f..2874a5f4e 100644 --- a/gitlab/cli.py +++ b/gitlab/cli.py @@ -149,8 +149,7 @@ def do_create(cls, gl, what, args): die("%s objects can't be created" % what) try: - o = cls(gl, args) - o.save() + o = cls.create(gl, args) except Exception as e: die("Impossible to create object (%s)" % str(e)) From 2a76b7490ba3dc6de6080d2dab55be017c09db59 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Thu, 31 Dec 2015 07:02:12 +0100 Subject: [PATCH 06/37] Rename the _created attribute _from_api --- gitlab/__init__.py | 17 ++++++++--------- gitlab/tests/test_gitlab.py | 4 ++-- gitlab/tests/test_gitlabobject.py | 6 +++--- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/gitlab/__init__.py b/gitlab/__init__.py index ab4fc8786..7b239032a 100644 --- a/gitlab/__init__.py +++ b/gitlab/__init__.py @@ -351,9 +351,9 @@ def list(self, obj_class, **kwargs): cls_kwargs = kwargs.copy() - # Add _created manually, because we are not creating objects + # Add _from_api manually, because we are not creating objects # through normal path - cls_kwargs['_created'] = True + cls_kwargs['_from_api'] = True get_all_results = params.get('all', False) @@ -536,7 +536,7 @@ def _list_projects(self, url, **kwargs): l = [] for o in r.json(): p = Project(self, o) - p._created = True + p._from_api = True l.append(p) return l @@ -737,7 +737,7 @@ def _create(self, **kwargs): json = self.gitlab.create(self, **kwargs) self._set_from_dict(json) - self._created = True + self._from_api = True def _update(self, **kwargs): if not self.canUpdate: @@ -747,7 +747,7 @@ def _update(self, **kwargs): self._set_from_dict(json) def save(self, **kwargs): - if self._created: + if self._from_api: self._update(**kwargs) else: self._create(**kwargs) @@ -756,7 +756,7 @@ def delete(self, **kwargs): if not self.canDelete: raise NotImplementedError - if not self._created: + if not self._from_api: raise GitlabDeleteError("Object not yet created") return self.gitlab.delete(self, **kwargs) @@ -772,7 +772,7 @@ def create(cls, gl, data, **kwargs): return obj def __init__(self, gl, data=None, **kwargs): - self._created = False + self._from_api = False self.gitlab = gl if (data is None or isinstance(data, six.integer_types) or @@ -780,8 +780,7 @@ def __init__(self, gl, data=None, **kwargs): if not self.canGet: raise NotImplementedError data = self.gitlab.get(self.__class__, data, **kwargs) - # Object is created because we got it from api - self._created = True + self._from_api = True self._set_from_dict(data) diff --git a/gitlab/tests/test_gitlab.py b/gitlab/tests/test_gitlab.py index 60cb94e34..a0f35bbeb 100644 --- a/gitlab/tests/test_gitlab.py +++ b/gitlab/tests/test_gitlab.py @@ -337,7 +337,7 @@ def resp_delete_group(url, request): def test_delete_unknown_path(self): obj = Project(self.gl, data={"name": "testname", "id": 1}) - obj._created = True + obj._from_api = True @urlmatch(scheme="http", netloc="localhost", path="/api/v3/projects/1", method="delete") @@ -398,7 +398,7 @@ def test_create_unknown_path(self): obj = User(self.gl, data={"email": "email", "password": "password", "username": "username", "name": "name", "can_create_group": True}) - obj._created = True + obj._from_api = True @urlmatch(scheme="http", netloc="localhost", path="/api/v3/projects/1", method="delete") diff --git a/gitlab/tests/test_gitlabobject.py b/gitlab/tests/test_gitlabobject.py index 01e954b6c..812e6c6fc 100644 --- a/gitlab/tests/test_gitlabobject.py +++ b/gitlab/tests/test_gitlabobject.py @@ -182,7 +182,7 @@ def test_get_list_or_object_with_get(self): def test_get_list_or_object_cant_get(self): with HTTMock(resp_get_issue): - gl_object = Issue(self.gl, data={"name": "name"}) + gl_object = UserProject(self.gl, data={"name": "name"}) self.assertRaises(NotImplementedError, gl_object._get_list_or_object, self.gl, id=1) @@ -245,7 +245,7 @@ def test_save_with_id(self): "password": "password", "id": 1, "username": "username"}) self.assertEqual(obj.name, "testname") - obj._created = True + obj._from_api = True obj.name = "newname" with HTTMock(resp_update_user): obj.save() @@ -259,7 +259,7 @@ def test_save_without_id(self): def test_delete(self): obj = Group(self.gl, data={"name": "testname", "id": 1}) - obj._created = True + obj._from_api = True with HTTMock(resp_delete_group): data = obj.delete() self.assertIs(data, True) From 1db3cc1e4f7e8f3bfae1f2e8cdbd377701789eb4 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Thu, 31 Dec 2015 07:07:45 +0100 Subject: [PATCH 07/37] fix the tests --- gitlab/__init__.py | 4 +++- gitlab/tests/test_gitlab.py | 10 +++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/gitlab/__init__.py b/gitlab/__init__.py index 7b239032a..ebd7648fa 100644 --- a/gitlab/__init__.py +++ b/gitlab/__init__.py @@ -697,7 +697,9 @@ def list(cls, gl, **kwargs): @classmethod def get(cls, gl, id, **kwargs): - if cls.canGet is True: + if cls.canGet is False: + raise NotImplementedError + elif cls.canGet is True: return cls(gl, id, **kwargs) elif cls.canGet == 'from_list': for obj in cls.list(gl, **kwargs): diff --git a/gitlab/tests/test_gitlab.py b/gitlab/tests/test_gitlab.py index a0f35bbeb..83319646f 100644 --- a/gitlab/tests/test_gitlab.py +++ b/gitlab/tests/test_gitlab.py @@ -677,15 +677,19 @@ def resp_get_group(url, request): self.assertEqual(data.id, 1) def test_Issue(self): - @urlmatch(scheme="http", netloc="localhost", path="/api/v3/issues/1", + @urlmatch(scheme="http", netloc="localhost", path="/api/v3/issues", method="get") def resp_get_issue(url, request): headers = {'content-type': 'application/json'} - content = '{"name": "name", "id": 1}'.encode("utf-8") + content = ('[{"name": "name", "id": 1}, ' + '{"name": "other_name", "id": 2}]') + content = content.encode("utf-8") return response(200, content, headers, None, 5, request) with HTTMock(resp_get_issue): - self.assertRaises(NotImplementedError, self.gl.Issue, id=1) + data = self.gl.Issue(id=2) + self.assertEqual(data.id, 2) + self.assertEqual(data.name, 'other_name') def test_User(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v3/users/1", From 7c38ef6f2f089c1fbf9fa0ade249bb460c96ee9d Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Thu, 31 Dec 2015 07:31:50 +0100 Subject: [PATCH 08/37] python3: fix CLI error when arguments are missing --- gitlab/cli.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/gitlab/cli.py b/gitlab/cli.py index 2874a5f4e..1b8356c15 100644 --- a/gitlab/cli.py +++ b/gitlab/cli.py @@ -246,7 +246,9 @@ def main(): "will be used."), required=False) - subparsers = parser.add_subparsers(dest='what') + subparsers = parser.add_subparsers(title='object', dest='what', + help="Object to manipulate.") + subparsers.required = True # populate argparse for all Gitlab Object classes = [] @@ -262,8 +264,10 @@ def main(): arg_name = clsToWhat(cls) object_group = subparsers.add_parser(arg_name) - object_subparsers = object_group.add_subparsers(dest='action') + object_subparsers = object_group.add_subparsers( + dest='action', help="Action to execute.") populate_sub_parser_by_class(cls, object_subparsers) + object_subparsers.required = True arg = parser.parse_args() args = arg.__dict__ From 118b2985249b3b152064af57e03231f1e1c59622 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Thu, 31 Dec 2015 08:31:00 +0100 Subject: [PATCH 09/37] remove deprecated methods --- gitlab/__init__.py | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/gitlab/__init__.py b/gitlab/__init__.py index ebd7648fa..9e9fac700 100644 --- a/gitlab/__init__.py +++ b/gitlab/__init__.py @@ -221,12 +221,6 @@ def _create_headers(self, content_type=None, headers={}): request_headers['Content-type'] = content_type return request_headers - def setToken(self, token): - """(DEPRECATED) Sets the private token for authentication.""" - warnings.warn("setToken is deprecated, use set_token instead", - DeprecationWarning) - self.set_token(token) - def set_token(self, token): """Sets the private token for authentication.""" self.private_token = token if token else None @@ -235,22 +229,11 @@ def set_token(self, token): elif "PRIVATE-TOKEN" in self.headers: del self.headers["PRIVATE-TOKEN"] - def setCredentials(self, email, password): - """(DEPRECATED) Sets the login and password for authentication.""" - warnings.warn("setCredential is deprecated, use set_credentials " - "instead", - DeprecationWarning) - self.set_credentials(email, password) - def set_credentials(self, email, password): """Sets the email/login and password for authentication.""" self.email = email self.password = password - def rawGet(self, path, content_type=None, **kwargs): - warnings.warn("rawGet is deprecated", DeprecationWarning) - return self._raw_get(path, content_type, **kwargs) - def _raw_get(self, path, content_type=None, **kwargs): url = '%s%s' % (self._url, path) headers = self._create_headers(content_type) @@ -265,10 +248,6 @@ def _raw_get(self, path, content_type=None, **kwargs): raise GitlabConnectionError( "Can't connect to GitLab server (%s)" % self._url) - def rawPost(self, path, data=None, content_type=None, **kwargs): - warnings.warn("rawPost is deprecated", DeprecationWarning) - return self._raw_post(path, data, content_type, **kwargs) - def _raw_post(self, path, data=None, content_type=None, **kwargs): url = '%s%s' % (self._url, path) headers = self._create_headers(content_type) @@ -281,10 +260,6 @@ def _raw_post(self, path, data=None, content_type=None, **kwargs): raise GitlabConnectionError( "Can't connect to GitLab server (%s)" % self._url) - def rawPut(self, path, data=None, content_type=None, **kwargs): - warnings.warn("rawPut is deprecated", DeprecationWarning) - return self._raw_put(path, data, content_type, **kwargs) - def _raw_put(self, path, data=None, content_type=None, **kwargs): url = '%s%s' % (self._url, path) headers = self._create_headers(content_type) @@ -298,10 +273,6 @@ def _raw_put(self, path, data=None, content_type=None, **kwargs): raise GitlabConnectionError( "Can't connect to GitLab server (%s)" % self._url) - def rawDelete(self, path, content_type=None, **kwargs): - warnings.warn("rawDelete is deprecated", DeprecationWarning) - return self._raw_delete(path, content_type, **kwargs) - def _raw_delete(self, path, content_type=None, **kwargs): url = '%s%s' % (self._url, path) headers = self._create_headers(content_type) From 8fa44554736c4155a1c3b013d29c0625277a2e07 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Thu, 31 Dec 2015 11:09:36 +0100 Subject: [PATCH 10/37] Split code in multiple files --- gitlab/__init__.py | 889 +------------------------------------------ gitlab/exceptions.py | 84 ++++ gitlab/objects.py | 801 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 900 insertions(+), 874 deletions(-) create mode 100644 gitlab/exceptions.py create mode 100644 gitlab/objects.py diff --git a/gitlab/__init__.py b/gitlab/__init__.py index 9e9fac700..920c97a1d 100644 --- a/gitlab/__init__.py +++ b/gitlab/__init__.py @@ -20,14 +20,14 @@ from __future__ import absolute_import import itertools import json -import sys import warnings import requests import six import gitlab.config - +from gitlab.exceptions import * # noqa +from gitlab.objects import * # noqa __title__ = 'python-gitlab' __version__ = '0.10' @@ -39,99 +39,14 @@ warnings.simplefilter('always', DeprecationWarning) -class jsonEncoder(json.JSONEncoder): - def default(self, obj): - if isinstance(obj, GitlabObject): - return obj.__dict__ - elif isinstance(obj, Gitlab): - return {'url': obj._url} - return json.JSONEncoder.default(self, obj) - - -class GitlabError(Exception): - def __init__(self, error_message="", response_code=None, - response_body=None): - - Exception.__init__(self, error_message) - # Http status code - self.response_code = response_code - # Full http response - self.response_body = response_body - # Parsed error message from gitlab - self.error_message = error_message - - def __str__(self): - if self.response_code is not None: - return "{0}: {1}".format(self.response_code, self.error_message) - else: - return "{0}".format(self.error_message) - - -class GitlabAuthenticationError(GitlabError): - pass - - -class GitlabConnectionError(GitlabError): - pass - - -class GitlabOperationError(GitlabError): - pass - - -class GitlabListError(GitlabOperationError): - pass - - -class GitlabGetError(GitlabOperationError): - pass - - -class GitlabCreateError(GitlabOperationError): - pass - - -class GitlabUpdateError(GitlabOperationError): - pass - - -class GitlabDeleteError(GitlabOperationError): - pass - - -class GitlabProtectError(GitlabOperationError): - pass - - -class GitlabTransferProjectError(GitlabOperationError): - pass - - -def _raise_error_from_response(response, error, expected_code=200): - """Tries to parse gitlab error message from response and raises error. - - Do nothing if the response status is the expected one. - - If response status code is 401, raises instead GitlabAuthenticationError. - - response: requests response object - error: Error-class to raise. Should be inherited from GitLabError - """ - - if expected_code == response.status_code: - return - - try: - message = response.json()['message'] - except (KeyError, ValueError): - message = response.content +def _sanitize(value): + if isinstance(value, six.string_types): + return value.replace('/', '%2F') + return value - if response.status_code == 401: - error = GitlabAuthenticationError - raise error(error_message=message, - response_code=response.status_code, - response_body=response.content) +def _sanitize_dict(src): + return dict((k, _sanitize(v)) for k, v in src.items()) class Gitlab(object): @@ -188,7 +103,7 @@ def credentials_auth(self): data = json.dumps({'email': self.email, 'password': self.password}) r = self._raw_post('/session', data, content_type='application/json') - _raise_error_from_response(r, GitlabAuthenticationError, 201) + raise_error_from_response(r, GitlabAuthenticationError, 201) self.user = CurrentUser(self, r.json()) self.set_token(self.user.private_token) @@ -314,7 +229,7 @@ def list(self, obj_class, **kwargs): raise GitlabConnectionError( "Can't connect to GitLab server (%s)" % self._url) - _raise_error_from_response(r, GitlabListError) + raise_error_from_response(r, GitlabListError) cls = obj_class if obj_class._returnClass: @@ -370,7 +285,7 @@ def get(self, obj_class, id=None, **kwargs): raise GitlabConnectionError( "Can't connect to GitLab server (%s)" % self._url) - _raise_error_from_response(r, GitlabGetError) + raise_error_from_response(r, GitlabGetError) return r.json() def delete(self, obj, **kwargs): @@ -404,7 +319,7 @@ def delete(self, obj, **kwargs): raise GitlabConnectionError( "Can't connect to GitLab server (%s)" % self._url) - _raise_error_from_response(r, GitlabDeleteError) + raise_error_from_response(r, GitlabDeleteError) return True def create(self, obj, **kwargs): @@ -434,7 +349,7 @@ def create(self, obj, **kwargs): raise GitlabConnectionError( "Can't connect to GitLab server (%s)" % self._url) - _raise_error_from_response(r, GitlabCreateError, 201) + raise_error_from_response(r, GitlabCreateError, 201) return r.json() def update(self, obj, **kwargs): @@ -463,7 +378,7 @@ def update(self, obj, **kwargs): raise GitlabConnectionError( "Can't connect to GitLab server (%s)" % self._url) - _raise_error_from_response(r, GitlabUpdateError) + raise_error_from_response(r, GitlabUpdateError) return r.json() def Hook(self, id=None, **kwargs): @@ -502,7 +417,7 @@ def UserProject(self, id=None, **kwargs): def _list_projects(self, url, **kwargs): r = self._raw_get(url, **kwargs) - _raise_error_from_response(r, GitlabListError) + raise_error_from_response(r, GitlabListError) l = [] for o in r.json(): @@ -577,777 +492,3 @@ def Team(self, id=None, **kwargs): to write it on the server. """ return Team._get_list_or_object(self, id, **kwargs) - - -def _get_display_encoding(): - return sys.stdout.encoding or sys.getdefaultencoding() - - -def _sanitize(value): - if isinstance(value, six.string_types): - return value.replace('/', '%2F') - return value - - -def _sanitize_dict(src): - return dict((k, _sanitize(v)) for k, v in src.items()) - - -class GitlabObject(object): - """Base class for all classes that interface with GitLab - - Args: - gl (gitlab.Gitlab): GitLab server connection - data: If data is integer or string type, get object from GitLab - data: If data is dictionary, create new object locally. To save object - in GitLab, call save-method - kwargs: Arbitrary keyword arguments - """ - #: Url to use in GitLab for this object - _url = None - # Some objects (e.g. merge requests) have different urls for singular and - # plural - _urlPlural = None - _returnClass = None - _constructorTypes = None - #: Whether _get_list_or_object should return list or object when id is None - getListWhenNoId = True - - #: Tells if GitLab-api allows retrieving single objects - canGet = True - #: Tells if GitLab-api allows listing of objects - canList = True - #: Tells if GitLab-api allows creation of new objects - canCreate = True - #: Tells if GitLab-api allows updating object - canUpdate = True - #: Tells if GitLab-api allows deleting object - canDelete = True - #: Attributes that are required for constructing url - requiredUrlAttrs = [] - #: Attributes that are required when retrieving list of objects - requiredListAttrs = [] - #: Attributes that are required when retrieving single object - requiredGetAttrs = [] - #: Attributes that are required when deleting object - requiredDeleteAttrs = [] - #: Attributes that are required when creating a new object - requiredCreateAttrs = [] - #: Attributes that are optional when creating a new object - optionalCreateAttrs = [] - #: Attributes that are required when updating an object - requiredUpdateAttrs = None - #: Attributes that are optional when updating an object - optionalUpdateAttrs = None - #: Whether the object ID is required in the GET url - getRequiresId = True - - idAttr = 'id' - shortPrintAttr = None - - def _data_for_gitlab(self, extra_parameters={}): - data = {} - for attribute in itertools.chain(self.requiredCreateAttrs, - self.optionalCreateAttrs): - if hasattr(self, attribute): - data[attribute] = getattr(self, attribute) - - data.update(extra_parameters) - - return json.dumps(data) - - @classmethod - def list(cls, gl, **kwargs): - if not cls.canList: - raise NotImplementedError - - if not cls._url: - raise NotImplementedError - - return gl.list(cls, **kwargs) - - @classmethod - def get(cls, gl, id, **kwargs): - if cls.canGet is False: - raise NotImplementedError - elif cls.canGet is True: - return cls(gl, id, **kwargs) - elif cls.canGet == 'from_list': - for obj in cls.list(gl, **kwargs): - obj_id = getattr(obj, obj.idAttr) - if str(obj_id) == str(id): - return obj - - raise GitlabGetError("Object not found") - - @classmethod - def _get_list_or_object(cls, gl, id, **kwargs): - if id is None and cls.getListWhenNoId: - return cls.list(gl, **kwargs) - else: - return cls.get(gl, id, **kwargs) - - def _get_object(self, k, v): - if self._constructorTypes and k in self._constructorTypes: - return globals()[self._constructorTypes[k]](self.gitlab, v) - else: - return v - - def _set_from_dict(self, data): - for k, v in data.items(): - if isinstance(v, list): - self.__dict__[k] = [] - for i in v: - self.__dict__[k].append(self._get_object(k, i)) - elif v is None: - self.__dict__[k] = None - else: - self.__dict__[k] = self._get_object(k, v) - - def _create(self, **kwargs): - if not self.canCreate: - raise NotImplementedError - - json = self.gitlab.create(self, **kwargs) - self._set_from_dict(json) - self._from_api = True - - def _update(self, **kwargs): - if not self.canUpdate: - raise NotImplementedError - - json = self.gitlab.update(self, **kwargs) - self._set_from_dict(json) - - def save(self, **kwargs): - if self._from_api: - self._update(**kwargs) - else: - self._create(**kwargs) - - def delete(self, **kwargs): - if not self.canDelete: - raise NotImplementedError - - if not self._from_api: - raise GitlabDeleteError("Object not yet created") - - return self.gitlab.delete(self, **kwargs) - - @classmethod - def create(cls, gl, data, **kwargs): - if not cls.canCreate: - raise NotImplementedError - - obj = cls(gl, data, **kwargs) - obj.save() - - return obj - - def __init__(self, gl, data=None, **kwargs): - self._from_api = False - self.gitlab = gl - - if (data is None or isinstance(data, six.integer_types) or - isinstance(data, six.string_types)): - if not self.canGet: - raise NotImplementedError - data = self.gitlab.get(self.__class__, data, **kwargs) - self._from_api = True - - self._set_from_dict(data) - - if kwargs: - for k, v in kwargs.items(): - self.__dict__[k] = v - - # Special handling for api-objects that don't have id-number in api - # responses. Currently only Labels and Files - if not hasattr(self, "id"): - self.id = None - - def __str__(self): - return '%s => %s' % (type(self), str(self.__dict__)) - - def display(self, pretty): - if pretty: - self.pretty_print() - else: - self.short_print() - - def short_print(self, depth=0): - id = self.__dict__[self.idAttr] - print("%s%s: %s" % (" " * depth * 2, self.idAttr, id)) - if self.shortPrintAttr: - print("%s%s: %s" % (" " * depth * 2, - self.shortPrintAttr.replace('_', '-'), - self.__dict__[self.shortPrintAttr])) - - @staticmethod - def _obj_to_str(obj): - if isinstance(obj, dict): - s = ", ".join(["%s: %s" % - (x, GitlabObject._obj_to_str(y)) - for (x, y) in obj.items()]) - return "{ %s }" % s - elif isinstance(obj, list): - s = ", ".join([GitlabObject._obj_to_str(x) for x in obj]) - return "[ %s ]" % s - elif six.PY2 and isinstance(obj, six.text_type): - return obj.encode(_get_display_encoding(), "replace") - else: - return str(obj) - - def pretty_print(self, depth=0): - id = self.__dict__[self.idAttr] - print("%s%s: %s" % (" " * depth * 2, self.idAttr, id)) - for k in sorted(self.__dict__.keys()): - if k == self.idAttr or k == 'id': - continue - if k[0] == '_': - continue - v = self.__dict__[k] - pretty_k = k.replace('_', '-') - if six.PY2: - pretty_k = pretty_k.encode(_get_display_encoding(), "replace") - if isinstance(v, GitlabObject): - if depth == 0: - print("%s:" % pretty_k) - v.pretty_print(1) - else: - print("%s: %s" % (pretty_k, v.id)) - else: - if isinstance(v, Gitlab): - continue - v = GitlabObject._obj_to_str(v) - print("%s%s: %s" % (" " * depth * 2, pretty_k, v)) - - def json(self): - return json.dumps(self.__dict__, cls=jsonEncoder) - - -class UserKey(GitlabObject): - _url = '/users/%(user_id)s/keys' - canGet = 'from_list' - canUpdate = False - requiredUrlAttrs = ['user_id'] - requiredCreateAttrs = ['title', 'key'] - - -class User(GitlabObject): - _url = '/users' - shortPrintAttr = 'username' - # FIXME: password is required for create but not for update - requiredCreateAttrs = ['email', 'username', 'name'] - optionalCreateAttrs = ['password', 'skype', 'linkedin', 'twitter', - 'projects_limit', 'extern_uid', 'provider', - 'bio', 'admin', 'can_create_group', 'website_url', - 'confirm'] - - def _data_for_gitlab(self, extra_parameters={}): - if hasattr(self, 'confirm'): - self.confirm = str(self.confirm).lower() - return super(User, self)._data_for_gitlab(extra_parameters) - - def Key(self, id=None, **kwargs): - return UserKey._get_list_or_object(self.gitlab, id, - user_id=self.id, - **kwargs) - - -class CurrentUserKey(GitlabObject): - _url = '/user/keys' - canUpdate = False - shortPrintAttr = 'title' - requiredCreateAttrs = ['title', 'key'] - - -class CurrentUser(GitlabObject): - _url = '/user' - canList = False - canCreate = False - canUpdate = False - canDelete = False - shortPrintAttr = 'username' - - def Key(self, id=None, **kwargs): - return CurrentUserKey._get_list_or_object(self.gitlab, id, **kwargs) - - -class GroupMember(GitlabObject): - _url = '/groups/%(group_id)s/members' - canGet = 'from_list' - requiredUrlAttrs = ['group_id'] - requiredCreateAttrs = ['access_level', 'user_id'] - requiredUpdateAttrs = ['access_level'] - shortPrintAttr = 'username' - - def _update(self, **kwargs): - self.user_id = self.id - super(GroupMember, self)._update(**kwargs) - - -class Group(GitlabObject): - _url = '/groups' - canUpdate = False - _constructorTypes = {'projects': 'Project'} - requiredCreateAttrs = ['name', 'path'] - shortPrintAttr = 'name' - - GUEST_ACCESS = 10 - REPORTER_ACCESS = 20 - DEVELOPER_ACCESS = 30 - MASTER_ACCESS = 40 - OWNER_ACCESS = 50 - - def Member(self, id=None, **kwargs): - return GroupMember._get_list_or_object(self.gitlab, id, - group_id=self.id, - **kwargs) - - def transfer_project(self, id, **kwargs): - url = '/groups/%d/projects/%d' % (self.id, id) - r = self.gitlab._raw_post(url, None, **kwargs) - _raise_error_from_response(r, GitlabTransferProjectError, 201) - - -class Hook(GitlabObject): - _url = '/hooks' - canUpdate = False - requiredCreateAttrs = ['url'] - shortPrintAttr = 'url' - - -class Issue(GitlabObject): - _url = '/issues' - _constructorTypes = {'author': 'User', 'assignee': 'User', - 'milestone': 'ProjectMilestone'} - canGet = 'from_list' - canDelete = False - canUpdate = False - canCreate = False - shortPrintAttr = 'title' - - -class ProjectBranch(GitlabObject): - _url = '/projects/%(project_id)s/repository/branches' - _constructorTypes = {'author': 'User', "committer": "User"} - - idAttr = 'name' - canUpdate = False - requiredUrlAttrs = ['project_id'] - requiredCreateAttrs = ['branch_name', 'ref'] - _constructorTypes = {'commit': 'ProjectCommit'} - - def protect(self, protect=True, **kwargs): - url = self._url % {'project_id': self.project_id} - action = 'protect' if protect else 'unprotect' - url = "%s/%s/%s" % (url, self.name, action) - r = self.gitlab._raw_put(url, data=None, content_type=None, **kwargs) - _raise_error_from_response(r, GitlabProtectError) - - if protect: - self.protected = protect - else: - del self.protected - - def unprotect(self, **kwargs): - self.protect(False, **kwargs) - - -class ProjectCommit(GitlabObject): - _url = '/projects/%(project_id)s/repository/commits' - canDelete = False - canUpdate = False - canCreate = False - requiredUrlAttrs = ['project_id'] - shortPrintAttr = 'title' - - def diff(self, **kwargs): - url = ('/projects/%(project_id)s/repository/commits/%(commit_id)s/diff' - % {'project_id': self.project_id, 'commit_id': self.id}) - r = self.gitlab._raw_get(url, **kwargs) - _raise_error_from_response(r, GitlabGetError) - - return r.json() - - def blob(self, filepath, **kwargs): - url = ('/projects/%(project_id)s/repository/blobs/%(commit_id)s' % - {'project_id': self.project_id, 'commit_id': self.id}) - url += '?filepath=%s' % filepath - r = self.gitlab._raw_get(url, **kwargs) - - _raise_error_from_response(r, GitlabGetError) - - return r.content - - -class ProjectKey(GitlabObject): - _url = '/projects/%(project_id)s/keys' - canUpdate = False - requiredUrlAttrs = ['project_id'] - requiredCreateAttrs = ['title', 'key'] - - -class ProjectEvent(GitlabObject): - _url = '/projects/%(project_id)s/events' - canGet = 'from_list' - canDelete = False - canUpdate = False - canCreate = False - requiredUrlAttrs = ['project_id'] - shortPrintAttr = 'target_title' - - -class ProjectHook(GitlabObject): - _url = '/projects/%(project_id)s/hooks' - requiredUrlAttrs = ['project_id'] - requiredCreateAttrs = ['url'] - optionalCreateAttrs = ['push_events', 'issues_events', - 'merge_requests_events', 'tag_push_events'] - shortPrintAttr = 'url' - - -class ProjectIssueNote(GitlabObject): - _url = '/projects/%(project_id)s/issues/%(issue_id)s/notes' - _constructorTypes = {'author': 'User'} - canUpdate = False - canDelete = False - requiredUrlAttrs = ['project_id', 'issue_id'] - requiredCreateAttrs = ['body'] - - -class ProjectIssue(GitlabObject): - _url = '/projects/%(project_id)s/issues/' - _constructorTypes = {'author': 'User', 'assignee': 'User', - 'milestone': 'ProjectMilestone'} - canDelete = False - requiredUrlAttrs = ['project_id'] - requiredCreateAttrs = ['title'] - # FIXME: state_event is only valid with update - optionalCreateAttrs = ['description', 'assignee_id', 'milestone_id', - 'labels', 'state_event'] - - shortPrintAttr = 'title' - - def _data_for_gitlab(self, extra_parameters={}): - # Gitlab-api returns labels in a json list and takes them in a - # comma separated list. - if hasattr(self, "labels"): - if (self.labels is not None and - not isinstance(self.labels, six.string_types)): - labels = ", ".join(self.labels) - extra_parameters['labels'] = labels - - return super(ProjectIssue, self)._data_for_gitlab(extra_parameters) - - def Note(self, id=None, **kwargs): - return ProjectIssueNote._get_list_or_object(self.gitlab, id, - project_id=self.project_id, - issue_id=self.id, - **kwargs) - - -class ProjectMember(GitlabObject): - _url = '/projects/%(project_id)s/members' - requiredUrlAttrs = ['project_id'] - requiredCreateAttrs = ['access_level', 'user_id'] - shortPrintAttr = 'username' - - -class ProjectNote(GitlabObject): - _url = '/projects/%(project_id)s/notes' - _constructorTypes = {'author': 'User'} - canUpdate = False - canDelete = False - requiredUrlAttrs = ['project_id'] - requiredCreateAttrs = ['body'] - - -class ProjectTag(GitlabObject): - _url = '/projects/%(project_id)s/repository/tags' - idAttr = 'name' - canGet = 'from_list' - canDelete = False - canUpdate = False - requiredUrlAttrs = ['project_id'] - requiredCreateAttrs = ['tag_name', 'ref'] - optionalCreateAttrs = ['message'] - shortPrintAttr = 'name' - - -class ProjectMergeRequestNote(GitlabObject): - _url = '/projects/%(project_id)s/merge_requests/%(merge_request_id)s/notes' - _constructorTypes = {'author': 'User'} - canDelete = False - requiredUrlAttrs = ['project_id', 'merge_request_id'] - requiredCreateAttrs = ['body'] - - -class ProjectMergeRequest(GitlabObject): - _url = '/projects/%(project_id)s/merge_request' - _urlPlural = '/projects/%(project_id)s/merge_requests' - _constructorTypes = {'author': 'User', 'assignee': 'User'} - canDelete = False - requiredUrlAttrs = ['project_id'] - requiredCreateAttrs = ['source_branch', 'target_branch', 'title'] - optionalCreateAttrs = ['assignee_id'] - - def Note(self, id=None, **kwargs): - return ProjectMergeRequestNote._get_list_or_object( - self.gitlab, id, project_id=self.project_id, - merge_request_id=self.id, **kwargs) - - -class ProjectMilestone(GitlabObject): - _url = '/projects/%(project_id)s/milestones' - canDelete = False - requiredUrlAttrs = ['project_id'] - requiredCreateAttrs = ['title'] - optionalCreateAttrs = ['description', 'due_date', 'state_event'] - shortPrintAttr = 'title' - - -class ProjectLabel(GitlabObject): - _url = '/projects/%(project_id)s/labels' - requiredUrlAttrs = ['project_id'] - idAttr = 'name' - requiredDeleteAttrs = ['name'] - requiredCreateAttrs = ['name', 'color'] - requiredUpdateAttrs = [] - # FIXME: new_name is only valid with update - optionalCreateAttrs = ['new_name'] - - -class ProjectFile(GitlabObject): - _url = '/projects/%(project_id)s/repository/files' - canList = False - requiredUrlAttrs = ['project_id'] - requiredGetAttrs = ['file_path', 'ref'] - requiredCreateAttrs = ['file_path', 'branch_name', 'content', - 'commit_message'] - optionalCreateAttrs = ['encoding'] - requiredDeleteAttrs = ['branch_name', 'commit_message'] - getListWhenNoId = False - shortPrintAttr = 'file_path' - getRequiresId = False - - -class ProjectSnippetNote(GitlabObject): - _url = '/projects/%(project_id)s/snippets/%(snippet_id)s/notes' - _constructorTypes = {'author': 'User'} - canUpdate = False - canDelete = False - requiredUrlAttrs = ['project_id', 'snippet_id'] - requiredCreateAttrs = ['body'] - - -class ProjectSnippet(GitlabObject): - _url = '/projects/%(project_id)s/snippets' - _constructorTypes = {'author': 'User'} - requiredUrlAttrs = ['project_id'] - requiredCreateAttrs = ['title', 'file_name', 'code'] - optionalCreateAttrs = ['lifetime'] - shortPrintAttr = 'title' - - def Content(self, **kwargs): - url = ("/projects/%(project_id)s/snippets/%(snippet_id)s/raw" % - {'project_id': self.project_id, 'snippet_id': self.id}) - r = self.gitlab._raw_get(url, **kwargs) - _raise_error_from_response(r, GitlabGetError) - return r.content - - def Note(self, id=None, **kwargs): - return ProjectSnippetNote._get_list_or_object( - self.gitlab, id, - project_id=self.project_id, - snippet_id=self.id, - **kwargs) - - -class UserProject(GitlabObject): - _url = '/projects/user/%(user_id)s' - _constructorTypes = {'owner': 'User', 'namespace': 'Group'} - canUpdate = False - canDelete = False - canList = False - canGet = False - requiredUrlAttrs = ['user_id'] - requiredCreateAttrs = ['name'] - optionalCreateAttrs = ['default_branch', 'issues_enabled', 'wall_enabled', - 'merge_requests_enabled', 'wiki_enabled', - 'snippets_enabled', 'public', 'visibility_level', - 'description'] - - -class Project(GitlabObject): - _url = '/projects' - _constructorTypes = {'owner': 'User', 'namespace': 'Group'} - requiredCreateAttrs = ['name'] - requiredUpdateAttrs = [] - optionalCreateAttrs = ['default_branch', 'issues_enabled', 'wall_enabled', - 'merge_requests_enabled', 'wiki_enabled', - 'snippets_enabled', 'public', 'visibility_level', - 'namespace_id', 'description', 'path', 'import_url'] - - shortPrintAttr = 'path' - - def Branch(self, id=None, **kwargs): - return ProjectBranch._get_list_or_object(self.gitlab, id, - project_id=self.id, - **kwargs) - - def Commit(self, id=None, **kwargs): - return ProjectCommit._get_list_or_object(self.gitlab, id, - project_id=self.id, - **kwargs) - - def Event(self, id=None, **kwargs): - return ProjectEvent._get_list_or_object(self.gitlab, id, - project_id=self.id, - **kwargs) - - def Hook(self, id=None, **kwargs): - return ProjectHook._get_list_or_object(self.gitlab, id, - project_id=self.id, - **kwargs) - - def Key(self, id=None, **kwargs): - return ProjectKey._get_list_or_object(self.gitlab, id, - project_id=self.id, - **kwargs) - - def Issue(self, id=None, **kwargs): - return ProjectIssue._get_list_or_object(self.gitlab, id, - project_id=self.id, - **kwargs) - - def Member(self, id=None, **kwargs): - return ProjectMember._get_list_or_object(self.gitlab, id, - project_id=self.id, - **kwargs) - - def MergeRequest(self, id=None, **kwargs): - return ProjectMergeRequest._get_list_or_object(self.gitlab, id, - project_id=self.id, - **kwargs) - - def Milestone(self, id=None, **kwargs): - return ProjectMilestone._get_list_or_object(self.gitlab, id, - project_id=self.id, - **kwargs) - - def Note(self, id=None, **kwargs): - return ProjectNote._get_list_or_object(self.gitlab, id, - project_id=self.id, - **kwargs) - - def Snippet(self, id=None, **kwargs): - return ProjectSnippet._get_list_or_object(self.gitlab, id, - project_id=self.id, - **kwargs) - - def Label(self, id=None, **kwargs): - return ProjectLabel._get_list_or_object(self.gitlab, id, - project_id=self.id, - **kwargs) - - def File(self, id=None, **kwargs): - return ProjectFile._get_list_or_object(self.gitlab, id, - project_id=self.id, - **kwargs) - - def Tag(self, id=None, **kwargs): - return ProjectTag._get_list_or_object(self.gitlab, id, - project_id=self.id, - **kwargs) - - def tree(self, path='', ref_name='', **kwargs): - url = "%s/%s/repository/tree" % (self._url, self.id) - url += '?path=%s&ref_name=%s' % (path, ref_name) - r = self.gitlab._raw_get(url, **kwargs) - _raise_error_from_response(r, GitlabGetError) - return r.json() - - def blob(self, sha, filepath, **kwargs): - url = "%s/%s/repository/blobs/%s" % (self._url, self.id, sha) - url += '?filepath=%s' % (filepath) - r = self.gitlab._raw_get(url, **kwargs) - _raise_error_from_response(r, GitlabGetError) - return r.content - - def archive(self, sha=None, **kwargs): - url = '/projects/%s/repository/archive' % self.id - if sha: - url += '?sha=%s' % sha - r = self.gitlab._raw_get(url, **kwargs) - _raise_error_from_response(r, GitlabGetError) - return r.content - - def create_file(self, path, branch, content, message, **kwargs): - """Creates file in project repository - - Args: - path (str): Full path to new file - branch (str): The name of branch - content (str): Content of the file - message (str): Commit message - kwargs: Arbitrary keyword arguments - - Raises: - GitlabCreateError: Operation failed - GitlabConnectionError: Connection to GitLab-server failed - """ - url = "/projects/%s/repository/files" % self.id - url += ("?file_path=%s&branch_name=%s&content=%s&commit_message=%s" % - (path, branch, content, message)) - r = self.gitlab._raw_post(url, data=None, content_type=None, **kwargs) - _raise_error_from_response(r, GitlabCreateError, 201) - - def update_file(self, path, branch, content, message, **kwargs): - url = "/projects/%s/repository/files" % self.id - url += ("?file_path=%s&branch_name=%s&content=%s&commit_message=%s" % - (path, branch, content, message)) - r = self.gitlab._raw_put(url, data=None, content_type=None, **kwargs) - _raise_error_from_response(r, GitlabUpdateError) - - def delete_file(self, path, branch, message, **kwargs): - url = "/projects/%s/repository/files" % self.id - url += ("?file_path=%s&branch_name=%s&commit_message=%s" % - (path, branch, message)) - r = self.gitlab._raw_delete(url, **kwargs) - _raise_error_from_response(r, GitlabDeleteError) - - -class TeamMember(GitlabObject): - _url = '/user_teams/%(team_id)s/members' - canUpdate = False - requiredUrlAttrs = ['teamd_id'] - requiredCreateAttrs = ['access_level'] - shortPrintAttr = 'username' - - -class TeamProject(GitlabObject): - _url = '/user_teams/%(team_id)s/projects' - _constructorTypes = {'owner': 'User', 'namespace': 'Group'} - canUpdate = False - requiredCreateAttrs = ['greatest_access_level'] - requiredUrlAttrs = ['team_id'] - shortPrintAttr = 'name' - - -class Team(GitlabObject): - _url = '/user_teams' - shortPrintAttr = 'name' - requiredCreateAttrs = ['name', 'path'] - canUpdate = False - - def Member(self, id=None, **kwargs): - return TeamMember._get_list_or_object(self.gitlab, id, - team_id=self.id, - **kwargs) - - def Project(self, id=None, **kwargs): - return TeamProject._get_list_or_object(self.gitlab, id, - team_id=self.id, - **kwargs) diff --git a/gitlab/exceptions.py b/gitlab/exceptions.py new file mode 100644 index 000000000..f538031a7 --- /dev/null +++ b/gitlab/exceptions.py @@ -0,0 +1,84 @@ +class GitlabError(Exception): + def __init__(self, error_message="", response_code=None, + response_body=None): + + Exception.__init__(self, error_message) + # Http status code + self.response_code = response_code + # Full http response + self.response_body = response_body + # Parsed error message from gitlab + self.error_message = error_message + + def __str__(self): + if self.response_code is not None: + return "{0}: {1}".format(self.response_code, self.error_message) + else: + return "{0}".format(self.error_message) + + +class GitlabAuthenticationError(GitlabError): + pass + + +class GitlabConnectionError(GitlabError): + pass + + +class GitlabOperationError(GitlabError): + pass + + +class GitlabListError(GitlabOperationError): + pass + + +class GitlabGetError(GitlabOperationError): + pass + + +class GitlabCreateError(GitlabOperationError): + pass + + +class GitlabUpdateError(GitlabOperationError): + pass + + +class GitlabDeleteError(GitlabOperationError): + pass + + +class GitlabProtectError(GitlabOperationError): + pass + + +class GitlabTransferProjectError(GitlabOperationError): + pass + + +def raise_error_from_response(response, error, expected_code=200): + """Tries to parse gitlab error message from response and raises error. + + Do nothing if the response status is the expected one. + + If response status code is 401, raises instead GitlabAuthenticationError. + + response: requests response object + error: Error-class to raise. Should be inherited from GitLabError + """ + + if expected_code == response.status_code: + return + + try: + message = response.json()['message'] + except (KeyError, ValueError): + message = response.content + + if response.status_code == 401: + error = GitlabAuthenticationError + + raise error(error_message=message, + response_code=response.status_code, + response_body=response.content) diff --git a/gitlab/objects.py b/gitlab/objects.py new file mode 100644 index 000000000..106056446 --- /dev/null +++ b/gitlab/objects.py @@ -0,0 +1,801 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2013-2015 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 division +from __future__ import absolute_import +import itertools +import json +import sys + +import six + +from gitlab.exceptions import * # noqa + + +class jsonEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, GitlabObject): + return obj.__dict__ + elif isinstance(obj, Gitlab): + return {'url': obj._url} + return json.JSONEncoder.default(self, obj) + + +class GitlabObject(object): + """Base class for all classes that interface with GitLab + + Args: + gl (gitlab.Gitlab): GitLab server connection + data: If data is integer or string type, get object from GitLab + data: If data is dictionary, create new object locally. To save object + in GitLab, call save-method + kwargs: Arbitrary keyword arguments + """ + #: Url to use in GitLab for this object + _url = None + # Some objects (e.g. merge requests) have different urls for singular and + # plural + _urlPlural = None + _returnClass = None + _constructorTypes = None + #: Whether _get_list_or_object should return list or object when id is None + getListWhenNoId = True + + #: Tells if GitLab-api allows retrieving single objects + canGet = True + #: Tells if GitLab-api allows listing of objects + canList = True + #: Tells if GitLab-api allows creation of new objects + canCreate = True + #: Tells if GitLab-api allows updating object + canUpdate = True + #: Tells if GitLab-api allows deleting object + canDelete = True + #: Attributes that are required for constructing url + requiredUrlAttrs = [] + #: Attributes that are required when retrieving list of objects + requiredListAttrs = [] + #: Attributes that are required when retrieving single object + requiredGetAttrs = [] + #: Attributes that are required when deleting object + requiredDeleteAttrs = [] + #: Attributes that are required when creating a new object + requiredCreateAttrs = [] + #: Attributes that are optional when creating a new object + optionalCreateAttrs = [] + #: Attributes that are required when updating an object + requiredUpdateAttrs = None + #: Attributes that are optional when updating an object + optionalUpdateAttrs = None + #: Whether the object ID is required in the GET url + getRequiresId = True + + idAttr = 'id' + shortPrintAttr = None + + def _data_for_gitlab(self, extra_parameters={}): + data = {} + for attribute in itertools.chain(self.requiredCreateAttrs, + self.optionalCreateAttrs): + if hasattr(self, attribute): + data[attribute] = getattr(self, attribute) + + data.update(extra_parameters) + + return json.dumps(data) + + @classmethod + def list(cls, gl, **kwargs): + if not cls.canList: + raise NotImplementedError + + if not cls._url: + raise NotImplementedError + + return gl.list(cls, **kwargs) + + @classmethod + def get(cls, gl, id, **kwargs): + if cls.canGet is False: + raise NotImplementedError + elif cls.canGet is True: + return cls(gl, id, **kwargs) + elif cls.canGet == 'from_list': + for obj in cls.list(gl, **kwargs): + obj_id = getattr(obj, obj.idAttr) + if str(obj_id) == str(id): + return obj + + raise GitlabGetError("Object not found") + + @classmethod + def _get_list_or_object(cls, gl, id, **kwargs): + if id is None and cls.getListWhenNoId: + return cls.list(gl, **kwargs) + else: + return cls.get(gl, id, **kwargs) + + def _get_object(self, k, v): + if self._constructorTypes and k in self._constructorTypes: + return globals()[self._constructorTypes[k]](self.gitlab, v) + else: + return v + + def _set_from_dict(self, data): + for k, v in data.items(): + if isinstance(v, list): + self.__dict__[k] = [] + for i in v: + self.__dict__[k].append(self._get_object(k, i)) + elif v is None: + self.__dict__[k] = None + else: + self.__dict__[k] = self._get_object(k, v) + + def _create(self, **kwargs): + if not self.canCreate: + raise NotImplementedError + + json = self.gitlab.create(self, **kwargs) + self._set_from_dict(json) + self._from_api = True + + def _update(self, **kwargs): + if not self.canUpdate: + raise NotImplementedError + + json = self.gitlab.update(self, **kwargs) + self._set_from_dict(json) + + def save(self, **kwargs): + if self._from_api: + self._update(**kwargs) + else: + self._create(**kwargs) + + def delete(self, **kwargs): + if not self.canDelete: + raise NotImplementedError + + if not self._from_api: + raise GitlabDeleteError("Object not yet created") + + return self.gitlab.delete(self, **kwargs) + + @classmethod + def create(cls, gl, data, **kwargs): + if not cls.canCreate: + raise NotImplementedError + + obj = cls(gl, data, **kwargs) + obj.save() + + return obj + + def __init__(self, gl, data=None, **kwargs): + self._from_api = False + self.gitlab = gl + + if (data is None or isinstance(data, six.integer_types) or + isinstance(data, six.string_types)): + if not self.canGet: + raise NotImplementedError + data = self.gitlab.get(self.__class__, data, **kwargs) + self._from_api = True + + self._set_from_dict(data) + + if kwargs: + for k, v in kwargs.items(): + self.__dict__[k] = v + + # Special handling for api-objects that don't have id-number in api + # responses. Currently only Labels and Files + if not hasattr(self, "id"): + self.id = None + + def __str__(self): + return '%s => %s' % (type(self), str(self.__dict__)) + + def display(self, pretty): + if pretty: + self.pretty_print() + else: + self.short_print() + + def short_print(self, depth=0): + id = self.__dict__[self.idAttr] + print("%s%s: %s" % (" " * depth * 2, self.idAttr, id)) + if self.shortPrintAttr: + print("%s%s: %s" % (" " * depth * 2, + self.shortPrintAttr.replace('_', '-'), + self.__dict__[self.shortPrintAttr])) + + @staticmethod + def _get_display_encoding(): + return sys.stdout.encoding or sys.getdefaultencoding() + + @staticmethod + def _obj_to_str(obj): + if isinstance(obj, dict): + s = ", ".join(["%s: %s" % + (x, GitlabObject._obj_to_str(y)) + for (x, y) in obj.items()]) + return "{ %s }" % s + elif isinstance(obj, list): + s = ", ".join([GitlabObject._obj_to_str(x) for x in obj]) + return "[ %s ]" % s + elif six.PY2 and isinstance(obj, six.text_type): + return obj.encode(GitlabObject._get_display_encoding(), "replace") + else: + return str(obj) + + def pretty_print(self, depth=0): + id = self.__dict__[self.idAttr] + print("%s%s: %s" % (" " * depth * 2, self.idAttr, id)) + for k in sorted(self.__dict__.keys()): + if k == self.idAttr or k == 'id': + continue + if k[0] == '_': + continue + v = self.__dict__[k] + pretty_k = k.replace('_', '-') + if six.PY2: + pretty_k = pretty_k.encode( + GitlabObject._get_display_encoding(), "replace") + if isinstance(v, GitlabObject): + if depth == 0: + print("%s:" % pretty_k) + v.pretty_print(1) + else: + print("%s: %s" % (pretty_k, v.id)) + else: + if hasattr(v, __name__) and v.__name__ == 'Gitlab': + continue + v = GitlabObject._obj_to_str(v) + print("%s%s: %s" % (" " * depth * 2, pretty_k, v)) + + def json(self): + return json.dumps(self.__dict__, cls=jsonEncoder) + + +class UserKey(GitlabObject): + _url = '/users/%(user_id)s/keys' + canGet = 'from_list' + canUpdate = False + requiredUrlAttrs = ['user_id'] + requiredCreateAttrs = ['title', 'key'] + + +class User(GitlabObject): + _url = '/users' + shortPrintAttr = 'username' + # FIXME: password is required for create but not for update + requiredCreateAttrs = ['email', 'username', 'name'] + optionalCreateAttrs = ['password', 'skype', 'linkedin', 'twitter', + 'projects_limit', 'extern_uid', 'provider', + 'bio', 'admin', 'can_create_group', 'website_url', + 'confirm'] + + def _data_for_gitlab(self, extra_parameters={}): + if hasattr(self, 'confirm'): + self.confirm = str(self.confirm).lower() + return super(User, self)._data_for_gitlab(extra_parameters) + + def Key(self, id=None, **kwargs): + return UserKey._get_list_or_object(self.gitlab, id, + user_id=self.id, + **kwargs) + + +class CurrentUserKey(GitlabObject): + _url = '/user/keys' + canUpdate = False + shortPrintAttr = 'title' + requiredCreateAttrs = ['title', 'key'] + + +class CurrentUser(GitlabObject): + _url = '/user' + canList = False + canCreate = False + canUpdate = False + canDelete = False + shortPrintAttr = 'username' + + def Key(self, id=None, **kwargs): + return CurrentUserKey._get_list_or_object(self.gitlab, id, **kwargs) + + +class GroupMember(GitlabObject): + _url = '/groups/%(group_id)s/members' + canGet = 'from_list' + requiredUrlAttrs = ['group_id'] + requiredCreateAttrs = ['access_level', 'user_id'] + requiredUpdateAttrs = ['access_level'] + shortPrintAttr = 'username' + + def _update(self, **kwargs): + self.user_id = self.id + super(GroupMember, self)._update(**kwargs) + + +class Group(GitlabObject): + _url = '/groups' + canUpdate = False + _constructorTypes = {'projects': 'Project'} + requiredCreateAttrs = ['name', 'path'] + shortPrintAttr = 'name' + + GUEST_ACCESS = 10 + REPORTER_ACCESS = 20 + DEVELOPER_ACCESS = 30 + MASTER_ACCESS = 40 + OWNER_ACCESS = 50 + + def Member(self, id=None, **kwargs): + return GroupMember._get_list_or_object(self.gitlab, id, + group_id=self.id, + **kwargs) + + def transfer_project(self, id, **kwargs): + url = '/groups/%d/projects/%d' % (self.id, id) + r = self.gitlab._raw_post(url, None, **kwargs) + raise_error_from_response(r, GitlabTransferProjectError, 201) + + +class Hook(GitlabObject): + _url = '/hooks' + canUpdate = False + requiredCreateAttrs = ['url'] + shortPrintAttr = 'url' + + +class Issue(GitlabObject): + _url = '/issues' + _constructorTypes = {'author': 'User', 'assignee': 'User', + 'milestone': 'ProjectMilestone'} + canGet = 'from_list' + canDelete = False + canUpdate = False + canCreate = False + shortPrintAttr = 'title' + + +class ProjectBranch(GitlabObject): + _url = '/projects/%(project_id)s/repository/branches' + _constructorTypes = {'author': 'User', "committer": "User"} + + idAttr = 'name' + canUpdate = False + requiredUrlAttrs = ['project_id'] + requiredCreateAttrs = ['branch_name', 'ref'] + _constructorTypes = {'commit': 'ProjectCommit'} + + def protect(self, protect=True, **kwargs): + url = self._url % {'project_id': self.project_id} + action = 'protect' if protect else 'unprotect' + url = "%s/%s/%s" % (url, self.name, action) + r = self.gitlab._raw_put(url, data=None, content_type=None, **kwargs) + raise_error_from_response(r, GitlabProtectError) + + if protect: + self.protected = protect + else: + del self.protected + + def unprotect(self, **kwargs): + self.protect(False, **kwargs) + + +class ProjectCommit(GitlabObject): + _url = '/projects/%(project_id)s/repository/commits' + canDelete = False + canUpdate = False + canCreate = False + requiredUrlAttrs = ['project_id'] + shortPrintAttr = 'title' + + def diff(self, **kwargs): + url = ('/projects/%(project_id)s/repository/commits/%(commit_id)s/diff' + % {'project_id': self.project_id, 'commit_id': self.id}) + r = self.gitlab._raw_get(url, **kwargs) + raise_error_from_response(r, GitlabGetError) + + return r.json() + + def blob(self, filepath, **kwargs): + url = ('/projects/%(project_id)s/repository/blobs/%(commit_id)s' % + {'project_id': self.project_id, 'commit_id': self.id}) + url += '?filepath=%s' % filepath + r = self.gitlab._raw_get(url, **kwargs) + + raise_error_from_response(r, GitlabGetError) + + return r.content + + +class ProjectKey(GitlabObject): + _url = '/projects/%(project_id)s/keys' + canUpdate = False + requiredUrlAttrs = ['project_id'] + requiredCreateAttrs = ['title', 'key'] + + +class ProjectEvent(GitlabObject): + _url = '/projects/%(project_id)s/events' + canGet = 'from_list' + canDelete = False + canUpdate = False + canCreate = False + requiredUrlAttrs = ['project_id'] + shortPrintAttr = 'target_title' + + +class ProjectHook(GitlabObject): + _url = '/projects/%(project_id)s/hooks' + requiredUrlAttrs = ['project_id'] + requiredCreateAttrs = ['url'] + optionalCreateAttrs = ['push_events', 'issues_events', + 'merge_requests_events', 'tag_push_events'] + shortPrintAttr = 'url' + + +class ProjectIssueNote(GitlabObject): + _url = '/projects/%(project_id)s/issues/%(issue_id)s/notes' + _constructorTypes = {'author': 'User'} + canUpdate = False + canDelete = False + requiredUrlAttrs = ['project_id', 'issue_id'] + requiredCreateAttrs = ['body'] + + +class ProjectIssue(GitlabObject): + _url = '/projects/%(project_id)s/issues/' + _constructorTypes = {'author': 'User', 'assignee': 'User', + 'milestone': 'ProjectMilestone'} + canDelete = False + requiredUrlAttrs = ['project_id'] + requiredCreateAttrs = ['title'] + # FIXME: state_event is only valid with update + optionalCreateAttrs = ['description', 'assignee_id', 'milestone_id', + 'labels', 'state_event'] + + shortPrintAttr = 'title' + + def _data_for_gitlab(self, extra_parameters={}): + # Gitlab-api returns labels in a json list and takes them in a + # comma separated list. + if hasattr(self, "labels"): + if (self.labels is not None and + not isinstance(self.labels, six.string_types)): + labels = ", ".join(self.labels) + extra_parameters['labels'] = labels + + return super(ProjectIssue, self)._data_for_gitlab(extra_parameters) + + def Note(self, id=None, **kwargs): + return ProjectIssueNote._get_list_or_object(self.gitlab, id, + project_id=self.project_id, + issue_id=self.id, + **kwargs) + + +class ProjectMember(GitlabObject): + _url = '/projects/%(project_id)s/members' + requiredUrlAttrs = ['project_id'] + requiredCreateAttrs = ['access_level', 'user_id'] + shortPrintAttr = 'username' + + +class ProjectNote(GitlabObject): + _url = '/projects/%(project_id)s/notes' + _constructorTypes = {'author': 'User'} + canUpdate = False + canDelete = False + requiredUrlAttrs = ['project_id'] + requiredCreateAttrs = ['body'] + + +class ProjectTag(GitlabObject): + _url = '/projects/%(project_id)s/repository/tags' + idAttr = 'name' + canGet = 'from_list' + canDelete = False + canUpdate = False + requiredUrlAttrs = ['project_id'] + requiredCreateAttrs = ['tag_name', 'ref'] + optionalCreateAttrs = ['message'] + shortPrintAttr = 'name' + + +class ProjectMergeRequestNote(GitlabObject): + _url = '/projects/%(project_id)s/merge_requests/%(merge_request_id)s/notes' + _constructorTypes = {'author': 'User'} + canDelete = False + requiredUrlAttrs = ['project_id', 'merge_request_id'] + requiredCreateAttrs = ['body'] + + +class ProjectMergeRequest(GitlabObject): + _url = '/projects/%(project_id)s/merge_request' + _urlPlural = '/projects/%(project_id)s/merge_requests' + _constructorTypes = {'author': 'User', 'assignee': 'User'} + canDelete = False + requiredUrlAttrs = ['project_id'] + requiredCreateAttrs = ['source_branch', 'target_branch', 'title'] + optionalCreateAttrs = ['assignee_id'] + + def Note(self, id=None, **kwargs): + return ProjectMergeRequestNote._get_list_or_object( + self.gitlab, id, project_id=self.project_id, + merge_request_id=self.id, **kwargs) + + +class ProjectMilestone(GitlabObject): + _url = '/projects/%(project_id)s/milestones' + canDelete = False + requiredUrlAttrs = ['project_id'] + requiredCreateAttrs = ['title'] + optionalCreateAttrs = ['description', 'due_date', 'state_event'] + shortPrintAttr = 'title' + + +class ProjectLabel(GitlabObject): + _url = '/projects/%(project_id)s/labels' + requiredUrlAttrs = ['project_id'] + idAttr = 'name' + requiredDeleteAttrs = ['name'] + requiredCreateAttrs = ['name', 'color'] + requiredUpdateAttrs = [] + # FIXME: new_name is only valid with update + optionalCreateAttrs = ['new_name'] + + +class ProjectFile(GitlabObject): + _url = '/projects/%(project_id)s/repository/files' + canList = False + requiredUrlAttrs = ['project_id'] + requiredGetAttrs = ['file_path', 'ref'] + requiredCreateAttrs = ['file_path', 'branch_name', 'content', + 'commit_message'] + optionalCreateAttrs = ['encoding'] + requiredDeleteAttrs = ['branch_name', 'commit_message'] + getListWhenNoId = False + shortPrintAttr = 'file_path' + getRequiresId = False + + +class ProjectSnippetNote(GitlabObject): + _url = '/projects/%(project_id)s/snippets/%(snippet_id)s/notes' + _constructorTypes = {'author': 'User'} + canUpdate = False + canDelete = False + requiredUrlAttrs = ['project_id', 'snippet_id'] + requiredCreateAttrs = ['body'] + + +class ProjectSnippet(GitlabObject): + _url = '/projects/%(project_id)s/snippets' + _constructorTypes = {'author': 'User'} + requiredUrlAttrs = ['project_id'] + requiredCreateAttrs = ['title', 'file_name', 'code'] + optionalCreateAttrs = ['lifetime'] + shortPrintAttr = 'title' + + def Content(self, **kwargs): + url = ("/projects/%(project_id)s/snippets/%(snippet_id)s/raw" % + {'project_id': self.project_id, 'snippet_id': self.id}) + r = self.gitlab._raw_get(url, **kwargs) + raise_error_from_response(r, GitlabGetError) + return r.content + + def Note(self, id=None, **kwargs): + return ProjectSnippetNote._get_list_or_object( + self.gitlab, id, + project_id=self.project_id, + snippet_id=self.id, + **kwargs) + + +class UserProject(GitlabObject): + _url = '/projects/user/%(user_id)s' + _constructorTypes = {'owner': 'User', 'namespace': 'Group'} + canUpdate = False + canDelete = False + canList = False + canGet = False + requiredUrlAttrs = ['user_id'] + requiredCreateAttrs = ['name'] + optionalCreateAttrs = ['default_branch', 'issues_enabled', 'wall_enabled', + 'merge_requests_enabled', 'wiki_enabled', + 'snippets_enabled', 'public', 'visibility_level', + 'description'] + + +class Project(GitlabObject): + _url = '/projects' + _constructorTypes = {'owner': 'User', 'namespace': 'Group'} + requiredCreateAttrs = ['name'] + requiredUpdateAttrs = [] + optionalCreateAttrs = ['default_branch', 'issues_enabled', 'wall_enabled', + 'merge_requests_enabled', 'wiki_enabled', + 'snippets_enabled', 'public', 'visibility_level', + 'namespace_id', 'description', 'path', 'import_url'] + + shortPrintAttr = 'path' + + def Branch(self, id=None, **kwargs): + return ProjectBranch._get_list_or_object(self.gitlab, id, + project_id=self.id, + **kwargs) + + def Commit(self, id=None, **kwargs): + return ProjectCommit._get_list_or_object(self.gitlab, id, + project_id=self.id, + **kwargs) + + def Event(self, id=None, **kwargs): + return ProjectEvent._get_list_or_object(self.gitlab, id, + project_id=self.id, + **kwargs) + + def Hook(self, id=None, **kwargs): + return ProjectHook._get_list_or_object(self.gitlab, id, + project_id=self.id, + **kwargs) + + def Key(self, id=None, **kwargs): + return ProjectKey._get_list_or_object(self.gitlab, id, + project_id=self.id, + **kwargs) + + def Issue(self, id=None, **kwargs): + return ProjectIssue._get_list_or_object(self.gitlab, id, + project_id=self.id, + **kwargs) + + def Member(self, id=None, **kwargs): + return ProjectMember._get_list_or_object(self.gitlab, id, + project_id=self.id, + **kwargs) + + def MergeRequest(self, id=None, **kwargs): + return ProjectMergeRequest._get_list_or_object(self.gitlab, id, + project_id=self.id, + **kwargs) + + def Milestone(self, id=None, **kwargs): + return ProjectMilestone._get_list_or_object(self.gitlab, id, + project_id=self.id, + **kwargs) + + def Note(self, id=None, **kwargs): + return ProjectNote._get_list_or_object(self.gitlab, id, + project_id=self.id, + **kwargs) + + def Snippet(self, id=None, **kwargs): + return ProjectSnippet._get_list_or_object(self.gitlab, id, + project_id=self.id, + **kwargs) + + def Label(self, id=None, **kwargs): + return ProjectLabel._get_list_or_object(self.gitlab, id, + project_id=self.id, + **kwargs) + + def File(self, id=None, **kwargs): + return ProjectFile._get_list_or_object(self.gitlab, id, + project_id=self.id, + **kwargs) + + def Tag(self, id=None, **kwargs): + return ProjectTag._get_list_or_object(self.gitlab, id, + project_id=self.id, + **kwargs) + + def tree(self, path='', ref_name='', **kwargs): + url = "%s/%s/repository/tree" % (self._url, self.id) + url += '?path=%s&ref_name=%s' % (path, ref_name) + r = self.gitlab._raw_get(url, **kwargs) + raise_error_from_response(r, GitlabGetError) + return r.json() + + def blob(self, sha, filepath, **kwargs): + url = "%s/%s/repository/blobs/%s" % (self._url, self.id, sha) + url += '?filepath=%s' % (filepath) + r = self.gitlab._raw_get(url, **kwargs) + raise_error_from_response(r, GitlabGetError) + return r.content + + def archive(self, sha=None, **kwargs): + url = '/projects/%s/repository/archive' % self.id + if sha: + url += '?sha=%s' % sha + r = self.gitlab._raw_get(url, **kwargs) + raise_error_from_response(r, GitlabGetError) + return r.content + + def create_file(self, path, branch, content, message, **kwargs): + """Creates file in project repository + + Args: + path (str): Full path to new file + branch (str): The name of branch + content (str): Content of the file + message (str): Commit message + kwargs: Arbitrary keyword arguments + + Raises: + GitlabCreateError: Operation failed + GitlabConnectionError: Connection to GitLab-server failed + """ + url = "/projects/%s/repository/files" % self.id + url += ("?file_path=%s&branch_name=%s&content=%s&commit_message=%s" % + (path, branch, content, message)) + r = self.gitlab._raw_post(url, data=None, content_type=None, **kwargs) + raise_error_from_response(r, GitlabCreateError, 201) + + def update_file(self, path, branch, content, message, **kwargs): + url = "/projects/%s/repository/files" % self.id + url += ("?file_path=%s&branch_name=%s&content=%s&commit_message=%s" % + (path, branch, content, message)) + r = self.gitlab._raw_put(url, data=None, content_type=None, **kwargs) + raise_error_from_response(r, GitlabUpdateError) + + def delete_file(self, path, branch, message, **kwargs): + url = "/projects/%s/repository/files" % self.id + url += ("?file_path=%s&branch_name=%s&commit_message=%s" % + (path, branch, message)) + r = self.gitlab._raw_delete(url, **kwargs) + raise_error_from_response(r, GitlabDeleteError) + + +class TeamMember(GitlabObject): + _url = '/user_teams/%(team_id)s/members' + canUpdate = False + requiredUrlAttrs = ['teamd_id'] + requiredCreateAttrs = ['access_level'] + shortPrintAttr = 'username' + + +class TeamProject(GitlabObject): + _url = '/user_teams/%(team_id)s/projects' + _constructorTypes = {'owner': 'User', 'namespace': 'Group'} + canUpdate = False + requiredCreateAttrs = ['greatest_access_level'] + requiredUrlAttrs = ['team_id'] + shortPrintAttr = 'name' + + +class Team(GitlabObject): + _url = '/user_teams' + shortPrintAttr = 'name' + requiredCreateAttrs = ['name', 'path'] + canUpdate = False + + def Member(self, id=None, **kwargs): + return TeamMember._get_list_or_object(self.gitlab, id, + team_id=self.id, + **kwargs) + + def Project(self, id=None, **kwargs): + return TeamProject._get_list_or_object(self.gitlab, id, + team_id=self.id, + **kwargs) From b66672ee18506035d08453dbfc5b429bdc81702d Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sat, 2 Jan 2016 10:42:37 +0100 Subject: [PATCH 11/37] add missing copyright header --- gitlab/exceptions.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/gitlab/exceptions.py b/gitlab/exceptions.py index f538031a7..c08166ee9 100644 --- a/gitlab/exceptions.py +++ b/gitlab/exceptions.py @@ -1,3 +1,20 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2013-2015 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 . + class GitlabError(Exception): def __init__(self, error_message="", response_code=None, response_body=None): From e5246bffd17eb9863516677a086928af40fba9f5 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sat, 2 Jan 2016 10:45:11 +0100 Subject: [PATCH 12/37] update the docs copyright years --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index dc845f907..ed0285d12 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -61,7 +61,7 @@ # General information about the project. project = 'python-gitlab' -copyright = '2014, Gauvain Pocentek, Mika Mäenpää' +copyright = '2013-2015, Gauvain Pocentek, Mika Mäenpää' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the From 46f74e8e4e6cd093a3be4309802f5a72ed305080 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sun, 3 Jan 2016 09:24:20 +0100 Subject: [PATCH 13/37] Implement managers to get access to resources This changes the 'default' API, using managers is the recommended way to get/list/create objects. Additional operations will be implemented in followup patchs. Old methods are deprecated and will disappear in a while. --- gitlab/__init__.py | 29 +++++ gitlab/exceptions.py | 1 + gitlab/objects.py | 284 +++++++++++++++++++++++++++++++++++++++---- 3 files changed, 288 insertions(+), 26 deletions(-) diff --git a/gitlab/__init__.py b/gitlab/__init__.py index 920c97a1d..b2d8cb6de 100644 --- a/gitlab/__init__.py +++ b/gitlab/__init__.py @@ -78,6 +78,35 @@ def __init__(self, url, private_token=None, #: (Passed to requests-library) self.ssl_verify = ssl_verify + self.user_keys = UserKeyManager(self) + self.users = UserManager(self) + self.group_members = GroupMemberManager(self) + self.groups = GroupManager(self) + self.hooks = HookManager(self) + self.issues = IssueManager(self) + self.project_branches = ProjectBranchManager(self) + self.project_commits = ProjectCommitManager(self) + self.project_keys = ProjectKeyManager(self) + self.project_events = ProjectEventManager(self) + self.project_hooks = ProjectHookManager(self) + self.project_issue_notes = ProjectIssueNoteManager(self) + self.project_issues = ProjectIssueManager(self) + self.project_members = ProjectMemberManager(self) + self.project_notes = ProjectNoteManager(self) + self.project_tags = ProjectTagManager(self) + self.project_mergerequest_notes = ProjectMergeRequestNoteManager(self) + self.project_mergerequests = ProjectMergeRequestManager(self) + self.project_milestones = ProjectMilestoneManager(self) + self.project_labels = ProjectLabelManager(self) + self.project_files = ProjectFileManager(self) + self.project_snippet_notes = ProjectSnippetNoteManager(self) + self.project_snippets = ProjectSnippetManager(self) + self.user_projects = UserProjectManager(self) + self.projects = ProjectManager(self) + self.team_members = TeamMemberManager(self) + self.team_projects = TeamProjectManager(self) + self.teams = TeamManager(self) + @staticmethod def from_config(gitlab_id=None, config_files=None): config = gitlab.config.GitlabConfigParser(gitlab_id=gitlab_id, diff --git a/gitlab/exceptions.py b/gitlab/exceptions.py index c08166ee9..c6df71cda 100644 --- a/gitlab/exceptions.py +++ b/gitlab/exceptions.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . + class GitlabError(Exception): def __init__(self, error_message="", response_code=None, response_body=None): diff --git a/gitlab/objects.py b/gitlab/objects.py index 106056446..e6f54675b 100644 --- a/gitlab/objects.py +++ b/gitlab/objects.py @@ -36,6 +36,42 @@ def default(self, obj): return json.JSONEncoder.default(self, obj) +class BaseManager(object): + obj_cls = None + + def __init__(self, gl, parent=None, args=[]): + self.gitlab = gl + self.args = args + self.parent = parent + + def get(self, id, **kwargs): + if self.parent is not None: + for attr, parent_attr in self.args: + kwargs.setdefault(attr, getattr(self.parent, parent_attr)) + + if not self.obj_cls.canGet: + raise NotImplementedError + return self.obj_cls.get(self.gitlab, id, **kwargs) + + def list(self, **kwargs): + if self.parent is not None: + for attr, parent_attr in self.args: + kwargs.setdefault(attr, getattr(self.parent, parent_attr)) + + if not self.obj_cls.canList: + raise NotImplementedError + return self.obj_cls.list(self.gitlab, **kwargs) + + def create(self, data, **kwargs): + if self.parent is not None: + for attr, parent_attr in self.args: + kwargs.setdefault(attr, getattr(self.parent, parent_attr)) + + if not self.obj_cls.canCreate: + raise NotImplementedError + return self.obj_cls.create(self.gitlab, data, **kwargs) + + class GitlabObject(object): """Base class for all classes that interface with GitLab @@ -84,6 +120,8 @@ class GitlabObject(object): optionalUpdateAttrs = None #: Whether the object ID is required in the GET url getRequiresId = True + #: List of managers to create + managers = [] idAttr = 'id' shortPrintAttr = None @@ -209,6 +247,13 @@ def __init__(self, gl, data=None, **kwargs): if not hasattr(self, "id"): self.id = None + self._set_managers() + + def _set_managers(self): + for var, cls, attrs in self.managers: + manager = cls(self.gitlab, self, attrs) + setattr(self, var, manager) + def __str__(self): return '%s => %s' % (type(self), str(self.__dict__)) @@ -282,6 +327,10 @@ class UserKey(GitlabObject): requiredCreateAttrs = ['title', 'key'] +class UserKeyManager(BaseManager): + obj_cls = UserKey + + class User(GitlabObject): _url = '/users' shortPrintAttr = 'username' @@ -291,6 +340,7 @@ class User(GitlabObject): 'projects_limit', 'extern_uid', 'provider', 'bio', 'admin', 'can_create_group', 'website_url', 'confirm'] + managers = [('keys', UserKeyManager, [('user_id', 'id')])] def _data_for_gitlab(self, extra_parameters={}): if hasattr(self, 'confirm'): @@ -298,11 +348,17 @@ def _data_for_gitlab(self, extra_parameters={}): return super(User, self)._data_for_gitlab(extra_parameters) def Key(self, id=None, **kwargs): + warnings.warn("`Key` is deprecated, use `keys` instead", + DeprecationWarning) return UserKey._get_list_or_object(self.gitlab, id, user_id=self.id, **kwargs) +class UserManager(BaseManager): + obj_cls = User + + class CurrentUserKey(GitlabObject): _url = '/user/keys' canUpdate = False @@ -310,6 +366,10 @@ class CurrentUserKey(GitlabObject): requiredCreateAttrs = ['title', 'key'] +class CurrentUserKeyManager(BaseManager): + obj_cls = CurrentUserKey + + class CurrentUser(GitlabObject): _url = '/user' canList = False @@ -317,8 +377,11 @@ class CurrentUser(GitlabObject): canUpdate = False canDelete = False shortPrintAttr = 'username' + managers = [('keys', CurrentUserKeyManager, [('user_id', 'id')])] def Key(self, id=None, **kwargs): + warnings.warn("`Key` is deprecated, use `keys` instead", + DeprecationWarning) return CurrentUserKey._get_list_or_object(self.gitlab, id, **kwargs) @@ -335,12 +398,17 @@ def _update(self, **kwargs): super(GroupMember, self)._update(**kwargs) +class GroupMemberManager(BaseManager): + obj_cls = GroupMember + + class Group(GitlabObject): _url = '/groups' canUpdate = False _constructorTypes = {'projects': 'Project'} requiredCreateAttrs = ['name', 'path'] shortPrintAttr = 'name' + managers = [('members', GroupMemberManager, [('group_id', 'id')])] GUEST_ACCESS = 10 REPORTER_ACCESS = 20 @@ -349,6 +417,8 @@ class Group(GitlabObject): OWNER_ACCESS = 50 def Member(self, id=None, **kwargs): + warnings.warn("`Member` is deprecated, use `members` instead", + DeprecationWarning) return GroupMember._get_list_or_object(self.gitlab, id, group_id=self.id, **kwargs) @@ -359,6 +429,10 @@ def transfer_project(self, id, **kwargs): raise_error_from_response(r, GitlabTransferProjectError, 201) +class GroupManager(BaseManager): + obj_cls = Group + + class Hook(GitlabObject): _url = '/hooks' canUpdate = False @@ -366,6 +440,10 @@ class Hook(GitlabObject): shortPrintAttr = 'url' +class HookManager(BaseManager): + obj_cls = Hook + + class Issue(GitlabObject): _url = '/issues' _constructorTypes = {'author': 'User', 'assignee': 'User', @@ -377,6 +455,10 @@ class Issue(GitlabObject): shortPrintAttr = 'title' +class IssueManager(BaseManager): + obj_cls = Issue + + class ProjectBranch(GitlabObject): _url = '/projects/%(project_id)s/repository/branches' _constructorTypes = {'author': 'User', "committer": "User"} @@ -403,6 +485,10 @@ def unprotect(self, **kwargs): self.protect(False, **kwargs) +class ProjectBranchManager(BaseManager): + obj_cls = ProjectBranch + + class ProjectCommit(GitlabObject): _url = '/projects/%(project_id)s/repository/commits' canDelete = False @@ -424,12 +510,15 @@ def blob(self, filepath, **kwargs): {'project_id': self.project_id, 'commit_id': self.id}) url += '?filepath=%s' % filepath r = self.gitlab._raw_get(url, **kwargs) - raise_error_from_response(r, GitlabGetError) return r.content +class ProjectCommitManager(BaseManager): + obj_cls = ProjectCommit + + class ProjectKey(GitlabObject): _url = '/projects/%(project_id)s/keys' canUpdate = False @@ -437,6 +526,10 @@ class ProjectKey(GitlabObject): requiredCreateAttrs = ['title', 'key'] +class ProjectKeyManager(BaseManager): + obj_cls = ProjectKey + + class ProjectEvent(GitlabObject): _url = '/projects/%(project_id)s/events' canGet = 'from_list' @@ -447,6 +540,10 @@ class ProjectEvent(GitlabObject): shortPrintAttr = 'target_title' +class ProjectEventManager(BaseManager): + obj_cls = ProjectEvent + + class ProjectHook(GitlabObject): _url = '/projects/%(project_id)s/hooks' requiredUrlAttrs = ['project_id'] @@ -456,6 +553,10 @@ class ProjectHook(GitlabObject): shortPrintAttr = 'url' +class ProjectHookManager(BaseManager): + obj_cls = ProjectHook + + class ProjectIssueNote(GitlabObject): _url = '/projects/%(project_id)s/issues/%(issue_id)s/notes' _constructorTypes = {'author': 'User'} @@ -465,6 +566,10 @@ class ProjectIssueNote(GitlabObject): requiredCreateAttrs = ['body'] +class ProjectIssueNoteManager(BaseManager): + obj_cls = ProjectIssueNote + + class ProjectIssue(GitlabObject): _url = '/projects/%(project_id)s/issues/' _constructorTypes = {'author': 'User', 'assignee': 'User', @@ -475,8 +580,9 @@ class ProjectIssue(GitlabObject): # FIXME: state_event is only valid with update optionalCreateAttrs = ['description', 'assignee_id', 'milestone_id', 'labels', 'state_event'] - shortPrintAttr = 'title' + managers = [('notes', ProjectIssueNoteManager, + [('project_id', 'project_id'), ('issue_id', 'id')])] def _data_for_gitlab(self, extra_parameters={}): # Gitlab-api returns labels in a json list and takes them in a @@ -490,12 +596,18 @@ def _data_for_gitlab(self, extra_parameters={}): return super(ProjectIssue, self)._data_for_gitlab(extra_parameters) def Note(self, id=None, **kwargs): + warnings.warn("`Note` is deprecated, use `notes` instead", + DeprecationWarning) return ProjectIssueNote._get_list_or_object(self.gitlab, id, project_id=self.project_id, issue_id=self.id, **kwargs) +class ProjectIssueManager(BaseManager): + obj_cls = ProjectIssue + + class ProjectMember(GitlabObject): _url = '/projects/%(project_id)s/members' requiredUrlAttrs = ['project_id'] @@ -503,6 +615,10 @@ class ProjectMember(GitlabObject): shortPrintAttr = 'username' +class ProjectMemberManager(BaseManager): + obj_cls = ProjectMember + + class ProjectNote(GitlabObject): _url = '/projects/%(project_id)s/notes' _constructorTypes = {'author': 'User'} @@ -512,6 +628,10 @@ class ProjectNote(GitlabObject): requiredCreateAttrs = ['body'] +class ProjectNoteManager(BaseManager): + obj_cls = ProjectNote + + class ProjectTag(GitlabObject): _url = '/projects/%(project_id)s/repository/tags' idAttr = 'name' @@ -524,6 +644,10 @@ class ProjectTag(GitlabObject): shortPrintAttr = 'name' +class ProjectTagManager(BaseManager): + obj_cls = ProjectTag + + class ProjectMergeRequestNote(GitlabObject): _url = '/projects/%(project_id)s/merge_requests/%(merge_request_id)s/notes' _constructorTypes = {'author': 'User'} @@ -532,6 +656,10 @@ class ProjectMergeRequestNote(GitlabObject): requiredCreateAttrs = ['body'] +class ProjectMergeRequestNoteManager(BaseManager): + obj_cls = ProjectMergeRequestNote + + class ProjectMergeRequest(GitlabObject): _url = '/projects/%(project_id)s/merge_request' _urlPlural = '/projects/%(project_id)s/merge_requests' @@ -540,13 +668,21 @@ class ProjectMergeRequest(GitlabObject): requiredUrlAttrs = ['project_id'] requiredCreateAttrs = ['source_branch', 'target_branch', 'title'] optionalCreateAttrs = ['assignee_id'] + managers = [('notes', ProjectMergeRequestNoteManager, + [('project_id', 'project_id'), ('merge_request_id', 'id')])] def Note(self, id=None, **kwargs): + warnings.warn("`Note` is deprecated, use `notes` instead", + DeprecationWarning) return ProjectMergeRequestNote._get_list_or_object( self.gitlab, id, project_id=self.project_id, merge_request_id=self.id, **kwargs) +class ProjectMergeRequestManager(BaseManager): + obj_cls = ProjectMergeRequest + + class ProjectMilestone(GitlabObject): _url = '/projects/%(project_id)s/milestones' canDelete = False @@ -556,6 +692,10 @@ class ProjectMilestone(GitlabObject): shortPrintAttr = 'title' +class ProjectMilestoneManager(BaseManager): + obj_cls = ProjectMilestone + + class ProjectLabel(GitlabObject): _url = '/projects/%(project_id)s/labels' requiredUrlAttrs = ['project_id'] @@ -567,6 +707,10 @@ class ProjectLabel(GitlabObject): optionalCreateAttrs = ['new_name'] +class ProjectLabelManager(BaseManager): + obj_cls = ProjectLabel + + class ProjectFile(GitlabObject): _url = '/projects/%(project_id)s/repository/files' canList = False @@ -581,6 +725,10 @@ class ProjectFile(GitlabObject): getRequiresId = False +class ProjectFileManager(BaseManager): + obj_cls = ProjectFile + + class ProjectSnippetNote(GitlabObject): _url = '/projects/%(project_id)s/snippets/%(snippet_id)s/notes' _constructorTypes = {'author': 'User'} @@ -590,6 +738,10 @@ class ProjectSnippetNote(GitlabObject): requiredCreateAttrs = ['body'] +class ProjectSnippetNoteManager(BaseManager): + obj_cls = ProjectSnippetNote + + class ProjectSnippet(GitlabObject): _url = '/projects/%(project_id)s/snippets' _constructorTypes = {'author': 'User'} @@ -597,6 +749,8 @@ class ProjectSnippet(GitlabObject): requiredCreateAttrs = ['title', 'file_name', 'code'] optionalCreateAttrs = ['lifetime'] shortPrintAttr = 'title' + managers = [('notes', ProjectSnippetNoteManager, + [('project_id', 'project_id'), ('snippet_id', 'id')])] def Content(self, **kwargs): url = ("/projects/%(project_id)s/snippets/%(snippet_id)s/raw" % @@ -606,6 +760,8 @@ def Content(self, **kwargs): return r.content def Note(self, id=None, **kwargs): + warnings.warn("`Note` is deprecated, use `notes` instead", + DeprecationWarning) return ProjectSnippetNote._get_list_or_object( self.gitlab, id, project_id=self.project_id, @@ -613,19 +769,8 @@ def Note(self, id=None, **kwargs): **kwargs) -class UserProject(GitlabObject): - _url = '/projects/user/%(user_id)s' - _constructorTypes = {'owner': 'User', 'namespace': 'Group'} - canUpdate = False - canDelete = False - canList = False - canGet = False - requiredUrlAttrs = ['user_id'] - requiredCreateAttrs = ['name'] - optionalCreateAttrs = ['default_branch', 'issues_enabled', 'wall_enabled', - 'merge_requests_enabled', 'wiki_enabled', - 'snippets_enabled', 'public', 'visibility_level', - 'description'] +class ProjectSnippetManager(BaseManager): + obj_cls = ProjectSnippet class Project(GitlabObject): @@ -637,75 +782,119 @@ class Project(GitlabObject): 'merge_requests_enabled', 'wiki_enabled', 'snippets_enabled', 'public', 'visibility_level', 'namespace_id', 'description', 'path', 'import_url'] - shortPrintAttr = 'path' + managers = [ + ('branches', ProjectBranchManager, [('project_id', 'id')]), + ('commits', ProjectCommitManager, [('project_id', 'id')]), + ('events', ProjectEventManager, [('project_id', 'id')]), + ('files', ProjectFileManager, [('project_id', 'id')]), + ('hooks', ProjectHookManager, [('project_id', 'id')]), + ('keys', ProjectKeyManager, [('project_id', 'id')]), + ('issues', ProjectIssueManager, [('project_id', 'id')]), + ('labels', ProjectLabelManager, [('project_id', 'id')]), + ('members', ProjectMemberManager, [('project_id', 'id')]), + ('mergerequests', ProjectMergeRequestManager, [('project_id', 'id')]), + ('milestones', ProjectMilestoneManager, [('project_id', 'id')]), + ('notes', ProjectNoteManager, [('project_id', 'id')]), + ('snippets', ProjectSnippetManager, [('project_id', 'id')]), + ('tags', ProjectTagManager, [('project_id', 'id')]), + ] def Branch(self, id=None, **kwargs): + warnings.warn("`Branch` is deprecated, use `branches` instead", + DeprecationWarning) return ProjectBranch._get_list_or_object(self.gitlab, id, project_id=self.id, **kwargs) def Commit(self, id=None, **kwargs): + warnings.warn("`Commit` is deprecated, use `commits` instead", + DeprecationWarning) return ProjectCommit._get_list_or_object(self.gitlab, id, project_id=self.id, **kwargs) def Event(self, id=None, **kwargs): + warnings.warn("`Event` is deprecated, use `events` instead", + DeprecationWarning) return ProjectEvent._get_list_or_object(self.gitlab, id, project_id=self.id, **kwargs) + def File(self, id=None, **kwargs): + warnings.warn("`File` is deprecated, use `files` instead", + DeprecationWarning) + return ProjectFile._get_list_or_object(self.gitlab, id, + project_id=self.id, + **kwargs) + def Hook(self, id=None, **kwargs): + warnings.warn("`Hook` is deprecated, use `hooks` instead", + DeprecationWarning) return ProjectHook._get_list_or_object(self.gitlab, id, project_id=self.id, **kwargs) def Key(self, id=None, **kwargs): + warnings.warn("`Key` is deprecated, use `keys` instead", + DeprecationWarning) return ProjectKey._get_list_or_object(self.gitlab, id, project_id=self.id, **kwargs) def Issue(self, id=None, **kwargs): + warnings.warn("`Issue` is deprecated, use `issues` instead", + DeprecationWarning) return ProjectIssue._get_list_or_object(self.gitlab, id, project_id=self.id, **kwargs) + def Label(self, id=None, **kwargs): + warnings.warn("`Label` is deprecated, use `labels` instead", + DeprecationWarning) + return ProjectLabel._get_list_or_object(self.gitlab, id, + project_id=self.id, + **kwargs) + def Member(self, id=None, **kwargs): + warnings.warn("`Member` is deprecated, use `members` instead", + DeprecationWarning) return ProjectMember._get_list_or_object(self.gitlab, id, project_id=self.id, **kwargs) def MergeRequest(self, id=None, **kwargs): + warnings.warn( + "`MergeRequest` is deprecated, use `mergerequests` instead", + DeprecationWarning) return ProjectMergeRequest._get_list_or_object(self.gitlab, id, project_id=self.id, **kwargs) def Milestone(self, id=None, **kwargs): + warnings.warn("`Milestone` is deprecated, use `milestones` instead", + DeprecationWarning) return ProjectMilestone._get_list_or_object(self.gitlab, id, project_id=self.id, **kwargs) def Note(self, id=None, **kwargs): + warnings.warn("`Note` is deprecated, use `notes` instead", + DeprecationWarning) return ProjectNote._get_list_or_object(self.gitlab, id, project_id=self.id, **kwargs) def Snippet(self, id=None, **kwargs): + warnings.warn("`Snippet` is deprecated, use `snippets` instead", + DeprecationWarning) return ProjectSnippet._get_list_or_object(self.gitlab, id, project_id=self.id, **kwargs) - def Label(self, id=None, **kwargs): - return ProjectLabel._get_list_or_object(self.gitlab, id, - project_id=self.id, - **kwargs) - - def File(self, id=None, **kwargs): - return ProjectFile._get_list_or_object(self.gitlab, id, - project_id=self.id, - **kwargs) - def Tag(self, id=None, **kwargs): + warnings.warn("`Tag` is deprecated, use `tags` instead", + DeprecationWarning) return ProjectTag._get_list_or_object(self.gitlab, id, project_id=self.id, **kwargs) @@ -775,6 +964,33 @@ class TeamMember(GitlabObject): shortPrintAttr = 'username' +class UserProject(GitlabObject): + _url = '/projects/user/%(user_id)s' + _constructorTypes = {'owner': 'User', 'namespace': 'Group'} + canUpdate = False + canDelete = False + canList = False + canGet = False + requiredUrlAttrs = ['user_id'] + requiredCreateAttrs = ['name'] + optionalCreateAttrs = ['default_branch', 'issues_enabled', 'wall_enabled', + 'merge_requests_enabled', 'wiki_enabled', + 'snippets_enabled', 'public', 'visibility_level', + 'description'] + + +class ProjectManager(BaseManager): + obj_cls = Project + + +class UserProjectManager(BaseManager): + obj_cls = UserProject + + +class TeamMemberManager(BaseManager): + obj_cls = TeamMember + + class TeamProject(GitlabObject): _url = '/user_teams/%(team_id)s/projects' _constructorTypes = {'owner': 'User', 'namespace': 'Group'} @@ -784,18 +1000,34 @@ class TeamProject(GitlabObject): shortPrintAttr = 'name' +class TeamProjectManager(BaseManager): + obj_cls = TeamProject + + class Team(GitlabObject): _url = '/user_teams' shortPrintAttr = 'name' requiredCreateAttrs = ['name', 'path'] canUpdate = False + managers = [ + ('members', TeamMemberManager, [('team_id', 'id')]), + ('projects', TeamProjectManager, [('team_id', 'id')]) + ] def Member(self, id=None, **kwargs): + warnings.warn("`Member` is deprecated, use `members` instead", + DeprecationWarning) return TeamMember._get_list_or_object(self.gitlab, id, team_id=self.id, **kwargs) def Project(self, id=None, **kwargs): + warnings.warn("`Project` is deprecated, use `projects` instead", + DeprecationWarning) return TeamProject._get_list_or_object(self.gitlab, id, team_id=self.id, **kwargs) + + +class TeamManager(BaseManager): + obj_cls = Team From d0da618a793ef974e1f547c2ac28481f3719c152 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sun, 3 Jan 2016 11:43:28 +0100 Subject: [PATCH 14/37] README update --- README.md | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 9bc367ea2..7fd0b0ceb 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ pip install python-gitlab python-gitlab is considered stable. -## Bugs reports +## Bug reports Please report bugs and feature requests at https://github.com/gpocentek/python-gitlab/issues @@ -51,10 +51,10 @@ gl.auth() print(gl.user) # Get a list of projects -for p in gl.Project(): +for p in gl.projects.list(): print(p.name) # get associated issues - issues = p.Issue() + issues = p.issues.list() for issue in issues: closed = 0 if not issue.closed else 1 print(" %d => %s (closed: %d)" % (issue.id, issue.title, closed)) @@ -63,23 +63,23 @@ for p in gl.Project(): issue.save() # Get the first 10 groups (pagination) -for g in gl.Group(page=1, per_page=10): +for g in gl.groups.list(page=1, per_page=10): print(g) # To use pagination and retrieve all the items -for g in gl.Group(all=True): +for g in gl.groups.list(all=True): print(g) # Create a new project (as another_user) -p = gl.Project({'name': 'myCoolProject', 'wiki_enabled': False}) -p.save(sudo="another_user") +p = gl.project.create({'name': 'myCoolProject', 'wiki_enabled': False}, + sudo="another_user") print(p) ````` ## Command line use To use the command line tool, you need to define which GitLab server(s) can be -accessed. this can be done in 2 files: +accessed. This can be done in 2 files: * /etc/python-gitlab.cfg * ~/.python-gitlab.cfg @@ -116,16 +116,16 @@ validated (use false for self signed certificates, only useful with https). The `timeout` option defines after how many seconds a request to the Gitlab server should be abandonned. -Choosing a different server than the default one can be done at run time: +You can choose a different server than the default one at run time: ````` -gitlab --gitlab=remote [command] +gitlab --gitlab remote [command] ````` gitlab always requires 2 mandatory arguments. -The first argument is the object type on which we will act, the second one is -the action: +The first argument is the object type on which the program will act, the second +one is the action: ````` gitlab project list @@ -148,24 +148,24 @@ Some examples: gitlab project list # limit to 5 items per request, display the 1st page only -gitlab project list --page=1 --per-page=5 +gitlab project list --page 1 --per-page 5 # get a specific project (id 2): -gitlab project get --id=2 +gitlab project get --id 2 # get a list of snippets for this project: -gitlab project-issue list --project-id=2 +gitlab project-issue list --project-id 2 # delete a Snippet (id 3): -gitlab project-snippet delete --id=3 --project-id=2 +gitlab project-snippet delete --id 3 --project-id 2 # update a Snippet: -gitlab project-snippet update --id=4 --project-id=2 --code="My New Code" +gitlab project-snippet update --id 4 --project-id 2 --code "My New Code" # create a Snippet: -gitlab project-snippet create --project-id=2 +gitlab project-snippet create --project-id 2 Impossible to create object (Missing attribute(s): title, file-name, code) # oops, let's add the attributes: -gitlab project-snippet create --project-id=2 --title="the title" --file-name="the name" --code="the code" +gitlab project-snippet create --project-id 2 --title "the title" --file-name "the name" --code "the code" ````` From bef97fe3a06802971d67fb70c5215f200cf31147 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sun, 3 Jan 2016 11:54:58 +0100 Subject: [PATCH 15/37] fix pretty_print with managers --- gitlab/objects.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gitlab/objects.py b/gitlab/objects.py index e6f54675b..b0ba7822a 100644 --- a/gitlab/objects.py +++ b/gitlab/objects.py @@ -294,7 +294,7 @@ def pretty_print(self, depth=0): id = self.__dict__[self.idAttr] print("%s%s: %s" % (" " * depth * 2, self.idAttr, id)) for k in sorted(self.__dict__.keys()): - if k == self.idAttr or k == 'id': + if k in (self.idAttr, 'id', 'gitlab'): continue if k[0] == '_': continue @@ -309,6 +309,8 @@ def pretty_print(self, depth=0): v.pretty_print(1) else: print("%s: %s" % (pretty_k, v.id)) + elif isinstance(v, BaseManager): + continue else: if hasattr(v, __name__) and v.__name__ == 'Gitlab': continue From fdf295f99f4e7f68e360280f103a164f447adf15 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sun, 3 Jan 2016 11:59:08 +0100 Subject: [PATCH 16/37] GitLab -> Gitlab (class names) --- gitlab/tests/test_gitlab.py | 6 +++--- gitlab/tests/test_gitlabobject.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gitlab/tests/test_gitlab.py b/gitlab/tests/test_gitlab.py index 83319646f..0fe73e107 100644 --- a/gitlab/tests/test_gitlab.py +++ b/gitlab/tests/test_gitlab.py @@ -30,7 +30,7 @@ from gitlab import * # noqa -class TestGitLabRawMethods(unittest.TestCase): +class TestGitlabRawMethods(unittest.TestCase): def setUp(self): self.gl = Gitlab("http://localhost", private_token="private_token", email="testuser@test.com", password="testpassword", @@ -154,7 +154,7 @@ def resp_cont(url, request): self.assertEqual(resp.status_code, 404) -class TestGitLabMethods(unittest.TestCase): +class TestGitlabMethods(unittest.TestCase): def setUp(self): self.gl = Gitlab("http://localhost", private_token="private_token", email="testuser@test.com", password="testpassword", @@ -503,7 +503,7 @@ def resp_cont(url, request): self.assertRaises(GitlabUpdateError, self.gl.update, obj) -class TestGitLab(unittest.TestCase): +class TestGitlab(unittest.TestCase): def setUp(self): self.gl = Gitlab("http://localhost", private_token="private_token", diff --git a/gitlab/tests/test_gitlabobject.py b/gitlab/tests/test_gitlabobject.py index 812e6c6fc..99a184b11 100644 --- a/gitlab/tests/test_gitlabobject.py +++ b/gitlab/tests/test_gitlabobject.py @@ -143,7 +143,7 @@ def resp_protect_branch_fail(url, request): return response(400, content, headers, None, 5, request) -class TestGitLabObject(unittest.TestCase): +class TestGitlabObject(unittest.TestCase): def setUp(self): self.gl = Gitlab("http://localhost", private_token="private_token", From 2a93c629ef88ffbe2564d154fa32fc723a4b0ea9 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sun, 3 Jan 2016 13:21:03 +0100 Subject: [PATCH 17/37] add unit tests for BaseManager --- gitlab/objects.py | 17 ++-- gitlab/tests/test_manager.py | 147 +++++++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+), 9 deletions(-) create mode 100644 gitlab/tests/test_manager.py diff --git a/gitlab/objects.py b/gitlab/objects.py index b0ba7822a..26896f243 100644 --- a/gitlab/objects.py +++ b/gitlab/objects.py @@ -44,29 +44,28 @@ def __init__(self, gl, parent=None, args=[]): self.args = args self.parent = parent - def get(self, id, **kwargs): + if self.obj_cls is None: + raise AttributeError("obj_cls must be defined") + + def _set_parent_args(self, **kwargs): if self.parent is not None: for attr, parent_attr in self.args: kwargs.setdefault(attr, getattr(self.parent, parent_attr)) + def get(self, id, **kwargs): + self._set_parent_args(**kwargs) if not self.obj_cls.canGet: raise NotImplementedError return self.obj_cls.get(self.gitlab, id, **kwargs) def list(self, **kwargs): - if self.parent is not None: - for attr, parent_attr in self.args: - kwargs.setdefault(attr, getattr(self.parent, parent_attr)) - + self._set_parent_args(**kwargs) if not self.obj_cls.canList: raise NotImplementedError return self.obj_cls.list(self.gitlab, **kwargs) def create(self, data, **kwargs): - if self.parent is not None: - for attr, parent_attr in self.args: - kwargs.setdefault(attr, getattr(self.parent, parent_attr)) - + self._set_parent_args(**kwargs) if not self.obj_cls.canCreate: raise NotImplementedError return self.obj_cls.create(self.gitlab, data, **kwargs) diff --git a/gitlab/tests/test_manager.py b/gitlab/tests/test_manager.py new file mode 100644 index 000000000..837e0136a --- /dev/null +++ b/gitlab/tests/test_manager.py @@ -0,0 +1,147 @@ +# -*- 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 . + +try: + import unittest +except ImportError: + import unittest2 as unittest + +from httmock import HTTMock # noqa +from httmock import response # noqa +from httmock import urlmatch # noqa + +from gitlab import * # noqa +from gitlab.objects import BaseManager # noqa + + +class FakeChildObject(GitlabObject): + _url = "/fake" + + +class FakeChildManager(BaseManager): + obj_cls = FakeChildObject + + +class FakeObject(GitlabObject): + _url = "/fake" + managers = [('children', FakeChildManager, [('child_id', 'id')])] + + +class FakeObjectManager(BaseManager): + obj_cls = FakeObject + + +class TestGitlabManager(unittest.TestCase): + def setUp(self): + self.gitlab = Gitlab("http://localhost", private_token="private_token", + email="testuser@test.com", + password="testpassword", ssl_verify=True) + + def test_constructor(self): + self.assertRaises(AttributeError, BaseManager, self.gitlab) + + @urlmatch(scheme="http", netloc="localhost", path="/api/v3/fake/1", + method="get") + def resp_get(url, request): + headers = {'content-type': 'application/json'} + content = '{"id": 1, "name": "fake_name"}'.encode("utf-8") + return response(200, content, headers, None, 5, request) + + with HTTMock(resp_get): + mgr = FakeObjectManager(self.gitlab) + fake_obj = mgr.get(1) + self.assertEqual(fake_obj.id, 1) + self.assertEqual(fake_obj.name, "fake_name") + self.assertEqual(mgr.gitlab, self.gitlab) + self.assertEqual(mgr.args, []) + self.assertEqual(mgr.parent, None) + + self.assertIsInstance(fake_obj.children, FakeChildManager) + self.assertEqual(fake_obj.children.gitlab, self.gitlab) + self.assertEqual(fake_obj.children.parent, fake_obj) + self.assertEqual(len(fake_obj.children.args), 1) + + fake_child = fake_obj.children.get(1) + self.assertEqual(fake_child.id, 1) + self.assertEqual(fake_child.name, "fake_name") + + def test_get(self): + mgr = FakeObjectManager(self.gitlab) + FakeObject.canGet = False + self.assertRaises(NotImplementedError, mgr.get, 1) + + @urlmatch(scheme="http", netloc="localhost", path="/api/v3/fake/1", + method="get") + def resp_get(url, request): + headers = {'content-type': 'application/json'} + content = '{"id": 1, "name": "fake_name"}'.encode("utf-8") + return response(200, content, headers, None, 5, request) + + with HTTMock(resp_get): + FakeObject.canGet = True + mgr = FakeObjectManager(self.gitlab) + fake_obj = mgr.get(1) + self.assertIsInstance(fake_obj, FakeObject) + self.assertEqual(fake_obj.id, 1) + self.assertEqual(fake_obj.name, "fake_name") + + def test_list(self): + mgr = FakeObjectManager(self.gitlab) + FakeObject.canList = False + self.assertRaises(NotImplementedError, mgr.list) + + @urlmatch(scheme="http", netloc="localhost", path="/api/v3/fake", + method="get") + def resp_get(url, request): + headers = {'content-type': 'application/json'} + content = ('[{"id": 1, "name": "fake_name1"},' + '{"id": 2, "name": "fake_name2"}]') + content = content.encode("utf-8") + return response(200, content, headers, None, 5, request) + + with HTTMock(resp_get): + FakeObject.canList = True + mgr = FakeObjectManager(self.gitlab) + fake_list = mgr.list() + self.assertEqual(len(fake_list), 2) + self.assertIsInstance(fake_list[0], FakeObject) + self.assertEqual(fake_list[0].id, 1) + self.assertEqual(fake_list[0].name, "fake_name1") + self.assertIsInstance(fake_list[1], FakeObject) + self.assertEqual(fake_list[1].id, 2) + self.assertEqual(fake_list[1].name, "fake_name2") + + def test_create(self): + mgr = FakeObjectManager(self.gitlab) + FakeObject.canCreate = False + self.assertRaises(NotImplementedError, mgr.create, {'foo': 'bar'}) + + @urlmatch(scheme="http", netloc="localhost", path="/api/v3/fake", + method="post") + def resp_post(url, request): + headers = {'content-type': 'application/json'} + data = '{"name": "fake_name"}' + content = '{"id": 1, "name": "fake_name"}'.encode("utf-8") + return response(201, content, headers, data, 5, request) + + with HTTMock(resp_post): + FakeObject.canCreate = True + mgr = FakeObjectManager(self.gitlab) + fake_obj = mgr.create({'name': 'fake_name'}) + self.assertIsInstance(fake_obj, FakeObject) + self.assertEqual(fake_obj.id, 1) + self.assertEqual(fake_obj.name, "fake_name") From 0ee53e0c5853c08b69d21ba6b89bd1bf8ee6bb18 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sun, 3 Jan 2016 16:27:03 +0100 Subject: [PATCH 18/37] Document the CLI --- docs/cli.rst | 130 +++++++++++++++++++++++++++++++++++++++++++++++-- docs/index.rst | 2 +- 2 files changed, 128 insertions(+), 4 deletions(-) diff --git a/docs/cli.rst b/docs/cli.rst index 2c0390144..fc4b7b245 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -1,4 +1,128 @@ -Command line usage -================== +#################### +``gitlab`` CLI usage +#################### -Document here how to use command line tool +``python-gitlab`` provides a :command:`gitlab` command-line tool to interact +with GitLab servers. It uses a configuration file to define how to connect to +the servers. + +Configuration +============= + +Files +----- + +``gitlab`` looks up 2 configuration files by default: + +``/etc/python-gitlab.cfg`` + System-wide configuration file + +``~/.python-gitlab.cfg`` + User configuration file + +You can use a different configuration file with the :option:`--config-file` +option. + +Content +------- + +The configuration file uses the ``INI`` format. It contains at least a +``[global]`` section, and a new section for each GitLab server. For example: + +.. code-block:: ini + + [global] + default = somewhere + ssl_verify = true + timeout = 5 + + [somewhere] + url = https://some.whe.re + private_token = vTbFeqJYCY3sibBP7BZM + + [elsewhere] + url = http://else.whe.re:8080 + private_token = CkqsjqcQSFH5FQKDccu4 + timeout = 1 + +The ``default`` option of the ``[global]`` section defines the GitLab server to +use if no server is explitly specified with the :option:`--gitlab` CLI option. + +The ``[global]`` section also defines the values for the default connexion +parameters. You can override the values in each GitLab server section. + +.. list-table:: Global options + :header-rows: 1 + + * - Option + - Possible values + - Description + * - ``ssl_verify`` + - ``True`` or ``False`` + - Verify the SSL certificate. Set to ``False`` if your SSL certificate is + auto-signed. + * - ``timeout`` + - Integer + - Number of seconds to wait for an answer before failing. + +You must define the ``url`` and ``private_token`` in each GitLab server +section. + +.. list-table:: GitLab server options + :header-rows: 1 + + * - Option + - Description + * - ``url`` + - URL for the GitLab server + * - ``private_token`` + - Your user token. Login/password is not supported. + +CLI +=== + +Objects and actions +------------------- + +The ``gitlab`` command expects two mandatory arguments. This first one is the +type of object that you want to manipulate. The second is the action that you +want to perform. For example: + +.. code-block:: console + + $ gitlab project list + +Use the :option:`--help` option to list the available object types and actions: + +.. code-block:: console + + $ gitlab --help + $ gitlab project --help + +Some actions require additional parameters. Use the :option:`--help` option to +list mandatory and optional arguments for an action: + +.. code-block:: console + + $ gitlab project create --help + +Optional arguments +------------------ + +Use the following optional arguments to change the behavior of ``gitlab``. +These options must be defined before the mandatory arguments. + +``--verbose``, ``-v`` + Outputs detail about retrieved objects. + +``--config-file``, ``-c`` + Path to a configuration file. + +``--gitlab``, ``-g`` + ID of a GitLab server defined in the configuration file. + +Example: + +.. code-block:: console + + $ gitlab -v -g elsewhere -c /tmp/gl.cfg project list diff --git a/docs/index.rst b/docs/index.rst index 601e07fce..ee9980e5c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -11,8 +11,8 @@ Contents: .. toctree:: :maxdepth: 2 - usage cli + usage api/gitlab From 7523a612ace8bfa770737b5218ccc899f59f85df Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sun, 3 Jan 2016 16:33:41 +0100 Subject: [PATCH 19/37] Document installation using pip and git --- docs/index.rst | 1 + docs/install.rst | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 docs/install.rst diff --git a/docs/index.rst b/docs/index.rst index ee9980e5c..8ddf9337b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -11,6 +11,7 @@ Contents: .. toctree:: :maxdepth: 2 + install cli usage api/gitlab diff --git a/docs/install.rst b/docs/install.rst new file mode 100644 index 000000000..6abba3f03 --- /dev/null +++ b/docs/install.rst @@ -0,0 +1,21 @@ +############ +Installation +############ + +``python-gitlab`` is compatible with python 2 and 3. + +Use :command:`pip` to install the latest stable version of ``python-gitlab``: + +.. code-block:: console + + $ pip install --upgrade python-gitlab + +The current development version is available on `github +`__. Use :command:`git` and +:command:`pip` to install it: + +.. code-block:: console + + $ git clone https://github.com/gpocentek/python-gitlab + $ cd python-gitlab + $ python setup.py install From 4a536274d2728b38210b020ce7c5ab7ac9ab8cad Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sun, 3 Jan 2016 16:47:08 +0100 Subject: [PATCH 20/37] Rework the requirements for RTD --- requirements.txt | 1 - rtd-requirements.txt | 4 ++++ test-requirements.txt | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 rtd-requirements.txt diff --git a/requirements.txt b/requirements.txt index 2f0ff665c..af8843719 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ requests>1.0 six -sphinxcontrib-napoleon diff --git a/rtd-requirements.txt b/rtd-requirements.txt new file mode 100644 index 000000000..dfd405c19 --- /dev/null +++ b/rtd-requirements.txt @@ -0,0 +1,4 @@ +-r requirements.txt +sphinx>=1.1.2,!=1.2.0,<1.3 +sphinx-argparse +sphinxcontrib-napoleon diff --git a/test-requirements.txt b/test-requirements.txt index 0930bb848..c16328a76 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -3,3 +3,4 @@ testrepository hacking>=0.9.2,<0.10 httmock sphinx>=1.1.2,!=1.2.0,<1.3 +sphinxcontrib-napoleon From 64d635676c410648906be963fd1521c4baf17f25 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sun, 3 Jan 2016 17:03:36 +0100 Subject: [PATCH 21/37] Remove extra dep on sphinx-argparse --- rtd-requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/rtd-requirements.txt b/rtd-requirements.txt index dfd405c19..4cb09b1c4 100644 --- a/rtd-requirements.txt +++ b/rtd-requirements.txt @@ -1,4 +1,3 @@ -r requirements.txt sphinx>=1.1.2,!=1.2.0,<1.3 -sphinx-argparse sphinxcontrib-napoleon From 2237d854f3c83f176b03392debf9785c53b0738b Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sun, 3 Jan 2016 17:48:26 +0100 Subject: [PATCH 22/37] Provide a getting started doc for the API --- docs/api-usage.rst | 119 ++++++++++++++++++++++++++++++++++++++++++++ docs/api/gitlab.rst | 5 +- docs/cli.rst | 2 + docs/index.rst | 2 +- docs/usage.rst | 4 -- 5 files changed, 125 insertions(+), 7 deletions(-) create mode 100644 docs/api-usage.rst delete mode 100644 docs/usage.rst diff --git a/docs/api-usage.rst b/docs/api-usage.rst new file mode 100644 index 000000000..85e4b1f3c --- /dev/null +++ b/docs/api-usage.rst @@ -0,0 +1,119 @@ +############################ +Getting started with the API +############################ + +The ``gitlab`` package provides 3 basic types: + +* ``gitlab.Gitlab`` is the primary class, handling the HTTP requests. It holds + the GitLab URL and authentication information. +* ``gitlab.GitlabObject`` is the base class for all the GitLab objects. These + objects provide an abstraction for GitLab resources (projects, groups, and so + on). +* ``gitlab.BaseManager`` is the base class for objects managers, providing the + API to manipulate the resources and their attributes. + +``gitlab.Gitlab`` class +======================= + +To connect to a GitLab server, create a ``gitlab.Gitlab`` object: + +.. code-block:: python + + import gitlab + + # private token authentication + gl = gitlab.Gitlab('http://10.0.0.1', 'JVNSESs8EwWRx5yDxM5q') + + # or username/password authentication + gl = gitlab.Gitlab('http://10.0.0.1', email='jdoe', password='s3cr3t') + + # make an API request to create the gl.user object. This is mandatory if you + # use the username/password authentication. + gl.auth() + +You can also use configuration files to create ``gitlab.Gitlab`` objects: + +.. code-block:: python + + gl = gitlab.Gitlab.from_config('somewhere', ['/tmp/gl.cfg']) + +See the :ref:`cli_configuration` section for more information about +configuration files. + + +Managers +======== + +The ``gitlab.Gitlab`` class provides managers to access the GitLab resources. +Each manager provides a set of methods to act on the resources. The available +methods depend on the resource type. Resources are represented as +``gitlab.GitlabObject``-derived objects. + +Examples: + +.. code-block:: python + + # list all the projects + projects = gl.projects.list() + for project in projects: + print(project) + + # get the group with id == 2 + group = gl.groups.get(2) + for group in groups: + print() + + # create a new user + user_data = {'email': 'jen@foo.com', 'username': 'jen', 'name': 'Jen'} + user = gl.users.create(user_data) + print(user) + +Some ``gitlab.GitlabObject`` classes also provide managers to access related +GitLab resources: + +.. code-block:: python + + # list the issues for a project + project = gl.projects.get(1) + issues = project.issues.list() + +Gitlab Objects +============== + +You can update or delete an object when it exists as a ``GitlabObject`` object: + +.. code-block:: python + + # update the attributes of a resource + project = gl.projects.get(1) + project.wall_enabled = False + # don't forget to apply your changes on the server: + project.save() + + # delete the resource + project.delete() + + +Some ``GitlabObject``-derived classes provide additional methods, allowing more +actions on the GitLab resources. For example: + +.. code-block:: python + + # get a tarball of the git repository + project = gl.projects.get(1) + project.archive() + +Pagination +========== + +You can use pagination to go throught long lists: + +.. code-block:: python + + ten_first_groups = gl.groups.list(page=0, per_page=10) + +Use the ``all`` parameter to get all the items: + +.. code-block:: python + + all_groups = gl.groups.list(all=True) diff --git a/docs/api/gitlab.rst b/docs/api/gitlab.rst index 0ad985eba..296a1e3bd 100644 --- a/docs/api/gitlab.rst +++ b/docs/api/gitlab.rst @@ -1,5 +1,6 @@ -API/gitlab-module -================= +############################ +``gitlab`` API documentation +############################ .. automodule:: gitlab :members: diff --git a/docs/cli.rst b/docs/cli.rst index fc4b7b245..ca19214dc 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -6,6 +6,8 @@ with GitLab servers. It uses a configuration file to define how to connect to the servers. +.. _cli_configuration: + Configuration ============= diff --git a/docs/index.rst b/docs/index.rst index 8ddf9337b..3b9178be1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,7 +13,7 @@ Contents: install cli - usage + api-usage api/gitlab diff --git a/docs/usage.rst b/docs/usage.rst deleted file mode 100644 index 1f0cbd555..000000000 --- a/docs/usage.rst +++ /dev/null @@ -1,4 +0,0 @@ -Usage -===== - -Document here how to use python-gitlab library. From dc0099d7901bd381fabadb8be77b93e7258454b3 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sun, 3 Jan 2016 17:51:01 +0100 Subject: [PATCH 23/37] README update --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 7fd0b0ceb..3516f7a60 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,7 @@ https://github.com/gpocentek/python-gitlab/issues ## Documentation -Work In Progress: http://python-gitlab.readthedocs.org/en/latest/ - -Patches are welcome! +See http://python-gitlab.readthedocs.org/en/stable/ ## Code snippet From cedf080ff8553b6ef5cd7995f5ab3608aaeb3793 Mon Sep 17 00:00:00 2001 From: fgouteroux Date: Wed, 6 Jan 2016 14:25:28 +0100 Subject: [PATCH 24/37] add fork project support --- gitlab/__init__.py | 7 +++++++ gitlab/objects.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/gitlab/__init__.py b/gitlab/__init__.py index b2d8cb6de..badab534d 100644 --- a/gitlab/__init__.py +++ b/gitlab/__init__.py @@ -444,6 +444,13 @@ def UserProject(self, id=None, **kwargs): """ return UserProject._get_list_or_object(self, id, **kwargs) + def ProjectFork(self, id=None, **kwargs): + """Fork a project for a user. + + id must be a dict. + """ + return ProjectFork._get_list_or_object(self, id, **kwargs) + def _list_projects(self, url, **kwargs): r = self._raw_get(url, **kwargs) raise_error_from_response(r, GitlabListError) diff --git a/gitlab/objects.py b/gitlab/objects.py index 26896f243..db21eafc4 100644 --- a/gitlab/objects.py +++ b/gitlab/objects.py @@ -545,6 +545,15 @@ class ProjectEventManager(BaseManager): obj_cls = ProjectEvent +class ProjectFork(GitlabObject): + _url = '/projects/fork/%(project_id)s' + canUpdate = False + canDelete = False + canList = False + canGet = False + requiredUrlAttrs = ['project_id'] + + class ProjectHook(GitlabObject): _url = '/projects/%(project_id)s/hooks' requiredUrlAttrs = ['project_id'] @@ -956,6 +965,25 @@ def delete_file(self, path, branch, message, **kwargs): r = self.gitlab._raw_delete(url, **kwargs) raise_error_from_response(r, GitlabDeleteError) + def create_fork_relation(self, forked_from_id): + """Create a forked from/to relation between existing projects. + + Args: + forked_from_id (int): The ID of the project that was forked from + + Raises: + GitlabCreateError: Operation failed + GitlabConnectionError: Connection to GitLab-server failed + """ + url = "/projects/%s/fork/%s" % (self.id, forked_from_id) + r = self.gitlab._raw_post(url) + raise_error_from_response(r, GitlabCreateError, 201) + + def delete_fork_relation(self): + url = "/projects/%s/fork" % self.id + r = self.gitlab._raw_delete(url) + raise_error_from_response(r, GitlabDeleteError) + class TeamMember(GitlabObject): _url = '/user_teams/%(team_id)s/members' From 2bf9794c81487883c346850a79d6b7db1295fd95 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Fri, 8 Jan 2016 21:42:47 +0100 Subject: [PATCH 25/37] Deprecate the "old" Gitlab methods Update the associated unit tests. --- gitlab/__init__.py | 15 +++++++++++++ gitlab/tests/test_gitlab.py | 42 +++++++++++++++++++++++++------------ 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/gitlab/__init__.py b/gitlab/__init__.py index b2d8cb6de..64d45e5d1 100644 --- a/gitlab/__init__.py +++ b/gitlab/__init__.py @@ -421,6 +421,8 @@ def Hook(self, id=None, **kwargs): object is NOT saved on the server. Use the save() method on the object to write it on the server. """ + warnings.warn("`Hook` is deprecated, use `hooks` instead", + DeprecationWarning) return Hook._get_list_or_object(self, id, **kwargs) def Project(self, id=None, **kwargs): @@ -435,6 +437,8 @@ def Project(self, id=None, **kwargs): object is NOT saved on the server. Use the save() method on the object to write it on the server. """ + warnings.warn("`Project` is deprecated, use `projects` instead", + DeprecationWarning) return Project._get_list_or_object(self, id, **kwargs) def UserProject(self, id=None, **kwargs): @@ -442,6 +446,9 @@ def UserProject(self, id=None, **kwargs): id must be a dict. """ + warnings.warn("`UserProject` is deprecated, " + "use `user_projects` instead", + DeprecationWarning) return UserProject._get_list_or_object(self, id, **kwargs) def _list_projects(self, url, **kwargs): @@ -484,6 +491,8 @@ def Group(self, id=None, **kwargs): save() method on the object to write it on the server. kwargs: Arbitrary keyword arguments """ + warnings.warn("`Group` is deprecated, use `groups` instead", + DeprecationWarning) return Group._get_list_or_object(self, id, **kwargs) def Issue(self, id=None, **kwargs): @@ -492,6 +501,8 @@ def Issue(self, id=None, **kwargs): Does not support creation or getting a single issue unlike other methods in this class yet. """ + warnings.warn("`Issue` is deprecated, use `issues` instead", + DeprecationWarning) return Issue._get_list_or_object(self, id, **kwargs) def User(self, id=None, **kwargs): @@ -506,6 +517,8 @@ def User(self, id=None, **kwargs): object is NOT saved on the server. Use the save() method on the object to write it on the server. """ + warnings.warn("`User` is deprecated, use `users` instead", + DeprecationWarning) return User._get_list_or_object(self, id, **kwargs) def Team(self, id=None, **kwargs): @@ -520,4 +533,6 @@ def Team(self, id=None, **kwargs): object is NOT saved on the server. Use the save() method on the object to write it on the server. """ + warnings.warn("`Team` is deprecated, use `teams` instead", + DeprecationWarning) return Team._get_list_or_object(self, id, **kwargs) diff --git a/gitlab/tests/test_gitlab.py b/gitlab/tests/test_gitlab.py index 0fe73e107..dc31875d5 100644 --- a/gitlab/tests/test_gitlab.py +++ b/gitlab/tests/test_gitlab.py @@ -620,7 +620,7 @@ def resp_cont(url, request): self.assertEqual(proj.id, 1) self.assertEqual(proj.name, "testproject") - def test_Hook(self): + def test_hooks(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v3/hooks/1", method="get") def resp_get_hook(url, request): @@ -629,12 +629,12 @@ def resp_get_hook(url, request): return response(200, content, headers, None, 5, request) with HTTMock(resp_get_hook): - data = self.gl.Hook(id=1) + data = self.gl.hooks.get(1) self.assertEqual(type(data), Hook) self.assertEqual(data.url, "testurl") self.assertEqual(data.id, 1) - def test_Project(self): + def test_projects(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v3/projects/1", method="get") def resp_get_project(url, request): @@ -643,12 +643,12 @@ def resp_get_project(url, request): return response(200, content, headers, None, 5, request) with HTTMock(resp_get_project): - data = self.gl.Project(id=1) + data = self.gl.projects.get(1) self.assertEqual(type(data), Project) self.assertEqual(data.name, "name") self.assertEqual(data.id, 1) - def test_UserProject(self): + def test_userprojects(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v3/projects/user/2", method="get") def resp_get_userproject(url, request): @@ -657,10 +657,10 @@ def resp_get_userproject(url, request): return response(200, content, headers, None, 5, request) with HTTMock(resp_get_userproject): - self.assertRaises(NotImplementedError, self.gl.UserProject, id=1, - user_id=2) + self.assertRaises(NotImplementedError, self.gl.user_projects.get, + 1, user_id=2) - def test_Group(self): + def test_groups(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v3/groups/1", method="get") def resp_get_group(url, request): @@ -670,13 +670,13 @@ def resp_get_group(url, request): return response(200, content, headers, None, 5, request) with HTTMock(resp_get_group): - data = self.gl.Group(id=1) + data = self.gl.groups.get(1) self.assertEqual(type(data), Group) self.assertEqual(data.name, "name") self.assertEqual(data.path, "path") self.assertEqual(data.id, 1) - def test_Issue(self): + def test_issues(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v3/issues", method="get") def resp_get_issue(url, request): @@ -687,11 +687,11 @@ def resp_get_issue(url, request): return response(200, content, headers, None, 5, request) with HTTMock(resp_get_issue): - data = self.gl.Issue(id=2) + data = self.gl.issues.get(2) self.assertEqual(data.id, 2) self.assertEqual(data.name, 'other_name') - def test_User(self): + def test_users(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v3/users/1", method="get") def resp_get_user(url, request): @@ -702,7 +702,23 @@ def resp_get_user(url, request): return response(200, content, headers, None, 5, request) with HTTMock(resp_get_user): - user = self.gl.User(id=1) + user = self.gl.users.get(1) self.assertEqual(type(user), User) self.assertEqual(user.name, "name") self.assertEqual(user.id, 1) + + def test_teams(self): + @urlmatch(scheme="http", netloc="localhost", + path="/api/v3/user_teams/1", method="get") + def resp_get_group(url, request): + headers = {'content-type': 'application/json'} + content = '{"name": "name", "id": 1, "path": "path"}' + content = content.encode('utf-8') + return response(200, content, headers, None, 5, request) + + with HTTMock(resp_get_group): + data = self.gl.teams.get(1) + self.assertEqual(type(data), Team) + self.assertEqual(data.name, "name") + self.assertEqual(data.path, "path") + self.assertEqual(data.id, 1) From e8631c1d505690a04704a9c19ba4a2d8564c6ef4 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Fri, 8 Jan 2016 22:05:37 +0100 Subject: [PATCH 26/37] Create a manager for ProjectFork objects --- gitlab/__init__.py | 8 +------- gitlab/objects.py | 5 +++++ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/gitlab/__init__.py b/gitlab/__init__.py index 4de778a02..02fb1754f 100644 --- a/gitlab/__init__.py +++ b/gitlab/__init__.py @@ -88,6 +88,7 @@ def __init__(self, url, private_token=None, self.project_commits = ProjectCommitManager(self) self.project_keys = ProjectKeyManager(self) self.project_events = ProjectEventManager(self) + self.project_forks = ProjectForkManager(self) self.project_hooks = ProjectHookManager(self) self.project_issue_notes = ProjectIssueNoteManager(self) self.project_issues = ProjectIssueManager(self) @@ -451,13 +452,6 @@ def UserProject(self, id=None, **kwargs): DeprecationWarning) return UserProject._get_list_or_object(self, id, **kwargs) - def ProjectFork(self, id=None, **kwargs): - """Fork a project for a user. - - id must be a dict. - """ - return ProjectFork._get_list_or_object(self, id, **kwargs) - def _list_projects(self, url, **kwargs): r = self._raw_get(url, **kwargs) raise_error_from_response(r, GitlabListError) diff --git a/gitlab/objects.py b/gitlab/objects.py index db21eafc4..3637fe8d7 100644 --- a/gitlab/objects.py +++ b/gitlab/objects.py @@ -554,6 +554,10 @@ class ProjectFork(GitlabObject): requiredUrlAttrs = ['project_id'] +class ProjectForkManager(BaseManager): + obj_cls = ProjectFork + + class ProjectHook(GitlabObject): _url = '/projects/%(project_id)s/hooks' requiredUrlAttrs = ['project_id'] @@ -798,6 +802,7 @@ class Project(GitlabObject): ('commits', ProjectCommitManager, [('project_id', 'id')]), ('events', ProjectEventManager, [('project_id', 'id')]), ('files', ProjectFileManager, [('project_id', 'id')]), + ('forks', ProjectForkManager, [('project_id', 'id')]), ('hooks', ProjectHookManager, [('project_id', 'id')]), ('keys', ProjectKeyManager, [('project_id', 'id')]), ('issues', ProjectIssueManager, [('project_id', 'id')]), From e48e14948f886a7bb71b22f82d71c2572a09341e Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Fri, 8 Jan 2016 22:14:53 +0100 Subject: [PATCH 27/37] CLI: fix the discovery of possible actions --- gitlab/cli.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gitlab/cli.py b/gitlab/cli.py index 1b8356c15..53f32af0e 100644 --- a/gitlab/cli.py +++ b/gitlab/cli.py @@ -66,8 +66,7 @@ def clsToWhat(cls): def populate_sub_parser_by_class(cls, sub_parser): for action_name in ACTIONS: attr = 'can' + action_name.capitalize() - y = getattr(cls, attr) or getattr(gitlab.GitlabObject, attr) - if not y: + if not getattr(cls, attr): continue sub_parser_action = sub_parser.add_parser(action_name) [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), From 689ecae70585e79c281224162a0ba2ab3921242a Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Fri, 8 Jan 2016 22:35:30 +0100 Subject: [PATCH 28/37] Implement ProjectManager search/list methods The existing Gitlab methods are deprecated. Unit tests have been added. --- gitlab/__init__.py | 9 +++++ gitlab/objects.py | 27 ++++++++++++++ gitlab/tests/test_manager.py | 69 ++++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+) diff --git a/gitlab/__init__.py b/gitlab/__init__.py index 02fb1754f..2c2f01d83 100644 --- a/gitlab/__init__.py +++ b/gitlab/__init__.py @@ -469,14 +469,23 @@ def search_projects(self, query, **kwargs): Returns a list of matching projects. """ + warnings.warn("`search_projects()` is deprecated, " + "use `projects.search()` instead", + DeprecationWarning) return self._list_projects("/projects/search/" + query, **kwargs) def all_projects(self, **kwargs): """Lists all the projects (need admin rights).""" + warnings.warn("`all_projects()` is deprecated, " + "use `projects.all()` instead", + DeprecationWarning) return self._list_projects("/projects/all", **kwargs) def owned_projects(self, **kwargs): """Lists owned projects.""" + warnings.warn("`owned_projects()` is deprecated, " + "use `projects.owned()` instead", + DeprecationWarning) return self._list_projects("/projects/owned", **kwargs) def Group(self, id=None, **kwargs): diff --git a/gitlab/objects.py b/gitlab/objects.py index 3637fe8d7..aaaadbbdc 100644 --- a/gitlab/objects.py +++ b/gitlab/objects.py @@ -1016,6 +1016,33 @@ class UserProject(GitlabObject): class ProjectManager(BaseManager): obj_cls = Project + def _custom_list(self, url, **kwargs): + r = self.gitlab._raw_get(url, **kwargs) + raise_error_from_response(r, GitlabListError) + + l = [] + for o in r.json(): + p = Project(self, o) + p._from_api = True + l.append(p) + + return l + + def search(self, query, **kwargs): + """Searches projects by name. + + Returns a list of matching projects. + """ + return self._custom_list("/projects/search/" + query, **kwargs) + + def all(self, **kwargs): + """Lists all the projects (need admin rights).""" + return self._custom_list("/projects/all", **kwargs) + + def owned(self, **kwargs): + """Lists owned projects.""" + return self._custom_list("/projects/owned", **kwargs) + class UserProjectManager(BaseManager): obj_cls = UserProject diff --git a/gitlab/tests/test_manager.py b/gitlab/tests/test_manager.py index 837e0136a..bba278f4a 100644 --- a/gitlab/tests/test_manager.py +++ b/gitlab/tests/test_manager.py @@ -145,3 +145,72 @@ def resp_post(url, request): self.assertIsInstance(fake_obj, FakeObject) self.assertEqual(fake_obj.id, 1) self.assertEqual(fake_obj.name, "fake_name") + + def test_project_manager_owned(self): + mgr = ProjectManager(self.gitlab) + + @urlmatch(scheme="http", netloc="localhost", + path="/api/v3/projects/owned", method="get") + def resp_get_all(url, request): + headers = {'content-type': 'application/json'} + content = ('[{"name": "name1", "id": 1}, ' + '{"name": "name2", "id": 2}]') + content = content.encode("utf-8") + return response(200, content, headers, None, 5, request) + + with HTTMock(resp_get_all): + data = mgr.owned() + self.assertEqual(type(data), list) + self.assertEqual(2, len(data)) + self.assertEqual(type(data[0]), Project) + self.assertEqual(type(data[1]), Project) + self.assertEqual(data[0].name, "name1") + self.assertEqual(data[1].name, "name2") + self.assertEqual(data[0].id, 1) + self.assertEqual(data[1].id, 2) + + def test_project_manager_all(self): + mgr = ProjectManager(self.gitlab) + + @urlmatch(scheme="http", netloc="localhost", + path="/api/v3/projects/all", method="get") + def resp_get_all(url, request): + headers = {'content-type': 'application/json'} + content = ('[{"name": "name1", "id": 1}, ' + '{"name": "name2", "id": 2}]') + content = content.encode("utf-8") + return response(200, content, headers, None, 5, request) + + with HTTMock(resp_get_all): + data = mgr.all() + self.assertEqual(type(data), list) + self.assertEqual(2, len(data)) + self.assertEqual(type(data[0]), Project) + self.assertEqual(type(data[1]), Project) + self.assertEqual(data[0].name, "name1") + self.assertEqual(data[1].name, "name2") + self.assertEqual(data[0].id, 1) + self.assertEqual(data[1].id, 2) + + def test_project_manager_search(self): + mgr = ProjectManager(self.gitlab) + + @urlmatch(scheme="http", netloc="localhost", + path="/api/v3/projects/search/foo", method="get") + def resp_get_all(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_all): + data = mgr.search('foo') + self.assertEqual(type(data), list) + self.assertEqual(2, len(data)) + self.assertEqual(type(data[0]), Project) + self.assertEqual(type(data[1]), Project) + self.assertEqual(data[0].name, "foo1") + self.assertEqual(data[1].name, "foo2") + self.assertEqual(data[0].id, 1) + self.assertEqual(data[1].id, 2) From 0ae315a4d1d154122208883bd006b2b882cb5113 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sat, 9 Jan 2016 00:37:46 +0100 Subject: [PATCH 29/37] unit tests for config parser --- gitlab/config.py | 5 +- gitlab/tests/test_config.py | 111 ++++++++++++++++++++++++++++++++++++ test-requirements.txt | 1 + 3 files changed, 113 insertions(+), 4 deletions(-) create mode 100644 gitlab/tests/test_config.py diff --git a/gitlab/config.py b/gitlab/config.py index c9dc5aa3f..4d0abb841 100644 --- a/gitlab/config.py +++ b/gitlab/config.py @@ -15,12 +15,9 @@ # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . -try: - import ConfigParser as configparser -except ImportError: - import configparser import os +from six.moves import configparser _DEFAULT_FILES = [ '/etc/python-gitlab.cfg', diff --git a/gitlab/tests/test_config.py b/gitlab/tests/test_config.py new file mode 100644 index 000000000..2b9cce412 --- /dev/null +++ b/gitlab/tests/test_config.py @@ -0,0 +1,111 @@ +# -*- 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 . + +try: + import unittest +except ImportError: + import unittest2 as unittest + +import mock +import six + +from gitlab import config + + +valid_config = u"""[global] +default = one +ssl_verify = true +timeout = 2 + +[one] +url = http://one.url +private_token = ABCDEF + +[two] +url = https://two.url +private_token = GHIJKL +ssl_verify = false +timeout = 10 +""" + +no_default_config = u"""[global] +[there] +url = http://there.url +private_token = ABCDEF +""" + +missing_attr_config = u"""[global] +[one] +url = http://one.url + +[two] +private_token = ABCDEF + +[three] +meh = hem +""" + + +class TestConfigParser(unittest.TestCase): + @mock.patch('six.moves.builtins.open') + def test_invalid_id(self, m_open): + fd = six.StringIO(no_default_config) + fd.close = mock.Mock(return_value=None) + m_open.return_value = fd + self.assertRaises(config.GitlabIDError, config.GitlabConfigParser) + + fd = six.StringIO(valid_config) + fd.close = mock.Mock(return_value=None) + m_open.return_value = fd + self.assertRaises(config.GitlabDataError, + config.GitlabConfigParser, + gitlab_id='not_there') + + @mock.patch('six.moves.builtins.open') + def test_invalid_data(self, m_open): + fd = six.StringIO(missing_attr_config) + fd.close = mock.Mock(return_value=None) + m_open.return_value = fd + self.assertRaises(config.GitlabDataError, config.GitlabConfigParser, + gitlab_id='one') + self.assertRaises(config.GitlabDataError, config.GitlabConfigParser, + gitlab_id='two') + self.assertRaises(config.GitlabDataError, config.GitlabConfigParser, + gitlab_id='three') + + @mock.patch('six.moves.builtins.open') + def test_valid_data(self, m_open): + fd = six.StringIO(valid_config) + fd.close = mock.Mock(return_value=None) + m_open.return_value = fd + + cp = config.GitlabConfigParser() + self.assertEqual("one", cp.gitlab_id) + self.assertEqual("http://one.url", cp.url) + self.assertEqual("ABCDEF", cp.token) + self.assertEqual(2, cp.timeout) + self.assertEqual(True, cp.ssl_verify) + + fd = six.StringIO(valid_config) + fd.close = mock.Mock(return_value=None) + m_open.return_value = fd + cp = config.GitlabConfigParser(gitlab_id="two") + self.assertEqual("two", cp.gitlab_id) + self.assertEqual("https://two.url", cp.url) + self.assertEqual("GHIJKL", cp.token) + self.assertEqual(10, cp.timeout) + self.assertEqual(False, cp.ssl_verify) diff --git a/test-requirements.txt b/test-requirements.txt index c16328a76..87b1721f1 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -2,5 +2,6 @@ discover testrepository hacking>=0.9.2,<0.10 httmock +mock sphinx>=1.1.2,!=1.2.0,<1.3 sphinxcontrib-napoleon From 5513d0f52cd488b14c94389a09d01877fa5596e0 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sat, 9 Jan 2016 08:36:18 +0100 Subject: [PATCH 30/37] Add support for groups search Factorize the code to avoid duplication with the ProjectManager class. Implement unit tests for the group search. Original patchh from Daniel Serodio (PR #55). --- gitlab/objects.py | 39 ++++++++++++++++++++++-------------- gitlab/tests/test_manager.py | 23 +++++++++++++++++++++ 2 files changed, 47 insertions(+), 15 deletions(-) diff --git a/gitlab/objects.py b/gitlab/objects.py index aaaadbbdc..ccd4f0a5c 100644 --- a/gitlab/objects.py +++ b/gitlab/objects.py @@ -70,6 +70,18 @@ def create(self, data, **kwargs): raise NotImplementedError return self.obj_cls.create(self.gitlab, data, **kwargs) + 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, j) + o._from_api = True + l.append(o) + + return l + class GitlabObject(object): """Base class for all classes that interface with GitLab @@ -433,6 +445,14 @@ def transfer_project(self, id, **kwargs): class GroupManager(BaseManager): obj_cls = Group + def search(self, query, **kwargs): + """Searches groups by name. + + Returns a list of matching groups. + """ + url = '/groups?search=' + query + return self._custom_list(url, Group, **kwargs) + class Hook(GitlabObject): _url = '/hooks' @@ -1016,32 +1036,21 @@ class UserProject(GitlabObject): class ProjectManager(BaseManager): obj_cls = Project - def _custom_list(self, url, **kwargs): - r = self.gitlab._raw_get(url, **kwargs) - raise_error_from_response(r, GitlabListError) - - l = [] - for o in r.json(): - p = Project(self, o) - p._from_api = True - l.append(p) - - return l - def search(self, query, **kwargs): """Searches projects by name. Returns a list of matching projects. """ - return self._custom_list("/projects/search/" + query, **kwargs) + return self._custom_list("/projects/search/" + query, Project, + **kwargs) def all(self, **kwargs): """Lists all the projects (need admin rights).""" - return self._custom_list("/projects/all", **kwargs) + return self._custom_list("/projects/all", Project, **kwargs) def owned(self, **kwargs): """Lists owned projects.""" - return self._custom_list("/projects/owned", **kwargs) + return self._custom_list("/projects/owned", Project, **kwargs) class UserProjectManager(BaseManager): diff --git a/gitlab/tests/test_manager.py b/gitlab/tests/test_manager.py index bba278f4a..041537c33 100644 --- a/gitlab/tests/test_manager.py +++ b/gitlab/tests/test_manager.py @@ -214,3 +214,26 @@ def resp_get_all(url, request): 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) + + @urlmatch(scheme="http", netloc="localhost", path="/api/v3/groups", + 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]), Group) + self.assertEqual(type(data[1]), Group) + self.assertEqual(data[0].name, "foo1") + self.assertEqual(data[1].name, "foo2") + self.assertEqual(data[0].id, 1) + self.assertEqual(data[1].id, 2) From 5ea6d0ab7a69000be8ed01eaf2c5a49a79e33215 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sat, 9 Jan 2016 08:47:26 +0100 Subject: [PATCH 31/37] implement group search in CLI --- gitlab/cli.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/gitlab/cli.py b/gitlab/cli.py index 53f32af0e..ab4b17c65 100644 --- a/gitlab/cli.py +++ b/gitlab/cli.py @@ -47,6 +47,7 @@ gitlab.Project: {SEARCH: {'requiredAttrs': ['query']}, OWNED: {'requiredAttrs': []}, ALL: {'requiredAttrs': []}}, + gitlab.Group: {SEARCH: {'requiredAttrs': ['query']}}, } @@ -209,23 +210,30 @@ def do_update(cls, gl, what, args): return o +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_project_search(gl, what, args): try: - return gl.search_projects(args['query']) + return gl.projects.search(args['query']) except Exception as e: die("Impossible to search projects (%s)" % str(e)) def do_project_all(gl, what, args): try: - return gl.all_projects() + return gl.projects.all() except Exception as e: die("Impossible to list all projects (%s)" % str(e)) def do_project_owned(gl, what, args): try: - return gl.owned_projects() + return gl.projects.owned() except Exception as e: die("Impossible to list owned projects (%s)" % str(e)) @@ -312,10 +320,15 @@ def main(): getattr(o, action)() elif action == SEARCH: - if cls != gitlab.Project: + + 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 do_project_search(gl, what, args): + for o in l: o.display(verbose) elif action == OWNED: From 610bde8da2430d95efb6881246ae1decff43c2ca Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sat, 9 Jan 2016 11:42:56 +0100 Subject: [PATCH 32/37] document the API migration from 0.10 --- docs/index.rst | 1 + docs/upgrade-from-0.10.rst | 125 +++++++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 docs/upgrade-from-0.10.rst diff --git a/docs/index.rst b/docs/index.rst index 3b9178be1..c952a2114 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -14,6 +14,7 @@ Contents: install cli api-usage + upgrade-from-0.10 api/gitlab diff --git a/docs/upgrade-from-0.10.rst b/docs/upgrade-from-0.10.rst new file mode 100644 index 000000000..7ff80ab38 --- /dev/null +++ b/docs/upgrade-from-0.10.rst @@ -0,0 +1,125 @@ +############################################# +Upgrading from python-gitlab 0.10 and earlier +############################################# + +``python-gitlab`` 0.11 introduces new objects which make the API cleaner and +easier to use. The feature set is unchanged but some methods have been +deprecated in favor of the new manager objects. + +Deprecated methods will be remove in a future release. + +Gitlab object migration +======================= + +The objects constructor methods are deprecated: + +* ``Hook()`` +* ``Project()`` +* ``UserProject()`` +* ``Group()`` +* ``Issue()`` +* ``User()`` +* ``Team()`` + +Use the new managers objects instead. For example: + +.. code-block:: python + + # Deprecated syntax + p1 = gl.Project({'name': 'myCoolProject'}) + p1.save() + p2 = gl.Project(id=1) + p_list = gl.Project() + + # New syntax + p1 = gl.projects.create({'name': 'myCoolProject'}) + p2 = gl.projects.get(1) + p_list = gl.projects.list() + +The following methods are also deprecated: + +* ``search_projects()`` +* ``owned_projects()`` +* ``all_projects()`` + +Use the ``projects`` manager instead: + +.. code-block:: python + + # Deprecated syntax + l1 = gl.search_projects('whatever') + l2 = gl.owned_projects() + l3 = gl.all_projects() + + # New syntax + l1 = gl.projects.search('whatever') + l2 = gl.projects.owned() + l3 = gl.projects.all() + +GitlabObject objects migration +============================== + +The following constructor methods are deprecated in favor of the matching +managers: + +.. list-table:: + :header-rows: 1 + + * - Deprecated method + - Matching manager + * - ``User.Key()`` + - ``User.keys`` + * - ``CurrentUser.Key()`` + - ``CurrentUser.keys`` + * - ``Group.Member()`` + - ``Group.members`` + * - ``ProjectIssue.Note()`` + - ``ProjectIssue.notes`` + * - ``ProjectMergeRequest.Note()`` + - ``ProjectMergeRequest.notes`` + * - ``ProjectSnippet.Note()`` + - ``ProjectSnippet.notes`` + * - ``Project.Branch()`` + - ``Project.branches`` + * - ``Project.Commit()`` + - ``Project.commits`` + * - ``Project.Event()`` + - ``Project.events`` + * - ``Project.File()`` + - ``Project.files`` + * - ``Project.Hook()`` + - ``Project.hooks`` + * - ``Project.Key()`` + - ``Project.keys`` + * - ``Project.Issue()`` + - ``Project.issues`` + * - ``Project.Label()`` + - ``Project.labels`` + * - ``Project.Member()`` + - ``Project.members`` + * - ``Project.MergeRequest()`` + - ``Project.mergerequests`` + * - ``Project.Milestone()`` + - ``Project.milestones`` + * - ``Project.Note()`` + - ``Project.notes`` + * - ``Project.Snippet()`` + - ``Project.snippets`` + * - ``Project.Tag()`` + - ``Project.tags`` + * - ``Team.Member()`` + - ``Team.members`` + * - ``Team.Project()`` + - ``Team.projects`` + +For example: + +.. code-block:: python + + # Deprecated syntax + p = gl.Project(id=2) + issues = p.Issue() + + # New syntax + p = gl.projects.get(2) + issues = p.issues.list() From 9c74442f8ad36786b06e8cfdcda7919d382ad31f Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sat, 9 Jan 2016 11:52:26 +0100 Subject: [PATCH 33/37] add missing import --- gitlab/objects.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gitlab/objects.py b/gitlab/objects.py index ccd4f0a5c..b430795a6 100644 --- a/gitlab/objects.py +++ b/gitlab/objects.py @@ -21,6 +21,7 @@ import itertools import json import sys +import warnings import six From 3fd64df1c731b516beb8fcfc181b0cdbf31c4776 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sat, 9 Jan 2016 12:09:47 +0100 Subject: [PATCH 34/37] Update ChangeLog for 0.11 --- ChangeLog | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/ChangeLog b/ChangeLog index 7b5e5fc0b..f41df7b63 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,19 @@ +Version 0.11 + + * functional_tests.sh: support python 2 and 3 + * Add a get method for GitlabObject + * CLI: Add the -g short option for --gitlab + * Provide a create method for GitlabObject's + * Rename the _created attribute _from_api + * More unit tests + * CLI: fix error when arguments are missing (python 3) + * Remove deprecated methods + * Implement managers to get access to resources + * Documentation improvements + * Add fork project support + * Deprecate the "old" Gitlab methods + * Add support for groups search + Version 0.10 * Implement pagination for list() (#63) From 0163499bace58a5487f4f09bef2f656fdb541871 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sat, 9 Jan 2016 12:15:50 +0100 Subject: [PATCH 35/37] Update AUTHORS --- AUTHORS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AUTHORS b/AUTHORS index 286a96d39..c2cd1bccf 100644 --- a/AUTHORS +++ b/AUTHORS @@ -20,3 +20,5 @@ Jason Antman Stefan Klug pa4373 Colin D Bennett +François Gouteroux +Daniel Serodio From ca44878787a3e907ea35fd4adbb0a5c3020b44ed Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sat, 9 Jan 2016 15:27:50 +0100 Subject: [PATCH 36/37] Bump version And update copyright years. --- docs/conf.py | 2 +- gitlab/__init__.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index ed0285d12..bbf3c67f6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -61,7 +61,7 @@ # General information about the project. project = 'python-gitlab' -copyright = '2013-2015, Gauvain Pocentek, Mika Mäenpää' +copyright = '2013-2016, Gauvain Pocentek, Mika Mäenpää' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/gitlab/__init__.py b/gitlab/__init__.py index 2c2f01d83..b70d0a89d 100644 --- a/gitlab/__init__.py +++ b/gitlab/__init__.py @@ -30,11 +30,11 @@ from gitlab.objects import * # noqa __title__ = 'python-gitlab' -__version__ = '0.10' +__version__ = '0.11' __author__ = 'Gauvain Pocentek' __email__ = 'gauvain@pocentek.net' __license__ = 'LGPL3' -__copyright__ = 'Copyright 2013-2015 Gauvain Pocentek' +__copyright__ = 'Copyright 2013-2016 Gauvain Pocentek' warnings.simplefilter('always', DeprecationWarning) From 3e8cf4e9ea59b97bb1703b9cee1c3a3d9e6c7c42 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sat, 9 Jan 2016 15:41:00 +0100 Subject: [PATCH 37/37] Rewrite the README And link to the docs on RTD. --- MANIFEST.in | 2 +- README.md | 169 ---------------------------------------------------- README.rst | 37 ++++++++++++ 3 files changed, 38 insertions(+), 170 deletions(-) delete mode 100644 README.md create mode 100644 README.rst diff --git a/MANIFEST.in b/MANIFEST.in index 1170660c4..29a34fdcb 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,3 @@ -include README.md COPYING AUTHORS ChangeLog requirements.txt test-requirements.txt +include COPYING AUTHORS ChangeLog requirements.txt test-requirements.txt include tox.ini .testr.conf recursive-include tools * diff --git a/README.md b/README.md deleted file mode 100644 index 3516f7a60..000000000 --- a/README.md +++ /dev/null @@ -1,169 +0,0 @@ -## Python GitLab - -python-gitlab is a Python package providing access to the GitLab server API. - -It supports the v3 api of GitLab. - -A CLI tool is also provided (called **gitlab**). - -## Installation - -### Requirements - -python-gitlab depends on: - -* [python-requests](http://docs.python-requests.org/en/latest/) -* [six](https://pythonhosted.org/six/) - -### Install with pip - -````` -pip install python-gitlab -````` - -## State - -python-gitlab is considered stable. - -## Bug reports - -Please report bugs and feature requests at -https://github.com/gpocentek/python-gitlab/issues - -## Documentation - -See http://python-gitlab.readthedocs.org/en/stable/ - -## Code snippet - -`````python -# See https://github.com/gitlabhq/gitlabhq/tree/master/doc/api for the source. -from gitlab import Gitlab - -# Register a connection to a gitlab instance, using its URL and a user private -# token -gl = Gitlab('http://192.168.123.107', 'JVNSESs8EwWRx5yDxM5q') -# Connect to get the current user -gl.auth() -# Print the user informations -print(gl.user) - -# Get a list of projects -for p in gl.projects.list(): - print(p.name) - # get associated issues - issues = p.issues.list() - for issue in issues: - closed = 0 if not issue.closed else 1 - print(" %d => %s (closed: %d)" % (issue.id, issue.title, closed)) - # and close them all - issue.state_event = "close" - issue.save() - -# Get the first 10 groups (pagination) -for g in gl.groups.list(page=1, per_page=10): - print(g) - -# To use pagination and retrieve all the items -for g in gl.groups.list(all=True): - print(g) - -# Create a new project (as another_user) -p = gl.project.create({'name': 'myCoolProject', 'wiki_enabled': False}, - sudo="another_user") -print(p) -````` - -## Command line use - -To use the command line tool, you need to define which GitLab server(s) can be -accessed. This can be done in 2 files: - -* /etc/python-gitlab.cfg -* ~/.python-gitlab.cfg - -Here's an example of the syntax: - -````` -[global] -# required setting -default = local - -# optional settings -ssl_verify = true -timeout = 5 - -[local] -url = http://10.0.3.2:8080 -# get the private token from the gitlab web interface -private_token = vTbFeqJYCY3sibBP7BZM - -[remote] -url = https://some.whe.re -private_token = thisisaprivatetoken -ssl_verify = false -````` - -The [global] section defines which server is accessed by default. -Each other section defines how to access a server. Only private token -authentication is supported (not user/password). - -The `ssl_verify` option defines if the server SSL certificate should be -validated (use false for self signed certificates, only useful with https). - -The `timeout` option defines after how many seconds a request to the Gitlab -server should be abandonned. - -You can choose a different server than the default one at run time: - -````` -gitlab --gitlab remote [command] -````` - -gitlab always requires 2 mandatory arguments. - -The first argument is the object type on which the program will act, the second -one is the action: - -````` -gitlab project list -````` - -Get help with: - -````` -# global help -gitlab --help - -# object help -gitlab project --help -````` - -Some examples: - -`````bash -# list all the projects: -gitlab project list - -# limit to 5 items per request, display the 1st page only -gitlab project list --page 1 --per-page 5 - -# get a specific project (id 2): -gitlab project get --id 2 - -# get a list of snippets for this project: -gitlab project-issue list --project-id 2 - -# delete a Snippet (id 3): -gitlab project-snippet delete --id 3 --project-id 2 - -# update a Snippet: -gitlab project-snippet update --id 4 --project-id 2 --code "My New Code" - -# create a Snippet: -gitlab project-snippet create --project-id 2 -Impossible to create object (Missing attribute(s): title, file-name, code) - -# oops, let's add the attributes: -gitlab project-snippet create --project-id 2 --title "the title" --file-name "the name" --code "the code" -````` diff --git a/README.rst b/README.rst new file mode 100644 index 000000000..2fe702e69 --- /dev/null +++ b/README.rst @@ -0,0 +1,37 @@ +Python GitLab +============= + +``python-gitlab`` is a Python package providing access to the GitLab server API. + +It supports the v3 api of GitLab, and provides a CLI tool (``gitlab``). + +Installation +============ + +Requirements +------------ + +python-gitlab depends on: + +* `python-requests `_ +* `six `_ + +Install with pip +---------------- + +.. code-block:: console + + pip install python-gitlab + +Bug reports +=========== + +Please report bugs and feature requests at +https://github.com/gpocentek/python-gitlab/issues. + + +Documentation +============= + +The documentation for CLI and API is available on `readthedocs +`_.