python-gitlab 4.5.0__py3-none-any.whl → 4.7.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 +44 -45
  3. gitlab/client.py +2 -2
  4. gitlab/mixins.py +22 -14
  5. gitlab/v4/cli.py +25 -12
  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 +6 -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 +25 -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.7.0.dist-info}/METADATA +3 -3
  38. {python_gitlab-4.5.0.dist-info → python_gitlab-4.7.0.dist-info}/RECORD +43 -42
  39. {python_gitlab-4.5.0.dist-info → python_gitlab-4.7.0.dist-info}/WHEEL +1 -1
  40. {python_gitlab-4.5.0.dist-info → python_gitlab-4.7.0.dist-info}/AUTHORS +0 -0
  41. {python_gitlab-4.5.0.dist-info → python_gitlab-4.7.0.dist-info}/COPYING +0 -0
  42. {python_gitlab-4.5.0.dist-info → python_gitlab-4.7.0.dist-info}/entry_points.txt +0 -0
  43. {python_gitlab-4.5.0.dist-info → python_gitlab-4.7.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.7.0"
gitlab/cli.py CHANGED
@@ -1,10 +1,10 @@
1
1
  import argparse
2
+ import dataclasses
2
3
  import functools
3
4
  import os
4
5
  import pathlib
5
6
  import re
6
7
  import sys
7
- import textwrap
8
8
  from types import ModuleType
9
9
  from typing import (
10
10
  Any,
@@ -29,12 +29,22 @@ from gitlab.base import RESTObject
29
29
  camel_upperlower_regex = re.compile(r"([A-Z]+)([A-Z][a-z])")
30
30
  camel_lowerupper_regex = re.compile(r"([a-z\d])([A-Z])")
31
31
 
32
+
33
+ @dataclasses.dataclass
34
+ class CustomAction:
35
+ required: Tuple[str, ...]
36
+ optional: Tuple[str, ...]
37
+ in_object: bool
38
+ requires_id: bool # if the `_id_attr` value should be a required argument
39
+ help: Optional[str] # help text for the custom action
40
+
41
+
32
42
  # custom_actions = {
33
43
  # cls: {
34
- # action: (mandatory_args, optional_args, in_obj),
44
+ # action: CustomAction,
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:
@@ -44,38 +54,14 @@ custom_actions: Dict[str, Dict[str, Tuple[Tuple[str, ...], Tuple[str, ...], bool
44
54
  __F = TypeVar("__F", bound=Callable[..., Any])
45
55
 
46
56
 
47
- class VerticalHelpFormatter(argparse.HelpFormatter):
48
- def format_help(self) -> str:
49
- result = super().format_help()
50
- output = ""
51
- indent = self._indent_increment * " "
52
- for line in result.splitlines(keepends=True):
53
- # All of our resources are on one line and wrapped inside braces.
54
- # For example: {application,resource1,resource2}
55
- # except if there are fewer resources - then the line and help text
56
- # are collapsed on the same line.
57
- # For example: {list} Action to execute on the GitLab resource.
58
- # We then put each resource on its own line to make it easier to read.
59
- if line.strip().startswith("{"):
60
- choice_string, help_string = line.split("}", 1)
61
- choice_list = choice_string.strip(" {").split(",")
62
- help_string = help_string.strip()
63
-
64
- if help_string:
65
- help_indent = len(max(choice_list, key=len)) * " "
66
- choice_list.append(f"{help_indent} {help_string}")
67
-
68
- choices = "\n".join(choice_list)
69
- line = f"{textwrap.indent(choices, indent)}\n"
70
- output += line
71
- return output
72
-
73
-
74
57
  def register_custom_action(
58
+ *,
75
59
  cls_names: Union[str, Tuple[str, ...]],
76
- mandatory: Tuple[str, ...] = (),
60
+ required: Tuple[str, ...] = (),
77
61
  optional: Tuple[str, ...] = (),
78
62
  custom_action: Optional[str] = None,
63
+ requires_id: bool = True, # if the `_id_attr` value should be a required argument
64
+ help: Optional[str] = None, # help text for the action
79
65
  ) -> Callable[[__F], __F]:
80
66
  def wrap(f: __F) -> __F:
81
67
  @functools.wraps(f)
@@ -98,7 +84,13 @@ def register_custom_action(
98
84
  custom_actions[final_name] = {}
99
85
 
100
86
  action = custom_action or f.__name__.replace("_", "-")
101
- custom_actions[final_name][action] = (mandatory, optional, in_obj)
87
+ custom_actions[final_name][action] = CustomAction(
88
+ required=required,
89
+ optional=optional,
90
+ in_object=in_obj,
91
+ requires_id=requires_id,
92
+ help=help,
93
+ )
102
94
 
103
95
  return cast(__F, wrapped_f)
104
96
 
@@ -134,7 +126,6 @@ def _get_base_parser(add_help: bool = True) -> argparse.ArgumentParser:
134
126
  parser = argparse.ArgumentParser(
135
127
  add_help=add_help,
136
128
  description="GitLab API Command Line Interface",
137
- formatter_class=VerticalHelpFormatter,
138
129
  allow_abbrev=False,
139
130
  )
140
131
  parser.add_argument("--version", help="Display the version.", action="store_true")
@@ -292,6 +283,12 @@ def _get_base_parser(add_help: bool = True) -> argparse.ArgumentParser:
292
283
  action="store_true",
293
284
  default=os.getenv("GITLAB_SKIP_LOGIN"),
294
285
  )
286
+ parser.add_argument(
287
+ "--no-mask-credentials",
288
+ help="Don't mask credentials in debug mode",
289
+ dest="mask_credentials",
290
+ action="store_false",
291
+ )
295
292
  return parser
296
293
 
297
294
 
@@ -379,29 +376,31 @@ def main() -> None:
379
376
  gitlab_resource = args.gitlab_resource
380
377
  resource_action = args.resource_action
381
378
  skip_login = args.skip_login
379
+ mask_credentials = args.mask_credentials
382
380
 
383
381
  args_dict = vars(args)
384
382
  # Remove CLI behavior-related args
385
383
  for item in (
386
- "gitlab",
384
+ "api_version",
387
385
  "config_file",
388
- "verbose",
389
386
  "debug",
387
+ "fields",
388
+ "gitlab",
390
389
  "gitlab_resource",
391
- "resource_action",
392
- "version",
390
+ "job_token",
391
+ "mask_credentials",
392
+ "oauth_token",
393
393
  "output",
394
- "fields",
394
+ "pagination",
395
+ "private_token",
396
+ "resource_action",
395
397
  "server_url",
398
+ "skip_login",
396
399
  "ssl_verify",
397
400
  "timeout",
398
- "api_version",
399
- "pagination",
400
401
  "user_agent",
401
- "private_token",
402
- "oauth_token",
403
- "job_token",
404
- "skip_login",
402
+ "verbose",
403
+ "version",
405
404
  ):
406
405
  args_dict.pop(item)
407
406
  args_dict = {k: _parse_value(v) for k, v in args_dict.items() if v is not None}
@@ -409,7 +408,7 @@ def main() -> None:
409
408
  try:
410
409
  gl = gitlab.Gitlab.merge_config(vars(options), gitlab_id, config_files)
411
410
  if debug:
412
- gl.enable_debug()
411
+ gl.enable_debug(mask_credentials=mask_credentials)
413
412
  if not skip_login and (gl.private_token or gl.oauth_token):
414
413
  gl.auth()
415
414
  except Exception as e:
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)
@@ -292,11 +300,14 @@ def _populate_sub_parser_by_class(
292
300
  if cls.__name__ in cli.custom_actions:
293
301
  name = cls.__name__
294
302
  for action_name in cli.custom_actions[name]:
303
+ custom_action = cli.custom_actions[name][action_name]
295
304
  # NOTE(jlvillal): If we put a function for the `default` value of
296
305
  # the `get` it will always get called, which will break things.
297
306
  action_parser = action_parsers.get(action_name)
298
307
  if action_parser is None:
299
- sub_parser_action = sub_parser.add_parser(action_name)
308
+ sub_parser_action = sub_parser.add_parser(
309
+ action_name, help=custom_action.help
310
+ )
300
311
  else:
301
312
  sub_parser_action = action_parser
302
313
  # Get the attributes for URL/path construction
@@ -309,17 +320,16 @@ def _populate_sub_parser_by_class(
309
320
 
310
321
  # We need to get the object somehow
311
322
  if not issubclass(cls, gitlab.mixins.GetWithoutIdMixin):
312
- if cls._id_attr is not None:
323
+ if cls._id_attr is not None and custom_action.requires_id:
313
324
  id_attr = cls._id_attr.replace("_", "-")
314
325
  sub_parser_action.add_argument(f"--{id_attr}", required=True)
315
326
 
316
- required, optional, dummy = cli.custom_actions[name][action_name]
317
- for x in required:
327
+ for x in custom_action.required:
318
328
  if x != cls._id_attr:
319
329
  sub_parser_action.add_argument(
320
330
  f"--{x.replace('_', '-')}", required=True
321
331
  )
322
- for x in optional:
332
+ for x in custom_action.optional:
323
333
  if x != cls._id_attr:
324
334
  sub_parser_action.add_argument(
325
335
  f"--{x.replace('_', '-')}", required=False
@@ -342,13 +352,13 @@ def _populate_sub_parser_by_class(
342
352
  )
343
353
  sub_parser_action.add_argument("--sudo", required=False)
344
354
 
345
- required, optional, dummy = cli.custom_actions[name][action_name]
346
- for x in required:
355
+ custom_action = cli.custom_actions[name][action_name]
356
+ for x in custom_action.required:
347
357
  if x != cls._id_attr:
348
358
  sub_parser_action.add_argument(
349
359
  f"--{x.replace('_', '-')}", required=True
350
360
  )
351
- for x in optional:
361
+ for x in custom_action.optional:
352
362
  if x != cls._id_attr:
353
363
  sub_parser_action.add_argument(
354
364
  f"--{x.replace('_', '-')}", required=False
@@ -374,8 +384,11 @@ def extend_parser(parser: argparse.ArgumentParser) -> argparse.ArgumentParser:
374
384
 
375
385
  for cls in sorted(classes, key=operator.attrgetter("__name__")):
376
386
  arg_name = cli.cls_to_gitlab_resource(cls)
387
+ mgr_cls_name = f"{cls.__name__}Manager"
388
+ mgr_cls = getattr(gitlab.v4.objects, mgr_cls_name)
377
389
  object_group = subparsers.add_parser(
378
- arg_name, formatter_class=cli.VerticalHelpFormatter
390
+ arg_name,
391
+ help=f"API endpoint: {mgr_cls._path}",
379
392
  )
380
393
 
381
394
  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,12 @@ 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",
41
+ required=("key_id",),
42
+ requires_id=False,
43
+ help="Enable a deploy key for the project",
44
+ )
40
45
  @exc.on_http_error(exc.GitlabProjectDeployKeyError)
41
46
  def enable(
42
47
  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.