python-gitlab 4.5.0__py3-none-any.whl → 4.6.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 (43) hide show
  1. gitlab/_version.py +1 -1
  2. gitlab/cli.py +20 -4
  3. gitlab/client.py +2 -2
  4. gitlab/mixins.py +22 -14
  5. gitlab/v4/cli.py +23 -11
  6. gitlab/v4/objects/__init__.py +1 -0
  7. gitlab/v4/objects/artifacts.py +5 -2
  8. gitlab/v4/objects/ci_lint.py +4 -4
  9. gitlab/v4/objects/commits.py +6 -6
  10. gitlab/v4/objects/container_registry.py +2 -2
  11. gitlab/v4/objects/deploy_keys.py +3 -1
  12. gitlab/v4/objects/deployments.py +2 -2
  13. gitlab/v4/objects/environments.py +1 -1
  14. gitlab/v4/objects/files.py +15 -7
  15. gitlab/v4/objects/geo_nodes.py +4 -4
  16. gitlab/v4/objects/groups.py +13 -7
  17. gitlab/v4/objects/integrations.py +3 -1
  18. gitlab/v4/objects/issues.py +6 -4
  19. gitlab/v4/objects/iterations.py +29 -2
  20. gitlab/v4/objects/jobs.py +11 -14
  21. gitlab/v4/objects/merge_request_approvals.py +8 -3
  22. gitlab/v4/objects/merge_requests.py +13 -12
  23. gitlab/v4/objects/milestones.py +4 -4
  24. gitlab/v4/objects/namespaces.py +3 -1
  25. gitlab/v4/objects/packages.py +4 -4
  26. gitlab/v4/objects/pipelines.py +4 -4
  27. gitlab/v4/objects/projects.py +21 -18
  28. gitlab/v4/objects/repositories.py +13 -9
  29. gitlab/v4/objects/runners.py +2 -2
  30. gitlab/v4/objects/secure_files.py +1 -1
  31. gitlab/v4/objects/service_accounts.py +18 -0
  32. gitlab/v4/objects/sidekiq.py +4 -4
  33. gitlab/v4/objects/snippets.py +3 -3
  34. gitlab/v4/objects/todos.py +2 -2
  35. gitlab/v4/objects/topics.py +2 -2
  36. gitlab/v4/objects/users.py +10 -10
  37. {python_gitlab-4.5.0.dist-info → python_gitlab-4.6.0.dist-info}/METADATA +3 -3
  38. {python_gitlab-4.5.0.dist-info → python_gitlab-4.6.0.dist-info}/RECORD +43 -42
  39. {python_gitlab-4.5.0.dist-info → python_gitlab-4.6.0.dist-info}/AUTHORS +0 -0
  40. {python_gitlab-4.5.0.dist-info → python_gitlab-4.6.0.dist-info}/COPYING +0 -0
  41. {python_gitlab-4.5.0.dist-info → python_gitlab-4.6.0.dist-info}/WHEEL +0 -0
  42. {python_gitlab-4.5.0.dist-info → python_gitlab-4.6.0.dist-info}/entry_points.txt +0 -0
  43. {python_gitlab-4.5.0.dist-info → python_gitlab-4.6.0.dist-info}/top_level.txt +0 -0
gitlab/_version.py CHANGED
@@ -3,4 +3,4 @@ __copyright__ = "Copyright 2013-2019 Gauvain Pocentek, 2019-2023 python-gitlab t
3
3
  __email__ = "gauvainpocentek@gmail.com"
4
4
  __license__ = "LGPL3"
5
5
  __title__ = "python-gitlab"
6
- __version__ = "4.5.0"
6
+ __version__ = "4.6.0"
gitlab/cli.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import argparse
2
+ import dataclasses
2
3
  import functools
3
4
  import os
4
5
  import pathlib
@@ -29,12 +30,21 @@ from gitlab.base import RESTObject
29
30
  camel_upperlower_regex = re.compile(r"([A-Z]+)([A-Z][a-z])")
30
31
  camel_lowerupper_regex = re.compile(r"([a-z\d])([A-Z])")
31
32
 
33
+
34
+ @dataclasses.dataclass
35
+ class CustomAction:
36
+ required: Tuple[str, ...]
37
+ optional: Tuple[str, ...]
38
+ in_object: bool
39
+ requires_id: bool # if the `_id_attr` value should be a required argument
40
+
41
+
32
42
  # custom_actions = {
33
43
  # cls: {
34
44
  # action: (mandatory_args, optional_args, in_obj),
35
45
  # },
36
46
  # }
37
- custom_actions: Dict[str, Dict[str, Tuple[Tuple[str, ...], Tuple[str, ...], bool]]] = {}
47
+ custom_actions: Dict[str, Dict[str, CustomAction]] = {}
38
48
 
39
49
 
40
50
  # For an explanation of how these type-hints work see:
@@ -72,10 +82,12 @@ class VerticalHelpFormatter(argparse.HelpFormatter):
72
82
 
73
83
 
74
84
  def register_custom_action(
85
+ *,
75
86
  cls_names: Union[str, Tuple[str, ...]],
76
- mandatory: Tuple[str, ...] = (),
87
+ required: Tuple[str, ...] = (),
77
88
  optional: Tuple[str, ...] = (),
78
89
  custom_action: Optional[str] = None,
90
+ requires_id: bool = True, # if the `_id_attr` value should be a required argument
79
91
  ) -> Callable[[__F], __F]:
80
92
  def wrap(f: __F) -> __F:
81
93
  @functools.wraps(f)
@@ -98,7 +110,12 @@ def register_custom_action(
98
110
  custom_actions[final_name] = {}
99
111
 
100
112
  action = custom_action or f.__name__.replace("_", "-")
101
- custom_actions[final_name][action] = (mandatory, optional, in_obj)
113
+ custom_actions[final_name][action] = CustomAction(
114
+ required=required,
115
+ optional=optional,
116
+ in_object=in_obj,
117
+ requires_id=requires_id,
118
+ )
102
119
 
103
120
  return cast(__F, wrapped_f)
104
121
 
@@ -134,7 +151,6 @@ def _get_base_parser(add_help: bool = True) -> argparse.ArgumentParser:
134
151
  parser = argparse.ArgumentParser(
135
152
  add_help=add_help,
136
153
  description="GitLab API Command Line Interface",
137
- formatter_class=VerticalHelpFormatter,
138
154
  allow_abbrev=False,
139
155
  )
140
156
  parser.add_argument("--version", help="Display the version.", action="store_true")
gitlab/client.py CHANGED
@@ -625,8 +625,8 @@ class Gitlab:
625
625
  for item in result.history:
626
626
  if item.status_code not in (301, 302):
627
627
  continue
628
- # GET methods can be redirected without issue
629
- if item.request.method == "GET":
628
+ # GET and HEAD methods can be redirected without issue
629
+ if item.request.method in ("GET", "HEAD"):
630
630
  continue
631
631
  target = item.headers.get("location")
632
632
  raise gitlab.exceptions.RedirectError(
gitlab/mixins.py CHANGED
@@ -550,7 +550,7 @@ class UserAgentDetailMixin(_RestObjectBase):
550
550
  _updated_attrs: Dict[str, Any]
551
551
  manager: base.RESTManager
552
552
 
553
- @cli.register_custom_action(("Snippet", "ProjectSnippet", "ProjectIssue"))
553
+ @cli.register_custom_action(cls_names=("Snippet", "ProjectSnippet", "ProjectIssue"))
554
554
  @exc.on_http_error(exc.GitlabGetError)
555
555
  def user_agent_detail(self, **kwargs: Any) -> Dict[str, Any]:
556
556
  """Get the user agent detail.
@@ -578,7 +578,8 @@ class AccessRequestMixin(_RestObjectBase):
578
578
  manager: base.RESTManager
579
579
 
580
580
  @cli.register_custom_action(
581
- ("ProjectAccessRequest", "GroupAccessRequest"), (), ("access_level",)
581
+ cls_names=("ProjectAccessRequest", "GroupAccessRequest"),
582
+ optional=("access_level",),
582
583
  )
583
584
  @exc.on_http_error(exc.GitlabUpdateError)
584
585
  def approve(
@@ -611,7 +612,7 @@ class DownloadMixin(_RestObjectBase):
611
612
  _updated_attrs: Dict[str, Any]
612
613
  manager: base.RESTManager
613
614
 
614
- @cli.register_custom_action(("GroupExport", "ProjectExport"))
615
+ @cli.register_custom_action(cls_names=("GroupExport", "ProjectExport"))
615
616
  @exc.on_http_error(exc.GitlabGetError)
616
617
  def download(
617
618
  self,
@@ -721,7 +722,7 @@ class SubscribableMixin(_RestObjectBase):
721
722
  manager: base.RESTManager
722
723
 
723
724
  @cli.register_custom_action(
724
- ("ProjectIssue", "ProjectMergeRequest", "ProjectLabel", "GroupLabel")
725
+ cls_names=("ProjectIssue", "ProjectMergeRequest", "ProjectLabel", "GroupLabel")
725
726
  )
726
727
  @exc.on_http_error(exc.GitlabSubscribeError)
727
728
  def subscribe(self, **kwargs: Any) -> None:
@@ -741,7 +742,7 @@ class SubscribableMixin(_RestObjectBase):
741
742
  self._update_attrs(server_data)
742
743
 
743
744
  @cli.register_custom_action(
744
- ("ProjectIssue", "ProjectMergeRequest", "ProjectLabel", "GroupLabel")
745
+ cls_names=("ProjectIssue", "ProjectMergeRequest", "ProjectLabel", "GroupLabel")
745
746
  )
746
747
  @exc.on_http_error(exc.GitlabUnsubscribeError)
747
748
  def unsubscribe(self, **kwargs: Any) -> None:
@@ -769,7 +770,7 @@ class TodoMixin(_RestObjectBase):
769
770
  _updated_attrs: Dict[str, Any]
770
771
  manager: base.RESTManager
771
772
 
772
- @cli.register_custom_action(("ProjectIssue", "ProjectMergeRequest"))
773
+ @cli.register_custom_action(cls_names=("ProjectIssue", "ProjectMergeRequest"))
773
774
  @exc.on_http_error(exc.GitlabTodoError)
774
775
  def todo(self, **kwargs: Any) -> None:
775
776
  """Create a todo associated to the object.
@@ -793,7 +794,7 @@ class TimeTrackingMixin(_RestObjectBase):
793
794
  _updated_attrs: Dict[str, Any]
794
795
  manager: base.RESTManager
795
796
 
796
- @cli.register_custom_action(("ProjectIssue", "ProjectMergeRequest"))
797
+ @cli.register_custom_action(cls_names=("ProjectIssue", "ProjectMergeRequest"))
797
798
  @exc.on_http_error(exc.GitlabTimeTrackingError)
798
799
  def time_stats(self, **kwargs: Any) -> Dict[str, Any]:
799
800
  """Get time stats for the object.
@@ -819,7 +820,9 @@ class TimeTrackingMixin(_RestObjectBase):
819
820
  assert not isinstance(result, requests.Response)
820
821
  return result
821
822
 
822
- @cli.register_custom_action(("ProjectIssue", "ProjectMergeRequest"), ("duration",))
823
+ @cli.register_custom_action(
824
+ cls_names=("ProjectIssue", "ProjectMergeRequest"), required=("duration",)
825
+ )
823
826
  @exc.on_http_error(exc.GitlabTimeTrackingError)
824
827
  def time_estimate(self, duration: str, **kwargs: Any) -> Dict[str, Any]:
825
828
  """Set an estimated time of work for the object.
@@ -839,7 +842,7 @@ class TimeTrackingMixin(_RestObjectBase):
839
842
  assert not isinstance(result, requests.Response)
840
843
  return result
841
844
 
842
- @cli.register_custom_action(("ProjectIssue", "ProjectMergeRequest"))
845
+ @cli.register_custom_action(cls_names=("ProjectIssue", "ProjectMergeRequest"))
843
846
  @exc.on_http_error(exc.GitlabTimeTrackingError)
844
847
  def reset_time_estimate(self, **kwargs: Any) -> Dict[str, Any]:
845
848
  """Resets estimated time for the object to 0 seconds.
@@ -857,7 +860,9 @@ class TimeTrackingMixin(_RestObjectBase):
857
860
  assert not isinstance(result, requests.Response)
858
861
  return result
859
862
 
860
- @cli.register_custom_action(("ProjectIssue", "ProjectMergeRequest"), ("duration",))
863
+ @cli.register_custom_action(
864
+ cls_names=("ProjectIssue", "ProjectMergeRequest"), required=("duration",)
865
+ )
861
866
  @exc.on_http_error(exc.GitlabTimeTrackingError)
862
867
  def add_spent_time(self, duration: str, **kwargs: Any) -> Dict[str, Any]:
863
868
  """Add time spent working on the object.
@@ -877,7 +882,7 @@ class TimeTrackingMixin(_RestObjectBase):
877
882
  assert not isinstance(result, requests.Response)
878
883
  return result
879
884
 
880
- @cli.register_custom_action(("ProjectIssue", "ProjectMergeRequest"))
885
+ @cli.register_custom_action(cls_names=("ProjectIssue", "ProjectMergeRequest"))
881
886
  @exc.on_http_error(exc.GitlabTimeTrackingError)
882
887
  def reset_spent_time(self, **kwargs: Any) -> Dict[str, Any]:
883
888
  """Resets the time spent working on the object.
@@ -904,7 +909,7 @@ class ParticipantsMixin(_RestObjectBase):
904
909
  _updated_attrs: Dict[str, Any]
905
910
  manager: base.RESTManager
906
911
 
907
- @cli.register_custom_action(("ProjectMergeRequest", "ProjectIssue"))
912
+ @cli.register_custom_action(cls_names=("ProjectMergeRequest", "ProjectIssue"))
908
913
  @exc.on_http_error(exc.GitlabListError)
909
914
  def participants(self, **kwargs: Any) -> Dict[str, Any]:
910
915
  """List the participants.
@@ -932,7 +937,8 @@ class ParticipantsMixin(_RestObjectBase):
932
937
 
933
938
  class BadgeRenderMixin(_RestManagerBase):
934
939
  @cli.register_custom_action(
935
- ("GroupBadgeManager", "ProjectBadgeManager"), ("link_url", "image_url")
940
+ cls_names=("GroupBadgeManager", "ProjectBadgeManager"),
941
+ required=("link_url", "image_url"),
936
942
  )
937
943
  @exc.on_http_error(exc.GitlabRenderError)
938
944
  def render(self, link_url: str, image_url: str, **kwargs: Any) -> Dict[str, Any]:
@@ -1025,7 +1031,9 @@ class UploadMixin(_RestObjectBase):
1025
1031
  data = self.attributes
1026
1032
  return self._upload_path.format(**data)
1027
1033
 
1028
- @cli.register_custom_action(("Project", "ProjectWiki"), ("filename", "filepath"))
1034
+ @cli.register_custom_action(
1035
+ cls_names=("Project", "ProjectWiki"), required=("filename", "filepath")
1036
+ )
1029
1037
  @exc.on_http_error(exc.GitlabUploadError)
1030
1038
  def upload(
1031
1039
  self,
gitlab/v4/cli.py CHANGED
@@ -82,7 +82,7 @@ class GitlabCLI:
82
82
 
83
83
  def do_custom(self) -> Any:
84
84
  class_instance: Union[gitlab.base.RESTManager, gitlab.base.RESTObject]
85
- in_obj = cli.custom_actions[self.cls_name][self.resource_action][2]
85
+ in_obj = cli.custom_actions[self.cls_name][self.resource_action].in_object
86
86
 
87
87
  # Get the object (lazy), then act
88
88
  if in_obj:
@@ -207,12 +207,20 @@ def _populate_sub_parser_by_class(
207
207
  mgr_cls = getattr(gitlab.v4.objects, mgr_cls_name)
208
208
 
209
209
  action_parsers: Dict[str, argparse.ArgumentParser] = {}
210
- for action_name in ["list", "get", "create", "update", "delete"]:
210
+ for action_name, help_text in [
211
+ ("list", "List the GitLab resources"),
212
+ ("get", "Get a GitLab resource"),
213
+ ("create", "Create a GitLab resource"),
214
+ ("update", "Update a GitLab resource"),
215
+ ("delete", "Delete a GitLab resource"),
216
+ ]:
211
217
  if not hasattr(mgr_cls, action_name):
212
218
  continue
213
219
 
214
220
  sub_parser_action = sub_parser.add_parser(
215
- action_name, conflict_handler="resolve"
221
+ action_name,
222
+ conflict_handler="resolve",
223
+ help=help_text,
216
224
  )
217
225
  action_parsers[action_name] = sub_parser_action
218
226
  sub_parser_action.add_argument("--sudo", required=False)
@@ -307,19 +315,19 @@ def _populate_sub_parser_by_class(
307
315
  )
308
316
  sub_parser_action.add_argument("--sudo", required=False)
309
317
 
318
+ custom_action = cli.custom_actions[name][action_name]
310
319
  # We need to get the object somehow
311
320
  if not issubclass(cls, gitlab.mixins.GetWithoutIdMixin):
312
- if cls._id_attr is not None:
321
+ if cls._id_attr is not None and custom_action.requires_id:
313
322
  id_attr = cls._id_attr.replace("_", "-")
314
323
  sub_parser_action.add_argument(f"--{id_attr}", required=True)
315
324
 
316
- required, optional, dummy = cli.custom_actions[name][action_name]
317
- for x in required:
325
+ for x in custom_action.required:
318
326
  if x != cls._id_attr:
319
327
  sub_parser_action.add_argument(
320
328
  f"--{x.replace('_', '-')}", required=True
321
329
  )
322
- for x in optional:
330
+ for x in custom_action.optional:
323
331
  if x != cls._id_attr:
324
332
  sub_parser_action.add_argument(
325
333
  f"--{x.replace('_', '-')}", required=False
@@ -342,13 +350,13 @@ def _populate_sub_parser_by_class(
342
350
  )
343
351
  sub_parser_action.add_argument("--sudo", required=False)
344
352
 
345
- required, optional, dummy = cli.custom_actions[name][action_name]
346
- for x in required:
353
+ custom_action = cli.custom_actions[name][action_name]
354
+ for x in custom_action.required:
347
355
  if x != cls._id_attr:
348
356
  sub_parser_action.add_argument(
349
357
  f"--{x.replace('_', '-')}", required=True
350
358
  )
351
- for x in optional:
359
+ for x in custom_action.optional:
352
360
  if x != cls._id_attr:
353
361
  sub_parser_action.add_argument(
354
362
  f"--{x.replace('_', '-')}", required=False
@@ -374,8 +382,12 @@ def extend_parser(parser: argparse.ArgumentParser) -> argparse.ArgumentParser:
374
382
 
375
383
  for cls in sorted(classes, key=operator.attrgetter("__name__")):
376
384
  arg_name = cli.cls_to_gitlab_resource(cls)
385
+ mgr_cls_name = f"{cls.__name__}Manager"
386
+ mgr_cls = getattr(gitlab.v4.objects, mgr_cls_name)
377
387
  object_group = subparsers.add_parser(
378
- arg_name, formatter_class=cli.VerticalHelpFormatter
388
+ arg_name,
389
+ formatter_class=cli.VerticalHelpFormatter,
390
+ help=f"API endpoint: {mgr_cls._path}",
379
391
  )
380
392
 
381
393
  object_subparsers = object_group.add_subparsers(
@@ -59,6 +59,7 @@ from .resource_groups import *
59
59
  from .reviewers import *
60
60
  from .runners import *
61
61
  from .secure_files import *
62
+ from .service_accounts import *
62
63
  from .settings import *
63
64
  from .sidekiq import *
64
65
  from .snippets import *
@@ -44,7 +44,9 @@ class ProjectArtifactManager(RESTManager):
44
44
  self.gitlab.http_delete(path, **kwargs)
45
45
 
46
46
  @cli.register_custom_action(
47
- "ProjectArtifactManager", ("ref_name", "job"), ("job_token",)
47
+ cls_names="ProjectArtifactManager",
48
+ required=("ref_name", "job"),
49
+ optional=("job_token",),
48
50
  )
49
51
  @exc.on_http_error(exc.GitlabGetError)
50
52
  def download(
@@ -93,7 +95,8 @@ class ProjectArtifactManager(RESTManager):
93
95
  )
94
96
 
95
97
  @cli.register_custom_action(
96
- "ProjectArtifactManager", ("ref_name", "artifact_path", "job")
98
+ cls_names="ProjectArtifactManager",
99
+ required=("ref_name", "artifact_path", "job"),
97
100
  )
98
101
  @exc.on_http_error(exc.GitlabGetError)
99
102
  def raw(
@@ -31,8 +31,8 @@ class CiLintManager(CreateMixin, RESTManager):
31
31
  )
32
32
 
33
33
  @register_custom_action(
34
- "CiLintManager",
35
- ("content",),
34
+ cls_names="CiLintManager",
35
+ required=("content",),
36
36
  optional=("include_merged_yaml", "include_jobs"),
37
37
  )
38
38
  def validate(self, *args: Any, **kwargs: Any) -> None:
@@ -63,8 +63,8 @@ class ProjectCiLintManager(GetWithoutIdMixin, CreateMixin, RESTManager):
63
63
  return cast(ProjectCiLint, super().get(**kwargs))
64
64
 
65
65
  @register_custom_action(
66
- "ProjectCiLintManager",
67
- ("content",),
66
+ cls_names="ProjectCiLintManager",
67
+ required=("content",),
68
68
  optional=("dry_run", "include_jobs", "ref"),
69
69
  )
70
70
  def validate(self, *args: Any, **kwargs: Any) -> None:
@@ -28,7 +28,7 @@ class ProjectCommit(RESTObject):
28
28
  discussions: ProjectCommitDiscussionManager
29
29
  statuses: "ProjectCommitStatusManager"
30
30
 
31
- @cli.register_custom_action("ProjectCommit")
31
+ @cli.register_custom_action(cls_names="ProjectCommit")
32
32
  @exc.on_http_error(exc.GitlabGetError)
33
33
  def diff(self, **kwargs: Any) -> Union[gitlab.GitlabList, List[Dict[str, Any]]]:
34
34
  """Generate the commit diff.
@@ -46,7 +46,7 @@ class ProjectCommit(RESTObject):
46
46
  path = f"{self.manager.path}/{self.encoded_id}/diff"
47
47
  return self.manager.gitlab.http_list(path, **kwargs)
48
48
 
49
- @cli.register_custom_action("ProjectCommit", ("branch",))
49
+ @cli.register_custom_action(cls_names="ProjectCommit", required=("branch",))
50
50
  @exc.on_http_error(exc.GitlabCherryPickError)
51
51
  def cherry_pick(self, branch: str, **kwargs: Any) -> None:
52
52
  """Cherry-pick a commit into a branch.
@@ -63,7 +63,7 @@ class ProjectCommit(RESTObject):
63
63
  post_data = {"branch": branch}
64
64
  self.manager.gitlab.http_post(path, post_data=post_data, **kwargs)
65
65
 
66
- @cli.register_custom_action("ProjectCommit", optional=("type",))
66
+ @cli.register_custom_action(cls_names="ProjectCommit", optional=("type",))
67
67
  @exc.on_http_error(exc.GitlabGetError)
68
68
  def refs(
69
69
  self, type: str = "all", **kwargs: Any
@@ -85,7 +85,7 @@ class ProjectCommit(RESTObject):
85
85
  query_data = {"type": type}
86
86
  return self.manager.gitlab.http_list(path, query_data=query_data, **kwargs)
87
87
 
88
- @cli.register_custom_action("ProjectCommit")
88
+ @cli.register_custom_action(cls_names="ProjectCommit")
89
89
  @exc.on_http_error(exc.GitlabGetError)
90
90
  def merge_requests(
91
91
  self, **kwargs: Any
@@ -105,7 +105,7 @@ class ProjectCommit(RESTObject):
105
105
  path = f"{self.manager.path}/{self.encoded_id}/merge_requests"
106
106
  return self.manager.gitlab.http_list(path, **kwargs)
107
107
 
108
- @cli.register_custom_action("ProjectCommit", ("branch",))
108
+ @cli.register_custom_action(cls_names="ProjectCommit", required=("branch",))
109
109
  @exc.on_http_error(exc.GitlabRevertError)
110
110
  def revert(
111
111
  self, branch: str, **kwargs: Any
@@ -127,7 +127,7 @@ class ProjectCommit(RESTObject):
127
127
  post_data = {"branch": branch}
128
128
  return self.manager.gitlab.http_post(path, post_data=post_data, **kwargs)
129
129
 
130
- @cli.register_custom_action("ProjectCommit")
130
+ @cli.register_custom_action(cls_names="ProjectCommit")
131
131
  @exc.on_http_error(exc.GitlabGetError)
132
132
  def signature(self, **kwargs: Any) -> Union[Dict[str, Any], requests.Response]:
133
133
  """Get the signature of the commit.
@@ -42,8 +42,8 @@ class ProjectRegistryTagManager(DeleteMixin, RetrieveMixin, RESTManager):
42
42
  _path = "/projects/{project_id}/registry/repositories/{repository_id}/tags"
43
43
 
44
44
  @cli.register_custom_action(
45
- "ProjectRegistryTagManager",
46
- ("name_regex_delete",),
45
+ cls_names="ProjectRegistryTagManager",
46
+ required=("name_regex_delete",),
47
47
  optional=("keep_n", "name_regex_keep", "older_than"),
48
48
  )
49
49
  @exc.on_http_error(exc.GitlabDeleteError)
@@ -36,7 +36,9 @@ class ProjectKeyManager(CRUDMixin, RESTManager):
36
36
  _create_attrs = RequiredOptional(required=("title", "key"), optional=("can_push",))
37
37
  _update_attrs = RequiredOptional(optional=("title", "can_push"))
38
38
 
39
- @cli.register_custom_action("ProjectKeyManager", ("key_id",))
39
+ @cli.register_custom_action(
40
+ cls_names="ProjectKeyManager", required=("key_id",), requires_id=False
41
+ )
40
42
  @exc.on_http_error(exc.GitlabProjectDeployKeyError)
41
43
  def enable(
42
44
  self, key_id: int, **kwargs: Any
@@ -23,8 +23,8 @@ class ProjectDeployment(SaveMixin, RESTObject):
23
23
  mergerequests: ProjectDeploymentMergeRequestManager
24
24
 
25
25
  @cli.register_custom_action(
26
- "ProjectDeployment",
27
- mandatory=("status",),
26
+ cls_names="ProjectDeployment",
27
+ required=("status",),
28
28
  optional=("comment", "represented_as"),
29
29
  )
30
30
  @exc.on_http_error(exc.GitlabDeploymentApprovalError)
@@ -24,7 +24,7 @@ __all__ = [
24
24
 
25
25
 
26
26
  class ProjectEnvironment(SaveMixin, ObjectDeleteMixin, RESTObject):
27
- @cli.register_custom_action("ProjectEnvironment")
27
+ @cli.register_custom_action(cls_names="ProjectEnvironment")
28
28
  @exc.on_http_error(exc.GitlabStopError)
29
29
  def stop(self, **kwargs: Any) -> Union[Dict[str, Any], requests.Response]:
30
30
  """Stop the environment.
@@ -40,6 +40,7 @@ class ProjectFile(SaveMixin, ObjectDeleteMixin, RESTObject):
40
40
  commit_message: str
41
41
  file_path: str
42
42
  manager: "ProjectFileManager"
43
+ content: str # since the `decode()` method uses `self.content`
43
44
 
44
45
  def decode(self) -> bytes:
45
46
  """Returns the decoded content of the file.
@@ -108,7 +109,9 @@ class ProjectFileManager(GetMixin, CreateMixin, UpdateMixin, DeleteMixin, RESTMa
108
109
  optional=("encoding", "author_email", "author_name"),
109
110
  )
110
111
 
111
- @cli.register_custom_action("ProjectFileManager", ("file_path", "ref"))
112
+ @cli.register_custom_action(
113
+ cls_names="ProjectFileManager", required=("file_path", "ref")
114
+ )
112
115
  # NOTE(jlvillal): Signature doesn't match UpdateMixin.update() so ignore
113
116
  # type error
114
117
  def get( # type: ignore
@@ -131,9 +134,9 @@ class ProjectFileManager(GetMixin, CreateMixin, UpdateMixin, DeleteMixin, RESTMa
131
134
  return cast(ProjectFile, GetMixin.get(self, file_path, ref=ref, **kwargs))
132
135
 
133
136
  @cli.register_custom_action(
134
- "ProjectFileManager",
135
- ("file_path", "branch", "content", "commit_message"),
136
- ("encoding", "author_email", "author_name"),
137
+ cls_names="ProjectFileManager",
138
+ required=("file_path", "branch", "content", "commit_message"),
139
+ optional=("encoding", "author_email", "author_name"),
137
140
  )
138
141
  @exc.on_http_error(exc.GitlabCreateError)
139
142
  def create(
@@ -198,7 +201,8 @@ class ProjectFileManager(GetMixin, CreateMixin, UpdateMixin, DeleteMixin, RESTMa
198
201
  return result
199
202
 
200
203
  @cli.register_custom_action(
201
- "ProjectFileManager", ("file_path", "branch", "commit_message")
204
+ cls_names="ProjectFileManager",
205
+ required=("file_path", "branch", "commit_message"),
202
206
  )
203
207
  @exc.on_http_error(exc.GitlabDeleteError)
204
208
  # NOTE(jlvillal): Signature doesn't match DeleteMixin.delete() so ignore
@@ -223,7 +227,9 @@ class ProjectFileManager(GetMixin, CreateMixin, UpdateMixin, DeleteMixin, RESTMa
223
227
  data = {"branch": branch, "commit_message": commit_message}
224
228
  self.gitlab.http_delete(path, query_data=data, **kwargs)
225
229
 
226
- @cli.register_custom_action("ProjectFileManager", ("file_path", "ref"))
230
+ @cli.register_custom_action(
231
+ cls_names="ProjectFileManager", required=("file_path", "ref")
232
+ )
227
233
  @exc.on_http_error(exc.GitlabGetError)
228
234
  def raw(
229
235
  self,
@@ -270,7 +276,9 @@ class ProjectFileManager(GetMixin, CreateMixin, UpdateMixin, DeleteMixin, RESTMa
270
276
  result, streamed, action, chunk_size, iterator=iterator
271
277
  )
272
278
 
273
- @cli.register_custom_action("ProjectFileManager", ("file_path", "ref"))
279
+ @cli.register_custom_action(
280
+ cls_names="ProjectFileManager", required=("file_path", "ref")
281
+ )
274
282
  @exc.on_http_error(exc.GitlabListError)
275
283
  def blame(self, file_path: str, ref: str, **kwargs: Any) -> List[Dict[str, Any]]:
276
284
  """Return the content of a file for a commit.
@@ -19,7 +19,7 @@ __all__ = [
19
19
 
20
20
 
21
21
  class GeoNode(SaveMixin, ObjectDeleteMixin, RESTObject):
22
- @cli.register_custom_action("GeoNode")
22
+ @cli.register_custom_action(cls_names="GeoNode")
23
23
  @exc.on_http_error(exc.GitlabRepairError)
24
24
  def repair(self, **kwargs: Any) -> None:
25
25
  """Repair the OAuth authentication of the geo node.
@@ -37,7 +37,7 @@ class GeoNode(SaveMixin, ObjectDeleteMixin, RESTObject):
37
37
  assert isinstance(server_data, dict)
38
38
  self._update_attrs(server_data)
39
39
 
40
- @cli.register_custom_action("GeoNode")
40
+ @cli.register_custom_action(cls_names="GeoNode")
41
41
  @exc.on_http_error(exc.GitlabGetError)
42
42
  def status(self, **kwargs: Any) -> Dict[str, Any]:
43
43
  """Get the status of the geo node.
@@ -69,7 +69,7 @@ class GeoNodeManager(RetrieveMixin, UpdateMixin, DeleteMixin, RESTManager):
69
69
  def get(self, id: Union[str, int], lazy: bool = False, **kwargs: Any) -> GeoNode:
70
70
  return cast(GeoNode, super().get(id=id, lazy=lazy, **kwargs))
71
71
 
72
- @cli.register_custom_action("GeoNodeManager")
72
+ @cli.register_custom_action(cls_names="GeoNodeManager")
73
73
  @exc.on_http_error(exc.GitlabGetError)
74
74
  def status(self, **kwargs: Any) -> List[Dict[str, Any]]:
75
75
  """Get the status of all the geo nodes.
@@ -89,7 +89,7 @@ class GeoNodeManager(RetrieveMixin, UpdateMixin, DeleteMixin, RESTManager):
89
89
  assert isinstance(result, list)
90
90
  return result
91
91
 
92
- @cli.register_custom_action("GeoNodeManager")
92
+ @cli.register_custom_action(cls_names="GeoNodeManager")
93
93
  @exc.on_http_error(exc.GitlabGetError)
94
94
  def current_failures(self, **kwargs: Any) -> List[Dict[str, Any]]:
95
95
  """Get the list of failures on the current geo node.
@@ -46,6 +46,7 @@ from .packages import GroupPackageManager # noqa: F401
46
46
  from .projects import GroupProjectManager, SharedProjectManager # noqa: F401
47
47
  from .push_rules import GroupPushRulesManager
48
48
  from .runners import GroupRunnerManager # noqa: F401
49
+ from .service_accounts import GroupServiceAccountManager # noqa: F401
49
50
  from .statistics import GroupIssuesStatisticsManager # noqa: F401
50
51
  from .variables import GroupVariableManager # noqa: F401
51
52
  from .wikis import GroupWikiManager # noqa: F401
@@ -102,8 +103,9 @@ class Group(SaveMixin, ObjectDeleteMixin, RESTObject):
102
103
  variables: GroupVariableManager
103
104
  wikis: GroupWikiManager
104
105
  saml_group_links: "GroupSAMLGroupLinkManager"
106
+ service_accounts: "GroupServiceAccountManager"
105
107
 
106
- @cli.register_custom_action("Group", ("project_id",))
108
+ @cli.register_custom_action(cls_names="Group", required=("project_id",))
107
109
  @exc.on_http_error(exc.GitlabTransferProjectError)
108
110
  def transfer_project(self, project_id: int, **kwargs: Any) -> None:
109
111
  """Transfer a project to this group.
@@ -119,7 +121,7 @@ class Group(SaveMixin, ObjectDeleteMixin, RESTObject):
119
121
  path = f"/groups/{self.encoded_id}/projects/{project_id}"
120
122
  self.manager.gitlab.http_post(path, **kwargs)
121
123
 
122
- @cli.register_custom_action("Group", (), ("group_id",))
124
+ @cli.register_custom_action(cls_names="Group", required=(), optional=("group_id",))
123
125
  @exc.on_http_error(exc.GitlabGroupTransferError)
124
126
  def transfer(self, group_id: Optional[int] = None, **kwargs: Any) -> None:
125
127
  """Transfer the group to a new parent group or make it a top-level group.
@@ -141,7 +143,7 @@ class Group(SaveMixin, ObjectDeleteMixin, RESTObject):
141
143
  post_data["group_id"] = group_id
142
144
  self.manager.gitlab.http_post(path, post_data=post_data, **kwargs)
143
145
 
144
- @cli.register_custom_action("Group", ("scope", "search"))
146
+ @cli.register_custom_action(cls_names="Group", required=("scope", "search"))
145
147
  @exc.on_http_error(exc.GitlabSearchError)
146
148
  def search(
147
149
  self, scope: str, search: str, **kwargs: Any
@@ -164,7 +166,7 @@ class Group(SaveMixin, ObjectDeleteMixin, RESTObject):
164
166
  path = f"/groups/{self.encoded_id}/search"
165
167
  return self.manager.gitlab.http_list(path, query_data=data, **kwargs)
166
168
 
167
- @cli.register_custom_action("Group")
169
+ @cli.register_custom_action(cls_names="Group")
168
170
  @exc.on_http_error(exc.GitlabCreateError)
169
171
  def ldap_sync(self, **kwargs: Any) -> None:
170
172
  """Sync LDAP groups.
@@ -179,7 +181,11 @@ class Group(SaveMixin, ObjectDeleteMixin, RESTObject):
179
181
  path = f"/groups/{self.encoded_id}/ldap_sync"
180
182
  self.manager.gitlab.http_post(path, **kwargs)
181
183
 
182
- @cli.register_custom_action("Group", ("group_id", "group_access"), ("expires_at",))
184
+ @cli.register_custom_action(
185
+ cls_names="Group",
186
+ required=("group_id", "group_access"),
187
+ optional=("expires_at",),
188
+ )
183
189
  @exc.on_http_error(exc.GitlabCreateError)
184
190
  def share(
185
191
  self,
@@ -213,7 +219,7 @@ class Group(SaveMixin, ObjectDeleteMixin, RESTObject):
213
219
  assert isinstance(server_data, dict)
214
220
  self._update_attrs(server_data)
215
221
 
216
- @cli.register_custom_action("Group", ("group_id",))
222
+ @cli.register_custom_action(cls_names="Group", required=("group_id",))
217
223
  @exc.on_http_error(exc.GitlabDeleteError)
218
224
  def unshare(self, group_id: int, **kwargs: Any) -> None:
219
225
  """Delete a shared group link within a group.
@@ -229,7 +235,7 @@ class Group(SaveMixin, ObjectDeleteMixin, RESTObject):
229
235
  path = f"/groups/{self.encoded_id}/share/{group_id}"
230
236
  self.manager.gitlab.http_delete(path, **kwargs)
231
237
 
232
- @cli.register_custom_action("Group")
238
+ @cli.register_custom_action(cls_names="Group")
233
239
  @exc.on_http_error(exc.GitlabRestoreError)
234
240
  def restore(self, **kwargs: Any) -> None:
235
241
  """Restore a group marked for deletion..
@@ -270,7 +270,9 @@ class ProjectIntegrationManager(
270
270
  ) -> ProjectIntegration:
271
271
  return cast(ProjectIntegration, super().get(id=id, lazy=lazy, **kwargs))
272
272
 
273
- @cli.register_custom_action(("ProjectIntegrationManager", "ProjectServiceManager"))
273
+ @cli.register_custom_action(
274
+ cls_names=("ProjectIntegrationManager", "ProjectServiceManager")
275
+ )
274
276
  def available(self) -> List[str]:
275
277
  """List the services known by python-gitlab.
276
278