summaryrefslogtreecommitdiff
path: root/gitlab
diff options
context:
space:
mode:
Diffstat (limited to 'gitlab')
-rw-r--r--gitlab/__init__.py13
-rw-r--r--gitlab/exceptions.py4
-rw-r--r--gitlab/mixins.py5
-rw-r--r--gitlab/tests/test_gitlab.py120
-rw-r--r--gitlab/utils.py4
-rw-r--r--gitlab/v4/objects.py70
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",))