| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 2cd15ac commit fdc854a
15 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -16,6 +16,7 @@ | |||
| 16 | 16 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
| 17 | 17 | """Wrapper for the GitLab API.""" | |
| 18 | 18 | ||
| 19 | + import copy | ||
| 19 | 20 | import time | |
| 20 | 21 | from typing import Any, cast, Dict, List, Optional, Tuple, TYPE_CHECKING, Union | |
| 21 | 22 | ||
@@ -26,6 +27,7 @@ | |||
| 26 | 27 | import gitlab.config | |
| 27 | 28 | import gitlab.const | |
| 28 | 29 | import gitlab.exceptions | |
| 30 | + from gitlab import types as gl_types | ||
| 29 | 31 | from gitlab import utils | |
| 30 | 32 | ||
| 31 | 33 | REDIRECT_MSG = ( | |
@@ -524,6 +526,28 @@ def _prepare_send_data( | |||
| 524 | 526 | ||
| 525 | 527 | return (post_data, None, "application/json") | |
| 526 | 528 | ||
| 529 | + @staticmethod | ||
| 530 | + def _prepare_dict_for_api(*, in_dict: Dict[str, Any]) -> Dict[str, Any]: | ||
| 531 | + result: Dict[str, Any] = {} | ||
| 532 | + for key, value in in_dict.items(): | ||
| 533 | + if isinstance(value, gl_types.GitlabAttribute): | ||
| 534 | + result[key] = value.get_for_api() | ||
| 535 | + else: | ||
| 536 | + result[key] = copy.deepcopy(in_dict[key]) | ||
| 537 | + return result | ||
| 538 | + | ||
| 539 | + @staticmethod | ||
| 540 | + def _param_dict_to_param_tuples(*, params: Dict[str, Any]) -> List[Tuple[str, Any]]: | ||
| 541 | + """Convert a dict to a list of key/values. This will be used to pass | ||
| 542 | + values to requests""" | ||
| 543 | + result: List[Tuple[str, Any]] = [] | ||
| 544 | + for key, value in params.items(): | ||
| 545 | + if isinstance(value, gl_types.GitlabAttribute): | ||
| 546 | + result.extend(value.get_as_tuple_list(key=key)) | ||
| 547 | + else: | ||
| 548 | + result.append((key, value)) | ||
| 549 | + return result | ||
| 550 | + | ||
| 527 | 551 | def http_request( | |
| 528 | 552 | self, | |
| 529 | 553 | verb: str, | |
@@ -584,6 +608,10 @@ def http_request( | |||
| 584 | 608 | else: | |
| 585 | 609 | utils.copy_dict(params, kwargs) | |
| 586 | 610 | ||
| 611 | + tuple_params = self._param_dict_to_param_tuples(params=params) | ||
| 612 | + if isinstance(post_data, dict): | ||
| 613 | + post_data = self._prepare_dict_for_api(in_dict=post_data) | ||
| 614 | + | ||
| 587 | 615 | opts = self._get_session_opts() | |
| 588 | 616 | ||
| 589 | 617 | verify = opts.pop("verify") | |
@@ -602,7 +630,9 @@ def http_request( | |||
| 602 | 630 | # The Requests behavior is right but it seems that web servers don't | |
| 603 | 631 | # always agree with this decision (this is the case with a default | |
| 604 | 632 | # gitlab installation) | |
| 605 | - req = requests.Request(verb, url, json=json, data=data, params=params, **opts) | ||
| 633 | + req = requests.Request( | ||
| 634 | + verb, url, json=json, data=data, params=tuple_params, **opts | ||
| 635 | + ) | ||
| 606 | 636 | prepped = self.session.prepare_request(req) | |
| 607 | 637 | if TYPE_CHECKING: | |
| 608 | 638 | assert prepped.url is not None | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -230,8 +230,7 @@ def list(self, **kwargs: Any) -> Union[base.RESTObjectList, List[base.RESTObject | |||
| 230 | 230 | if self._types: | |
| 231 | 231 | for attr_name, type_cls in self._types.items(): | |
| 232 | 232 | if attr_name in data.keys(): | |
| 233 | - type_obj = type_cls(data[attr_name]) | ||
| 234 | - data[attr_name] = type_obj.get_for_api() | ||
| 233 | + data[attr_name] = type_cls(data[attr_name]) | ||
| 235 | 234 | ||
| 236 | 235 | # Allow to overwrite the path, handy for custom listings | |
| 237 | 236 | path = data.pop("path", self.path) | |
@@ -307,14 +306,13 @@ def create( | |||
| 307 | 306 | for attr_name, type_cls in self._types.items(): | |
| 308 | 307 | if attr_name in data.keys(): | |
| 309 | 308 | type_obj = type_cls(data[attr_name]) | |
| 310 | - | ||
| 311 | 309 | # if the type if FileAttribute we need to pass the data as | |
| 312 | 310 | # file | |
| 313 | 311 | if isinstance(type_obj, g_types.FileAttribute): | |
| 314 | 312 | k = type_obj.get_file_name(attr_name) | |
| 315 | 313 | files[attr_name] = (k, data.pop(attr_name)) | |
| 316 | 314 | else: | |
| 317 | - data[attr_name] = type_obj.get_for_api() | ||
| 315 | + data[attr_name] = type_obj | ||
| 318 | 316 | ||
| 319 | 317 | # Handle specific URL for creation | |
| 320 | 318 | path = kwargs.pop("path", self.path) | |
@@ -410,7 +408,7 @@ def update( | |||
| 410 | 408 | k = type_obj.get_file_name(attr_name) | |
| 411 | 409 | files[attr_name] = (k, new_data.pop(attr_name)) | |
| 412 | 410 | else: | |
| 413 | - new_data[attr_name] = type_obj.get_for_api() | ||
| 411 | + new_data[attr_name] = type_obj | ||
| 414 | 412 | ||
| 415 | 413 | http_method = self._get_update_method() | |
| 416 | 414 | result = http_method(path, post_data=new_data, files=files, **kwargs) | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -15,7 +15,7 @@ | |||
| 15 | 15 | # You should have received a copy of the GNU Lesser General Public License | |
| 16 | 16 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
| 17 | 17 | ||
| 18 | - from typing import Any, Optional, TYPE_CHECKING | ||
| 18 | + from typing import Any, List, Optional, Tuple, TYPE_CHECKING | ||
| 19 | 19 | ||
| 20 | 20 | ||
| 21 | 21 | class GitlabAttribute(object): | |
@@ -31,8 +31,43 @@ def set_from_cli(self, cli_value: Any) -> None: | |||
| 31 | 31 | def get_for_api(self) -> Any: | |
| 32 | 32 | return self._value | |
| 33 | 33 | ||
| 34 | + def get_as_tuple_list(self, *, key: str) -> List[Tuple[str, Any]]: | ||
| 35 | + return [(key, self._value)] | ||
| 36 | + | ||
| 37 | + | ||
| 38 | + class ArrayAttribute(GitlabAttribute): | ||
| 39 | + """To support `array` types as documented in | ||
| 40 | + https://docs.gitlab.com/ee/api/#array""" | ||
| 41 | + | ||
| 42 | + def set_from_cli(self, cli_value: str) -> None: | ||
| 43 | + if not cli_value.strip(): | ||
| 44 | + self._value = [] | ||
| 45 | + else: | ||
| 46 | + self._value = [item.strip() for item in cli_value.split(",")] | ||
| 47 | + | ||
| 48 | + def get_for_api(self) -> str: | ||
| 49 | + # Do not comma-split single value passed as string | ||
| 50 | + if isinstance(self._value, str): | ||
| 51 | + return self._value | ||
| 52 | + | ||
| 53 | + if TYPE_CHECKING: | ||
| 54 | + assert isinstance(self._value, list) | ||
| 55 | + return ",".join([str(x) for x in self._value]) | ||
| 56 | + | ||
| 57 | + def get_as_tuple_list(self, *, key: str) -> List[Tuple[str, str]]: | ||
| 58 | + if isinstance(self._value, str): | ||
| 59 | + return [(f"{key}[]", self._value)] | ||
| 60 | + | ||
| 61 | + if TYPE_CHECKING: | ||
| 62 | + assert isinstance(self._value, list) | ||
| 63 | + return [(f"{key}[]", str(value)) for value in self._value] | ||
| 64 | + | ||
| 65 | + | ||
| 66 | + class CsvStringAttribute(GitlabAttribute): | ||
| 67 | + """For values which are sent to the server as a Comma Separated Values | ||
| 68 | + (CSV) string. We allow them to be specified as a list and we convert it | ||
| 69 | + into a CSV""" | ||
| 34 | 70 | ||
| 35 | - class ListAttribute(GitlabAttribute): | ||
| 36 | 71 | def set_from_cli(self, cli_value: str) -> None: | |
| 37 | 72 | if not cli_value.strip(): | |
| 38 | 73 | self._value = [] | |
@@ -48,11 +83,17 @@ def get_for_api(self) -> str: | |||
| 48 | 83 | assert isinstance(self._value, list) | |
| 49 | 84 | return ",".join([str(x) for x in self._value]) | |
| 50 | 85 | ||
| 86 | + def get_as_tuple_list(self, *, key: str) -> List[Tuple[str, str]]: | ||
| 87 | + return [(key, self.get_for_api())] | ||
| 88 | + | ||
| 51 | 89 | ||
| 52 | 90 | class LowercaseStringAttribute(GitlabAttribute): | |
| 53 | 91 | def get_for_api(self) -> str: | |
| 54 | 92 | return str(self._value).lower() | |
| 55 | 93 | ||
| 94 | + def get_as_tuple_list(self, *, key: str) -> List[Tuple[str, str]]: | ||
| 95 | + return [(key, self.get_for_api())] | ||
| 96 | + | ||
| 56 | 97 | ||
| 57 | 98 | class FileAttribute(GitlabAttribute): | |
| 58 | 99 | def get_file_name(self, attr_name: Optional[str] = None) -> Optional[str]: | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -39,7 +39,7 @@ class GroupDeployTokenManager(ListMixin, CreateMixin, DeleteMixin, RESTManager): | |||
| 39 | 39 | "username", | |
| 40 | 40 | ), | |
| 41 | 41 | ) | |
| 42 | - _types = {"scopes": types.ListAttribute} | ||
| 42 | + _types = {"scopes": types.CsvStringAttribute} | ||
| 43 | 43 | ||
| 44 | 44 | ||
| 45 | 45 | class ProjectDeployToken(ObjectDeleteMixin, RESTObject): | |
@@ -60,4 +60,4 @@ class ProjectDeployTokenManager(ListMixin, CreateMixin, DeleteMixin, RESTManager | |||
| 60 | 60 | "username", | |
| 61 | 61 | ), | |
| 62 | 62 | ) | |
| 63 | - _types = {"scopes": types.ListAttribute} | ||
| 63 | + _types = {"scopes": types.CsvStringAttribute} | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -40,7 +40,7 @@ class GroupEpicManager(CRUDMixin, RESTManager): | |||
| 40 | 40 | _update_attrs = RequiredOptional( | |
| 41 | 41 | optional=("title", "labels", "description", "start_date", "end_date"), | |
| 42 | 42 | ) | |
| 43 | - _types = {"labels": types.ListAttribute} | ||
| 43 | + _types = {"labels": types.CsvStringAttribute} | ||
| 44 | 44 | ||
| 45 | 45 | ||
| 46 | 46 | class GroupEpicIssue(ObjectDeleteMixin, SaveMixin, RESTObject): | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -290,7 +290,7 @@ class GroupManager(CRUDMixin, RESTManager): | |||
| 290 | 290 | "shared_runners_setting", | |
| 291 | 291 | ), | |
| 292 | 292 | ) | |
| 293 | - _types = {"avatar": types.ImageAttribute, "skip_groups": types.ListAttribute} | ||
| 293 | + _types = {"avatar": types.ImageAttribute, "skip_groups": types.ArrayAttribute} | ||
| 294 | 294 | ||
| 295 | 295 | def get(self, id: Union[str, int], lazy: bool = False, **kwargs: Any) -> Group: | |
| 296 | 296 | return cast(Group, super().get(id=id, lazy=lazy, **kwargs)) | |
@@ -350,7 +350,7 @@ class GroupSubgroupManager(ListMixin, RESTManager): | |||
| 350 | 350 | "with_custom_attributes", | |
| 351 | 351 | "min_access_level", | |
| 352 | 352 | ) | |
| 353 | - _types = {"skip_groups": types.ListAttribute} | ||
| 353 | + _types = {"skip_groups": types.ArrayAttribute} | ||
| 354 | 354 | ||
| 355 | 355 | ||
| 356 | 356 | class GroupDescendantGroup(RESTObject): | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -63,7 +63,7 @@ class IssueManager(RetrieveMixin, RESTManager): | |||
| 63 | 63 | "updated_after", | |
| 64 | 64 | "updated_before", | |
| 65 | 65 | ) | |
| 66 | - _types = {"iids": types.ListAttribute, "labels": types.ListAttribute} | ||
| 66 | + _types = {"iids": types.ArrayAttribute, "labels": types.CsvStringAttribute} | ||
| 67 | 67 | ||
| 68 | 68 | ||
| 69 | 69 | class GroupIssue(RESTObject): | |
@@ -90,7 +90,7 @@ class GroupIssueManager(ListMixin, RESTManager): | |||
| 90 | 90 | "updated_after", | |
| 91 | 91 | "updated_before", | |
| 92 | 92 | ) | |
| 93 | - _types = {"iids": types.ListAttribute, "labels": types.ListAttribute} | ||
| 93 | + _types = {"iids": types.ArrayAttribute, "labels": types.CsvStringAttribute} | ||
| 94 | 94 | ||
| 95 | 95 | ||
| 96 | 96 | class ProjectIssue( | |
@@ -220,7 +220,7 @@ class ProjectIssueManager(CRUDMixin, RESTManager): | |||
| 220 | 220 | "discussion_locked", | |
| 221 | 221 | ), | |
| 222 | 222 | ) | |
| 223 | - _types = {"iids": types.ListAttribute, "labels": types.ListAttribute} | ||
| 223 | + _types = {"iids": types.ArrayAttribute, "labels": types.CsvStringAttribute} | ||
| 224 | 224 | ||
| 225 | 225 | ||
| 226 | 226 | class ProjectIssueLink(ObjectDeleteMixin, RESTObject): | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -39,7 +39,7 @@ class GroupMemberManager(CRUDMixin, RESTManager): | |||
| 39 | 39 | _update_attrs = RequiredOptional( | |
| 40 | 40 | required=("access_level",), optional=("expires_at",) | |
| 41 | 41 | ) | |
| 42 | - _types = {"user_ids": types.ListAttribute} | ||
| 42 | + _types = {"user_ids": types.ArrayAttribute} | ||
| 43 | 43 | ||
| 44 | 44 | def get( | |
| 45 | 45 | self, id: Union[str, int], lazy: bool = False, **kwargs: Any | |
@@ -95,7 +95,7 @@ class ProjectMemberManager(CRUDMixin, RESTManager): | |||
| 95 | 95 | _update_attrs = RequiredOptional( | |
| 96 | 96 | required=("access_level",), optional=("expires_at",) | |
| 97 | 97 | ) | |
| 98 | - _types = {"user_ids": types.ListAttribute} | ||
| 98 | + _types = {"user_ids": types.ArrayAttribute} | ||
| 99 | 99 | ||
| 100 | 100 | def get( | |
| 101 | 101 | self, id: Union[str, int], lazy: bool = False, **kwargs: Any | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -95,10 +95,10 @@ class MergeRequestManager(ListMixin, RESTManager): | |||
| 95 | 95 | "deployed_after", | |
| 96 | 96 | ) | |
| 97 | 97 | _types = { | |
| 98 | - "approver_ids": types.ListAttribute, | ||
| 99 | - "approved_by_ids": types.ListAttribute, | ||
| 100 | - "in": types.ListAttribute, | ||
| 101 | - "labels": types.ListAttribute, | ||
| 98 | + "approver_ids": types.ArrayAttribute, | ||
| 99 | + "approved_by_ids": types.ArrayAttribute, | ||
| 100 | + "in": types.CsvStringAttribute, | ||
| 101 | + "labels": types.CsvStringAttribute, | ||
| 102 | 102 | } | |
| 103 | 103 | ||
| 104 | 104 | ||
@@ -133,9 +133,9 @@ class GroupMergeRequestManager(ListMixin, RESTManager): | |||
| 133 | 133 | "wip", | |
| 134 | 134 | ) | |
| 135 | 135 | _types = { | |
| 136 | - "approver_ids": types.ListAttribute, | ||
| 137 | - "approved_by_ids": types.ListAttribute, | ||
| 138 | - "labels": types.ListAttribute, | ||
| 136 | + "approver_ids": types.ArrayAttribute, | ||
| 137 | + "approved_by_ids": types.ArrayAttribute, | ||
| 138 | + "labels": types.CsvStringAttribute, | ||
| 139 | 139 | } | |
| 140 | 140 | ||
| 141 | 141 | ||
@@ -450,10 +450,10 @@ class ProjectMergeRequestManager(CRUDMixin, RESTManager): | |||
| 450 | 450 | "wip", | |
| 451 | 451 | ) | |
| 452 | 452 | _types = { | |
| 453 | - "approver_ids": types.ListAttribute, | ||
| 454 | - "approved_by_ids": types.ListAttribute, | ||
| 455 | - "iids": types.ListAttribute, | ||
| 456 | - "labels": types.ListAttribute, | ||
| 453 | + "approver_ids": types.ArrayAttribute, | ||
| 454 | + "approved_by_ids": types.ArrayAttribute, | ||
| 455 | + "iids": types.ArrayAttribute, | ||
| 456 | + "labels": types.CsvStringAttribute, | ||
| 457 | 457 | } | |
| 458 | 458 | ||
| 459 | 459 | def get( | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -89,7 +89,7 @@ class GroupMilestoneManager(CRUDMixin, RESTManager): | |||
| 89 | 89 | optional=("title", "description", "due_date", "start_date", "state_event"), | |
| 90 | 90 | ) | |
| 91 | 91 | _list_filters = ("iids", "state", "search") | |
| 92 | - _types = {"iids": types.ListAttribute} | ||
| 92 | + _types = {"iids": types.ArrayAttribute} | ||
| 93 | 93 | ||
| 94 | 94 | ||
| 95 | 95 | class ProjectMilestone(PromoteMixin, SaveMixin, ObjectDeleteMixin, RESTObject): | |
@@ -164,4 +164,4 @@ class ProjectMilestoneManager(CRUDMixin, RESTManager): | |||
| 164 | 164 | optional=("title", "description", "due_date", "start_date", "state_event"), | |
| 165 | 165 | ) | |
| 166 | 166 | _list_filters = ("iids", "state", "search") | |
| 167 | - _types = {"iids": types.ListAttribute} | ||
| 167 | + _types = {"iids": types.ArrayAttribute} | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments