From ac430a3cac4be76efc02e4321f7ee88867d28712 Mon Sep 17 00:00:00 2001 From: Jerome Robert Date: Sun, 8 Oct 2017 09:02:34 +0200 Subject: [PATCH 01/29] Fix trigger variables in v4 API (#334) Fix trigger variables in v4 API Close #333 --- gitlab/v4/objects.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index 4bd5aada0..88b4a3be3 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -19,8 +19,6 @@ from __future__ import absolute_import import base64 -import six - from gitlab.base import * # noqa from gitlab import cli from gitlab.exceptions import * # noqa @@ -2080,9 +2078,7 @@ def trigger_pipeline(self, ref, token, variables={}, **kwargs): GitlabCreateError: If the server failed to perform the request """ path = '/projects/%s/trigger/pipeline' % self.get_id() - form = {r'variables[%s]' % k: v for k, v in six.iteritems(variables)} - post_data = {'ref': ref, 'token': token} - post_data.update(form) + post_data = {'ref': ref, 'token': token, 'variables': variables} self.manager.gitlab.http_post(path, post_data=post_data, **kwargs) # see #56 - add file attachment features From 87649035230cc1161a3e8e8e648d4f65f8480ac0 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sun, 8 Oct 2017 08:40:55 +0200 Subject: [PATCH 02/29] Make the delete() method handle / in ids Replace the / with the HTTP %2F as is done with other methods. Closes #337 --- gitlab/mixins.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gitlab/mixins.py b/gitlab/mixins.py index aa529897b..2b58d49e7 100644 --- a/gitlab/mixins.py +++ b/gitlab/mixins.py @@ -235,6 +235,9 @@ def delete(self, id, **kwargs): GitlabAuthenticationError: If authentication is not correct GitlabDeleteError: If the server cannot perform the request """ + import pdb; pdb.set_trace() + if not isinstance(id, int): + id = id.replace('/', '%2F') path = '%s/%s' % (self.path, id) self.gitlab.http_delete(path, **kwargs) From 72664c45baa59507028aeb3986bba42c75c3cbb8 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sun, 8 Oct 2017 08:46:53 +0200 Subject: [PATCH 03/29] [docs] update the file upload samples Closes #335 --- docs/gl_objects/projects.py | 4 ++-- docs/gl_objects/projects.rst | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/gl_objects/projects.py b/docs/gl_objects/projects.py index f0a4d1a66..849d6f4f0 100644 --- a/docs/gl_objects/projects.py +++ b/docs/gl_objects/projects.py @@ -379,7 +379,7 @@ # end project file upload with data # project file upload markdown -uploaded_file = project.upload_file("filename.txt", filedata="data") +uploaded_file = project.upload("filename.txt", filedata="data") issue = project.issues.get(issue_id) issue.notes.create({ "body": "See the attached file: {}".format(uploaded_file["markdown"]) @@ -387,7 +387,7 @@ # project file upload markdown # project file upload markdown custom -uploaded_file = project.upload_file("filename.txt", filedata="data") +uploaded_file = project.upload("filename.txt", filedata="data") issue = project.issues.get(issue_id) issue.notes.create({ "body": "See the [attached file]({})".format(uploaded_file["url"]) diff --git a/docs/gl_objects/projects.rst b/docs/gl_objects/projects.rst index b6cf311c5..7f3d47997 100644 --- a/docs/gl_objects/projects.rst +++ b/docs/gl_objects/projects.rst @@ -781,7 +781,7 @@ Delete a list: :end-before: # end board lists delete -File Uploads +File uploads ============ Reference @@ -790,12 +790,10 @@ Reference * v4 API: + :attr:`gitlab.v4.objects.Project.upload` - + :class:`gitlab.v4.objects.ProjectUpload` * v3 API: + :attr:`gitlab.v3.objects.Project.upload` - + :class:`gitlab.v3.objects.ProjectUpload` * Gitlab API: https://docs.gitlab.com/ce/api/projects.html#upload-a-file From f3f300c493c3a944e57b212088f5719474b98081 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sun, 8 Oct 2017 09:06:56 +0200 Subject: [PATCH 04/29] Tags release description: support / in tag names --- gitlab/v4/objects.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index 88b4a3be3..0d8dffdc7 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -1020,7 +1020,8 @@ def set_release_description(self, description, **kwargs): GitlabCreateError: If the server fails to create the release GitlabUpdateError: If the server fails to update the release """ - path = '%s/%s/release' % (self.manager.path, self.get_id()) + id = self.get_id().replace('/', '%2F') + path = '%s/%s/release' % (self.manager.path, id) data = {'description': description} if self.release is None: try: From 316754dd8290ee80c8c197eb1eca559fce97792e Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sun, 8 Oct 2017 09:55:39 +0200 Subject: [PATCH 05/29] Drop leftover pdb call --- gitlab/mixins.py | 1 - 1 file changed, 1 deletion(-) diff --git a/gitlab/mixins.py b/gitlab/mixins.py index 2b58d49e7..2acc54b72 100644 --- a/gitlab/mixins.py +++ b/gitlab/mixins.py @@ -235,7 +235,6 @@ def delete(self, id, **kwargs): GitlabAuthenticationError: If authentication is not correct GitlabDeleteError: If the server cannot perform the request """ - import pdb; pdb.set_trace() if not isinstance(id, int): id = id.replace('/', '%2F') path = '%s/%s' % (self.path, id) From 5945537c157818483a4a14138619fa6b9341e6b3 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sun, 8 Oct 2017 10:05:16 +0200 Subject: [PATCH 06/29] [docs] improve the labels usage documentation Closes #329 --- docs/gl_objects/labels.py | 10 ++++++++++ docs/gl_objects/labels.rst | 6 ++++++ docs/gl_objects/mrs.py | 3 ++- docs/gl_objects/projects.py | 4 ++-- 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/docs/gl_objects/labels.py b/docs/gl_objects/labels.py index 57892b5d1..a63e295f5 100644 --- a/docs/gl_objects/labels.py +++ b/docs/gl_objects/labels.py @@ -24,3 +24,13 @@ # or label.delete() # end delete + +# use +# Labels are defined as lists in issues and merge requests. The labels must +# exist. +issue = p.issues.create({'title': 'issue title', + 'description': 'issue description', + 'labels': ['foo']}) +issue.labels.append('bar') +issue.save() +# end use diff --git a/docs/gl_objects/labels.rst b/docs/gl_objects/labels.rst index d44421723..3c8034d77 100644 --- a/docs/gl_objects/labels.rst +++ b/docs/gl_objects/labels.rst @@ -52,3 +52,9 @@ Delete a label for a project: .. literalinclude:: labels.py :start-after: # delete :end-before: # end delete + +Managing labels in issues and merge requests: + +.. literalinclude:: labels.py + :start-after: # use + :end-before: # end use diff --git a/docs/gl_objects/mrs.py b/docs/gl_objects/mrs.py index bc30b4342..1e54c80bb 100644 --- a/docs/gl_objects/mrs.py +++ b/docs/gl_objects/mrs.py @@ -13,7 +13,8 @@ # create mr = project.mergerequests.create({'source_branch': 'cool_feature', 'target_branch': 'master', - 'title': 'merge cool feature'}) + 'title': 'merge cool feature', + 'labels': ['label1', 'label2']}) # end create # update diff --git a/docs/gl_objects/projects.py b/docs/gl_objects/projects.py index 849d6f4f0..6ef6069af 100644 --- a/docs/gl_objects/projects.py +++ b/docs/gl_objects/projects.py @@ -384,7 +384,7 @@ issue.notes.create({ "body": "See the attached file: {}".format(uploaded_file["markdown"]) }) -# project file upload markdown +# end project file upload markdown # project file upload markdown custom uploaded_file = project.upload("filename.txt", filedata="data") @@ -392,4 +392,4 @@ issue.notes.create({ "body": "See the [attached file]({})".format(uploaded_file["url"]) }) -# project file upload markdown +# end project file upload markdown custom From d6fa94ef38c638206d1d18bbd6ddf3f56057b1ce Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sun, 8 Oct 2017 10:21:34 +0200 Subject: [PATCH 07/29] Add support for listing project users https://docs.gitlab.com/ce/api/projects.html#get-project-users Closes #328 --- docs/gl_objects/projects.py | 7 +++++++ docs/gl_objects/projects.rst | 6 ++++++ gitlab/v4/objects.py | 12 ++++++++++++ 3 files changed, 25 insertions(+) diff --git a/docs/gl_objects/projects.py b/docs/gl_objects/projects.py index 6ef6069af..6cdd26072 100644 --- a/docs/gl_objects/projects.py +++ b/docs/gl_objects/projects.py @@ -393,3 +393,10 @@ "body": "See the [attached file]({})".format(uploaded_file["url"]) }) # end project file upload markdown custom + +# users list +users = p.users.list() + +# search for users +users = p.users.list(search='pattern') +# end users list diff --git a/docs/gl_objects/projects.rst b/docs/gl_objects/projects.rst index 7f3d47997..8465eb9ac 100644 --- a/docs/gl_objects/projects.rst +++ b/docs/gl_objects/projects.rst @@ -160,6 +160,12 @@ Get a list of contributors for the repository: :start-after: # repository contributors :end-before: # end repository contributors +Get a list of users for the repository: + +.. literalinclude:: projects.py + :start-after: # users list + :end-before: # end users list + Project files ============= diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index 0d8dffdc7..e43d65ebc 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -1616,6 +1616,17 @@ class ProjectTriggerManager(CRUDMixin, RESTManager): _update_attrs = (('description', ), tuple()) +class ProjectUser(User): + pass + + +class ProjectUserManager(ListMixin, RESTManager): + _path = '/projects/%(project_id)s/users' + _obj_cls = ProjectUser + _from_parent_attrs = {'project_id': 'id'} + _list_filters = ('search',) + + class ProjectVariable(SaveMixin, ObjectDeleteMixin, RESTObject): _id_attr = 'key' @@ -1795,6 +1806,7 @@ class Project(SaveMixin, ObjectDeleteMixin, RESTObject): ('services', 'ProjectServiceManager'), ('snippets', 'ProjectSnippetManager'), ('tags', 'ProjectTagManager'), + ('users', 'ProjectUserManager'), ('triggers', 'ProjectTriggerManager'), ('variables', 'ProjectVariableManager'), ) From 9d0a47987a316f9eb1bbb65c587d6fa75e4c6409 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Thu, 12 Oct 2017 07:59:11 +0200 Subject: [PATCH 08/29] ProjectFileManager.create: handle / in file paths Replace / with %2F as is done in other methods. Fixes #339 --- gitlab/v4/objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index e43d65ebc..941e17de8 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -1430,7 +1430,7 @@ def create(self, data, **kwargs): """ self._check_missing_create_attrs(data) - file_path = data.pop('file_path') + file_path = data.pop('file_path').replace('/', '%2F') path = '%s/%s' % (self.path, file_path) server_data = self.gitlab.http_post(path, post_data=data, **kwargs) return self._obj_cls(self, server_data) From dc504ab815cc9ad74a6a6beaf6faa88a5d99c293 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sat, 21 Oct 2017 07:43:29 +0200 Subject: [PATCH 09/29] Snippet notes support all the CRUD methods Fixes #343 --- gitlab/v4/objects.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index 941e17de8..f70a96cd4 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -1545,16 +1545,17 @@ def create(self, data, **kwargs): return CreateMixin.create(self, data, path=path, **kwargs) -class ProjectSnippetNote(RESTObject): +class ProjectSnippetNote(SaveMixin, ObjectDeleteMixin, RESTObject): _constructor_types = {'author': 'User'} -class ProjectSnippetNoteManager(RetrieveMixin, CreateMixin, RESTManager): +class ProjectSnippetNoteManager(CRUDMixin, RESTManager): _path = '/projects/%(project_id)s/snippets/%(snippet_id)s/notes' _obj_cls = ProjectSnippetNote _from_parent_attrs = {'project_id': 'project_id', 'snippet_id': 'id'} _create_attrs = (('body', ), tuple()) + _update_attrs = (('body', ), tuple()) class ProjectSnippet(SaveMixin, ObjectDeleteMixin, RESTObject): From 32ea62af967e5ee0304d8e16d7000bb052a506e4 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sat, 21 Oct 2017 07:45:47 +0200 Subject: [PATCH 10/29] Remove support for "constructor types" in v4 In v3 we create objects from json dicts when it makes sense. Support for this feature has not been kept in v4, and we didn't get requests for it so let's drop the _constructor_types definitions. --- gitlab/base.py | 4 ---- gitlab/v4/objects.py | 29 ++++++----------------------- 2 files changed, 6 insertions(+), 27 deletions(-) diff --git a/gitlab/base.py b/gitlab/base.py index ccc9e4a24..795d7fa41 100644 --- a/gitlab/base.py +++ b/gitlab/base.py @@ -553,10 +553,6 @@ def __init__(self, manager, attrs): '_module': importlib.import_module(self.__module__) }) self.__dict__['_parent_attrs'] = self.manager.parent_attrs - - # TODO(gpocentek): manage the creation of new objects from the received - # data (_constructor_types) - self._create_managers() def __getattr__(self, name): diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index f70a96cd4..bc96a2466 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -135,7 +135,7 @@ class UserKeyManager(GetFromListMixin, CreateMixin, DeleteMixin, RESTManager): class UserProject(RESTObject): - _constructor_types = {'owner': 'User', 'namespace': 'Group'} + pass class UserProjectManager(CreateMixin, RESTManager): @@ -418,9 +418,6 @@ class HookManager(NoUpdateMixin, RESTManager): class Issue(RESTObject): _url = '/issues' - _constructor_types = {'author': 'User', - 'assignee': 'User', - 'milestone': 'ProjectMilestone'} _short_print_attr = 'title' @@ -442,7 +439,6 @@ class LicenseManager(RetrieveMixin, RESTManager): class Snippet(SaveMixin, ObjectDeleteMixin, RESTObject): - _constructor_types = {'author': 'User'} _short_print_attr = 'title' @cli.register_custom_action('Snippet') @@ -508,7 +504,7 @@ class NamespaceManager(GetFromListMixin, RESTManager): class ProjectBoardList(SaveMixin, ObjectDeleteMixin, RESTObject): - _constructor_types = {'label': 'ProjectLabel'} + pass class ProjectBoardListManager(CRUDMixin, RESTManager): @@ -521,7 +517,6 @@ class ProjectBoardListManager(CRUDMixin, RESTManager): class ProjectBoard(RESTObject): - _constructor_types = {'labels': 'ProjectBoardList'} _managers = (('lists', 'ProjectBoardListManager'), ) @@ -532,7 +527,6 @@ class ProjectBoardManager(GetFromListMixin, RESTManager): class ProjectBranch(ObjectDeleteMixin, RESTObject): - _constructor_types = {'author': 'User', "committer": "User"} _id_attr = 'name' @cli.register_custom_action('ProjectBranch', tuple(), @@ -585,10 +579,6 @@ class ProjectBranchManager(NoUpdateMixin, RESTManager): class ProjectJob(RESTObject): - _constructor_types = {'user': 'User', - 'commit': 'ProjectCommit', - 'runner': 'Runner'} - @cli.register_custom_action('ProjectJob') @exc.on_http_error(exc.GitlabJobCancelError) def cancel(self, **kwargs): @@ -907,7 +897,7 @@ class ProjectHookManager(CRUDMixin, RESTManager): class ProjectIssueNote(SaveMixin, ObjectDeleteMixin, RESTObject): - _constructor_types = {'author': 'User'} + pass class ProjectIssueNoteManager(CRUDMixin, RESTManager): @@ -920,8 +910,6 @@ class ProjectIssueNoteManager(CRUDMixin, RESTManager): class ProjectIssue(SubscribableMixin, TodoMixin, TimeTrackingMixin, SaveMixin, ObjectDeleteMixin, RESTObject): - _constructor_types = {'author': 'User', 'assignee': 'User', 'milestone': - 'ProjectMilestone'} _short_print_attr = 'title' _id_attr = 'iid' _managers = (('notes', 'ProjectIssueNoteManager'), ) @@ -978,7 +966,7 @@ class ProjectMemberManager(CRUDMixin, RESTManager): class ProjectNote(RESTObject): - _constructor_types = {'author': 'User'} + pass class ProjectNoteManager(RetrieveMixin, RESTManager): @@ -999,8 +987,6 @@ class ProjectNotificationSettingsManager(NotificationSettingsManager): class ProjectTag(ObjectDeleteMixin, RESTObject): - _constructor_types = {'release': 'ProjectTagRelease', - 'commit': 'ProjectCommit'} _id_attr = 'name' _short_print_attr = 'name' @@ -1058,7 +1044,7 @@ class ProjectMergeRequestDiffManager(RetrieveMixin, RESTManager): class ProjectMergeRequestNote(SaveMixin, ObjectDeleteMixin, RESTObject): - _constructor_types = {'author': 'User'} + pass class ProjectMergeRequestNoteManager(CRUDMixin, RESTManager): @@ -1071,7 +1057,6 @@ class ProjectMergeRequestNoteManager(CRUDMixin, RESTManager): class ProjectMergeRequest(SubscribableMixin, TodoMixin, TimeTrackingMixin, SaveMixin, ObjectDeleteMixin, RESTObject): - _constructor_types = {'author': 'User', 'assignee': 'User'} _id_attr = 'iid' _managers = ( @@ -1546,7 +1531,7 @@ def create(self, data, **kwargs): class ProjectSnippetNote(SaveMixin, ObjectDeleteMixin, RESTObject): - _constructor_types = {'author': 'User'} + pass class ProjectSnippetNoteManager(CRUDMixin, RESTManager): @@ -1560,7 +1545,6 @@ class ProjectSnippetNoteManager(CRUDMixin, RESTManager): class ProjectSnippet(SaveMixin, ObjectDeleteMixin, RESTObject): _url = '/projects/%(project_id)s/snippets' - _constructor_types = {'author': 'User'} _short_print_attr = 'title' _managers = (('notes', 'ProjectSnippetNoteManager'), ) @@ -1779,7 +1763,6 @@ class ProjectRunnerManager(NoUpdateMixin, RESTManager): class Project(SaveMixin, ObjectDeleteMixin, RESTObject): - _constructor_types = {'owner': 'User', 'namespace': 'Group'} _short_print_attr = 'path' _managers = ( ('accessrequests', 'ProjectAccessRequestManager'), From 8c9ad299a20dcd23f9da499ad5ed785814c7b32e Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sat, 21 Oct 2017 07:59:26 +0200 Subject: [PATCH 11/29] Change ProjectUser and GroupProject base class python-gitlab shouldn't try to provide features that are not existing in the Gitlab API: GroupProject and ProjectUser objects should not provide unsupported API methods (no get, no create, no update). This Closes #346 by making explicit that we don't support these non-existant methods. --- RELEASE_NOTES.rst | 19 +++++++++++++++++++ gitlab/v4/objects.py | 4 ++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/RELEASE_NOTES.rst b/RELEASE_NOTES.rst index c495cb0ac..0c0098e7f 100644 --- a/RELEASE_NOTES.rst +++ b/RELEASE_NOTES.rst @@ -4,6 +4,25 @@ Release notes This page describes important changes between python-gitlab releases. +Changes from 1.0.2 to 1.1 +========================= + +* The ``ProjectUser`` class doesn't inherit from ``User`` anymore, and the + ``GroupProject`` class doesn't inherit from ``Project`` anymore. The Gitlab + API doesn't provide the same set of features for these objects, so + python-gitlab objects shouldn't try to workaround that. + + You can create ``User`` or ``Project`` objects from ``ProjectUser`` and + ``GroupProject`` objects using the ``id`` attribute: + + .. code-block:: python + + for gr_project in group.projects.list(): + # lazy object creation doesn't need an Gitlab API request + project = gl.projects.get(gr_project.id, lazy=True) + project.default_branch = 'develop' + project.save() + Changes from 0.21 to 1.0.0 ========================== diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index bc96a2466..0fe2ea577 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -1601,7 +1601,7 @@ class ProjectTriggerManager(CRUDMixin, RESTManager): _update_attrs = (('description', ), tuple()) -class ProjectUser(User): +class ProjectUser(RESTObject): pass @@ -2244,7 +2244,7 @@ class ProjectManager(CRUDMixin, RESTManager): 'with_issues_enabled', 'with_merge_requests_enabled') -class GroupProject(Project): +class GroupProject(RESTObject): pass From b23e344c89c26dd782ec5098b65b226b3323d6eb Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sat, 21 Oct 2017 08:53:16 +0200 Subject: [PATCH 12/29] [docs] document `get_create_attrs` in the API tutorial --- docs/api-usage.rst | 63 ++++++++++++++++++++++++++-------------------- 1 file changed, 36 insertions(+), 27 deletions(-) diff --git a/docs/api-usage.rst b/docs/api-usage.rst index ecb0e645f..4fefd083b 100644 --- a/docs/api-usage.rst +++ b/docs/api-usage.rst @@ -7,32 +7,7 @@ python-gitlab supports both GitLab v3 and v4 APIs. v3 being deprecated by GitLab, its support in python-gitlab will be minimal. The development team will focus on v4. -v3 is still the default API used by python-gitlab, for compatibility reasons.. - - -Base types -========== - -The ``gitlab`` package provides some base types. - -* ``gitlab.Gitlab`` is the primary class, handling the HTTP requests. It holds - the GitLab URL and authentication information. - -For v4 the following types are defined: - -* ``gitlab.base.RESTObject`` is the base class for all the GitLab v4 objects. - These objects provide an abstraction for GitLab resources (projects, groups, - and so on). -* ``gitlab.base.RESTManager`` is the base class for v4 objects managers, - providing the API to manipulate the resources and their attributes. - -For v3 the following types are defined: - -* ``gitlab.base.GitlabObject`` is the base class for all the GitLab v3 objects. - These objects provide an abstraction for GitLab resources (projects, groups, - and so on). -* ``gitlab.base.BaseManager`` is the base class for v3 objects managers, - providing the API to manipulate the resources and their attributes. +v3 is still the default API used by python-gitlab, for compatibility reasons. ``gitlab.Gitlab`` class @@ -109,6 +84,17 @@ Examples: user = gl.users.create(user_data) print(user) +You can list the mandatory and optional attributes for object creation +with the manager's ``get_create_attrs()`` method. It returns 2 tuples, the +first one is the list of mandatory attributes, the second one the list of +optional attribute: + +.. code-block:: python + + # v4 only + print(gl.projects.get_create_attrs()) + (('name',), ('path', 'namespace_id', ...)) + The attributes of objects are defined upon object creation, and depend on the GitLab API itself. To list the available information associated with an object use the python introspection tools for v3, or the ``attributes`` attribute for @@ -150,7 +136,6 @@ You can update or delete a remote object when it exists locally: # delete the resource project.delete() - Some classes provide additional methods, allowing more actions on the GitLab resources. For example: @@ -160,6 +145,30 @@ resources. For example: project = gl.projects.get(1) project.star() +Base types +========== + +The ``gitlab`` package provides some base types. + +* ``gitlab.Gitlab`` is the primary class, handling the HTTP requests. It holds + the GitLab URL and authentication information. + +For v4 the following types are defined: + +* ``gitlab.base.RESTObject`` is the base class for all the GitLab v4 objects. + These objects provide an abstraction for GitLab resources (projects, groups, + and so on). +* ``gitlab.base.RESTManager`` is the base class for v4 objects managers, + providing the API to manipulate the resources and their attributes. + +For v3 the following types are defined: + +* ``gitlab.base.GitlabObject`` is the base class for all the GitLab v3 objects. + These objects provide an abstraction for GitLab resources (projects, groups, + and so on). +* ``gitlab.base.BaseManager`` is the base class for v3 objects managers, + providing the API to manipulate the resources and their attributes. + Lazy objects (v4 only) ====================== From 1b5d4809d8a6a5a6b130265d5ab8fb97fc725ee8 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sat, 21 Oct 2017 08:59:14 +0200 Subject: [PATCH 13/29] Document the Gitlab session parameter Provide a proxy setup example. Closes #341 --- docs/api-usage.rst | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/api-usage.rst b/docs/api-usage.rst index 4fefd083b..c27ba258b 100644 --- a/docs/api-usage.rst +++ b/docs/api-usage.rst @@ -234,3 +234,27 @@ user. For example: .. code-block:: python p = gl.projects.create({'name': 'awesome_project'}, sudo='user1') + +Advanced HTTP configuration +=========================== + +python-gitlab relies on ``requests`` ``Session`` objects to perform all the +HTTP requests to the Gitlab servers. + +You can provide your own ``Session`` object with custom configuration when +you create a ``Gitlab`` object. + +The following sample illustrates how to define a proxy configuration when using +python-gitlab: + +.. code-block:: python + + import gitlab + import requests + + session = requests.Session() + session.proxies = { + 'https': os.environ.get('https_proxy'), + 'http': os.environ.get('http_proxy'), + } + gl = gitlab.gitlab(url, token, api_version=4, session=session) From fe5805f3b60fc97c107e1c9b0a4ff299459ca800 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sat, 21 Oct 2017 09:21:11 +0200 Subject: [PATCH 14/29] ProjectFileManager: custom update() method Closes #340 --- gitlab/v4/objects.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index 0fe2ea577..80c428609 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -1420,6 +1420,30 @@ def create(self, data, **kwargs): server_data = self.gitlab.http_post(path, post_data=data, **kwargs) return self._obj_cls(self, server_data) + @exc.on_http_error(exc.GitlabUpdateError) + def update(self, file_path, new_data={}, **kwargs): + """Update an object on the server. + + Args: + id: ID of the object to update (can be None if not required) + new_data: the update data for the object + **kwargs: Extra options to send to the Gitlab server (e.g. sudo) + + Returns: + dict: The new object data (*not* a RESTObject) + + Raises: + GitlabAuthenticationError: If authentication is not correct + GitlabUpdateError: If the server cannot perform the request + """ + + data = new_data.copy() + file_path = file_path.replace('/', '%2F') + data['file_path'] = file_path + path = '%s/%s' % (self.path, file_path) + self._check_missing_update_attrs(data) + return self.gitlab.http_put(path, post_data=data, **kwargs) + @cli.register_custom_action('ProjectFileManager', ('file_path', 'branch', 'commit_message')) @exc.on_http_error(exc.GitlabDeleteError) From 3a8c4800b31981444fb8fa614e185e2b6a310954 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Tue, 31 Oct 2017 06:57:22 +0100 Subject: [PATCH 15/29] Project: add support for printing_merge_request_link_enabled attr Closes #353 --- gitlab/v4/objects.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index 80c428609..701690839 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -2251,7 +2251,7 @@ class ProjectManager(CRUDMixin, RESTManager): 'shared_runners_enabled', 'visibility', 'import_url', 'public_jobs', 'only_allow_merge_if_build_succeeds', 'only_allow_merge_if_all_discussions_are_resolved', 'lfs_enabled', - 'request_access_enabled') + 'request_access_enabled', 'printing_merge_request_link_enabled') ) _update_attrs = ( tuple(), @@ -2261,7 +2261,7 @@ class ProjectManager(CRUDMixin, RESTManager): 'shared_runners_enabled', 'visibility', 'import_url', 'public_jobs', 'only_allow_merge_if_build_succeeds', 'only_allow_merge_if_all_discussions_are_resolved', 'lfs_enabled', - 'request_access_enabled') + 'request_access_enabled', 'printing_merge_request_link_enabled') ) _list_filters = ('search', 'owned', 'starred', 'archived', 'visibility', 'order_by', 'sort', 'simple', 'membership', 'statistics', From 4c3aa23775f509aa1c69732ea0a66262f1f5269e Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Tue, 31 Oct 2017 07:37:01 +0100 Subject: [PATCH 16/29] Update the ssl_verify docstring --- gitlab/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gitlab/__init__.py b/gitlab/__init__.py index 25b0b866c..fc054c875 100644 --- a/gitlab/__init__.py +++ b/gitlab/__init__.py @@ -61,7 +61,9 @@ class Gitlab(object): private_token (str): The user private token email (str): The user email or login. password (str): The user password (associated with email). - ssl_verify (bool): Whether SSL certificates should be validated. + ssl_verify (bool|str): Whether SSL certificates should be validated. If + the value is a string, it is the path to a CA file used for + certificate validation. timeout (float): Timeout to use for requests to the GitLab server. http_username (str): Username for HTTP authentication http_password (str): Password for HTTP authentication From cf6767ca90df9081b48d1b75a30d74b6afc799af Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Wed, 1 Nov 2017 09:24:19 +0100 Subject: [PATCH 17/29] Move group related code for readability --- gitlab/v4/objects.py | 144 +++++++++++++++++++++---------------------- 1 file changed, 72 insertions(+), 72 deletions(-) diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index 701690839..1fb32b458 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -360,6 +360,17 @@ class GitlabciymlManager(RetrieveMixin, RESTManager): _obj_cls = Gitlabciyml +class GroupAccessRequest(AccessRequestMixin, ObjectDeleteMixin, RESTObject): + pass + + +class GroupAccessRequestManager(GetFromListMixin, CreateMixin, DeleteMixin, + RESTManager): + _path = '/groups/%(group_id)s/access_requests' + _obj_cls = GroupAccessRequest + _from_parent_attrs = {'group_id': 'id'} + + class GroupIssue(RESTObject): pass @@ -394,15 +405,71 @@ class GroupNotificationSettingsManager(NotificationSettingsManager): _from_parent_attrs = {'group_id': 'id'} -class GroupAccessRequest(AccessRequestMixin, ObjectDeleteMixin, RESTObject): +class GroupProject(RESTObject): pass -class GroupAccessRequestManager(GetFromListMixin, CreateMixin, DeleteMixin, - RESTManager): - _path = '/groups/%(group_id)s/access_requests' - _obj_cls = GroupAccessRequest +class GroupProjectManager(GetFromListMixin, RESTManager): + _path = '/groups/%(group_id)s/projects' + _obj_cls = GroupProject + _from_parent_attrs = {'group_id': 'id'} + _list_filters = ('archived', 'visibility', 'order_by', 'sort', 'search', + 'ci_enabled_first') + + +class GroupVariable(SaveMixin, ObjectDeleteMixin, RESTObject): + _id_attr = 'key' + + +class GroupVariableManager(CRUDMixin, RESTManager): + _path = '/groups/%(group_id)s/variables' + _obj_cls = GroupVariable _from_parent_attrs = {'group_id': 'id'} + _create_attrs = (('key', 'value'), ('protected',)) + _update_attrs = (('key', 'value'), ('protected',)) + + +class Group(SaveMixin, ObjectDeleteMixin, RESTObject): + _short_print_attr = 'name' + _managers = ( + ('accessrequests', 'GroupAccessRequestManager'), + ('members', 'GroupMemberManager'), + ('notificationsettings', 'GroupNotificationSettingsManager'), + ('projects', 'GroupProjectManager'), + ('issues', 'GroupIssueManager'), + ('variables', 'GroupVariableManager'), + ) + + @cli.register_custom_action('Group', ('to_project_id', )) + @exc.on_http_error(exc.GitlabTransferProjectError) + def transfer_project(self, to_project_id, **kwargs): + """Transfer a project to this group. + + Args: + to_project_id (int): ID of the project to transfer + **kwargs: Extra options to send to the server (e.g. sudo) + + Raises: + GitlabAuthenticationError: If authentication is not correct + GitlabTransferProjectError: If the project could not be transfered + """ + path = '/groups/%d/projects/%d' % (self.id, to_project_id) + self.manager.gitlab.http_post(path, **kwargs) + + +class GroupManager(CRUDMixin, RESTManager): + _path = '/groups' + _obj_cls = Group + _create_attrs = ( + ('name', 'path'), + ('description', 'visibility', 'parent_id', 'lfs_enabled', + 'request_access_enabled') + ) + _update_attrs = ( + tuple(), + ('name', 'path', 'description', 'visibility', 'lfs_enabled', + 'request_access_enabled') + ) class Hook(ObjectDeleteMixin, RESTObject): @@ -2266,70 +2333,3 @@ class ProjectManager(CRUDMixin, RESTManager): _list_filters = ('search', 'owned', 'starred', 'archived', 'visibility', 'order_by', 'sort', 'simple', 'membership', 'statistics', 'with_issues_enabled', 'with_merge_requests_enabled') - - -class GroupProject(RESTObject): - pass - - -class GroupProjectManager(GetFromListMixin, RESTManager): - _path = '/groups/%(group_id)s/projects' - _obj_cls = GroupProject - _from_parent_attrs = {'group_id': 'id'} - _list_filters = ('archived', 'visibility', 'order_by', 'sort', 'search', - 'ci_enabled_first') - - -class GroupVariable(SaveMixin, ObjectDeleteMixin, RESTObject): - _id_attr = 'key' - - -class GroupVariableManager(CRUDMixin, RESTManager): - _path = '/groups/%(group_id)s/variables' - _obj_cls = GroupVariable - _from_parent_attrs = {'group_id': 'id'} - _create_attrs = (('key', 'value'), ('protected',)) - _update_attrs = (('key', 'value'), ('protected',)) - - -class Group(SaveMixin, ObjectDeleteMixin, RESTObject): - _short_print_attr = 'name' - _managers = ( - ('accessrequests', 'GroupAccessRequestManager'), - ('members', 'GroupMemberManager'), - ('notificationsettings', 'GroupNotificationSettingsManager'), - ('projects', 'GroupProjectManager'), - ('issues', 'GroupIssueManager'), - ('variables', 'GroupVariableManager'), - ) - - @cli.register_custom_action('Group', ('to_project_id', )) - @exc.on_http_error(exc.GitlabTransferProjectError) - def transfer_project(self, to_project_id, **kwargs): - """Transfer a project to this group. - - Args: - to_project_id (int): ID of the project to transfer - **kwargs: Extra options to send to the server (e.g. sudo) - - Raises: - GitlabAuthenticationError: If authentication is not correct - GitlabTransferProjectError: If the project could not be transfered - """ - path = '/groups/%d/projects/%d' % (self.id, to_project_id) - self.manager.gitlab.http_post(path, **kwargs) - - -class GroupManager(CRUDMixin, RESTManager): - _path = '/groups' - _obj_cls = Group - _create_attrs = ( - ('name', 'path'), - ('description', 'visibility', 'parent_id', 'lfs_enabled', - 'request_access_enabled') - ) - _update_attrs = ( - tuple(), - ('name', 'path', 'description', 'visibility', 'lfs_enabled', - 'request_access_enabled') - ) From aba713a0bdbcdb5f898c5e7dcf276811bde6e99b Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Wed, 1 Nov 2017 09:56:54 +0100 Subject: [PATCH 18/29] Add support for group milestones Closes #349 --- docs/gl_objects/milestones.py | 10 +++-- docs/gl_objects/milestones.rst | 12 ++++- gitlab/v4/objects.py | 80 +++++++++++++++++++++++++++++++--- tools/python_test_v4.py | 16 ++++++- 4 files changed, 106 insertions(+), 12 deletions(-) diff --git a/docs/gl_objects/milestones.py b/docs/gl_objects/milestones.py index 19770bcf1..d1985d969 100644 --- a/docs/gl_objects/milestones.py +++ b/docs/gl_objects/milestones.py @@ -1,13 +1,16 @@ # list -milestones = project.milestones.list() +p_milestones = project.milestones.list() +g_milestones = group.milestones.list() # end list # filter -milestones = project.milestones.list(state='closed') +p_milestones = project.milestones.list(state='closed') +g_milestones = group.milestones.list(state='active') # end filter # get -milestone = project.milestones.get(milestone_id) +p_milestone = project.milestones.get(milestone_id) +g_milestone = group.milestones.get(milestone_id) # end get # create @@ -36,4 +39,3 @@ # merge_requests merge_requests = milestone.merge_requests() # end merge_requests - diff --git a/docs/gl_objects/milestones.rst b/docs/gl_objects/milestones.rst index fbe5d879c..c96560a89 100644 --- a/docs/gl_objects/milestones.rst +++ b/docs/gl_objects/milestones.rst @@ -11,6 +11,10 @@ Reference + :class:`gitlab.v4.objects.ProjectMilestoneManager` + :attr:`gitlab.v4.objects.Project.milestones` + + :class:`gitlab.v4.objects.GroupMilestone` + + :class:`gitlab.v4.objects.GroupMilestoneManager` + + :attr:`gitlab.v4.objects.Group.milestones` + * v3 API: + :class:`gitlab.v3.objects.ProjectMilestone` @@ -18,12 +22,15 @@ Reference + :attr:`gitlab.v3.objects.Project.milestones` + :attr:`gitlab.Gitlab.project_milestones` -* GitLab API: https://docs.gitlab.com/ce/api/milestones.html +* GitLab API: + + + https://docs.gitlab.com/ce/api/milestones.html + + https://docs.gitlab.com/ce/api/group_milestones.html Examples -------- -List the milestones for a project: +List the milestones for a project or a group: .. literalinclude:: milestones.py :start-after: # list @@ -33,6 +40,7 @@ You can filter the list using the following parameters: * ``iid``: unique ID of the milestone for the project * ``state``: either ``active`` or ``closed`` +* ``search``: to search using a string .. literalinclude:: milestones.py :start-after: # filter diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index 1fb32b458..535c23c71 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -395,6 +395,75 @@ class GroupMemberManager(GetFromListMixin, CreateMixin, UpdateMixin, _update_attrs = (('access_level', ), ('expires_at', )) +class GroupMergeRequest(RESTObject): + pass + + +class GroupMergeRequestManager(RESTManager): + pass + + +class GroupMilestone(SaveMixin, ObjectDeleteMixin, RESTObject): + _short_print_attr = 'title' + + @cli.register_custom_action('GroupMilestone') + @exc.on_http_error(exc.GitlabListError) + def issues(self, **kwargs): + """List issues related to this milestone. + + Args: + **kwargs: Extra options to send to the server (e.g. sudo) + + Raises: + GitlabAuthenticationError: If authentication is not correct + GitlabListError: If the list could not be retrieved + + Returns: + RESTObjectList: The list of issues + """ + + path = '%s/%s/issues' % (self.manager.path, self.get_id()) + data_list = self.manager.gitlab.http_list(path, as_list=False, + **kwargs) + manager = GroupIssueManager(self.manager.gitlab, + parent=self.manager._parent) + # FIXME(gpocentek): the computed manager path is not correct + return RESTObjectList(manager, GroupIssue, data_list) + + @cli.register_custom_action('GroupMilestone') + @exc.on_http_error(exc.GitlabListError) + def merge_requests(self, **kwargs): + """List the merge requests related to this milestone. + + Args: + **kwargs: Extra options to send to the server (e.g. sudo) + + Raises: + GitlabAuthenticationError: If authentication is not correct + GitlabListError: If the list could not be retrieved + + Returns: + RESTObjectList: The list of merge requests + """ + path = '%s/%s/merge_requests' % (self.manager.path, self.get_id()) + data_list = self.manager.gitlab.http_list(path, as_list=False, + **kwargs) + manager = GroupIssueManager(self.manager.gitlab, + parent=self.manager._parent) + # FIXME(gpocentek): the computed manager path is not correct + return RESTObjectList(manager, GroupMergeRequest, data_list) + + +class GroupMilestoneManager(CRUDMixin, RESTManager): + _path = '/groups/%(group_id)s/milestones' + _obj_cls = GroupMilestone + _from_parent_attrs = {'group_id': 'id'} + _create_attrs = (('title', ), ('description', 'due_date', 'start_date')) + _update_attrs = (tuple(), ('title', 'description', 'due_date', + 'start_date', 'state_event')) + _list_filters = ('iids', 'state', 'search') + + class GroupNotificationSettings(NotificationSettings): pass @@ -434,6 +503,7 @@ class Group(SaveMixin, ObjectDeleteMixin, RESTObject): _managers = ( ('accessrequests', 'GroupAccessRequestManager'), ('members', 'GroupMemberManager'), + ('milestones', 'GroupMilestoneManager'), ('notificationsettings', 'GroupNotificationSettingsManager'), ('projects', 'GroupProjectManager'), ('issues', 'GroupIssueManager'), @@ -1293,8 +1363,8 @@ def issues(self, **kwargs): path = '%s/%s/issues' % (self.manager.path, self.get_id()) data_list = self.manager.gitlab.http_list(path, as_list=False, **kwargs) - manager = ProjectCommitManager(self.manager.gitlab, - parent=self.manager._parent) + manager = ProjectIssueManager(self.manager.gitlab, + parent=self.manager._parent) # FIXME(gpocentek): the computed manager path is not correct return RESTObjectList(manager, ProjectIssue, data_list) @@ -1316,8 +1386,8 @@ def merge_requests(self, **kwargs): path = '%s/%s/merge_requests' % (self.manager.path, self.get_id()) data_list = self.manager.gitlab.http_list(path, as_list=False, **kwargs) - manager = ProjectCommitManager(self.manager.gitlab, - parent=self.manager._parent) + manager = ProjectMergeRequestManager(self.manager.gitlab, + parent=self.manager._parent) # FIXME(gpocentek): the computed manager path is not correct return RESTObjectList(manager, ProjectMergeRequest, data_list) @@ -1330,7 +1400,7 @@ class ProjectMilestoneManager(CRUDMixin, RESTManager): 'state_event')) _update_attrs = (tuple(), ('title', 'description', 'due_date', 'start_date', 'state_event')) - _list_filters = ('iids', 'state') + _list_filters = ('iids', 'state', 'search') class ProjectLabel(SubscribableMixin, SaveMixin, ObjectDeleteMixin, diff --git a/tools/python_test_v4.py b/tools/python_test_v4.py index 386b59b7d..1c1d4d3a3 100644 --- a/tools/python_test_v4.py +++ b/tools/python_test_v4.py @@ -170,6 +170,18 @@ settings = group2.notificationsettings.get() assert(settings.level == 'disabled') +# group milestones +gm1 = group1.milestones.create({'title': 'groupmilestone1'}) +assert(len(group1.milestones.list()) == 1) +gm1.due_date = '2020-01-01T00:00:00Z' +gm1.save() +gm1.state_event = 'close' +gm1.save() +gm1 = group1.milestones.get(gm1.id) +assert(gm1.state == 'closed') +assert(len(gm1.issues()) == 0) +assert(len(gm1.merge_requests()) == 0) + # group variables group1.variables.create({'key': 'foo', 'value': 'bar'}) g_v = group1.variables.get('foo') @@ -330,8 +342,10 @@ m1.save() m1.state_event = 'close' m1.save() -m1 = admin_project.milestones.get(1) +m1 = admin_project.milestones.get(m1.id) assert(m1.state == 'closed') +assert(len(m1.issues()) == 0) +assert(len(m1.merge_requests()) == 0) # issues issue1 = admin_project.issues.create({'title': 'my issue 1', From d0c4118020e11c3132a46fc50d3caecf9a41e7d2 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Wed, 1 Nov 2017 11:26:56 +0100 Subject: [PATCH 19/29] Add support for GPG keys Closes #355 --- docs/gl_objects/users.py | 71 ++++--------- docs/gl_objects/users.rst | 218 +++++++++++++++++++++++--------------- gitlab/v4/objects.py | 24 +++++ tools/python_test_v4.py | 47 ++++++++ 4 files changed, 222 insertions(+), 138 deletions(-) diff --git a/docs/gl_objects/users.py b/docs/gl_objects/users.py index 798678d13..c3618b988 100644 --- a/docs/gl_objects/users.py +++ b/docs/gl_objects/users.py @@ -36,37 +36,44 @@ # end block # key list -keys = gl.user_keys.list(user_id=1) -# or keys = user.keys.list() # end key list # key get -key = gl.user_keys.list(1, user_id=1) -# or key = user.keys.get(1) # end key get # key create -k = gl.user_keys.create({'title': 'my_key', - 'key': open('/home/me/.ssh/id_rsa.pub').read()}, - user_id=2) -# or k = user.keys.create({'title': 'my_key', 'key': open('/home/me/.ssh/id_rsa.pub').read()}) # end key create # key delete -gl.user_keys.delete(1, user_id=1) -# or user.keys.delete(1) # or key.delete() # end key delete -# email list -emails = gl.user_emails.list(user_id=1) +# gpgkey list +gpgkeys = user.gpgkeys.list() +# end gpgkey list + +# gpgkey get +gpgkey = user.gpgkeys.get(1) +# end gpgkey get + +# gpgkey create +# get the key with `gpg --export -a GPG_KEY_ID` +k = user.gpgkeys.create({'key': public_key_content}) +# end gpgkey create + +# gpgkey delete +user.gpgkeys.delete(1) # or +gpgkey.delete() +# end gpgkey delete + +# email list emails = user.emails.list() # end email list @@ -77,14 +84,10 @@ # end email get # email create -k = gl.user_emails.create({'email': 'foo@bar.com'}, user_id=2) -# or k = user.emails.create({'email': 'foo@bar.com'}) # end email create # email delete -gl.user_emails.delete(1, user_id=1) -# or user.emails.delete(1) # or email.delete() @@ -94,39 +97,3 @@ gl.auth() current_user = gl.user # end currentuser get - -# currentuser key list -keys = gl.user.keys.list() -# end currentuser key list - -# currentuser key get -key = gl.user.keys.get(1) -# end currentuser key get - -# currentuser key create -key = gl.user.keys.create({'id': 'my_key', 'key': key_content}) -# end currentuser key create - -# currentuser key delete -gl.user.keys.delete(1) -# or -key.delete() -# end currentuser key delete - -# currentuser email list -emails = gl.user.emails.list() -# end currentuser email list - -# currentuser email get -email = gl.user.emails.get(1) -# end currentuser email get - -# currentuser email create -email = gl.user.emails.create({'email': 'foo@bar.com'}) -# end currentuser email create - -# currentuser email delete -gl.user.emails.delete(1) -# or -email.delete() -# end currentuser email delete diff --git a/docs/gl_objects/users.rst b/docs/gl_objects/users.rst index 8df93b03f..d5b29764d 100644 --- a/docs/gl_objects/users.rst +++ b/docs/gl_objects/users.rst @@ -1,14 +1,32 @@ -##### +###################### +Users and current user +###################### + +The Gitlab API exposes user-related method that can be manipulated by admins +only. + +The currently logged-in user is also exposed. + Users -##### +===== + +References +---------- + +* v4 API: + + + :class:`gitlab.v4.objects.User` + + :class:`gitlab.v4.objects.UserManager` + + :attr:`gitlab.Gitlab.users` -Use :class:`~gitlab.objects.User` objects to manipulate repository branches. +* v3 API: -To create :class:`~gitlab.objects.User` objects use the -:attr:`gitlab.Gitlab.users` manager. + + :class:`gitlab.v3.objects.User` + + :class:`gitlab.v3.objects.UserManager` + + :attr:`gitlab.Gitlab.users` Examples -======== +-------- Get the list of users: @@ -52,14 +70,97 @@ Block/Unblock a user: :start-after: # block :end-before: # end block +Current User +============ + +* v4 API: + + + :class:`gitlab.v4.objects.CurrentUser` + + :class:`gitlab.v4.objects.CurrentUserManager` + + :attr:`gitlab.Gitlab.user` + +* v3 API: + + + :class:`gitlab.v3.objects.CurrentUser` + + :class:`gitlab.v3.objects.CurrentUserManager` + + :attr:`gitlab.Gitlab.user` + +Examples +-------- + +Get the current user: + +.. literalinclude:: users.py + :start-after: # currentuser get + :end-before: # end currentuser get + +GPG keys +======== + +You can manipulate GPG keys for the current user and for the other users if you +are admin. + +* v4 API: + + + :class:`gitlab.v4.objects.CurrentUserGPGKey` + + :class:`gitlab.v4.objects.CurrentUserGPGKeyManager` + + :attr:`gitlab.v4.objects.CurrentUser.gpgkeys` + + :class:`gitlab.v4.objects.UserGPGKey` + + :class:`gitlab.v4.objects.UserGPGKeyManager` + + :attr:`gitlab.v4.objects.User.gpgkeys` + +Exemples +-------- + +List GPG keys for a user: + +.. literalinclude:: users.py + :start-after: # gpgkey list + :end-before: # end gpgkey list + +Get an GPG gpgkey for a user: + +.. literalinclude:: users.py + :start-after: # gpgkey get + :end-before: # end gpgkey get + +Create an GPG gpgkey for a user: + +.. literalinclude:: users.py + :start-after: # gpgkey create + :end-before: # end gpgkey create + +Delete an GPG gpgkey for a user: + +.. literalinclude:: users.py + :start-after: # gpgkey delete + :end-before: # end gpgkey delete + SSH keys ======== -Use the :class:`~gitlab.objects.UserKey` objects to manage user keys. +You can manipulate SSH keys for the current user and for the other users if you +are admin. + +* v4 API: + + + :class:`gitlab.v4.objects.CurrentUserKey` + + :class:`gitlab.v4.objects.CurrentUserKeyManager` + + :attr:`gitlab.v4.objects.CurrentUser.keys` + + :class:`gitlab.v4.objects.UserKey` + + :class:`gitlab.v4.objects.UserKeyManager` + + :attr:`gitlab.v4.objects.User.keys` -To create :class:`~gitlab.objects.UserKey` objects use the -:attr:`User.keys ` or :attr:`gitlab.Gitlab.user_keys` -managers. +* v3 API: + + + :class:`gitlab.v3.objects.CurrentUserKey` + + :class:`gitlab.v3.objects.CurrentUserKeyManager` + + :attr:`gitlab.v3.objects.CurrentUser.keys` + + :attr:`gitlab.Gitlab.user.keys` + + :class:`gitlab.v3.objects.UserKey` + + :class:`gitlab.v3.objects.UserKeyManager` + + :attr:`gitlab.v3.objects.User.keys` + + :attr:`gitlab.Gitlab.user_keys` Exemples -------- @@ -91,10 +192,28 @@ Delete an SSH key for a user: Emails ====== -Use the :class:`~gitlab.objects.UserEmail` objects to manage user emails. +You can manipulate emails for the current user and for the other users if you +are admin. + +* v4 API: + + + :class:`gitlab.v4.objects.CurrentUserEmail` + + :class:`gitlab.v4.objects.CurrentUserEmailManager` + + :attr:`gitlab.v4.objects.CurrentUser.emails` + + :class:`gitlab.v4.objects.UserEmail` + + :class:`gitlab.v4.objects.UserEmailManager` + + :attr:`gitlab.v4.objects.User.emails` -To create :class:`~gitlab.objects.UserEmail` objects use the :attr:`User.emails -` or :attr:`gitlab.Gitlab.user_emails` managers. +* v3 API: + + + :class:`gitlab.v3.objects.CurrentUserEmail` + + :class:`gitlab.v3.objects.CurrentUserEmailManager` + + :attr:`gitlab.v3.objects.CurrentUser.emails` + + :attr:`gitlab.Gitlab.user.emails` + + :class:`gitlab.v3.objects.UserEmail` + + :class:`gitlab.v3.objects.UserEmailManager` + + :attr:`gitlab.v3.objects.User.emails` + + :attr:`gitlab.Gitlab.user_emails` Exemples -------- @@ -122,76 +241,3 @@ Delete an email for a user: .. literalinclude:: users.py :start-after: # email delete :end-before: # end email delete - -Current User -============ - -Use the :class:`~gitlab.objects.CurrentUser` object to get information about -the currently logged-in user. - -Use the :class:`~gitlab.objects.CurrentUserKey` objects to manage user keys. - -To create :class:`~gitlab.objects.CurrentUserKey` objects use the -:attr:`gitlab.objects.CurrentUser.keys ` manager. - -Use the :class:`~gitlab.objects.CurrentUserEmail` objects to manage user emails. - -To create :class:`~gitlab.objects.CurrentUserEmail` objects use the -:attr:`gitlab.objects.CurrentUser.emails ` manager. - -Examples --------- - -Get the current user: - -.. literalinclude:: users.py - :start-after: # currentuser get - :end-before: # end currentuser get - -List the current user SSH keys: - -.. literalinclude:: users.py - :start-after: # currentuser key list - :end-before: # end currentuser key list - -Get a key for the current user: - -.. literalinclude:: users.py - :start-after: # currentuser key get - :end-before: # end currentuser key get - -Create a key for the current user: - -.. literalinclude:: users.py - :start-after: # currentuser key create - :end-before: # end currentuser key create - -Delete a key for the current user: - -.. literalinclude:: users.py - :start-after: # currentuser key delete - :end-before: # end currentuser key delete - -List the current user emails: - -.. literalinclude:: users.py - :start-after: # currentuser email list - :end-before: # end currentuser email list - -Get an email for the current user: - -.. literalinclude:: users.py - :start-after: # currentuser email get - :end-before: # end currentuser email get - -Create an email for the current user: - -.. literalinclude:: users.py - :start-after: # currentuser email create - :end-before: # end currentuser email create - -Delete an email for the current user: - -.. literalinclude:: users.py - :start-after: # currentuser email delete - :end-before: # end currentuser email delete diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index 535c23c71..55eb00480 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -123,6 +123,17 @@ class UserEmailManager(RetrieveMixin, CreateMixin, DeleteMixin, RESTManager): _create_attrs = (('email', ), tuple()) +class UserGPGKey(ObjectDeleteMixin, RESTObject): + pass + + +class UserGPGKeyManager(RetrieveMixin, CreateMixin, DeleteMixin, RESTManager): + _path = '/users/%(user_id)s/gpg_keys' + _obj_cls = UserGPGKey + _from_parent_attrs = {'user_id': 'id'} + _create_attrs = (('key',), tuple()) + + class UserKey(ObjectDeleteMixin, RESTObject): pass @@ -155,6 +166,7 @@ class User(SaveMixin, ObjectDeleteMixin, RESTObject): _short_print_attr = 'username' _managers = ( ('emails', 'UserEmailManager'), + ('gpgkeys', 'UserGPGKeyManager'), ('keys', 'UserKeyManager'), ('projects', 'UserProjectManager'), ) @@ -241,6 +253,17 @@ class CurrentUserEmailManager(RetrieveMixin, CreateMixin, DeleteMixin, _create_attrs = (('email', ), tuple()) +class CurrentUserGPGKey(ObjectDeleteMixin, RESTObject): + pass + + +class CurrentUserGPGKeyManager(RetrieveMixin, CreateMixin, DeleteMixin, + RESTManager): + _path = '/user/gpg_keys' + _obj_cls = CurrentUserGPGKey + _create_attrs = (('key',), tuple()) + + class CurrentUserKey(ObjectDeleteMixin, RESTObject): _short_print_attr = 'title' @@ -257,6 +280,7 @@ class CurrentUser(RESTObject): _short_print_attr = 'username' _managers = ( ('emails', 'CurrentUserEmailManager'), + ('gpgkeys', 'CurrentUserGPGKeyManager'), ('keys', 'CurrentUserKeyManager'), ) diff --git a/tools/python_test_v4.py b/tools/python_test_v4.py index 1c1d4d3a3..8a8be68fb 100644 --- a/tools/python_test_v4.py +++ b/tools/python_test_v4.py @@ -20,6 +20,37 @@ "rke9IepE7SPBT41C+YtUX4dfDZDmczM1cE0YL/krdUCfuZHMa4ZS2YyNd6slufc" "vn bar@foo") +GPG_KEY = '''-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFn5mzYBCADH6SDVPAp1zh/hxmTi0QplkOfExBACpuY6OhzNdIg+8/528b3g +Y5YFR6T/HLv/PmeHskUj21end1C0PNG2T9dTx+2Vlh9ISsSG1kyF9T5fvMR3bE0x +Dl6S489CXZrjPTS9SHk1kF+7dwjUxLJyxF9hPiSihFefDFu3NeOtG/u8vbC1mewQ +ZyAYue+mqtqcCIFFoBz7wHKMWjIVSJSyTkXExu4OzpVvy3l2EikbvavI3qNz84b+ +Mgkv/kiBlNoCy3CVuPk99RYKZ3lX1vVtqQ0OgNGQvb4DjcpyjmbKyibuZwhDjIOh +au6d1OyEbayTntd+dQ4j9EMSnEvm/0MJ4eXPABEBAAG0G0dpdGxhYlRlc3QxIDxm +YWtlQGZha2UudGxkPokBNwQTAQgAIQUCWfmbNgIbAwULCQgHAgYVCAkKCwIEFgID +AQIeAQIXgAAKCRBgxELHf8f3hF3yB/wNJlWPKY65UsB4Lo0hs1OxdxCDqXogSi0u +6crDEIiyOte62pNZKzWy8TJcGZvznRTZ7t8hXgKFLz3PRMcl+vAiRC6quIDUj+2V +eYfwaItd1lUfzvdCaC7Venf4TQ74f5vvNg/zoGwE6eRoSbjlLv9nqsxeA0rUBUQL +LYikWhVMP3TrlfgfduYvh6mfgh57BDLJ9kJVpyfxxx9YLKZbaas9sPa6LgBtR555 +JziUxHmbEv8XCsUU8uoFeP1pImbNBplqE3wzJwzOMSmmch7iZzrAwfN7N2j3Wj0H +B5kQddJ9dmB4BbU0IXGhWczvdpxboI2wdY8a1JypxOdePoph/43iuQENBFn5mzYB +CADnTPY0Zf3d9zLjBNgIb3yDl94uOcKCq0twNmyjMhHzGqw+UMe9BScy34GL94Al +xFRQoaL+7P8hGsnsNku29A/VDZivcI+uxTx4WQ7OLcn7V0bnHV4d76iky2ufbUt/ +GofthjDs1SonePO2N09sS4V4uK0d5N4BfCzzXgvg8etCLxNmC9BGt7AaKUUzKBO4 +2QvNNaC2C/8XEnOgNWYvR36ylAXAmo0sGFXUsBCTiq1fugS9pwtaS2JmaVpZZ3YT +pMZlS0+SjC5BZYFqSmKCsA58oBRzCxQz57nR4h5VEflgD+Hy0HdW0UHETwz83E6/ +U0LL6YyvhwFr6KPq5GxinSvfABEBAAGJAR8EGAEIAAkFAln5mzYCGwwACgkQYMRC +x3/H94SJgwgAlKQb10/xcL/epdDkR7vbiei7huGLBpRDb/L5fM8B5W77Qi8Xmuqj +cCu1j99ZCA5hs/vwVn8j8iLSBGMC5gxcuaar/wtmiaEvT9fO/h6q4opG7NcuiJ8H +wRj8ccJmRssNqDD913PLz7T40Ts62blhrEAlJozGVG/q7T3RAZcskOUHKeHfc2RI +YzGsC/I9d7k6uxAv1L9Nm5F2HaAQDzhkdd16nKkGaPGR35cT1JLInkfl5cdm7ldN +nxs4TLO3kZjUTgWKdhpgRNF5hwaz51ZjpebaRf/ZqRuNyX4lIRolDxzOn/+O1o8L +qG2ZdhHHmSK2LaQLFiSprUkikStNU9BqSQ== +=5OGa +-----END PGP PUBLIC KEY BLOCK-----''' + + # login/password authentication gl = gitlab.Gitlab('http://localhost:8080', email=LOGIN, password=PASSWORD) gl.auth() @@ -80,6 +111,14 @@ foobar_user.bio = 'This is the user bio' foobar_user.save() +# GPG keys +gkey = new_user.gpgkeys.create({'key': GPG_KEY}) +assert(len(new_user.gpgkeys.list()) == 1) +# Seems broken on the gitlab side +# gkey = new_user.gpgkeys.get(gkey.id) +gkey.delete() +assert(len(new_user.gpgkeys.list()) == 0) + # SSH keys key = new_user.keys.create({'title': 'testkey', 'key': SSH_KEY}) assert(len(new_user.keys.list()) == 1) @@ -102,6 +141,14 @@ mail.delete() assert(len(gl.user.emails.list()) == 0) +# current user GPG keys +gkey = gl.user.gpgkeys.create({'key': GPG_KEY}) +assert(len(gl.user.gpgkeys.list()) == 1) +# Seems broken on the gitlab side +gkey = gl.user.gpgkeys.get(gkey.id) +gkey.delete() +assert(len(gl.user.gpgkeys.list()) == 0) + # current user key key = gl.user.keys.create({'title': 'testkey', 'key': SSH_KEY}) assert(len(gl.user.keys.list()) == 1) From 4744200d982f7fc556d1202330b218850bd232d6 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Wed, 1 Nov 2017 11:34:52 +0100 Subject: [PATCH 20/29] Move the ProjectManager class for readability --- gitlab/v4/objects.py | 56 ++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index 55eb00480..992259dea 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -2317,6 +2317,34 @@ def upload(self, filename, filedata=None, filepath=None, **kwargs): } +class ProjectManager(CRUDMixin, RESTManager): + _path = '/projects' + _obj_cls = Project + _create_attrs = ( + ('name', ), + ('path', 'namespace_id', 'description', 'issues_enabled', + 'merge_requests_enabled', 'jobs_enabled', 'wiki_enabled', + 'snippets_enabled', 'container_registry_enabled', + 'shared_runners_enabled', 'visibility', 'import_url', 'public_jobs', + 'only_allow_merge_if_build_succeeds', + 'only_allow_merge_if_all_discussions_are_resolved', 'lfs_enabled', + 'request_access_enabled', 'printing_merge_request_link_enabled') + ) + _update_attrs = ( + tuple(), + ('name', 'path', 'default_branch', 'description', 'issues_enabled', + 'merge_requests_enabled', 'jobs_enabled', 'wiki_enabled', + 'snippets_enabled', 'container_registry_enabled', + 'shared_runners_enabled', 'visibility', 'import_url', 'public_jobs', + 'only_allow_merge_if_build_succeeds', + 'only_allow_merge_if_all_discussions_are_resolved', 'lfs_enabled', + 'request_access_enabled', 'printing_merge_request_link_enabled') + ) + _list_filters = ('search', 'owned', 'starred', 'archived', 'visibility', + 'order_by', 'sort', 'simple', 'membership', 'statistics', + 'with_issues_enabled', 'with_merge_requests_enabled') + + class Runner(SaveMixin, ObjectDeleteMixin, RESTObject): pass @@ -2399,31 +2427,3 @@ def mark_all_as_done(self, **kwargs): return int(result) except ValueError: return 0 - - -class ProjectManager(CRUDMixin, RESTManager): - _path = '/projects' - _obj_cls = Project - _create_attrs = ( - ('name', ), - ('path', 'namespace_id', 'description', 'issues_enabled', - 'merge_requests_enabled', 'jobs_enabled', 'wiki_enabled', - 'snippets_enabled', 'container_registry_enabled', - 'shared_runners_enabled', 'visibility', 'import_url', 'public_jobs', - 'only_allow_merge_if_build_succeeds', - 'only_allow_merge_if_all_discussions_are_resolved', 'lfs_enabled', - 'request_access_enabled', 'printing_merge_request_link_enabled') - ) - _update_attrs = ( - tuple(), - ('name', 'path', 'default_branch', 'description', 'issues_enabled', - 'merge_requests_enabled', 'jobs_enabled', 'wiki_enabled', - 'snippets_enabled', 'container_registry_enabled', - 'shared_runners_enabled', 'visibility', 'import_url', 'public_jobs', - 'only_allow_merge_if_build_succeeds', - 'only_allow_merge_if_all_discussions_are_resolved', 'lfs_enabled', - 'request_access_enabled', 'printing_merge_request_link_enabled') - ) - _list_filters = ('search', 'owned', 'starred', 'archived', 'visibility', - 'order_by', 'sort', 'simple', 'membership', 'statistics', - 'with_issues_enabled', 'with_merge_requests_enabled') From 5082879dcfbe322bb16e4c2387c25ec4f4407cb1 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Wed, 1 Nov 2017 12:13:46 +0100 Subject: [PATCH 21/29] Add support for wiki pages --- docs/api-objects.rst | 1 + docs/gl_objects/wikis.py | 21 ++++++++++++++++++ docs/gl_objects/wikis.rst | 46 +++++++++++++++++++++++++++++++++++++++ gitlab/v4/objects.py | 15 +++++++++++++ tools/python_test_v4.py | 12 ++++++++++ 5 files changed, 95 insertions(+) create mode 100644 docs/gl_objects/wikis.py create mode 100644 docs/gl_objects/wikis.rst diff --git a/docs/api-objects.rst b/docs/api-objects.rst index 4b40ce17b..e549924c2 100644 --- a/docs/api-objects.rst +++ b/docs/api-objects.rst @@ -30,3 +30,4 @@ API examples gl_objects/todos gl_objects/users gl_objects/sidekiq + gl_objects/wikis diff --git a/docs/gl_objects/wikis.py b/docs/gl_objects/wikis.py new file mode 100644 index 000000000..0c92fe6d5 --- /dev/null +++ b/docs/gl_objects/wikis.py @@ -0,0 +1,21 @@ +# list +pages = project.wikis.list() +# end list + +# get +page = project.wikis.get(page_slug) +# end get + +# create +page = project.wikis.create({'title': 'Wiki Page 1', + 'content': open(a_file).read()}) +# end create + +# update +page.content = 'My new content' +page.save() +# end update + +# delete +page.delete() +# end delete diff --git a/docs/gl_objects/wikis.rst b/docs/gl_objects/wikis.rst new file mode 100644 index 000000000..0934654f7 --- /dev/null +++ b/docs/gl_objects/wikis.rst @@ -0,0 +1,46 @@ +########## +Wiki pages +########## + + +References +========== + +* v4 API: + + + :class:`gitlab.v4.objects.ProjectWiki` + + :class:`gitlab.v4.objects.ProjectWikiManager` + + :attr:`gitlab.v4.objects.Project.wikis` + +Examples +-------- + +Get the list of wiki pages for a project: + +.. literalinclude:: wikis.py + :start-after: # list + :end-before: # end list + +Get a single wiki page: + +.. literalinclude:: wikis.py + :start-after: # get + :end-before: # end get + +Create a wiki page: + +.. literalinclude:: wikis.py + :start-after: # create + :end-before: # end create + +Update a wiki page: + +.. literalinclude:: wikis.py + :start-after: # update + :end-before: # end update + +Delete a wiki page: + +.. literalinclude:: wikis.py + :start-after: # delete + :end-before: # end delete diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index 992259dea..a57adffed 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -1947,6 +1947,20 @@ class ProjectRunnerManager(NoUpdateMixin, RESTManager): _create_attrs = (('runner_id', ), tuple()) +class ProjectWiki(SaveMixin, ObjectDeleteMixin, RESTObject): + _id_attr = 'slug' + _short_print_attr = 'slug' + + +class ProjectWikiManager(CRUDMixin, RESTManager): + _path = '/projects/%(project_id)s/wikis' + _obj_cls = ProjectWiki + _from_parent_attrs = {'project_id': 'id'} + _create_attrs = (('title', 'content'), ('format', )) + _update_attrs = (tuple(), ('title', 'content', 'format')) + _list_filters = ('with_content', ) + + class Project(SaveMixin, ObjectDeleteMixin, RESTObject): _short_print_attr = 'path' _managers = ( @@ -1978,6 +1992,7 @@ class Project(SaveMixin, ObjectDeleteMixin, RESTObject): ('users', 'ProjectUserManager'), ('triggers', 'ProjectTriggerManager'), ('variables', 'ProjectVariableManager'), + ('wikis', 'ProjectWikiManager'), ) @cli.register_custom_action('Project', tuple(), ('path', 'ref')) diff --git a/tools/python_test_v4.py b/tools/python_test_v4.py index 8a8be68fb..0b1793a78 100644 --- a/tools/python_test_v4.py +++ b/tools/python_test_v4.py @@ -496,6 +496,18 @@ #lists = board.lists.list() #assert(len(lists) == begin_size - 1) +# project wiki +wiki_content = 'Wiki page content' +wp = admin_project.wikis.create({'title': 'wikipage', 'content': wiki_content}) +assert(len(admin_project.wikis.list()) == 1) +wp = admin_project.wikis.get(wp.slug) +assert(wp.content == wiki_content) +# update and delete seem broken +# wp.content = 'new content' +# wp.save() +# wp.delete() +# assert(len(admin_project.wikis.list()) == 0) + # namespaces ns = gl.namespaces.list(all=True) assert(len(ns) != 0) From d415cc0929aed8bf95cbbb54f64d457e42d77696 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Wed, 1 Nov 2017 15:27:27 +0100 Subject: [PATCH 22/29] Update the repository_blob documentation Fixes #312 --- docs/gl_objects/projects.py | 5 ++++- docs/gl_objects/projects.rst | 2 +- gitlab/v4/objects.py | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/gl_objects/projects.py b/docs/gl_objects/projects.py index 6cdd26072..4a6f3ad37 100644 --- a/docs/gl_objects/projects.py +++ b/docs/gl_objects/projects.py @@ -142,7 +142,10 @@ # end repository tree # repository blob -file_content = p.repository_blob('master', 'README.rst') +items = project.repository_tree(path='docs', ref='branch1') +file_info = p.repository_blob(items[0]['id']) +content = base64.b64decode(file_info['content']) +size = file_info['size'] # end repository blob # repository raw_blob diff --git a/docs/gl_objects/projects.rst b/docs/gl_objects/projects.rst index 8465eb9ac..eb15a3bf1 100644 --- a/docs/gl_objects/projects.rst +++ b/docs/gl_objects/projects.rst @@ -120,7 +120,7 @@ List the repository tree: :start-after: # repository tree :end-before: # end repository tree -Get the content of a file for a commit: +Get the content and metadata of a file for a commit, using a blob sha: .. literalinclude:: projects.py :start-after: # repository blob diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index a57adffed..5a3f17c42 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -2024,7 +2024,7 @@ def repository_tree(self, path='', ref='', **kwargs): @cli.register_custom_action('Project', ('sha', )) @exc.on_http_error(exc.GitlabGetError) def repository_blob(self, sha, **kwargs): - """Return a blob by blob SHA. + """Return a file by blob SHA. Args: sha(str): ID of the blob @@ -2035,7 +2035,7 @@ def repository_blob(self, sha, **kwargs): GitlabGetError: If the server failed to perform the request Returns: - str: The blob metadata + dict: The blob content and metadata """ path = '/projects/%s/repository/blobs/%s' % (self.get_id(), sha) From 9dd410feec4fe4e85eb735ad0007adcf06fe03cc Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Wed, 1 Nov 2017 16:01:40 +0100 Subject: [PATCH 23/29] Fix the CLI for objects without ID (API v4) Fixes #319 --- gitlab/mixins.py | 2 +- gitlab/v4/cli.py | 19 ++++++++++++------- tools/cli_test_v4.sh | 11 ++++++++++- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/gitlab/mixins.py b/gitlab/mixins.py index 2acc54b72..d01715284 100644 --- a/gitlab/mixins.py +++ b/gitlab/mixins.py @@ -51,7 +51,7 @@ def get(self, id, lazy=False, **kwargs): class GetWithoutIdMixin(object): @exc.on_http_error(exc.GitlabGetError) - def get(self, **kwargs): + def get(self, id=None, **kwargs): """Retrieve a single object. Args: diff --git a/gitlab/v4/cli.py b/gitlab/v4/cli.py index 6e664b392..939a7ccb6 100644 --- a/gitlab/v4/cli.py +++ b/gitlab/v4/cli.py @@ -83,7 +83,7 @@ def do_list(self): def do_get(self): id = None - if gitlab.mixins.GetWithoutIdMixin not in inspect.getmro(self.cls): + if gitlab.mixins.GetWithoutIdMixin not in inspect.getmro(self.mgr_cls): id = self.args.pop(self.cls._id_attr) try: @@ -99,7 +99,9 @@ def do_delete(self): cli.die("Impossible to destroy object", e) def do_update(self): - id = self.args.pop(self.cls._id_attr) + id = None + if gitlab.mixins.GetWithoutIdMixin not in inspect.getmro(self.mgr_cls): + id = self.args.pop(self.cls._id_attr) try: return self.mgr.update(id, self.args) except Exception as e: @@ -282,15 +284,18 @@ def display_dict(d, padding): return # not a dict, we assume it's a RESTObject - id = getattr(obj, obj._id_attr, None) - print('%s: %s' % (obj._id_attr, id)) + if obj._id_attr: + id = getattr(obj, obj._id_attr, None) + print('%s: %s' % (obj._id_attr, id)) attrs = obj.attributes - attrs.pop(obj._id_attr) + if obj._id_attr: + attrs.pop(obj._id_attr) display_dict(attrs, padding) else: - id = getattr(obj, obj._id_attr) - print('%s: %s' % (obj._id_attr.replace('_', '-'), id)) + if obj._id_attr: + id = getattr(obj, obj._id_attr) + print('%s: %s' % (obj._id_attr.replace('_', '-'), id)) if hasattr(obj, '_short_print_attr'): value = getattr(obj, obj._short_print_attr) print('%s: %s' % (obj._short_print_attr, value)) diff --git a/tools/cli_test_v4.sh b/tools/cli_test_v4.sh index 813d85b06..01f84e830 100644 --- a/tools/cli_test_v4.sh +++ b/tools/cli_test_v4.sh @@ -95,9 +95,18 @@ testcase "branch deletion" ' ' testcase "project upload" ' - GITLAB project upload --id "$PROJECT_ID" --filename '$(basename $0)' --filepath '$0' + GITLAB project upload --id "$PROJECT_ID" \ + --filename '$(basename $0)' --filepath '$0' >/dev/null 2>&1 ' testcase "project deletion" ' GITLAB project delete --id "$PROJECT_ID" ' + +testcase "application settings get" ' + GITLAB application-settings get >/dev/null 2>&1 +' + +testcase "application settings update" ' + GITLAB application-settings update --signup-enabled false +' From fba7730161c15be222a22b4618d79bb92a87ef1f Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Wed, 1 Nov 2017 16:26:21 +0100 Subject: [PATCH 24/29] Add a contributed Dockerfile Thanks oupala! Closes #295 --- contrib/docker/Dockerfile | 10 ++++++++++ contrib/docker/README.rst | 19 +++++++++++++++++++ contrib/docker/python-gitlab.cfg | 15 +++++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 contrib/docker/Dockerfile create mode 100644 contrib/docker/README.rst create mode 100644 contrib/docker/python-gitlab.cfg diff --git a/contrib/docker/Dockerfile b/contrib/docker/Dockerfile new file mode 100644 index 000000000..6663cac5d --- /dev/null +++ b/contrib/docker/Dockerfile @@ -0,0 +1,10 @@ +FROM python:slim + +# Install python-gitlab +RUN pip install --upgrade python-gitlab + +# Copy sample configuration file +COPY python-gitlab.cfg / + +# Define the entrypoint that enable a configuration file +ENTRYPOINT ["gitlab", "--config-file", "/python-gitlab.cfg"] diff --git a/contrib/docker/README.rst b/contrib/docker/README.rst new file mode 100644 index 000000000..90a576cf4 --- /dev/null +++ b/contrib/docker/README.rst @@ -0,0 +1,19 @@ +python-gitlab docker image +========================== + +Dockerfile contributed by *oupala*: +https://github.com/python-gitlab/python-gitlab/issues/295 + +How to build +------------ + +``docker build -t me/python-gitlab:VERSION .`` + +How to use +---------- + +``docker run -it -v /path/to/python-gitlab.cfg:/python-gitlab.cfg python-gitlab ...`` + +To make things easier you can create a shell alias: + +``alias gitlab='docker run --rm -it -v /path/to/python-gitlab.cfg:/python-gitlab.cfg python-gitlab`` diff --git a/contrib/docker/python-gitlab.cfg b/contrib/docker/python-gitlab.cfg new file mode 100644 index 000000000..0e519545f --- /dev/null +++ b/contrib/docker/python-gitlab.cfg @@ -0,0 +1,15 @@ +[global] +default = somewhere +ssl_verify = true +timeout = 5 +api_version = 3 + +[somewhere] +url = https://some.whe.re +private_token = vTbFeqJYCY3sibBP7BZM +api_version = 4 + +[elsewhere] +url = http://else.whe.re:8080 +private_token = CkqsjqcQSFH5FQKDccu4 +timeout = 1 From 38d446737f45ea54136d1f03f75fbddf46c45e00 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Wed, 1 Nov 2017 17:07:30 +0100 Subject: [PATCH 25/29] Pagination generators: expose more information Expose the X-* pagination attributes returned by the Gitlab server when requesting lists. Closes #304 --- docs/api-usage.rst | 9 +++++++++ gitlab/__init__.py | 37 +++++++++++++++++++++++++++++++++++++ gitlab/base.py | 36 ++++++++++++++++++++++++++++++++++++ gitlab/tests/test_gitlab.py | 6 ++++++ 4 files changed, 88 insertions(+) diff --git a/docs/api-usage.rst b/docs/api-usage.rst index c27ba258b..ad188ceff 100644 --- a/docs/api-usage.rst +++ b/docs/api-usage.rst @@ -225,6 +225,15 @@ handle the next calls to the API when required: for item in items: print(item.attributes) +The generator exposes extra listing information as received by the server: + +* ``current_page``: current page number (first page is 1) +* ``prev_page``: if ``None`` the current page is the first one +* ``next_page``: if ``None`` the current page is the last one +* ``per_page``: number of items per page +* ``total_pages``: total number of pages available +* ``total``: total number of items in the list + Sudo ==== diff --git a/gitlab/__init__.py b/gitlab/__init__.py index fc054c875..b72103059 100644 --- a/gitlab/__init__.py +++ b/gitlab/__init__.py @@ -864,6 +864,7 @@ def _query(self, url, query_data={}, **kwargs): except KeyError: self._next_url = None self._current_page = result.headers.get('X-Page') + self._prev_page = result.headers.get('X-Prev-Page') self._next_page = result.headers.get('X-Next-Page') self._per_page = result.headers.get('X-Per-Page') self._total_pages = result.headers.get('X-Total-Pages') @@ -877,6 +878,42 @@ def _query(self, url, query_data={}, **kwargs): self._current = 0 + @property + def current_page(self): + """The current page number.""" + return int(self._current_page) + + @property + def prev_page(self): + """The next page number. + + If None, the current page is the last. + """ + return int(self._prev_page) if self._prev_page else None + + @property + def next_page(self): + """The next page number. + + If None, the current page is the last. + """ + return int(self._next_page) if self._next_page else None + + @property + def per_page(self): + """The number of items per page.""" + return int(self._per_page) + + @property + def total_pages(self): + """The total number of pages.""" + return int(self._total_pages) + + @property + def total(self): + """The total number of items.""" + return int(self._total) + def __iter__(self): return self diff --git a/gitlab/base.py b/gitlab/base.py index 795d7fa41..4213d2ff5 100644 --- a/gitlab/base.py +++ b/gitlab/base.py @@ -670,6 +670,42 @@ def next(self): data = self._list.next() return self._obj_cls(self.manager, data) + @property + def current_page(self): + """The current page number.""" + return self._list.current_page + + @property + def prev_page(self): + """The next page number. + + If None, the current page is the last. + """ + return self._list.prev_page + + @property + def next_page(self): + """The next page number. + + If None, the current page is the last. + """ + return self._list.next_page + + @property + def per_page(self): + """The number of items per page.""" + return self._list.per_page + + @property + def total_pages(self): + """The total number of pages.""" + return self._list.total_pages + + @property + def total(self): + """The total number of items.""" + return self._list.total + class RESTManager(object): """Base class for CRUD operations on objects. diff --git a/gitlab/tests/test_gitlab.py b/gitlab/tests/test_gitlab.py index 6bc427df7..0f396241c 100644 --- a/gitlab/tests/test_gitlab.py +++ b/gitlab/tests/test_gitlab.py @@ -209,6 +209,12 @@ def resp_2(url, request): self.assertEqual(len(obj), 2) self.assertEqual(obj._next_url, 'http://localhost/api/v4/tests?per_page=1&page=2') + self.assertEqual(obj.current_page, 1) + self.assertEqual(obj.prev_page, None) + self.assertEqual(obj.next_page, 2) + self.assertEqual(obj.per_page, 1) + self.assertEqual(obj.total_pages, 2) + self.assertEqual(obj.total, 2) with HTTMock(resp_2): l = list(obj) From 226e6ce9e5217367c896125a2b4b9d16afd2cf94 Mon Sep 17 00:00:00 2001 From: Lyudmil Nenov Date: Fri, 3 Nov 2017 16:05:17 +0200 Subject: [PATCH 26/29] Module's base objects serialization (#359) Make gitlab objects serializable With current implementation of API v3 and v4 support, some instances have properties of type module and are not serializable. Handle these properties manually with setstate and getstate methods. --- gitlab/__init__.py | 11 +++++++++++ gitlab/base.py | 22 ++++++++++++++++++++++ gitlab/tests/test_base.py | 10 ++++++++++ gitlab/tests/test_gitlab.py | 9 +++++++++ gitlab/tests/test_gitlabobject.py | 10 ++++++++++ 5 files changed, 62 insertions(+) diff --git a/gitlab/__init__.py b/gitlab/__init__.py index b72103059..965bf382b 100644 --- a/gitlab/__init__.py +++ b/gitlab/__init__.py @@ -138,6 +138,17 @@ def __init__(self, url, private_token=None, email=None, password=None, manager = getattr(objects, cls_name)(self) setattr(self, var_name, manager) + def __getstate__(self): + state = self.__dict__.copy() + state.pop('_objects') + return state + + def __setstate__(self, state): + self.__dict__.update(state) + objects = importlib.import_module('gitlab.v%s.objects' % + self._api_version) + self._objects = objects + @property def api_version(self): return self._api_version diff --git a/gitlab/base.py b/gitlab/base.py index 4213d2ff5..ec5f6987a 100644 --- a/gitlab/base.py +++ b/gitlab/base.py @@ -416,6 +416,17 @@ def __init__(self, gl, data=None, **kwargs): if not hasattr(self, "id"): self.id = None + def __getstate__(self): + state = self.__dict__.copy() + module = state.pop('_module') + state['_module_name'] = module.__name__ + return state + + def __setstate__(self, state): + module_name = state.pop('_module_name') + self.__dict__.update(state) + self._module = importlib.import_module(module_name) + def _set_manager(self, var, cls, attrs): manager = cls(self.gitlab, self, attrs) setattr(self, var, manager) @@ -555,6 +566,17 @@ def __init__(self, manager, attrs): self.__dict__['_parent_attrs'] = self.manager.parent_attrs self._create_managers() + def __getstate__(self): + state = self.__dict__.copy() + module = state.pop('_module') + state['_module_name'] = module.__name__ + return state + + def __setstate__(self, state): + module_name = state.pop('_module_name') + self.__dict__.update(state) + self._module = importlib.import_module(module_name) + def __getattr__(self, name): try: return self.__dict__['_updated_attrs'][name] diff --git a/gitlab/tests/test_base.py b/gitlab/tests/test_base.py index c55f0003c..31dd96771 100644 --- a/gitlab/tests/test_base.py +++ b/gitlab/tests/test_base.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 . +import pickle try: import unittest except ImportError: @@ -86,6 +87,15 @@ def test_instanciate(self): self.assertEqual(self.manager, obj.manager) self.assertEqual(self.gitlab, obj.manager.gitlab) + def test_pickability(self): + obj = FakeObject(self.manager, {'foo': 'bar'}) + original_obj_module = obj._module + pickled = pickle.dumps(obj) + unpickled = pickle.loads(pickled) + self.assertIsInstance(unpickled, FakeObject) + self.assertTrue(hasattr(unpickled, '_module')) + self.assertEqual(unpickled._module, original_obj_module) + def test_attrs(self): obj = FakeObject(self.manager, {'foo': 'bar'}) diff --git a/gitlab/tests/test_gitlab.py b/gitlab/tests/test_gitlab.py index 0f396241c..027de0c02 100644 --- a/gitlab/tests/test_gitlab.py +++ b/gitlab/tests/test_gitlab.py @@ -18,6 +18,7 @@ from __future__ import print_function +import pickle try: import unittest except ImportError: @@ -890,6 +891,14 @@ def setUp(self): email="testuser@test.com", password="testpassword", ssl_verify=True) + def test_pickability(self): + original_gl_objects = self.gl._objects + pickled = pickle.dumps(self.gl) + unpickled = pickle.loads(pickled) + self.assertIsInstance(unpickled, Gitlab) + self.assertTrue(hasattr(unpickled, '_objects')) + self.assertEqual(unpickled._objects, original_gl_objects) + def test_credentials_auth_nopassword(self): self.gl.email = None self.gl.password = None diff --git a/gitlab/tests/test_gitlabobject.py b/gitlab/tests/test_gitlabobject.py index 695f900d8..f7fd1872f 100644 --- a/gitlab/tests/test_gitlabobject.py +++ b/gitlab/tests/test_gitlabobject.py @@ -21,6 +21,7 @@ from __future__ import absolute_import import json +import pickle try: import unittest except ImportError: @@ -158,6 +159,15 @@ def test_json(self): self.assertEqual(data["username"], "testname") self.assertEqual(data["gitlab"]["url"], "http://localhost/api/v3") + def test_pickability(self): + gl_object = CurrentUser(self.gl, data={"username": "testname"}) + original_obj_module = gl_object._module + pickled = pickle.dumps(gl_object) + unpickled = pickle.loads(pickled) + self.assertIsInstance(unpickled, CurrentUser) + self.assertTrue(hasattr(unpickled, '_module')) + self.assertEqual(unpickled._module, original_obj_module) + def test_data_for_gitlab(self): class FakeObj1(GitlabObject): _url = '/fake1' From fa897468cf565fb8546b47637cd9703981aedbc0 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Fri, 3 Nov 2017 15:22:11 +0100 Subject: [PATCH 27/29] [doc] Add sample code for client-side certificates Closes #23 --- docs/api-usage.rst | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/api-usage.rst b/docs/api-usage.rst index ad188ceff..edd41d010 100644 --- a/docs/api-usage.rst +++ b/docs/api-usage.rst @@ -253,6 +253,9 @@ HTTP requests to the Gitlab servers. You can provide your own ``Session`` object with custom configuration when you create a ``Gitlab`` object. +Proxy configuration +------------------- + The following sample illustrates how to define a proxy configuration when using python-gitlab: @@ -267,3 +270,23 @@ python-gitlab: 'http': os.environ.get('http_proxy'), } gl = gitlab.gitlab(url, token, api_version=4, session=session) + +Reference: +http://docs.python-requests.org/en/master/user/advanced/#proxies + +Client side certificate +----------------------- + +The following sample illustrates how to use a client-side certificate: + +.. code-block:: python + + import gitlab + import requests + + session = requests.Session() + s.cert = ('/path/to/client.cert', '/path/to/client.key') + gl = gitlab.gitlab(url, token, api_version=4, session=session) + +Reference: +http://docs.python-requests.org/en/master/user/advanced/#client-side-certificates From 9eff543a42014ba30cf8af099534d507f7acebd4 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Fri, 3 Nov 2017 15:27:13 +0100 Subject: [PATCH 28/29] improve comment in release notes --- RELEASE_NOTES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE_NOTES.rst b/RELEASE_NOTES.rst index 0c0098e7f..44705ee4c 100644 --- a/RELEASE_NOTES.rst +++ b/RELEASE_NOTES.rst @@ -18,7 +18,7 @@ Changes from 1.0.2 to 1.1 .. code-block:: python for gr_project in group.projects.list(): - # lazy object creation doesn't need an Gitlab API request + # lazy object creation avoids a Gitlab API request project = gl.projects.get(gr_project.id, lazy=True) project.default_branch = 'develop' project.save() From 32f7e17208987fa345670421c333e22ae6aced6a Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Fri, 3 Nov 2017 15:37:15 +0100 Subject: [PATCH 29/29] 1.1.0 release --- AUTHORS | 2 ++ ChangeLog.rst | 27 +++++++++++++++++++++++++++ gitlab/__init__.py | 2 +- 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 81c476f01..7937908c0 100644 --- a/AUTHORS +++ b/AUTHORS @@ -43,11 +43,13 @@ James E. Flemer James Johnson Jamie Bliss Jason Antman +Jerome Robert Johan Brandhorst Jonathon Reinhart Jon Banafato Koen Smets Kris Gambirazzi +Lyudmil Nenov Mart Sõmermaa massimone88 Matej Zerovnik diff --git a/ChangeLog.rst b/ChangeLog.rst index 7dbdda67b..fe6b2014a 100644 --- a/ChangeLog.rst +++ b/ChangeLog.rst @@ -1,6 +1,32 @@ ChangeLog ========= +Version 1.1.0_ - 2017-11-03 +--------------------------- + +* Fix trigger variables in v4 API +* Make the delete() method handle / in ids +* [docs] update the file upload samples +* Tags release description: support / in tag names +* [docs] improve the labels usage documentation +* Add support for listing project users +* ProjectFileManager.create: handle / in file paths +* Change ProjectUser and GroupProject base class +* [docs] document `get_create_attrs` in the API tutorial +* Document the Gitlab session parameter +* ProjectFileManager: custom update() method +* Project: add support for printing_merge_request_link_enabled attr +* Update the ssl_verify docstring +* Add support for group milestones +* Add support for GPG keys +* Add support for wiki pages +* Update the repository_blob documentation +* Fix the CLI for objects without ID (API v4) +* Add a contributed Dockerfile +* Pagination generators: expose more information +* Module's base objects serialization +* [doc] Add sample code for client-side certificates + Version 1.0.2_ - 2017-09-29 --------------------------- @@ -469,6 +495,7 @@ Version 0.1 - 2013-07-08 * Initial release +.. _1.1.0: https://github.com/python-gitlab/python-gitlab/compare/1.0.2...1.1.0 .. _1.0.2: https://github.com/python-gitlab/python-gitlab/compare/1.0.1...1.0.2 .. _1.0.1: https://github.com/python-gitlab/python-gitlab/compare/1.0.0...1.0.1 .. _1.0.0: https://github.com/python-gitlab/python-gitlab/compare/0.21.2...1.0.0 diff --git a/gitlab/__init__.py b/gitlab/__init__.py index 965bf382b..d5b480be6 100644 --- a/gitlab/__init__.py +++ b/gitlab/__init__.py @@ -34,7 +34,7 @@ from gitlab.v3.objects import * # noqa __title__ = 'python-gitlab' -__version__ = '1.0.2' +__version__ = '1.1.0' __author__ = 'Gauvain Pocentek' __email__ = 'gauvain@pocentek.net' __license__ = 'LGPL3'