python-gitlab 5.5.0__py3-none-any.whl → 6.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. gitlab/__init__.py +0 -1
  2. gitlab/_backends/protocol.py +9 -13
  3. gitlab/_backends/requests_backend.py +12 -12
  4. gitlab/_version.py +1 -1
  5. gitlab/base.py +48 -48
  6. gitlab/cli.py +14 -24
  7. gitlab/client.py +114 -140
  8. gitlab/config.py +16 -17
  9. gitlab/exceptions.py +7 -5
  10. gitlab/mixins.py +154 -238
  11. gitlab/types.py +13 -14
  12. gitlab/utils.py +32 -43
  13. gitlab/v4/cli.py +50 -53
  14. gitlab/v4/objects/__init__.py +1 -0
  15. gitlab/v4/objects/access_requests.py +11 -3
  16. gitlab/v4/objects/appearance.py +12 -14
  17. gitlab/v4/objects/applications.py +5 -6
  18. gitlab/v4/objects/artifacts.py +10 -17
  19. gitlab/v4/objects/audit_events.py +4 -19
  20. gitlab/v4/objects/award_emojis.py +13 -57
  21. gitlab/v4/objects/badges.py +4 -19
  22. gitlab/v4/objects/boards.py +7 -27
  23. gitlab/v4/objects/branches.py +3 -15
  24. gitlab/v4/objects/broadcast_messages.py +3 -13
  25. gitlab/v4/objects/bulk_imports.py +6 -14
  26. gitlab/v4/objects/ci_lint.py +7 -13
  27. gitlab/v4/objects/cluster_agents.py +3 -13
  28. gitlab/v4/objects/clusters.py +13 -23
  29. gitlab/v4/objects/commits.py +23 -28
  30. gitlab/v4/objects/container_registry.py +13 -19
  31. gitlab/v4/objects/custom_attributes.py +16 -21
  32. gitlab/v4/objects/deploy_keys.py +22 -19
  33. gitlab/v4/objects/deploy_tokens.py +14 -32
  34. gitlab/v4/objects/deployments.py +13 -15
  35. gitlab/v4/objects/discussions.py +13 -29
  36. gitlab/v4/objects/draft_notes.py +4 -14
  37. gitlab/v4/objects/environments.py +13 -21
  38. gitlab/v4/objects/epics.py +14 -17
  39. gitlab/v4/objects/events.py +27 -79
  40. gitlab/v4/objects/export_import.py +7 -19
  41. gitlab/v4/objects/features.py +11 -12
  42. gitlab/v4/objects/files.py +23 -38
  43. gitlab/v4/objects/geo_nodes.py +7 -11
  44. gitlab/v4/objects/group_access_tokens.py +6 -13
  45. gitlab/v4/objects/groups.py +42 -37
  46. gitlab/v4/objects/hooks.py +4 -17
  47. gitlab/v4/objects/integrations.py +7 -18
  48. gitlab/v4/objects/invitations.py +12 -23
  49. gitlab/v4/objects/issues.py +21 -27
  50. gitlab/v4/objects/iterations.py +4 -8
  51. gitlab/v4/objects/job_token_scope.py +18 -14
  52. gitlab/v4/objects/jobs.py +17 -32
  53. gitlab/v4/objects/keys.py +8 -11
  54. gitlab/v4/objects/labels.py +19 -30
  55. gitlab/v4/objects/ldap.py +25 -9
  56. gitlab/v4/objects/member_roles.py +102 -0
  57. gitlab/v4/objects/members.py +11 -29
  58. gitlab/v4/objects/merge_request_approvals.py +47 -38
  59. gitlab/v4/objects/merge_requests.py +30 -40
  60. gitlab/v4/objects/merge_trains.py +3 -6
  61. gitlab/v4/objects/milestones.py +23 -29
  62. gitlab/v4/objects/namespaces.py +4 -10
  63. gitlab/v4/objects/notes.py +26 -69
  64. gitlab/v4/objects/notification_settings.py +5 -14
  65. gitlab/v4/objects/package_protection_rules.py +8 -8
  66. gitlab/v4/objects/packages.py +22 -37
  67. gitlab/v4/objects/pages.py +8 -14
  68. gitlab/v4/objects/personal_access_tokens.py +7 -10
  69. gitlab/v4/objects/pipelines.py +38 -47
  70. gitlab/v4/objects/project_access_tokens.py +6 -13
  71. gitlab/v4/objects/projects.py +54 -76
  72. gitlab/v4/objects/push_rules.py +13 -15
  73. gitlab/v4/objects/registry_protection_repository_rules.py +6 -7
  74. gitlab/v4/objects/registry_protection_rules.py +7 -11
  75. gitlab/v4/objects/releases.py +6 -20
  76. gitlab/v4/objects/repositories.py +25 -34
  77. gitlab/v4/objects/resource_groups.py +10 -15
  78. gitlab/v4/objects/reviewers.py +4 -2
  79. gitlab/v4/objects/runners.py +14 -13
  80. gitlab/v4/objects/secure_files.py +8 -21
  81. gitlab/v4/objects/service_accounts.py +7 -5
  82. gitlab/v4/objects/settings.py +13 -14
  83. gitlab/v4/objects/sidekiq.py +17 -18
  84. gitlab/v4/objects/snippets.py +78 -66
  85. gitlab/v4/objects/statistics.py +8 -23
  86. gitlab/v4/objects/status_checks.py +6 -3
  87. gitlab/v4/objects/tags.py +3 -13
  88. gitlab/v4/objects/templates.py +11 -59
  89. gitlab/v4/objects/todos.py +3 -6
  90. gitlab/v4/objects/topics.py +10 -21
  91. gitlab/v4/objects/triggers.py +3 -13
  92. gitlab/v4/objects/users.py +87 -93
  93. gitlab/v4/objects/variables.py +4 -19
  94. gitlab/v4/objects/wikis.py +4 -19
  95. {python_gitlab-5.5.0.dist-info → python_gitlab-6.0.0.dist-info}/METADATA +3 -2
  96. python_gitlab-6.0.0.dist-info/RECORD +107 -0
  97. {python_gitlab-5.5.0.dist-info → python_gitlab-6.0.0.dist-info}/WHEEL +1 -1
  98. python_gitlab-5.5.0.dist-info/RECORD +0 -106
  99. {python_gitlab-5.5.0.dist-info → python_gitlab-6.0.0.dist-info}/entry_points.txt +0 -0
  100. {python_gitlab-5.5.0.dist-info → python_gitlab-6.0.0.dist-info/licenses}/AUTHORS +0 -0
  101. {python_gitlab-5.5.0.dist-info → python_gitlab-6.0.0.dist-info/licenses}/COPYING +0 -0
  102. {python_gitlab-5.5.0.dist-info → python_gitlab-6.0.0.dist-info}/top_level.txt +0 -0
