summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--gitlab/client.py30
-rw-r--r--gitlab/mixins.py8
-rw-r--r--gitlab/types.py45
-rw-r--r--gitlab/v4/objects/deploy_tokens.py4
-rw-r--r--gitlab/v4/objects/epics.py2
-rw-r--r--gitlab/v4/objects/groups.py4
-rw-r--r--gitlab/v4/objects/issues.py6
-rw-r--r--gitlab/v4/objects/members.py4
-rw-r--r--gitlab/v4/objects/merge_requests.py22
-rw-r--r--gitlab/v4/objects/milestones.py4
-rw-r--r--gitlab/v4/objects/projects.py7
-rw-r--r--gitlab/v4/objects/runners.py6
-rw-r--r--gitlab/v4/objects/settings.py12
-rw-r--r--gitlab/v4/objects/users.py2
-rw-r--r--tests/unit/test_types.py77
15 files changed, 178 insertions, 55 deletions
diff --git a/gitlab/client.py b/gitlab/client.py
index b791c8f..a3e0cc4 100644
--- a/gitlab/client.py
+++ b/gitlab/client.py
@@ -16,6 +16,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""Wrapper for the GitLab API."""
+import copy
import os
import time
from typing import Any, cast, Dict, List, Optional, Tuple, TYPE_CHECKING, Union
@@ -27,6 +28,7 @@ from requests_toolbelt.multipart.encoder import MultipartEncoder # type: ignore
import gitlab.config
import gitlab.const
import gitlab.exceptions
+from gitlab import types as gl_types
from gitlab import utils
REDIRECT_MSG = (
@@ -604,6 +606,28 @@ class Gitlab(object):
return (post_data, None, "application/json")
+ @staticmethod
+ def _prepare_dict_for_api(*, in_dict: Dict[str, Any]) -> Dict[str, Any]:
+ result: Dict[str, Any] = {}
+ for key, value in in_dict.items():
+ if isinstance(value, gl_types.GitlabAttribute):
+ result[key] = value.get_for_api()
+ else:
+ result[key] = copy.deepcopy(in_dict[key])
+ return result
+
+ @staticmethod
+ def _param_dict_to_param_tuples(*, params: Dict[str, Any]) -> List[Tuple[str, Any]]:
+ """Convert a dict to a list of key/values. This will be used to pass
+ values to requests"""
+ result: List[Tuple[str, Any]] = []
+ for key, value in params.items():
+ if isinstance(value, gl_types.GitlabAttribute):
+ result.extend(value.get_as_tuple_list(key=key))
+ else:
+ result.append((key, value))
+ return result
+
def http_request(
self,
verb: str,
@@ -663,6 +687,10 @@ class Gitlab(object):
else:
utils.copy_dict(params, kwargs)
+ tuple_params = self._param_dict_to_param_tuples(params=params)
+ if isinstance(post_data, dict):
+ post_data = self._prepare_dict_for_api(in_dict=post_data)
+
opts = self._get_session_opts()
verify = opts.pop("verify")
@@ -682,7 +710,7 @@ class Gitlab(object):
url=url,
json=json,
data=data,
- params=params,
+ params=tuple_params,
timeout=timeout,
verify=verify,
stream=streamed,
diff --git a/gitlab/mixins.py b/gitlab/mixins.py
index d66b2eb..9f75b54 100644
--- a/gitlab/mixins.py
+++ b/gitlab/mixins.py
@@ -230,8 +230,7 @@ class ListMixin(_RestManagerBase):
if self._types:
for attr_name, type_cls in self._types.items():
if attr_name in data.keys():
- type_obj = type_cls(data[attr_name])
- data[attr_name] = type_obj.get_for_api()
+ data[attr_name] = type_cls(data[attr_name])
# Allow to overwrite the path, handy for custom listings
path = data.pop("path", self.path)
@@ -307,14 +306,13 @@ class CreateMixin(_RestManagerBase):
for attr_name, type_cls in self._types.items():
if attr_name in data.keys():
type_obj = type_cls(data[attr_name])
-
# if the type if FileAttribute we need to pass the data as
# file
if isinstance(type_obj, g_types.FileAttribute):
k = type_obj.get_file_name(attr_name)
files[attr_name] = (k, data.pop(attr_name))
else:
- data[attr_name] = type_obj.get_for_api()
+ data[attr_name] = type_obj
# Handle specific URL for creation
path = kwargs.pop("path", self.path)
@@ -410,7 +408,7 @@ class UpdateMixin(_RestManagerBase):
k = type_obj.get_file_name(attr_name)
files[attr_name] = (k, new_data.pop(attr_name))
else:
- new_data[attr_name] = type_obj.get_for_api()
+ new_data[attr_name] = type_obj
http_method = self._get_update_method()
result = http_method(path, post_data=new_data, files=files, **kwargs)
diff --git a/gitlab/types.py b/gitlab/types.py
index 5a15090..b610104 100644
--- a/gitlab/types.py
+++ b/gitlab/types.py
@@ -15,7 +15,7 @@
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-from typing import Any, Optional, TYPE_CHECKING
+from typing import Any, List, Optional, Tuple, TYPE_CHECKING
class GitlabAttribute(object):
@@ -31,8 +31,43 @@ class GitlabAttribute(object):
def get_for_api(self) -> Any:
return self._value
+ def get_as_tuple_list(self, *, key: str) -> List[Tuple[str, Any]]:
+ return [(key, self._value)]
+
+
+class ArrayAttribute(GitlabAttribute):
+ """To support `array` types as documented in
+ https://docs.gitlab.com/ee/api/#array"""
+
+ def set_from_cli(self, cli_value: str) -> None:
+ if not cli_value.strip():
+ self._value = []
+ else:
+ self._value = [item.strip() for item in cli_value.split(",")]
+
+ def get_for_api(self) -> str:
+ # Do not comma-split single value passed as string
+ if isinstance(self._value, str):
+ return self._value
+
+ if TYPE_CHECKING:
+ assert isinstance(self._value, list)
+ return ",".join([str(x) for x in self._value])
+
+ def get_as_tuple_list(self, *, key: str) -> List[Tuple[str, str]]:
+ if isinstance(self._value, str):
+ return [(f"{key}[]", self._value)]
+
+ if TYPE_CHECKING:
+ assert isinstance(self._value, list)
+ return [(f"{key}[]", str(value)) for value in self._value]
+
+
+class CommaSeparatedListAttribute(GitlabAttribute):
+ """For values which are sent to the server as a Comma Separated Values
+ (CSV) string. We allow them to be specified as a list and we convert it
+ into a CSV"""
-class ListAttribute(GitlabAttribute):
def set_from_cli(self, cli_value: str) -> None:
if not cli_value.strip():
self._value = []
@@ -48,11 +83,17 @@ class ListAttribute(GitlabAttribute):
assert isinstance(self._value, list)
return ",".join([str(x) for x in self._value])
+ def get_as_tuple_list(self, *, key: str) -> List[Tuple[str, str]]:
+ return [(key, self.get_for_api())]
+
class LowercaseStringAttribute(GitlabAttribute):
def get_for_api(self) -> str:
return str(self._value).lower()
+ def get_as_tuple_list(self, *, key: str) -> List[Tuple[str, str]]:
+ return [(key, self.get_for_api())]
+
class FileAttribute(GitlabAttribute):
def get_file_name(self, attr_name: Optional[str] = None) -> Optional[str]:
diff --git a/gitlab/v4/objects/deploy_tokens.py b/gitlab/v4/objects/deploy_tokens.py
index 97f3270..563c1d6 100644
--- a/gitlab/v4/objects/deploy_tokens.py
+++ b/gitlab/v4/objects/deploy_tokens.py
@@ -39,7 +39,7 @@ class GroupDeployTokenManager(ListMixin, CreateMixin, DeleteMixin, RESTManager):
"username",
),
)
- _types = {"scopes": types.ListAttribute}
+ _types = {"scopes": types.CommaSeparatedListAttribute}
class ProjectDeployToken(ObjectDeleteMixin, RESTObject):
@@ -60,4 +60,4 @@ class ProjectDeployTokenManager(ListMixin, CreateMixin, DeleteMixin, RESTManager
"username",
),
)
- _types = {"scopes": types.ListAttribute}
+ _types = {"scopes": types.CommaSeparatedListAttribute}
diff --git a/gitlab/v4/objects/epics.py b/gitlab/v4/objects/epics.py
index bb0bb79..d33821c 100644
--- a/gitlab/v4/objects/epics.py
+++ b/gitlab/v4/objects/epics.py
@@ -42,7 +42,7 @@ class GroupEpicManager(CRUDMixin, RESTManager):
_update_attrs = RequiredOptional(
optional=("title", "labels", "description", "start_date", "end_date"),
)
- _types = {"labels": types.ListAttribute}
+ _types = {"labels": types.CommaSeparatedListAttribute}
def get(self, id: Union[str, int], lazy: bool = False, **kwargs: Any) -> GroupEpic:
return cast(GroupEpic, super().get(id=id, lazy=lazy, **kwargs))
diff --git a/gitlab/v4/objects/groups.py b/gitlab/v4/objects/groups.py
index f2b90d1..99d10ce 100644
--- a/gitlab/v4/objects/groups.py
+++ b/gitlab/v4/objects/groups.py
@@ -314,7 +314,7 @@ class GroupManager(CRUDMixin, RESTManager):
"shared_runners_setting",
),
)
- _types = {"avatar": types.ImageAttribute, "skip_groups": types.ListAttribute}
+ _types = {"avatar": types.ImageAttribute, "skip_groups": types.ArrayAttribute}
def get(self, id: Union[str, int], lazy: bool = False, **kwargs: Any) -> Group:
return cast(Group, super().get(id=id, lazy=lazy, **kwargs))
@@ -374,7 +374,7 @@ class GroupSubgroupManager(ListMixin, RESTManager):
"with_custom_attributes",
"min_access_level",
)
- _types = {"skip_groups": types.ListAttribute}
+ _types = {"skip_groups": types.ArrayAttribute}
class GroupDescendantGroup(RESTObject):
diff --git a/gitlab/v4/objects/issues.py b/gitlab/v4/objects/issues.py
index 585e02e..f20252b 100644
--- a/gitlab/v4/objects/issues.py
+++ b/gitlab/v4/objects/issues.py
@@ -65,7 +65,7 @@ class IssueManager(RetrieveMixin, RESTManager):
"updated_after",
"updated_before",
)
- _types = {"iids": types.ListAttribute, "labels": types.ListAttribute}
+ _types = {"iids": types.ArrayAttribute, "labels": types.CommaSeparatedListAttribute}
def get(self, id: Union[str, int], lazy: bool = False, **kwargs: Any) -> Issue:
return cast(Issue, super().get(id=id, lazy=lazy, **kwargs))
@@ -95,7 +95,7 @@ class GroupIssueManager(ListMixin, RESTManager):
"updated_after",
"updated_before",
)
- _types = {"iids": types.ListAttribute, "labels": types.ListAttribute}
+ _types = {"iids": types.ArrayAttribute, "labels": types.CommaSeparatedListAttribute}
class ProjectIssue(
@@ -233,7 +233,7 @@ class ProjectIssueManager(CRUDMixin, RESTManager):
"discussion_locked",
),
)
- _types = {"iids": types.ListAttribute, "labels": types.ListAttribute}
+ _types = {"iids": types.ArrayAttribute, "labels": types.CommaSeparatedListAttribute}
def get(
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
diff --git a/gitlab/v4/objects/members.py b/gitlab/v4/objects/members.py
index c7be039..5ee0b0e 100644
--- a/gitlab/v4/objects/members.py
+++ b/gitlab/v4/objects/members.py
@@ -41,7 +41,7 @@ class GroupMemberManager(CRUDMixin, RESTManager):
_update_attrs = RequiredOptional(
required=("access_level",), optional=("expires_at",)
)
- _types = {"user_ids": types.ListAttribute}
+ _types = {"user_ids": types.ArrayAttribute}
def get(
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
@@ -101,7 +101,7 @@ class ProjectMemberManager(CRUDMixin, RESTManager):
_update_attrs = RequiredOptional(
required=("access_level",), optional=("expires_at",)
)
- _types = {"user_ids": types.ListAttribute}
+ _types = {"user_ids": types.ArrayAttribute}
def get(
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
diff --git a/gitlab/v4/objects/merge_requests.py b/gitlab/v4/objects/merge_requests.py
index 9a4f8c8..9f2368b 100644
--- a/gitlab/v4/objects/merge_requests.py
+++ b/gitlab/v4/objects/merge_requests.py
@@ -95,10 +95,10 @@ class MergeRequestManager(ListMixin, RESTManager):
"deployed_after",
)
_types = {
- "approver_ids": types.ListAttribute,
- "approved_by_ids": types.ListAttribute,
- "in": types.ListAttribute,
- "labels": types.ListAttribute,
+ "approver_ids": types.ArrayAttribute,
+ "approved_by_ids": types.ArrayAttribute,
+ "in": types.CommaSeparatedListAttribute,
+ "labels": types.CommaSeparatedListAttribute,
}
@@ -133,9 +133,9 @@ class GroupMergeRequestManager(ListMixin, RESTManager):
"wip",
)
_types = {
- "approver_ids": types.ListAttribute,
- "approved_by_ids": types.ListAttribute,
- "labels": types.ListAttribute,
+ "approver_ids": types.ArrayAttribute,
+ "approved_by_ids": types.ArrayAttribute,
+ "labels": types.CommaSeparatedListAttribute,
}
@@ -455,10 +455,10 @@ class ProjectMergeRequestManager(CRUDMixin, RESTManager):
"wip",
)
_types = {
- "approver_ids": types.ListAttribute,
- "approved_by_ids": types.ListAttribute,
- "iids": types.ListAttribute,
- "labels": types.ListAttribute,
+ "approver_ids": types.ArrayAttribute,
+ "approved_by_ids": types.ArrayAttribute,
+ "iids": types.ArrayAttribute,
+ "labels": types.CommaSeparatedListAttribute,
}
def get(
diff --git a/gitlab/v4/objects/milestones.py b/gitlab/v4/objects/milestones.py
index 6b1e28d..da75826 100644
--- a/gitlab/v4/objects/milestones.py
+++ b/gitlab/v4/objects/milestones.py
@@ -93,7 +93,7 @@ class GroupMilestoneManager(CRUDMixin, RESTManager):
optional=("title", "description", "due_date", "start_date", "state_event"),
)
_list_filters = ("iids", "state", "search")
- _types = {"iids": types.ListAttribute}
+ _types = {"iids": types.ArrayAttribute}
def get(
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
@@ -177,7 +177,7 @@ class ProjectMilestoneManager(CRUDMixin, RESTManager):
optional=("title", "description", "due_date", "start_date", "state_event"),
)
_list_filters = ("iids", "state", "search")
- _types = {"iids": types.ListAttribute}
+ _types = {"iids": types.ArrayAttribute}
def get(
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
diff --git a/gitlab/v4/objects/projects.py b/gitlab/v4/objects/projects.py
index f9988db..23f3d3c 100644
--- a/gitlab/v4/objects/projects.py
+++ b/gitlab/v4/objects/projects.py
@@ -125,7 +125,7 @@ class ProjectGroupManager(ListMixin, RESTManager):
"shared_min_access_level",
"shared_visible_only",
)
- _types = {"skip_groups": types.ListAttribute}
+ _types = {"skip_groups": types.ArrayAttribute}
class Project(RefreshMixin, SaveMixin, ObjectDeleteMixin, RepositoryMixin, RESTObject):
@@ -807,7 +807,10 @@ class ProjectManager(CRUDMixin, RESTManager):
"with_merge_requests_enabled",
"with_programming_language",
)
- _types = {"avatar": types.ImageAttribute, "topic": types.ListAttribute}
+ _types = {
+ "avatar": types.ImageAttribute,
+ "topic": types.CommaSeparatedListAttribute,
+ }
def get(self, id: Union[str, int], lazy: bool = False, **kwargs: Any) -> Project:
return cast(Project, super().get(id=id, lazy=lazy, **kwargs))
diff --git a/gitlab/v4/objects/runners.py b/gitlab/v4/objects/runners.py
index d340b99..1826945 100644
--- a/gitlab/v4/objects/runners.py
+++ b/gitlab/v4/objects/runners.py
@@ -68,7 +68,7 @@ class RunnerManager(CRUDMixin, RESTManager):
),
)
_list_filters = ("scope", "tag_list")
- _types = {"tag_list": types.ListAttribute}
+ _types = {"tag_list": types.CommaSeparatedListAttribute}
@cli.register_custom_action("RunnerManager", tuple(), ("scope",))
@exc.on_http_error(exc.GitlabListError)
@@ -130,7 +130,7 @@ class GroupRunnerManager(ListMixin, RESTManager):
_from_parent_attrs = {"group_id": "id"}
_create_attrs = RequiredOptional(required=("runner_id",))
_list_filters = ("scope", "tag_list")
- _types = {"tag_list": types.ListAttribute}
+ _types = {"tag_list": types.CommaSeparatedListAttribute}
class ProjectRunner(ObjectDeleteMixin, RESTObject):
@@ -143,4 +143,4 @@ class ProjectRunnerManager(CreateMixin, DeleteMixin, ListMixin, RESTManager):
_from_parent_attrs = {"project_id": "id"}
_create_attrs = RequiredOptional(required=("runner_id",))
_list_filters = ("scope", "tag_list")
- _types = {"tag_list": types.ListAttribute}
+ _types = {"tag_list": types.CommaSeparatedListAttribute}
diff --git a/gitlab/v4/objects/settings.py b/gitlab/v4/objects/settings.py
index 96f2539..7612403 100644
--- a/gitlab/v4/objects/settings.py
+++ b/gitlab/v4/objects/settings.py
@@ -80,12 +80,12 @@ class ApplicationSettingsManager(GetWithoutIdMixin, UpdateMixin, RESTManager):
),
)
_types = {
- "asset_proxy_allowlist": types.ListAttribute,
- "disabled_oauth_sign_in_sources": types.ListAttribute,
- "domain_allowlist": types.ListAttribute,
- "domain_denylist": types.ListAttribute,
- "import_sources": types.ListAttribute,
- "restricted_visibility_levels": types.ListAttribute,
+ "asset_proxy_allowlist": types.ArrayAttribute,
+ "disabled_oauth_sign_in_sources": types.ArrayAttribute,
+ "domain_allowlist": types.ArrayAttribute,
+ "domain_denylist": types.ArrayAttribute,
+ "import_sources": types.ArrayAttribute,
+ "restricted_visibility_levels": types.ArrayAttribute,
}
@exc.on_http_error(exc.GitlabUpdateError)
diff --git a/gitlab/v4/objects/users.py b/gitlab/v4/objects/users.py
index f5b8f6c..b2de337 100644
--- a/gitlab/v4/objects/users.py
+++ b/gitlab/v4/objects/users.py
@@ -369,7 +369,7 @@ class ProjectUserManager(ListMixin, RESTManager):
_obj_cls = ProjectUser
_from_parent_attrs = {"project_id": "id"}
_list_filters = ("search", "skip_users")
- _types = {"skip_users": types.ListAttribute}
+ _types = {"skip_users": types.ArrayAttribute}
class UserEmail(ObjectDeleteMixin, RESTObject):
diff --git a/tests/unit/test_types.py b/tests/unit/test_types.py
index a2e5ff5..768db19 100644
--- a/tests/unit/test_types.py
+++ b/tests/unit/test_types.py
@@ -25,13 +25,15 @@ def test_gitlab_attribute_get():
o.set_from_cli("whatever2")
assert o.get() == "whatever2"
assert o.get_for_api() == "whatever2"
+ assert o.get_as_tuple_list(key="foo") == [("foo", "whatever2")]
o = types.GitlabAttribute()
assert o._value is None
-def test_list_attribute_input():
- o = types.ListAttribute()
+# ArrayAttribute tests
+def test_array_attribute_input():
+ o = types.ArrayAttribute()
o.set_from_cli("foo,bar,baz")
assert o.get() == ["foo", "bar", "baz"]
@@ -39,8 +41,8 @@ def test_list_attribute_input():
assert o.get() == ["foo"]
-def test_list_attribute_empty_input():
- o = types.ListAttribute()
+def test_array_attribute_empty_input():
+ o = types.ArrayAttribute()
o.set_from_cli("")
assert o.get() == []
@@ -48,27 +50,78 @@ def test_list_attribute_empty_input():
assert o.get() == []
-def test_list_attribute_get_for_api_from_cli():
- o = types.ListAttribute()
+def test_array_attribute_get_for_api_from_cli():
+ o = types.ArrayAttribute()
o.set_from_cli("foo,bar,baz")
assert o.get_for_api() == "foo,bar,baz"
-def test_list_attribute_get_for_api_from_list():
- o = types.ListAttribute(["foo", "bar", "baz"])
+def test_array_attribute_get_for_api_from_list():
+ o = types.ArrayAttribute(["foo", "bar", "baz"])
assert o.get_for_api() == "foo,bar,baz"
-def test_list_attribute_get_for_api_from_int_list():
- o = types.ListAttribute([1, 9, 7])
+def test_array_attribute_get_for_api_from_int_list():
+ o = types.ArrayAttribute([1, 9, 7])
assert o.get_for_api() == "1,9,7"
-def test_list_attribute_does_not_split_string():
- o = types.ListAttribute("foo")
+def test_array_attribute_get_as_tuple_list_from_list():
+ o = types.ArrayAttribute(["foo", "bar", "baz"])
+ assert o.get_as_tuple_list(key="identifier") == [
+ ("identifier[]", "foo"),
+ ("identifier[]", "bar"),
+ ("identifier[]", "baz"),
+ ]
+
+
+def test_array_attribute_get_as_tuple_list_from_int_list():
+ o = types.ArrayAttribute([1, 9, 7])
+ assert o.get_as_tuple_list(key="identifier") == [
+ ("identifier[]", "1"),
+ ("identifier[]", "9"),
+ ("identifier[]", "7"),
+ ]
+
+
+def test_array_attribute_does_not_split_string():
+ o = types.ArrayAttribute("foo")
assert o.get_for_api() == "foo"
+# CommaSeparatedListAttribute tests
+def test_csv_string_attribute_get_for_api_from_cli():
+ o = types.CommaSeparatedListAttribute()
+ o.set_from_cli("foo,bar,baz")
+ assert o.get_for_api() == "foo,bar,baz"
+
+
+def test_csv_string_attribute_get_for_api_from_list():
+ o = types.CommaSeparatedListAttribute(["foo", "bar", "baz"])
+ assert o.get_for_api() == "foo,bar,baz"
+
+
+def test_csv_string_attribute_get_for_api_from_int_list():
+ o = types.CommaSeparatedListAttribute([1, 9, 7])
+ assert o.get_for_api() == "1,9,7"
+
+
+def test_csv_string_attribute_get_as_tuple_list_from_list():
+ o = types.CommaSeparatedListAttribute(["foo", "bar", "baz"])
+ assert o.get_as_tuple_list(key="identifier") == [("identifier", "foo,bar,baz")]
+
+
+def test_csv_string_attribute_get_as_tuple_list_from_int_list():
+ o = types.CommaSeparatedListAttribute([1, 9, 7])
+ assert o.get_as_tuple_list(key="identifier") == [("identifier", "1,9,7")]
+
+
+# LowercaseStringAttribute tests
def test_lowercase_string_attribute_get_for_api():
o = types.LowercaseStringAttribute("FOO")
assert o.get_for_api() == "foo"
+
+
+def test_lowercase_string_attribute_get_as_tuple():
+ o = types.LowercaseStringAttribute("FOO")
+ assert o.get_as_tuple_list(key="user_name") == [("user_name", "foo")]