summaryrefslogtreecommitdiff
path: root/gitlab
diff options
context:
space:
mode:
authorMax Wittig <max.wittig@siemens.com>2020-04-06 09:40:35 +0200
committerGitHub <noreply@github.com>2020-04-06 09:40:35 +0200
commitc161852b5a976d11f682c5af00ff3f4e8daa26ef (patch)
treecb2b89648e7f3678e0e6bfb47f60f77979296f99 /gitlab
parentfa34f5e20ecbd3f5d868df2fa9e399ac6559c5d5 (diff)
parent847da6063b4c63c8133e5e5b5b45e5b4f004bdc4 (diff)
downloadgitlab-c161852b5a976d11f682c5af00ff3f4e8daa26ef.tar.gz
Merge pull request #1063 from python-gitlab/feat/group-import-export
Feat: support for group import/export API
Diffstat (limited to 'gitlab')
-rw-r--r--gitlab/exceptions.py4
-rw-r--r--gitlab/mixins.py29
-rw-r--r--gitlab/tests/objects/mocks.py35
-rw-r--r--gitlab/tests/objects/test_commits.py3
-rw-r--r--gitlab/tests/objects/test_groups.py101
-rw-r--r--gitlab/tests/objects/test_projects.py29
-rw-r--r--gitlab/tests/test_gitlab.py17
-rw-r--r--gitlab/v4/objects.py79
8 files changed, 223 insertions, 74 deletions
diff --git a/gitlab/exceptions.py b/gitlab/exceptions.py
index d6791f2..099a901 100644
--- a/gitlab/exceptions.py
+++ b/gitlab/exceptions.py
@@ -209,6 +209,10 @@ class GitlabAttachFileError(GitlabOperationError):
pass
+class GitlabImportError(GitlabOperationError):
+ pass
+
+
class GitlabCherryPickError(GitlabOperationError):
pass
diff --git a/gitlab/mixins.py b/gitlab/mixins.py
index dde11d0..9c00c32 100644
--- a/gitlab/mixins.py
+++ b/gitlab/mixins.py
@@ -443,6 +443,35 @@ class AccessRequestMixin(object):
self._update_attrs(server_data)
+class DownloadMixin(object):
+ @cli.register_custom_action(("GroupExport", "ProjectExport"))
+ @exc.on_http_error(exc.GitlabGetError)
+ def download(self, streamed=False, action=None, chunk_size=1024, **kwargs):
+ """Download the archive of a resource export.
+
+ Args:
+ streamed (bool): If True the data will be processed by chunks of
+ `chunk_size` and each chunk is passed to `action` for
+ reatment
+ action (callable): Callable responsible of dealing with chunk of
+ data
+ chunk_size (int): Size of each chunk
+ **kwargs: Extra options to send to the server (e.g. sudo)
+
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabGetError: If the server failed to perform the request
+
+ Returns:
+ str: The blob content if streamed is False, None otherwise
+ """
+ path = "%s/download" % (self.manager.path)
+ result = self.manager.gitlab.http_get(
+ path, streamed=streamed, raw=True, **kwargs
+ )
+ return utils.response_content(result, streamed, action, chunk_size)
+
+
class SubscribableMixin(object):
@cli.register_custom_action(
("ProjectIssue", "ProjectMergeRequest", "ProjectLabel", "GroupLabel")
diff --git a/gitlab/tests/objects/mocks.py b/gitlab/tests/objects/mocks.py
new file mode 100644
index 0000000..e051339
--- /dev/null
+++ b/gitlab/tests/objects/mocks.py
@@ -0,0 +1,35 @@
+"""Common mocks for resources in gitlab.v4.objects"""
+
+from httmock import response, urlmatch
+
+
+headers = {"content-type": "application/json"}
+binary_content = b"binary content"
+
+
+@urlmatch(
+ scheme="http",
+ netloc="localhost",
+ path="/api/v4/(groups|projects)/1/export",
+ method="post",
+)
+def resp_create_export(url, request):
+ """Common mock for Group/Project Export POST response."""
+ content = """{
+ "message": "202 Accepted"
+ }"""
+ content = content.encode("utf-8")
+ return response(202, content, headers, None, 25, request)
+
+
+@urlmatch(
+ scheme="http",
+ netloc="localhost",
+ path="/api/v4/(groups|projects)/1/export/download",
+ method="get",
+)
+def resp_download_export(url, request):
+ """Common mock for Group/Project Export Download GET response."""
+ headers = {"content-type": "application/octet-stream"}
+ content = binary_content
+ return response(200, content, headers, None, 25, request)
diff --git a/gitlab/tests/objects/test_commits.py b/gitlab/tests/objects/test_commits.py
index 23a4285..7e7c3b4 100644
--- a/gitlab/tests/objects/test_commits.py
+++ b/gitlab/tests/objects/test_commits.py
@@ -1,6 +1,7 @@
from httmock import urlmatch, response, with_httmock
-from .test_projects import headers, TestProject
+from .mocks import headers
+from .test_projects import TestProject
@urlmatch(
diff --git a/gitlab/tests/objects/test_groups.py b/gitlab/tests/objects/test_groups.py
new file mode 100644
index 0000000..075d915
--- /dev/null
+++ b/gitlab/tests/objects/test_groups.py
@@ -0,0 +1,101 @@
+import unittest
+
+from httmock import response, urlmatch, with_httmock
+
+import gitlab
+from .mocks import * # noqa
+
+
+@urlmatch(scheme="http", netloc="localhost", path="/api/v4/groups/1", method="get")
+def resp_get_group(url, request):
+ content = '{"name": "name", "id": 1, "path": "path"}'
+ content = content.encode("utf-8")
+ return response(200, content, headers, None, 5, request)
+
+
+@urlmatch(scheme="http", netloc="localhost", path="/api/v4/groups", method="post")
+def resp_create_group(url, request):
+ content = '{"name": "name", "id": 1, "path": "path"}'
+ content = content.encode("utf-8")
+ return response(200, content, headers, None, 5, request)
+
+
+@urlmatch(
+ scheme="http", netloc="localhost", path="/api/v4/groups/import", method="post",
+)
+def resp_create_import(url, request):
+ """Mock for Group import tests.
+
+ GitLab does not respond with import status for group imports.
+ """
+
+ content = """{
+ "message": "202 Accepted"
+ }"""
+ content = content.encode("utf-8")
+ return response(202, content, headers, None, 25, request)
+
+
+class TestGroup(unittest.TestCase):
+ def setUp(self):
+ self.gl = gitlab.Gitlab(
+ "http://localhost",
+ private_token="private_token",
+ ssl_verify=True,
+ api_version=4,
+ )
+
+ @with_httmock(resp_get_group)
+ def test_get_group(self):
+ data = self.gl.groups.get(1)
+ self.assertIsInstance(data, gitlab.v4.objects.Group)
+ self.assertEqual(data.name, "name")
+ self.assertEqual(data.path, "path")
+ self.assertEqual(data.id, 1)
+
+ @with_httmock(resp_create_group)
+ def test_create_group(self):
+ name, path = "name", "path"
+ data = self.gl.groups.create({"name": name, "path": path})
+ self.assertIsInstance(data, gitlab.v4.objects.Group)
+ self.assertEqual(data.name, name)
+ self.assertEqual(data.path, path)
+
+
+class TestGroupExport(TestGroup):
+ def setUp(self):
+ super(TestGroupExport, self).setUp()
+ self.group = self.gl.groups.get(1, lazy=True)
+
+ @with_httmock(resp_create_export)
+ def test_create_group_export(self):
+ export = self.group.exports.create()
+ self.assertEqual(export.message, "202 Accepted")
+
+ @unittest.skip("GitLab API endpoint not implemented")
+ @with_httmock(resp_create_export)
+ def test_refresh_group_export_status(self):
+ export = self.group.exports.create()
+ export.refresh()
+ self.assertEqual(export.export_status, "finished")
+
+ @with_httmock(resp_create_export, resp_download_export)
+ def test_download_group_export(self):
+ export = self.group.exports.create()
+ download = export.download()
+ self.assertIsInstance(download, bytes)
+ self.assertEqual(download, binary_content)
+
+
+class TestGroupImport(TestGroup):
+ @with_httmock(resp_create_import)
+ def test_import_group(self):
+ group_import = self.gl.groups.import_group("file", "api-group", "API Group")
+ self.assertEqual(group_import["message"], "202 Accepted")
+
+ @unittest.skip("GitLab API endpoint not implemented")
+ @with_httmock(resp_create_import)
+ def test_refresh_group_import_status(self):
+ group_import = self.group.imports.get()
+ group_import.refresh()
+ self.assertEqual(group_import.import_status, "finished")
diff --git a/gitlab/tests/objects/test_projects.py b/gitlab/tests/objects/test_projects.py
index d87f759..1c2347a 100644
--- a/gitlab/tests/objects/test_projects.py
+++ b/gitlab/tests/objects/test_projects.py
@@ -10,21 +10,7 @@ from gitlab import * # noqa
from gitlab.v4.objects import * # noqa
from httmock import HTTMock, urlmatch, response, with_httmock # noqa
-
-headers = {"content-type": "application/json"}
-binary_content = b"binary content"
-
-
-@urlmatch(
- scheme="http", netloc="localhost", path="/api/v4/projects/1/export", method="post",
-)
-def resp_create_export(url, request):
- """Common mock for Project Export tests."""
- content = """{
- "message": "202 Accepted"
- }"""
- content = content.encode("utf-8")
- return response(202, content, headers, None, 25, request)
+from .mocks import * # noqa
@urlmatch(
@@ -52,19 +38,6 @@ def resp_export_status(url, request):
@urlmatch(
- scheme="http",
- netloc="localhost",
- path="/api/v4/projects/1/export/download",
- method="get",
-)
-def resp_download_export(url, request):
- """Mock for Project Export Download GET response."""
- headers = {"content-type": "application/octet-stream"}
- content = binary_content
- return response(200, content, headers, None, 25, request)
-
-
-@urlmatch(
scheme="http", netloc="localhost", path="/api/v4/projects/import", method="post",
)
def resp_import_project(url, request):
diff --git a/gitlab/tests/test_gitlab.py b/gitlab/tests/test_gitlab.py
index d104c7d..8261cc6 100644
--- a/gitlab/tests/test_gitlab.py
+++ b/gitlab/tests/test_gitlab.py
@@ -626,23 +626,6 @@ class TestGitlab(unittest.TestCase):
self.assertIsInstance(statistics, ProjectIssuesStatistics)
self.assertEqual(statistics.statistics["counts"]["all"], 20)
- def test_groups(self):
- @urlmatch(
- scheme="http", netloc="localhost", path="/api/v4/groups/1", method="get"
- )
- def resp_get_group(url, request):
- headers = {"content-type": "application/json"}
- content = '{"name": "name", "id": 1, "path": "path"}'
- content = content.encode("utf-8")
- return response(200, content, headers, None, 5, request)
-
- with HTTMock(resp_get_group):
- data = self.gl.groups.get(1)
- self.assertIsInstance(data, Group)
- self.assertEqual(data.name, "name")
- self.assertEqual(data.path, "path")
- self.assertEqual(data.id, 1)
-
def test_issues(self):
@urlmatch(
scheme="http", netloc="localhost", path="/api/v4/issues", method="get"
diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py
index 0b95944..9855196 100644
--- a/gitlab/v4/objects.py
+++ b/gitlab/v4/objects.py
@@ -991,6 +991,26 @@ class GroupEpicManager(CRUDMixin, RESTManager):
_types = {"labels": types.ListAttribute}
+class GroupExport(DownloadMixin, RESTObject):
+ _id_attr = None
+
+
+class GroupExportManager(GetWithoutIdMixin, CreateMixin, RESTManager):
+ _path = "/groups/%(group_id)s/export"
+ _obj_cls = GroupExport
+ _from_parent_attrs = {"group_id": "id"}
+
+
+class GroupImport(RESTObject):
+ _id_attr = None
+
+
+class GroupImportManager(GetWithoutIdMixin, RESTManager):
+ _path = "/groups/%(group_id)s/import"
+ _obj_cls = GroupImport
+ _from_parent_attrs = {"group_id": "id"}
+
+
class GroupIssue(RESTObject):
pass
@@ -1290,7 +1310,9 @@ class Group(SaveMixin, ObjectDeleteMixin, RESTObject):
("badges", "GroupBadgeManager"),
("boards", "GroupBoardManager"),
("customattributes", "GroupCustomAttributeManager"),
+ ("exports", "GroupExportManager"),
("epics", "GroupEpicManager"),
+ ("imports", "GroupImportManager"),
("issues", "GroupIssueManager"),
("labels", "GroupLabelManager"),
("members", "GroupMemberManager"),
@@ -1454,6 +1476,34 @@ class GroupManager(CRUDMixin, RESTManager):
),
)
+ @exc.on_http_error(exc.GitlabImportError)
+ def import_group(self, file, path, name, parent_id=None, **kwargs):
+ """Import a group from an archive file.
+
+ Args:
+ file: Data or file object containing the group
+ path (str): The path for the new group to be imported.
+ name (str): The name for the new group.
+ parent_id (str): ID of a parent group that the group will
+ be imported into.
+ **kwargs: Extra options to send to the server (e.g. sudo)
+
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabImportError: If the server failed to perform the request
+
+ Returns:
+ dict: A representation of the import status.
+ """
+ files = {"file": ("file.tar.gz", file)}
+ data = {"path": path, "name": name}
+ if parent_id is not None:
+ data["parent_id"] = parent_id
+
+ return self.gitlab.http_post(
+ "/groups/import", post_data=data, files=files, **kwargs
+ )
+
class Hook(ObjectDeleteMixin, RESTObject):
_url = "/hooks"
@@ -4120,36 +4170,9 @@ class ProjectWikiManager(CRUDMixin, RESTManager):
_list_filters = ("with_content",)
-class ProjectExport(RefreshMixin, RESTObject):
+class ProjectExport(DownloadMixin, RefreshMixin, RESTObject):
_id_attr = None
- @cli.register_custom_action("ProjectExport")
- @exc.on_http_error(exc.GitlabGetError)
- def download(self, streamed=False, action=None, chunk_size=1024, **kwargs):
- """Download the archive of a project export.
-
- Args:
- streamed (bool): If True the data will be processed by chunks of
- `chunk_size` and each chunk is passed to `action` for
- reatment
- action (callable): Callable responsible of dealing with chunk of
- data
- chunk_size (int): Size of each chunk
- **kwargs: Extra options to send to the server (e.g. sudo)
-
- Raises:
- GitlabAuthenticationError: If authentication is not correct
- GitlabGetError: If the server failed to perform the request
-
- Returns:
- str: The blob content if streamed is False, None otherwise
- """
- path = "/projects/%s/export/download" % self.project_id
- result = self.manager.gitlab.http_get(
- path, streamed=streamed, raw=True, **kwargs
- )
- return utils.response_content(result, streamed, action, chunk_size)
-
class ProjectExportManager(GetWithoutIdMixin, CreateMixin, RESTManager):
_path = "/projects/%(project_id)s/export"