gitlab/types.py CHANGED
@@ -1,18 +1,17 @@
1
+ from __future__ import annotations
2
+
1
3
  import dataclasses
2
- from typing import Any, Dict, List, Optional, Tuple, TYPE_CHECKING
4
+ from typing import Any, TYPE_CHECKING
3
5
 
4
6
 
5
7
  @dataclasses.dataclass(frozen=True)
6
8
  class RequiredOptional:
7
- required: Tuple[str, ...] = ()
8
- optional: Tuple[str, ...] = ()
9
- exclusive: Tuple[str, ...] = ()
9
+ required: tuple[str, ...] = ()
10
+ optional: tuple[str, ...] = ()
11
+ exclusive: tuple[str, ...] = ()
10
12
 
11
13
  def validate_attrs(
12
- self,
13
- *,
14
- data: Dict[str, Any],
15
- excludes: Optional[List[str]] = None,
14
+ self, *, data: dict[str, Any], excludes: list[str] | None = None
16
15
  ) -> None:
17
16
  if excludes is None:
18
17
  excludes = []
@@ -46,7 +45,7 @@ class GitlabAttribute:
46
45
  def set_from_cli(self, cli_value: Any) -> None:
47
46
  self._value = cli_value
48
47
 
49
- def get_for_api(self, *, key: str) -> Tuple[str, Any]:
48
+ def get_for_api(self, *, key: str) -> tuple[str, Any]:
50
49
  return (key, self._value)
51
50
 
52
51
 
@@ -59,7 +58,7 @@ class _ListArrayAttribute(GitlabAttribute):
59
58
  else:
60
59
  self._value = [item.strip() for item in cli_value.split(",")]
61
60
 
62
- def get_for_api(self, *, key: str) -> Tuple[str, str]:
61
+ def get_for_api(self, *, key: str) -> tuple[str, str]:
63
62
  # Do not comma-split single value passed as string
64
63
  if isinstance(self._value, str):
65
64
  return (key, self._value)
@@ -73,7 +72,7 @@ class ArrayAttribute(_ListArrayAttribute):
73
72
  """To support `array` types as documented in
74
73
  https://docs.gitlab.com/ee/api/#array"""
75
74
 
76
- def get_for_api(self, *, key: str) -> Tuple[str, Any]:
75
+ def get_for_api(self, *, key: str) -> tuple[str, Any]:
77
76
  if isinstance(self._value, str):
78
77
  return (f"{key}[]", self._value)
79
78
 
@@ -89,17 +88,17 @@ class CommaSeparatedListAttribute(_ListArrayAttribute):
89
88
 
90
89
 
