diff options
Diffstat (limited to 'gitlab')
-rw-r--r-- | gitlab/__init__.py | 13 | ||||
-rw-r--r-- | gitlab/exceptions.py | 4 | ||||
-rw-r--r-- | gitlab/mixins.py | 5 | ||||
-rw-r--r-- | gitlab/tests/test_gitlab.py | 120 | ||||
-rw-r--r-- | gitlab/utils.py | 4 | ||||
-rw-r--r-- | gitlab/v4/objects.py | 70 |
6 files changed, 210 insertions, 6 deletions
diff --git a/gitlab/__init__.py b/gitlab/__init__.py index c9716c2..f924372 100644 --- a/gitlab/__init__.py +++ b/gitlab/__init__.py @@ -30,7 +30,7 @@ from gitlab.exceptions import * # noqa from gitlab import utils # noqa __title__ = "python-gitlab" -__version__ = "2.0.0" +__version__ = "2.1.0" __author__ = "Gauvain Pocentek" __email__ = "gauvainpocentek@gmail.com" __license__ = "LGPL3" @@ -90,8 +90,8 @@ class Gitlab(object): self._api_version = str(api_version) self._server_version = self._server_revision = None - self._base_url = url - self._url = "%s/api/v%s" % (url, api_version) + self._base_url = url.rstrip("/") + self._url = "%s/api/v%s" % (self._base_url, api_version) #: Timeout to use for requests to gitlab server self.timeout = timeout #: Headers that will be used in request to GitLab @@ -144,6 +144,7 @@ class Gitlab(object): self.features = objects.FeatureManager(self) self.pagesdomains = objects.PagesDomainManager(self) self.user_activities = objects.UserActivitiesManager(self) + self.applications = objects.ApplicationManager(self) def __enter__(self): return self @@ -640,6 +641,12 @@ class Gitlab(object): get_all = kwargs.pop("all", False) url = self._build_url(path) + # use keyset pagination automatically, if all=True + order_by = kwargs.get("order_by") + if get_all and (not order_by or order_by == "id"): + kwargs["pagination"] = "keyset" + kwargs["order_by"] = "id" + if get_all is True and as_list is True: return list(GitlabList(self, url, query_data, **kwargs)) diff --git a/gitlab/exceptions.py b/gitlab/exceptions.py index aff3c87..d6791f2 100644 --- a/gitlab/exceptions.py +++ b/gitlab/exceptions.py @@ -245,6 +245,10 @@ class GitlabRepairError(GitlabOperationError): pass +class GitlabRevertError(GitlabOperationError): + pass + + class GitlabLicenseError(GitlabOperationError): pass diff --git a/gitlab/mixins.py b/gitlab/mixins.py index 8544499..dde11d0 100644 --- a/gitlab/mixins.py +++ b/gitlab/mixins.py @@ -170,7 +170,7 @@ class CreateMixin(object): return getattr(self, "_create_attrs", (tuple(), tuple())) @exc.on_http_error(exc.GitlabCreateError) - def create(self, data, **kwargs): + def create(self, data=None, **kwargs): """Create a new object. Args: @@ -186,6 +186,9 @@ class CreateMixin(object): GitlabAuthenticationError: If authentication is not correct GitlabCreateError: If the server cannot perform the request """ + if data is None: + data = {} + self._check_missing_create_attrs(data) files = {} diff --git a/gitlab/tests/test_gitlab.py b/gitlab/tests/test_gitlab.py index 3eccf6e..249d0c5 100644 --- a/gitlab/tests/test_gitlab.py +++ b/gitlab/tests/test_gitlab.py @@ -376,6 +376,23 @@ class TestGitlabHttpMethods(unittest.TestCase): self.assertRaises(GitlabHttpError, self.gl.http_delete, "/not_there") +class TestGitlabStripBaseUrl(unittest.TestCase): + def setUp(self): + self.gl = Gitlab( + "http://localhost/", private_token="private_token", api_version=4 + ) + + def test_strip_base_url(self): + self.assertEqual(self.gl.url, "http://localhost") + + def test_strip_api_url(self): + self.assertEqual(self.gl.api_url, "http://localhost/api/v4") + + def test_build_url(self): + r = self.gl._build_url("/projects") + self.assertEqual(r, "http://localhost/api/v4/projects") + + class TestGitlabAuth(unittest.TestCase): def test_invalid_auth_args(self): self.assertRaises( @@ -658,6 +675,38 @@ class TestGitlab(unittest.TestCase): self.assertEqual(user.name, "name") self.assertEqual(user.id, 1) + def test_user_memberships(self): + @urlmatch( + scheme="http", + netloc="localhost", + path="/api/v4/users/1/memberships", + method="get", + ) + def resp_get_user_memberships(url, request): + headers = {"content-type": "application/json"} + content = """[ + { + "source_id": 1, + "source_name": "Project one", + "source_type": "Project", + "access_level": "20" + }, + { + "source_id": 3, + "source_name": "Group three", + "source_type": "Namespace", + "access_level": "20" + } + ]""" + content = content.encode("utf-8") + return response(200, content, headers, None, 5, request) + + with HTTMock(resp_get_user_memberships): + user = self.gl.users.get(1, lazy=True) + memberships = user.memberships.list() + self.assertIsInstance(memberships[0], UserMembership) + self.assertEqual(memberships[0].source_type, "Project") + def test_user_status(self): @urlmatch( scheme="http", @@ -794,6 +843,50 @@ class TestGitlab(unittest.TestCase): self.gl.users.get(1, lazy=True).activate() self.gl.users.get(1, lazy=True).deactivate() + def test_commit_revert(self): + @urlmatch( + scheme="http", + netloc="localhost", + path="/api/v4/projects/1/repository/commits/6b2257ea", + method="get", + ) + def resp_get_commit(url, request): + headers = {"content-type": "application/json"} + content = """{ + "id": "6b2257eabcec3db1f59dafbd84935e3caea04235", + "short_id": "6b2257ea", + "title": "Initial commit" + }""" + content = content.encode("utf-8") + return response(200, content, headers, None, 5, request) + + @urlmatch( + scheme="http", + netloc="localhost", + path="/api/v4/projects/1/repository/commits/6b2257ea", + method="post", + ) + def resp_revert_commit(url, request): + headers = {"content-type": "application/json"} + content = """{ + "id": "8b090c1b79a14f2bd9e8a738f717824ff53aebad", + "short_id": "8b090c1b", + "title":"Revert \\"Initial commit\\"" + }""" + content = content.encode("utf-8") + return response(200, content, headers, None, 5, request) + + with HTTMock(resp_get_commit): + project = self.gl.projects.get(1, lazy=True) + commit = project.commits.get("6b2257ea") + self.assertEqual(commit.short_id, "6b2257ea") + self.assertEqual(commit.title, "Initial commit") + + with HTTMock(resp_revert_commit): + revert_commit = commit.revert(branch="master") + self.assertEqual(revert_commit["short_id"], "8b090c1b") + self.assertEqual(revert_commit["title"], 'Revert "Initial commit"') + def test_update_submodule(self): @urlmatch( scheme="http", netloc="localhost", path="/api/v4/projects/1$", method="get" @@ -871,6 +964,33 @@ class TestGitlab(unittest.TestCase): self.assertEqual(ret["full_path"], "/".join((base_path, name))) self.assertTrue(ret["full_name"].endswith(name)) + def test_applications(self): + content = '{"name": "test_app", "redirect_uri": "http://localhost:8080", "scopes": ["api", "email"]}' + json_content = json.loads(content) + + @urlmatch( + scheme="http", + netloc="localhost", + path="/api/v4/applications", + method="post", + ) + def resp_application_create(url, request): + headers = {"content-type": "application/json"} + return response(200, json_content, headers, None, 5, request) + + with HTTMock(resp_application_create): + application = self.gl.applications.create( + { + "name": "test_app", + "redirect_uri": "http://localhost:8080", + "scopes": ["api", "email"], + "confidential": False, + } + ) + self.assertEqual(application.name, "test_app") + self.assertEqual(application.redirect_uri, "http://localhost:8080") + self.assertEqual(application.scopes, ["api", "email"]) + def _default_config(self): fd, temp_path = tempfile.mkstemp() os.write(fd, valid_config) diff --git a/gitlab/utils.py b/gitlab/utils.py index 0992ed7..4241787 100644 --- a/gitlab/utils.py +++ b/gitlab/utils.py @@ -55,3 +55,7 @@ def sanitized_url(url): parsed = urlparse(url) new_path = parsed.path.replace(".", "%2E") return parsed._replace(path=new_path).geturl() + + +def remove_none_from_dict(data): + return {k: v for k, v in data.items() if v is not None} diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index f22229c..13fbb53 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -229,6 +229,17 @@ class UserImpersonationTokenManager(NoUpdateMixin, RESTManager): _list_filters = ("state",) +class UserMembership(RESTObject): + _id_attr = "source_id" + + +class UserMembershipManager(RetrieveMixin, RESTManager): + _path = "/users/%(user_id)s/memberships" + _obj_cls = UserMembership + _from_parent_attrs = {"user_id": "id"} + _list_filters = ("type",) + + class UserProject(RESTObject): pass @@ -311,6 +322,7 @@ class User(SaveMixin, ObjectDeleteMixin, RESTObject): ("gpgkeys", "UserGPGKeyManager"), ("impersonationtokens", "UserImpersonationTokenManager"), ("keys", "UserKeyManager"), + ("memberships", "UserMembershipManager"), ("projects", "UserProjectManager"), ("status", "UserStatusManager"), ) @@ -414,6 +426,7 @@ class UserManager(CRUDMixin, RESTManager): "search", "custom_attributes", "status", + "two_factor", ) _create_attrs = ( tuple(), @@ -438,6 +451,8 @@ class UserManager(CRUDMixin, RESTManager): "organization", "location", "avatar", + "public_email", + "private_profile", ), ) _update_attrs = ( @@ -459,6 +474,8 @@ class UserManager(CRUDMixin, RESTManager): "organization", "location", "avatar", + "public_email", + "private_profile", ), ) _types = {"confirm": types.LowercaseStringAttribute, "avatar": types.ImageAttribute} @@ -719,7 +736,16 @@ class FeatureManager(ListMixin, DeleteMixin, RESTManager): _obj_cls = Feature @exc.on_http_error(exc.GitlabSetError) - def set(self, name, value, feature_group=None, user=None, **kwargs): + def set( + self, + name, + value, + feature_group=None, + user=None, + group=None, + project=None, + **kwargs + ): """Create or update the object. Args: @@ -727,6 +753,8 @@ class FeatureManager(ListMixin, DeleteMixin, RESTManager): value (bool/int): The value to set for the object feature_group (str): A feature group name user (str): A GitLab username + group (str): A GitLab group + project (str): A GitLab project in form group/project **kwargs: Extra options to send to the server (e.g. sudo) Raises: @@ -737,7 +765,14 @@ class FeatureManager(ListMixin, DeleteMixin, RESTManager): obj: The created/updated attribute """ path = "%s/%s" % (self.path, name.replace("/", "%2F")) - data = {"value": value, "feature_group": feature_group, "user": user} + data = { + "value": value, + "feature_group": feature_group, + "user": user, + "group": group, + "project": project, + } + data = utils.remove_none_from_dict(data) server_data = self.gitlab.http_post(path, post_data=data, **kwargs) return self._obj_cls(self, server_data) @@ -2113,6 +2148,26 @@ class ProjectCommit(RESTObject): path = "%s/%s/merge_requests" % (self.manager.path, self.get_id()) return self.manager.gitlab.http_get(path, **kwargs) + @cli.register_custom_action("ProjectCommit", ("branch",)) + @exc.on_http_error(exc.GitlabRevertError) + def revert(self, branch, **kwargs): + """Revert a commit on a given branch. + + Args: + branch (str): Name of target branch + **kwargs: Extra options to send to the server (e.g. sudo) + + Raises: + GitlabAuthenticationError: If authentication is not correct + GitlabRevertError: If the revert could not be performed + + Returns: + dict: The new commit data (*not* a RESTObject) + """ + path = "%s/%s/revert" % (self.manager.path, self.get_id()) + post_data = {"branch": branch} + return self.manager.gitlab.http_post(path, post_data=post_data, **kwargs) + class ProjectCommitManager(RetrieveMixin, CreateMixin, RESTManager): _path = "/projects/%(project_id)s/repository/commits" @@ -5087,3 +5142,14 @@ class GeoNodeManager(RetrieveMixin, UpdateMixin, DeleteMixin, RESTManager): list: The list of failures """ return self.gitlab.http_list("/geo_nodes/current/failures", **kwargs) + + +class Application(ObjectDeleteMixin, RESTObject): + _url = "/applications" + _short_print_attr = "name" + + +class ApplicationManager(ListMixin, CreateMixin, DeleteMixin, RESTManager): + _path = "/applications" + _obj_cls = Application + _create_attrs = (("name", "redirect_uri", "scopes"), ("confidential",)) |