91
90
  class LowercaseStringAttribute(GitlabAttribute):
92
- def get_for_api(self, *, key: str) -> Tuple[str, str]:
91
+ def get_for_api(self, *, key: str) -> tuple[str, str]:
93
92
  return (key, str(self._value).lower())
94
93
 
95
94
 
96
95
  class FileAttribute(GitlabAttribute):
97
96
  @staticmethod
98
- def get_file_name(attr_name: Optional[str] = None) -> Optional[str]:
97
+ def get_file_name(attr_name: str | None = None) -> str | None:
99
98
  return attr_name
100
99
 
101
100
 
102
101
  class ImageAttribute(FileAttribute):
103
102
  @staticmethod
104
- def get_file_name(attr_name: Optional[str] = None) -> str:
103
+ def get_file_name(attr_name: str | None = None) -> str:
105
104
  return f"{attr_name}.png" if attr_name else "image.png"
gitlab/utils.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import dataclasses
2
4
  import email.message
3
5
  import logging
@@ -6,18 +8,8 @@ import time
6
8
  import traceback
7
9
  import urllib.parse
8
10
  import warnings
9
- from typing import (
10
- Any,
11
- Callable,
12
- Dict,
13
- Iterator,
14
- Literal,
15
- MutableMapping,
16
- Optional,
17
- Tuple,
18
- Type,
19
- Union,
20
- )
11
+ from collections.abc import Iterator, MutableMapping
12
+ from typing import Any, Callable, Literal
21
13
 
22
14
  import requests
23
15
 
@@ -29,7 +21,7 @@ class _StdoutStream:
29
21
  print(chunk)
30
22
 
31
23
 
32
- def get_base_url(url: Optional[str] = None) -> str:
24
+ def get_base_url(url: str | None = None) -> str:
33
25
  """Return the base URL with the trailing slash stripped.
34
26
  If the URL is a Falsy value, return the default URL.
35
27
  Returns:
@@ -41,7 +33,7 @@ def get_base_url(url: Optional[str] = None) -> str:
41
33
  return url.rstrip("/")
42
34
 
43
35
 
44
- def get_content_type(content_type: Optional[str]) -> str:
36
+ def get_content_type(content_type: str | None) -> str:
45
37
  message = email.message.Message()
46
38
  if content_type is not None:
47
39
  message["content-type"] = content_type
@@ -54,11 +46,11 @@ class MaskingFormatter(logging.Formatter):
54
46
 
55
47
  def __init__(
56
48
  self,
57
- fmt: Optional[str] = logging.BASIC_FORMAT,
58
- datefmt: Optional[str] = None,
49
+ fmt: str | None = logging.BASIC_FORMAT,
50
+ datefmt: str | None = None,
59
51
  style: Literal["%", "{", "$"] = "%",
60
52
  validate: bool = True,
61
- masked: Optional[str] = None,
53
+ masked: str | None = None,
62
54
  ) -> None:
63
55
  super().__init__(fmt, datefmt, style, validate)
64
56
  self.masked = masked
@@ -77,11 +69,11 @@ class MaskingFormatter(logging.Formatter):
77
69
  def response_content(
78
70
  response: requests.Response,
79
71
  streamed: bool,
80
- action: Optional[Callable[[bytes], Any]],
72
+ action: Callable[[bytes], Any] | None,
81
73
  chunk_size: int,
82
74
  *,
83
75
  iterator: bool,
84
- ) -> Optional[Union[bytes, Iterator[Any]]]:
76
+ ) -> bytes | Iterator[Any] | None:
85
77
  if iterator:
86
78
  return response.iter_content(chunk_size=chunk_size)
87
79
 
@@ -101,17 +93,15 @@ class Retry:
101
93
  def __init__(
102
94
  self,
103
95
  max_retries: int,
104
- obey_rate_limit: Optional[bool] = True,
105
- retry_transient_errors: Optional[bool] = False,
96
+ obey_rate_limit: bool | None = True,
97
+ retry_transient_errors: bool | None = False,
106
98
  ) -> None:
107
99
  self.cur_retries = 0
108
100
  self.max_retries = max_retries
109
101
  self.obey_rate_limit = obey_rate_limit
110
102
  self.retry_transient_errors = retry_transient_errors
111
103
 
112
- def _retryable_status_code(
113
- self, status_code: Optional[int], reason: str = ""
114
- ) -> bool:
104
+ def _retryable_status_code(self, status_code: int | None, reason: str = "") -> bool:
115
105
  if status_code == 429 and self.obey_rate_limit:
116
106
  return True
117
107
 
@@ -126,8 +116,8 @@ class Retry:
126
116
 
127
117
  def handle_retry_on_status(
128
118
  self,
129
- status_code: Optional[int],
130
- headers: Optional[MutableMapping[str, str]] = None,
119
+ status_code: int | None,
120
+ headers: MutableMapping[str, str] | None = None,
131
121
  reason: str = "",
132
122
  ) -> bool:
133
123
  if not self._retryable_status_code(status_code, reason):
@@ -163,12 +153,12 @@ class Retry:
163
153
 
164
154
 
165
155
  def _transform_types(
166
- data: Dict[str, Any],
167
- custom_types: Dict[str, Any],
156
+ data: dict[str, Any],
157
+ custom_types: dict[str, Any],
168
158
  *,
169
159
  transform_data: bool,
170
- transform_files: Optional[bool] = True,
171
- ) -> Tuple[Dict[str, Any], Dict[str, Any]]:
160
+ transform_files: bool | None = True,
161
+ ) -> tuple[dict[str, Any], dict[str, Any]]:
172
162
  """Copy the data dict with attributes that have custom types and transform them
173
163
  before being sent to the server.
174
164
 
@@ -198,6 +188,12 @@ def _transform_types(
198
188
 
199
189
  # if the type is FileAttribute we need to pass the data as file
200
190
  if isinstance(gitlab_attribute, types.FileAttribute) and transform_files:
191
+ # The GitLab API accepts mixed types
192
+ # (e.g. a file for avatar image or empty string for removing the avatar)
193
+ # So if string is empty, keep it in data dict
194
+ if isinstance(data[attr_name], str) and data[attr_name] == "":
195
+ continue
196
+
201
197
  key = gitlab_attribute.get_file_name(attr_name)
202
198
  files[attr_name] = (key, data.pop(attr_name))
203
199
  continue
@@ -214,11 +210,7 @@ def _transform_types(
214
210
  return data, files
215
211
 
216
212
 
217
- def copy_dict(
218
- *,
219
- src: Dict[str, Any],
220
- dest: Dict[str, Any],
221
- ) -> None:
213
+ def copy_dict(*, src: dict[str, Any], dest: dict[str, Any]) -> None:
222
214
  for k, v in src.items():
223
215
  if isinstance(v, dict):
224
216
  # NOTE(jlvillal): This provides some support for the `hash` type
@@ -247,7 +239,7 @@ class EncodedId(str):
247
239
  https://docs.gitlab.com/ee/api/index.html#path-parameters
248
240
  """
249
241
 
250
- def __new__(cls, value: Union[str, int, "EncodedId"]) -> "EncodedId":
242
+ def __new__(cls, value: str | int | EncodedId) -> EncodedId:
251
243
  if isinstance(value, EncodedId):
252
244
  return value
253
245
 
@@ -258,15 +250,15 @@ class EncodedId(str):
258
250
  return super().__new__(cls, value)
259
251
 
260
252
 
261
- def remove_none_from_dict(data: Dict[str, Any]) -> Dict[str, Any]:
253
+ def remove_none_from_dict(data: dict[str, Any]) -> dict[str, Any]:
262
254
  return {k: v for k, v in data.items() if v is not None}
263
255
 
264
256
 
265
257
  def warn(
266
258
  message: str,
267
259
  *,
268
- category: Optional[Type[Warning]] = None,
269
- source: Optional[Any] = None,
260
+ category: type[Warning] | None = None,
261
+ source: Any | None = None,
270
262
  show_caller: bool = True,
271
263
  ) -> None:
272
264
  """This `warnings.warn` wrapper function attempts to show the location causing the
@@ -290,10 +282,7 @@ def warn(
290
282
  if show_caller:
291
283
  message += warning_from
292
284
  warnings.warn(
293
- message=message,
294
- category=category,
295
- stacklevel=stacklevel,
296
- source=source,
285
+ message=message, category=category, stacklevel=stacklevel, source=source
297
286
  )
298
287
 
299
288
 
gitlab/v4/cli.py CHANGED
@@ -1,8 +1,10 @@
1
+ from __future__ import annotations
2
+
1
3
  import argparse
2
4
  import json
3
5
  import operator
4
6
  import sys
5
- from typing import Any, Dict, List, Optional, Type, TYPE_CHECKING, Union
7
+ from typing import Any, TYPE_CHECKING
6
8
 
7
9
  import gitlab
8
10
  import gitlab.base
@@ -17,9 +19,9 @@ class GitlabCLI:
17
19
  gl: gitlab.Gitlab,
18
20
  gitlab_resource: str,
19
21
  resource_action: str,
20
- args: Dict[str, str],
22
+ args: dict[str, str],
21
23
  ) -> None:
22
- self.cls: Type[gitlab.base.RESTObject] = cli.gitlab_resource_to_cls(
24
+ self.cls: type[gitlab.base.RESTObject] = cli.gitlab_resource_to_cls(
23
25
  gitlab_resource, namespace=gitlab.v4.objects
24
26
  )
25
27
  self.cls_name = self.cls.__name__
@@ -27,26 +29,17 @@ class GitlabCLI:
27
29
  self.resource_action = resource_action.lower()
28
30
  self.gl = gl
29
31
  self.args = args
30
- self.parent_args: Dict[str, Any] = {}
31
- self.mgr_cls: Union[
32
- Type[gitlab.mixins.CreateMixin],
33
- Type[gitlab.mixins.DeleteMixin],
34
- Type[gitlab.mixins.GetMixin],
35
- Type[gitlab.mixins.GetWithoutIdMixin],
36
- Type[gitlab.mixins.ListMixin],
37
- Type[gitlab.mixins.UpdateMixin],
38
- ] = getattr(gitlab.v4.objects, f"{self.cls.__name__}Manager")
32
+ self.parent_args: dict[str, Any] = {}
33
+ self.mgr_cls: Any = getattr(gitlab.v4.objects, f"{self.cls.__name__}Manager")
39
34
  # We could do something smart, like splitting the manager name to find
40
35
  # parents, build the chain of managers to get to the final object.
41
36
  # Instead we do something ugly and efficient: interpolate variables in
42
37
  # the class _path attribute, and replace the value with the result.
43
- if TYPE_CHECKING:
44
- assert self.mgr_cls._path is not None
45
38
 
46
39
  self._process_from_parent_attrs()
47
40
 
48
41
  self.mgr_cls._path = self.mgr_cls._path.format(**self.parent_args)
49
- self.mgr = self.mgr_cls(gl)
42
+ self.mgr: Any = self.mgr_cls(gl)
50
43
  self.mgr._from_parent_attrs = self.parent_args
51
44
  if self.mgr_cls._types:
52
45
  for attr_name, type_cls in self.mgr_cls._types.items():
@@ -82,7 +75,9 @@ class GitlabCLI:
82
75
  return self.do_custom()
83
76
 
84
77
  def do_custom(self) -> Any:
85
- class_instance: Union[gitlab.base.RESTManager, gitlab.base.RESTObject]
78
+ class_instance: (
79
+ gitlab.base.RESTManager[gitlab.base.RESTObject] | gitlab.base.RESTObject
80
+ )
86
81
  in_obj = cli.custom_actions[self.cls_name][self.resource_action].in_object
87
82
 
88
83
  # Get the object (lazy), then act
@@ -132,13 +127,13 @@ class GitlabCLI:
132
127
  assert isinstance(self.mgr, gitlab.mixins.CreateMixin)
133
128
  try:
134
129
  result = self.mgr.create(self.args)
130
+ if TYPE_CHECKING:
131
+ assert isinstance(result, gitlab.base.RESTObject)
135
132
  except Exception as e: # pragma: no cover, cli.die is unit-tested
136
133
  cli.die("Impossible to create object", e)
137
134
  return result
138
135
 
139
- def do_list(
140
- self,
141
- ) -> Union[gitlab.base.RESTObjectList, List[gitlab.base.RESTObject]]:
136
+ def do_list(self) -> list[gitlab.base.RESTObject]:
142
137
  if TYPE_CHECKING:
143
138
  assert isinstance(self.mgr, gitlab.mixins.ListMixin)
144
139
  message_details = gitlab.utils.WarnMessageData(
@@ -150,15 +145,19 @@ class GitlabCLI:
150
145
  )
151
146
 
152
147
  try:
153
- result = self.mgr.list(**self.args, message_details=message_details)
148
+ result = self.mgr.list(
149
+ **self.args, message_details=message_details, iterator=False
150
+ )
154
151
  except Exception as e: # pragma: no cover, cli.die is unit-tested
155
152
  cli.die("Impossible to list objects", e)
156
153
  return result
157
154
 
158
- def do_get(self) -> Optional[gitlab.base.RESTObject]:
155
+ def do_get(self) -> gitlab.base.RESTObject | None:
159
156
  if isinstance(self.mgr, gitlab.mixins.GetWithoutIdMixin):
160
157
  try:
161
158
  result = self.mgr.get(id=None, **self.args)
159
+ if TYPE_CHECKING:
160
+ assert isinstance(result, gitlab.base.RESTObject) or result is None
162
161
  except Exception as e: # pragma: no cover, cli.die is unit-tested
163
162
  cli.die("Impossible to get object", e)
164
163
  return result
@@ -170,6 +169,8 @@ class GitlabCLI:
170
169
  id = self.args.pop(self.cls._id_attr)
171
170
  try:
172
171
  result = self.mgr.get(id, lazy=False, **self.args)
172
+ if TYPE_CHECKING:
173
+ assert isinstance(result, gitlab.base.RESTObject) or result is None
173
174
  except Exception as e: # pragma: no cover, cli.die is unit-tested
174
175
  cli.die("Impossible to get object", e)
175
176
  return result
@@ -184,7 +185,7 @@ class GitlabCLI:
184
185
  except Exception as e: # pragma: no cover, cli.die is unit-tested
185
186
  cli.die("Impossible to destroy object", e)
186
187
 
187
- def do_update(self) -> Dict[str, Any]:
188
+ def do_update(self) -> dict[str, Any]:
188
189
  if TYPE_CHECKING:
189
190
  assert isinstance(self.mgr, gitlab.mixins.UpdateMixin)
190
191
  if issubclass(self.mgr_cls, gitlab.mixins.GetWithoutIdMixin):
@@ -209,13 +210,12 @@ else:
209
210
 
210
211
 
211
212
  def _populate_sub_parser_by_class(
212
- cls: Type[gitlab.base.RESTObject],
213
- sub_parser: _SubparserType,
213
+ cls: type[gitlab.base.RESTObject], sub_parser: _SubparserType
214
214
  ) -> None:
215
215
  mgr_cls_name = f"{cls.__name__}Manager"
216
216
  mgr_cls = getattr(gitlab.v4.objects, mgr_cls_name)
217
217
 
218
- action_parsers: Dict[str, argparse.ArgumentParser] = {}
218
+ action_parsers: dict[str, argparse.ArgumentParser] = {}
219
219
  for action_name, help_text in [
220
220
  ("list", "List the GitLab resources"),
221
221
  ("get", "Get a GitLab resource"),
@@ -227,9 +227,7 @@ def _populate_sub_parser_by_class(
227
227
  continue
228
228
 
229
229
  sub_parser_action = sub_parser.add_parser(
230
- action_name,
231
- conflict_handler="resolve",
232
- help=help_text,
230
+ action_name, conflict_handler="resolve", help=help_text
233
231
  )
234
232
  action_parsers[action_name] = sub_parser_action
235
233
  sub_parser_action.add_argument("--sudo", required=False)
@@ -401,16 +399,20 @@ def extend_parser(parser: argparse.ArgumentParser) -> argparse.ArgumentParser:
401
399
  if not isinstance(cls, type):
402
400
  continue
403
401
  if issubclass(cls, gitlab.base.RESTManager):
404
- if cls._obj_cls is not None:
405
- classes.add(cls._obj_cls)
402
+ classes.add(cls._obj_cls)
406
403
 
407
404
  for cls in sorted(classes, key=operator.attrgetter("__name__")):
405
+ if cls is gitlab.base.RESTObject:
406
+ # Skip managers where _obj_cls is a plain RESTObject class
407
+ # Those managers do not actually manage any objects and
408
+ # can only be used to calls specific API paths.
409
+ continue
410
+
408
411
  arg_name = cli.cls_to_gitlab_resource(cls)
409
412
  mgr_cls_name = f"{cls.__name__}Manager"
410
413
  mgr_cls = getattr(gitlab.v4.objects, mgr_cls_name)
411
414
  object_group = subparsers.add_parser(
412
- arg_name,
413
- help=f"API endpoint: {mgr_cls._path}",
415
+ arg_name, help=f"API endpoint: {mgr_cls._path}"
414
416
  )
415
417
 
416
418
  object_subparsers = object_group.add_subparsers(
@@ -425,8 +427,8 @@ def extend_parser(parser: argparse.ArgumentParser) -> argparse.ArgumentParser:
425
427
 
426
428
 
427
429
  def get_dict(
428
- obj: Union[str, Dict[str, Any], gitlab.base.RESTObject], fields: List[str]
429
- ) -> Union[str, Dict[str, Any]]:
430
+ obj: str | dict[str, Any] | gitlab.base.RESTObject, fields: list[str]
431
+ ) -> str | dict[str, Any]:
430
432
  if not isinstance(obj, gitlab.base.RESTObject):
431
433
  return obj
432
434
 
@@ -437,13 +439,13 @@ def get_dict(
437
439
 
438
440
  class JSONPrinter:
439
441
  @staticmethod
440
- def display(d: Union[str, Dict[str, Any]], **_kwargs: Any) -> None:
442
+ def display(d: str | dict[str, Any], **_kwargs: Any) -> None:
441
443
  print(json.dumps(d))
442
444
 
443
445
  @staticmethod
444
446
  def display_list(
445
- data: List[Union[str, Dict[str, Any], gitlab.base.RESTObject]],
446
- fields: List[str],
447
+ data: list[str | dict[str, Any] | gitlab.base.RESTObject],
448
+ fields: list[str],
447
449
  **_kwargs: Any,
448
450
  ) -> None:
449
451
  print(json.dumps([get_dict(obj, fields) for obj in data]))
@@ -451,7 +453,7 @@ class JSONPrinter:
451
453
 
452
454
  class YAMLPrinter:
453
455
  @staticmethod
454
- def display(d: Union[str, Dict[str, Any]], **_kwargs: Any) -> None:
456
+ def display(d: str | dict[str, Any], **_kwargs: Any) -> None:
455
457
  try:
456
458
  import yaml # noqa
457
459
 
@@ -465,8 +467,8 @@ class YAMLPrinter:
465
467
 
466
468
  @staticmethod
467
469
  def display_list(
468
- data: List[Union[str, Dict[str, Any], gitlab.base.RESTObject]],
469
- fields: List[str],
470
+ data: list[str | dict[str, Any] | gitlab.base.RESTObject],
471
+ fields: list[str],
470
472
  **_kwargs: Any,
471
473
  ) -> None:
472
474
  try:
@@ -486,14 +488,14 @@ class YAMLPrinter:
486
488
 
487
489
 
488
490
  class LegacyPrinter:
489
- def display(self, _d: Union[str, Dict[str, Any]], **kwargs: Any) -> None:
491
+ def display(self, _d: str | dict[str, Any], **kwargs: Any) -> None:
490
492
  verbose = kwargs.get("verbose", False)
491
493
  padding = kwargs.get("padding", 0)
492
- obj: Optional[Union[Dict[str, Any], gitlab.base.RESTObject]] = kwargs.get("obj")
494
+ obj: dict[str, Any] | gitlab.base.RESTObject | None = kwargs.get("obj")
493
495
  if TYPE_CHECKING:
494
496
  assert obj is not None
495
497
 
496
- def display_dict(d: Dict[str, Any], padding: int) -> None:
498
+ def display_dict(d: dict[str, Any], padding: int) -> None:
497
499
  for k in sorted(d.keys()):
498
500
  v = d[k]
499
501
  if isinstance(v, dict):
@@ -547,10 +549,7 @@ class LegacyPrinter:
547
549
  )
548
550
 
549
551
  def display_list(
550
- self,
551
- data: List[Union[str, gitlab.base.RESTObject]],
552
- fields: List[str],
553
- **kwargs: Any,
552
+ self, data: list[str | gitlab.base.RESTObject], fields: list[str], **kwargs: Any
554
553
  ) -> None:
555
554
  verbose = kwargs.get("verbose", False)
556
555
  for obj in data:
@@ -561,9 +560,7 @@ class LegacyPrinter:
561
560
  print("")
562
561
 
563
562
 
564
- PRINTERS: Dict[
565
- str, Union[Type[JSONPrinter], Type[LegacyPrinter], Type[YAMLPrinter]]
566
- ] = {
563
+ PRINTERS: dict[str, type[JSONPrinter] | type[LegacyPrinter] | type[YAMLPrinter]] = {
567
564
  "json": JSONPrinter,
568
565
  "legacy": LegacyPrinter,
569
566
  "yaml": YAMLPrinter,
@@ -574,10 +571,10 @@ def run(
574
571
  gl: gitlab.Gitlab,
575
572
  gitlab_resource: str,
576
573
  resource_action: str,
577
- args: Dict[str, Any],
574
+ args: dict[str, Any],
578
575
  verbose: bool,
579
576
  output: str,
580
- fields: List[str],
577
+ fields: list[str],
581
578
  ) -> None:
582
579
  g_cli = GitlabCLI(
583
580
  gl=gl,
@@ -587,7 +584,7 @@ def run(
587
584
  )
588
585
  data = g_cli.run()
589
586
 
590
- printer: Union[JSONPrinter, LegacyPrinter, YAMLPrinter] = PRINTERS[output]()
587
+ printer: JSONPrinter | LegacyPrinter | YAMLPrinter = PRINTERS[output]()
591
588
 
592
589
  if isinstance(data, dict):
593
590
  printer.display(data, verbose=True, obj=data)
@@ -39,6 +39,7 @@ from .jobs import *
39
39
  from .keys import *
40
40
  from .labels import *
41
41
  from .ldap import *
42
+ from .member_roles import *
42
43
  from .members import *
43
44
  from .merge_request_approvals import *
44
45
  from .merge_requests import *
@@ -1,4 +1,4 @@
1
- from gitlab.base import RESTManager, RESTObject
1
+ from gitlab.base import RESTObject
2
2
  from gitlab.mixins import (
3
3
  AccessRequestMixin,
4
4
  CreateMixin,
@@ -19,7 +19,11 @@ class GroupAccessRequest(AccessRequestMixin, ObjectDeleteMixin, RESTObject):
19
19
  pass
20
20
 
21
21
 
22
- class GroupAccessRequestManager(ListMixin, CreateMixin, DeleteMixin, RESTManager):
22
+ class GroupAccessRequestManager(
23
+ ListMixin[GroupAccessRequest],
24
+ CreateMixin[GroupAccessRequest],
25
+ DeleteMixin[GroupAccessRequest],
26
+ ):
23
27
  _path = "/groups/{group_id}/access_requests"
24
28
  _obj_cls = GroupAccessRequest
25
29
  _from_parent_attrs = {"group_id": "id"}
@@ -29,7 +33,11 @@ class ProjectAccessRequest(AccessRequestMixin, ObjectDeleteMixin, RESTObject):
29
33
  pass
30
34
 
31
35
 
32
- class ProjectAccessRequestManager(ListMixin, CreateMixin, DeleteMixin, RESTManager):
36
+ class ProjectAccessRequestManager(
37
+ ListMixin[ProjectAccessRequest],
38
+ CreateMixin[ProjectAccessRequest],
39
+ DeleteMixin[ProjectAccessRequest],
40
+ ):
33
41
  _path = "/projects/{project_id}/access_requests"
34
42
  _obj_cls = ProjectAccessRequest
35
43
  _from_parent_attrs = {"project_id": "id"}
@@ -1,21 +1,22 @@
1
- from typing import Any, cast, Dict, Optional, Union
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
2
4
 
3
5
  from gitlab import exceptions as exc
4
- from gitlab.base import RESTManager, RESTObject
6
+ from gitlab.base import RESTObject
5
7
  from gitlab.mixins import GetWithoutIdMixin, SaveMixin, UpdateMixin
6
8
  from gitlab.types import RequiredOptional
7
9
 
8
- __all__ = [
9
- "ApplicationAppearance",
10
- "ApplicationAppearanceManager",
11
- ]
10
+ __all__ = ["ApplicationAppearance", "ApplicationAppearanceManager"]
12
11
 
13
12
 
14
13
  class ApplicationAppearance(SaveMixin, RESTObject):
15
14
  _id_attr = None
16
15
 
17
16
 
18
- class ApplicationAppearanceManager(GetWithoutIdMixin, UpdateMixin, RESTManager):
17
+ class ApplicationAppearanceManager(
18
+ GetWithoutIdMixin[ApplicationAppearance], UpdateMixin[ApplicationAppearance]
19
+ ):
19
20
  _path = "/application/appearance"
20
21
  _obj_cls = ApplicationAppearance
21
22
  _update_attrs = RequiredOptional(
@@ -31,16 +32,16 @@ class ApplicationAppearanceManager(GetWithoutIdMixin, UpdateMixin, RESTManager):
31
32
  "message_background_color",
32
33
  "message_font_color",
33
34
  "email_header_and_footer_enabled",
34
- ),
35
+ )
35
36
  )
36
37
 
37
38
  @exc.on_http_error(exc.GitlabUpdateError)
38
39
  def update(
39
40
  self,
40
- id: Optional[Union[str, int]] = None,
41
- new_data: Optional[Dict[str, Any]] = None,
41
+ id: str | int | None = None,
42
+ new_data: dict[str, Any] | None = None,
42
43
  **kwargs: Any,
43
- ) -> Dict[str, Any]:
44
+ ) -> dict[str, Any]:
44
45
  """Update an object on the server.
45
46
 
46
47
  Args:
@@ -58,6 +59,3 @@ class ApplicationAppearanceManager(GetWithoutIdMixin, UpdateMixin, RESTManager):
58
59
  new_data = new_data or {}
59
60
  data = new_data.copy()
60
61
  return super().update(id, data, **kwargs)
61
-
62
- def get(self, **kwargs: Any) -> ApplicationAppearance:
63
- return cast(ApplicationAppearance, super().get(**kwargs))
@@ -1,11 +1,8 @@
1
- from gitlab.base import RESTManager, RESTObject
1
+ from gitlab.base import RESTObject
2
2
  from gitlab.mixins import CreateMixin, DeleteMixin, ListMixin, ObjectDeleteMixin
3
3
  from gitlab.types import RequiredOptional
4
4
 
5
- __all__ = [
6
- "Application",
7
- "ApplicationManager",
8
- ]
5
+ __all__ = ["Application", "ApplicationManager"]
9
6
 
10
7
 
11
8
  class Application(ObjectDeleteMixin, RESTObject):
@@ -13,7 +10,9 @@ class Application(ObjectDeleteMixin, RESTObject):
13
10
  _repr_attr = "name"
14
11
 
15
12
 
16
- class ApplicationManager(ListMixin, CreateMixin, DeleteMixin, RESTManager):
13
+ class ApplicationManager(
14
+ ListMixin[Application], CreateMixin[Application], DeleteMixin[Application]
15
+ ):
17
16
  _path = "/applications"
18
17
  _obj_cls = Application
19
18
  _create_attrs = RequiredOptional(