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
@@ -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
 
@@ -126,7 +126,7 @@ class ProjectIssue(
126
126
  resource_iteration_events: ProjectIssueResourceIterationEventManager
127
127
  resource_weight_events: ProjectIssueResourceWeightEventManager
128
128
 
129
- @cli.register_custom_action("ProjectIssue", ("to_project_id",))
129
+ @cli.register_custom_action(cls_names="ProjectIssue", required=("to_project_id",))
130
130
  @exc.on_http_error(exc.GitlabUpdateError)
131
131
  def move(self, to_project_id: int, **kwargs: Any) -> None:
132
132
  """Move the issue to another project.
@@ -146,7 +146,9 @@ class ProjectIssue(
146
146
  assert isinstance(server_data, dict)
147
147
  self._update_attrs(server_data)
148
148
 
149
- @cli.register_custom_action("ProjectIssue", ("move_after_id", "move_before_id"))
149
+ @cli.register_custom_action(
150
+ cls_names="ProjectIssue", required=("move_after_id", "move_before_id")
151
+ )
150
152
  @exc.on_http_error(exc.GitlabUpdateError)
151
153
  def reorder(
152
154
  self,
@@ -178,7 +180,7 @@ class ProjectIssue(
178
180
  assert isinstance(server_data, dict)
179
181
  self._update_attrs(server_data)
180
182
 
181
- @cli.register_custom_action("ProjectIssue")
183
+ @cli.register_custom_action(cls_names="ProjectIssue")
182
184
  @exc.on_http_error(exc.GitlabGetError)
183
185
  def related_merge_requests(self, **kwargs: Any) -> Dict[str, Any]:
184
186
  """List merge requests related to the issue.
@@ -199,7 +201,7 @@ class ProjectIssue(
199
201
  assert isinstance(result, dict)
200
202
  return result
201
203
 
202
- @cli.register_custom_action("ProjectIssue")
204
+ @cli.register_custom_action(cls_names="ProjectIssue")
203
205
  @exc.on_http_error(exc.GitlabGetError)
204
206
  def closed_by(self, **kwargs: Any) -> Dict[str, Any]:
205
207
  """List merge requests that will close the issue when merged.
@@ -1,3 +1,4 @@
1
+ from gitlab import types
1
2
  from gitlab.base import RESTManager, RESTObject
2
3
  from gitlab.mixins import ListMixin
3
4
 
@@ -16,11 +17,37 @@ class GroupIterationManager(ListMixin, RESTManager):
16
17
  _path = "/groups/{group_id}/iterations"
17
18
  _obj_cls = GroupIteration
18
19
  _from_parent_attrs = {"group_id": "id"}
19
- _list_filters = ("state", "search", "include_ancestors")
20
+ # When using the API, the "in" keyword collides with python's "in" keyword
21
+ # raising a SyntaxError.
22
+ # For this reason, we have to use the query_parameters argument:
23
+ # group.iterations.list(query_parameters={"in": "title"})
24
+ _list_filters = (
25
+ "include_ancestors",
26
+ "include_descendants",
27
+ "in",
28
+ "search",
29
+ "state",
30
+ "updated_after",
31
+ "updated_before",
32
+ )
33
+ _types = {"in": types.ArrayAttribute}
20
34
 
21
35
 
22
36
  class ProjectIterationManager(ListMixin, RESTManager):
23
37
  _path = "/projects/{project_id}/iterations"
24
38
  _obj_cls = GroupIteration
25
39
  _from_parent_attrs = {"project_id": "id"}
26
- _list_filters = ("state", "search", "include_ancestors")
40
+ # When using the API, the "in" keyword collides with python's "in" keyword
41
+ # raising a SyntaxError.
42
+ # For this reason, we have to use the query_parameters argument:
43
+ # project.iterations.list(query_parameters={"in": "title"})
44
+ _list_filters = (
45
+ "include_ancestors",
46
+ "include_descendants",
47
+ "in",
48
+ "search",
49
+ "state",
50
+ "updated_after",
51
+ "updated_before",
52
+ )
53
+ _types = {"in": types.ArrayAttribute}
gitlab/v4/objects/jobs.py CHANGED
@@ -16,7 +16,7 @@ __all__ = [
16
16
 
17
17
 
18
18
  class ProjectJob(RefreshMixin, RESTObject):
19
- @cli.register_custom_action("ProjectJob")
19
+ @cli.register_custom_action(cls_names="ProjectJob")
20
20
  @exc.on_http_error(exc.GitlabJobCancelError)
21
21
  def cancel(self, **kwargs: Any) -> Dict[str, Any]:
22
22
  """Cancel the job.
@@ -34,7 +34,7 @@ class ProjectJob(RefreshMixin, RESTObject):
34
34
  assert isinstance(result, dict)
35
35
  return result
36
36
 
37
- @cli.register_custom_action("ProjectJob")
37
+ @cli.register_custom_action(cls_names="ProjectJob")
38
38
  @exc.on_http_error(exc.GitlabJobRetryError)
39
39
  def retry(self, **kwargs: Any) -> Dict[str, Any]:
40
40
  """Retry the job.
@@ -52,7 +52,7 @@ class ProjectJob(RefreshMixin, RESTObject):
52
52
  assert isinstance(result, dict)
53
53
  return result
54
54
 
55
- @cli.register_custom_action("ProjectJob")
55
+ @cli.register_custom_action(cls_names="ProjectJob")
56
56
  @exc.on_http_error(exc.GitlabJobPlayError)
57
57
  def play(self, **kwargs: Any) -> None:
58
58
  """Trigger a job explicitly.
@@ -70,7 +70,7 @@ class ProjectJob(RefreshMixin, RESTObject):
70
70
  assert isinstance(result, dict)
71
71
  self._update_attrs(result)
72
72
 
73
- @cli.register_custom_action("ProjectJob")
73
+ @cli.register_custom_action(cls_names="ProjectJob")
74
74
  @exc.on_http_error(exc.GitlabJobEraseError)
75
75
  def erase(self, **kwargs: Any) -> None:
76
76
  """Erase the job (remove job artifacts and trace).
@@ -85,7 +85,7 @@ class ProjectJob(RefreshMixin, RESTObject):
85
85
  path = f"{self.manager.path}/{self.encoded_id}/erase"
86
86
  self.manager.gitlab.http_post(path, **kwargs)
87
87
 
88
- @cli.register_custom_action("ProjectJob")
88
+ @cli.register_custom_action(cls_names="ProjectJob")
89
89
  @exc.on_http_error(exc.GitlabCreateError)
90
90
  def keep_artifacts(self, **kwargs: Any) -> None:
91
91
  """Prevent artifacts from being deleted when expiration is set.
@@ -100,7 +100,7 @@ class ProjectJob(RefreshMixin, RESTObject):
100
100
  path = f"{self.manager.path}/{self.encoded_id}/artifacts/keep"
101
101
  self.manager.gitlab.http_post(path, **kwargs)
102
102
 
103
- @cli.register_custom_action("ProjectJob")
103
+ @cli.register_custom_action(cls_names="ProjectJob")
104
104
  @exc.on_http_error(exc.GitlabCreateError)
105
105
  def delete_artifacts(self, **kwargs: Any) -> None:
106
106
  """Delete artifacts of a job.
@@ -115,7 +115,7 @@ class ProjectJob(RefreshMixin, RESTObject):
115
115
  path = f"{self.manager.path}/{self.encoded_id}/artifacts"
116
116
  self.manager.gitlab.http_delete(path, **kwargs)
117
117
 
118
- @cli.register_custom_action("ProjectJob")
118
+ @cli.register_custom_action(cls_names="ProjectJob")
119
119
  @exc.on_http_error(exc.GitlabGetError)
120
120
  def artifacts(
121
121
  self,
@@ -156,7 +156,7 @@ class ProjectJob(RefreshMixin, RESTObject):
156
156
  result, streamed, action, chunk_size, iterator=iterator
157
157
  )
158
158
 
159
- @cli.register_custom_action("ProjectJob")
159
+ @cli.register_custom_action(cls_names="ProjectJob")
160
160
  @exc.on_http_error(exc.GitlabGetError)
161
161
  def artifact(
162
162
  self,
@@ -199,7 +199,7 @@ class ProjectJob(RefreshMixin, RESTObject):
199
199
  result, streamed, action, chunk_size, iterator=iterator
200
200
  )
201
201
 
202
- @cli.register_custom_action("ProjectJob")
202
+ @cli.register_custom_action(cls_names="ProjectJob")
203
203
  @exc.on_http_error(exc.GitlabGetError)
204
204
  def trace(
205
205
  self,
@@ -209,7 +209,7 @@ class ProjectJob(RefreshMixin, RESTObject):
209
209
  *,
210
210
  iterator: bool = False,
211
211
  **kwargs: Any,
212
- ) -> Dict[str, Any]:
212
+ ) -> Optional[Union[bytes, Iterator[Any]]]:
213
213
  """Get the job trace.
214
214
 
215
215
  Args:
@@ -236,12 +236,9 @@ class ProjectJob(RefreshMixin, RESTObject):
236
236
  )
237
237
  if TYPE_CHECKING:
238
238
  assert isinstance(result, requests.Response)
239
- return_value = utils.response_content(
239
+ return utils.response_content(
240
240
  result, streamed, action, chunk_size, iterator=iterator
241
241
  )
242
- if TYPE_CHECKING:
243
- assert isinstance(return_value, dict)
244
- return return_value
245
242
 
246
243
 
247
244
  class ProjectJobManager(RetrieveMixin, RESTManager):
@@ -89,6 +89,8 @@ class ProjectMergeRequestApprovalManager(GetWithoutIdMixin, UpdateMixin, RESTMan
89
89
  approver_ids: Optional[List[int]] = None,
90
90
  approver_group_ids: Optional[List[int]] = None,
91
91
  approval_rule_name: str = "name",
92
+ *,
93
+ approver_usernames: Optional[List[str]] = None,
92
94
  **kwargs: Any,
93
95
  ) -> RESTObject:
94
96
  """Change MR-level allowed approvers and approver groups.
@@ -104,6 +106,7 @@ class ProjectMergeRequestApprovalManager(GetWithoutIdMixin, UpdateMixin, RESTMan
104
106
  """
105
107
  approver_ids = approver_ids or []
106
108
  approver_group_ids = approver_group_ids or []
109
+ approver_usernames = approver_usernames or []
107
110
 
108
111
  data = {
109
112
  "name": approval_rule_name,
@@ -111,6 +114,7 @@ class ProjectMergeRequestApprovalManager(GetWithoutIdMixin, UpdateMixin, RESTMan
111
114
  "rule_type": "regular",
112
115
  "user_ids": approver_ids,
113
116
  "group_ids": approver_group_ids,
117
+ "usernames": approver_usernames,
114
118
  }
115
119
  if TYPE_CHECKING:
116
120
  assert self._parent is not None
@@ -118,12 +122,13 @@ class ProjectMergeRequestApprovalManager(GetWithoutIdMixin, UpdateMixin, RESTMan
118
122
  self._parent.approval_rules
119
123
  )
120
124
  # update any existing approval rule matching the name
121
- existing_approval_rules = approval_rules.list()
125
+ existing_approval_rules = approval_rules.list(iterator=True)
122
126
  for ar in existing_approval_rules:
123
127
  if ar.name == approval_rule_name:
124
128
  ar.user_ids = data["user_ids"]
125
129
  ar.approvals_required = data["approvals_required"]
126
130
  ar.group_ids = data["group_ids"]
131
+ ar.usernames = data["usernames"]
127
132
  ar.save()
128
133
  return ar
129
134
  # if there was no rule matching the rule name, create a new one
@@ -145,14 +150,14 @@ class ProjectMergeRequestApprovalRuleManager(CRUDMixin, RESTManager):
145
150
  "name",
146
151
  "approvals_required",
147
152
  ),
148
- optional=("user_ids", "group_ids"),
153
+ optional=("user_ids", "group_ids", "usernames"),
149
154
  )
150
155
  # Important: When approval_project_rule_id is set, the name, users and
151
156
  # groups of project-level rule will be copied. The approvals_required
152
157
  # specified will be used.
153
158
  _create_attrs = RequiredOptional(
154
159
  required=("name", "approvals_required"),
155
- optional=("approval_project_rule_id", "user_ids", "group_ids"),
160
+ optional=("approval_project_rule_id", "user_ids", "group_ids", "usernames"),
156
161
  )
157
162
 
158
163
  def get(
@@ -168,7 +168,7 @@ class ProjectMergeRequest(
168
168
  resourcestateevents: ProjectMergeRequestResourceStateEventManager
169
169
  reviewer_details: ProjectMergeRequestReviewerDetailManager
170
170
 
171
- @cli.register_custom_action("ProjectMergeRequest")
171
+ @cli.register_custom_action(cls_names="ProjectMergeRequest")
172
172
  @exc.on_http_error(exc.GitlabMROnBuildSuccessError)
173
173
  def cancel_merge_when_pipeline_succeeds(self, **kwargs: Any) -> Dict[str, str]:
174
174
  """Cancel merge when the pipeline succeeds.
@@ -197,7 +197,7 @@ class ProjectMergeRequest(
197
197
  assert isinstance(server_data, dict)
198
198
  return server_data
199
199
 
200
- @cli.register_custom_action("ProjectMergeRequest")
200
+ @cli.register_custom_action(cls_names="ProjectMergeRequest")
201
201
  @exc.on_http_error(exc.GitlabListError)
202
202
  def closes_issues(self, **kwargs: Any) -> RESTObjectList:
203
203
  """List issues that will close on merge."
@@ -222,7 +222,7 @@ class ProjectMergeRequest(
222
222
  manager = ProjectIssueManager(self.manager.gitlab, parent=self.manager._parent)
223
223
  return RESTObjectList(manager, ProjectIssue, data_list)
224
224
 
225
- @cli.register_custom_action("ProjectMergeRequest")
225
+ @cli.register_custom_action(cls_names="ProjectMergeRequest")
226
226
  @exc.on_http_error(exc.GitlabListError)
227
227
  def commits(self, **kwargs: Any) -> RESTObjectList:
228
228
  """List the merge request commits.
@@ -248,7 +248,9 @@ class ProjectMergeRequest(
248
248
  manager = ProjectCommitManager(self.manager.gitlab, parent=self.manager._parent)
249
249
  return RESTObjectList(manager, ProjectCommit, data_list)
250
250
 
251
- @cli.register_custom_action("ProjectMergeRequest", optional=("access_raw_diffs",))
251
+ @cli.register_custom_action(
252
+ cls_names="ProjectMergeRequest", optional=("access_raw_diffs",)
253
+ )
252
254
  @exc.on_http_error(exc.GitlabListError)
253
255
  def changes(self, **kwargs: Any) -> Union[Dict[str, Any], requests.Response]:
254
256
  """List the merge request changes.
@@ -266,7 +268,7 @@ class ProjectMergeRequest(
266
268
  path = f"{self.manager.path}/{self.encoded_id}/changes"
267
269
  return self.manager.gitlab.http_get(path, **kwargs)
268
270
 
269
- @cli.register_custom_action("ProjectMergeRequest", (), ("sha",))
271
+ @cli.register_custom_action(cls_names="ProjectMergeRequest", optional=("sha",))
270
272
  @exc.on_http_error(exc.GitlabMRApprovalError)
271
273
  def approve(self, sha: Optional[str] = None, **kwargs: Any) -> Dict[str, Any]:
272
274
  """Approve the merge request.
@@ -295,7 +297,7 @@ class ProjectMergeRequest(
295
297
  self._update_attrs(server_data)
296
298
  return server_data
297
299
 
298
- @cli.register_custom_action("ProjectMergeRequest")
300
+ @cli.register_custom_action(cls_names="ProjectMergeRequest")
299
301
  @exc.on_http_error(exc.GitlabMRApprovalError)
300
302
  def unapprove(self, **kwargs: Any) -> None:
301
303
  """Unapprove the merge request.
@@ -317,7 +319,7 @@ class ProjectMergeRequest(
317
319
  assert isinstance(server_data, dict)
318
320
  self._update_attrs(server_data)
319
321
 
320
- @cli.register_custom_action("ProjectMergeRequest")
322
+ @cli.register_custom_action(cls_names="ProjectMergeRequest")
321
323
  @exc.on_http_error(exc.GitlabMRRebaseError)
322
324
  def rebase(self, **kwargs: Any) -> Union[Dict[str, Any], requests.Response]:
323
325
  """Attempt to rebase the source branch onto the target branch
@@ -333,7 +335,7 @@ class ProjectMergeRequest(
333
335
  data: Dict[str, Any] = {}
334
336
  return self.manager.gitlab.http_put(path, post_data=data, **kwargs)
335
337
 
336
- @cli.register_custom_action("ProjectMergeRequest")
338
+ @cli.register_custom_action(cls_names="ProjectMergeRequest")
337
339
  @exc.on_http_error(exc.GitlabMRResetApprovalError)
338
340
  def reset_approvals(
339
341
  self, **kwargs: Any
@@ -351,7 +353,7 @@ class ProjectMergeRequest(
351
353
  data: Dict[str, Any] = {}
352
354
  return self.manager.gitlab.http_put(path, post_data=data, **kwargs)
353
355
 
354
- @cli.register_custom_action("ProjectMergeRequest")
356
+ @cli.register_custom_action(cls_names="ProjectMergeRequest")
355
357
  @exc.on_http_error(exc.GitlabGetError)
356
358
  def merge_ref(self, **kwargs: Any) -> Union[Dict[str, Any], requests.Response]:
357
359
  """Attempt to merge changes between source and target branches into
@@ -367,9 +369,8 @@ class ProjectMergeRequest(
367
369
  return self.manager.gitlab.http_get(path, **kwargs)
368
370
 
369
371
  @cli.register_custom_action(
370
- "ProjectMergeRequest",
371
- (),
372
- (
372
+ cls_names="ProjectMergeRequest",
373
+ optional=(
373
374
  "merge_commit_message",
374
375
  "should_remove_source_branch",
375
376
  "merge_when_pipeline_succeeds",
@@ -31,7 +31,7 @@ __all__ = [
31
31
  class GroupMilestone(SaveMixin, ObjectDeleteMixin, RESTObject):
32
32
  _repr_attr = "title"
33
33
 
34
- @cli.register_custom_action("GroupMilestone")
34
+ @cli.register_custom_action(cls_names="GroupMilestone")
35
35
  @exc.on_http_error(exc.GitlabListError)
36
36
  def issues(self, **kwargs: Any) -> RESTObjectList:
37
37
  """List issues related to this milestone.
@@ -58,7 +58,7 @@ class GroupMilestone(SaveMixin, ObjectDeleteMixin, RESTObject):
58
58
  # FIXME(gpocentek): the computed manager path is not correct
59
59
  return RESTObjectList(manager, GroupIssue, data_list)
60
60
 
61
- @cli.register_custom_action("GroupMilestone")
61
+ @cli.register_custom_action(cls_names="GroupMilestone")
62
62
  @exc.on_http_error(exc.GitlabListError)
63
63
  def merge_requests(self, **kwargs: Any) -> RESTObjectList:
64
64
  """List the merge requests related to this milestone.
@@ -108,7 +108,7 @@ class ProjectMilestone(PromoteMixin, SaveMixin, ObjectDeleteMixin, RESTObject):
108
108
  _repr_attr = "title"
109
109
  _update_method = UpdateMethod.POST
110
110
 
111
- @cli.register_custom_action("ProjectMilestone")
111
+ @cli.register_custom_action(cls_names="ProjectMilestone")
112
112
  @exc.on_http_error(exc.GitlabListError)
113
113
  def issues(self, **kwargs: Any) -> RESTObjectList:
114
114
  """List issues related to this milestone.
@@ -135,7 +135,7 @@ class ProjectMilestone(PromoteMixin, SaveMixin, ObjectDeleteMixin, RESTObject):
135
135
  # FIXME(gpocentek): the computed manager path is not correct
136
136
  return RESTObjectList(manager, ProjectIssue, data_list)
137
137
 
138
- @cli.register_custom_action("ProjectMilestone")
138
+ @cli.register_custom_action(cls_names="ProjectMilestone")
139
139
  @exc.on_http_error(exc.GitlabListError)
140
140
  def merge_requests(self, **kwargs: Any) -> RESTObjectList:
141
141
  """List the merge requests related to this milestone.
@@ -24,7 +24,9 @@ class NamespaceManager(RetrieveMixin, RESTManager):
24
24
  def get(self, id: Union[str, int], lazy: bool = False, **kwargs: Any) -> Namespace:
25
25
  return cast(Namespace, super().get(id=id, lazy=lazy, **kwargs))
26
26
 
27
- @cli.register_custom_action("NamespaceManager", ("namespace", "parent_id"))
27
+ @cli.register_custom_action(
28
+ cls_names="NamespaceManager", required=("namespace", "parent_id")
29
+ )
28
30
  @exc.on_http_error(exc.GitlabGetError)
29
31
  def exists(self, namespace: str, **kwargs: Any) -> Namespace:
30
32
  """Get existence of a namespace by path.
@@ -48,8 +48,8 @@ class GenericPackageManager(RESTManager):
48
48
  _from_parent_attrs = {"project_id": "id"}
49
49
 
50
50
  @cli.register_custom_action(
51
- "GenericPackageManager",
52
- ("package_name", "package_version", "file_name", "path"),
51
+ cls_names="GenericPackageManager",
52
+ required=("package_name", "package_version", "file_name", "path"),
53
53
  )
54
54
  @exc.on_http_error(exc.GitlabUploadError)
55
55
  def upload(
@@ -123,8 +123,8 @@ class GenericPackageManager(RESTManager):
123
123
  return self._obj_cls(self, attrs=attrs)
124
124
 
125
125
  @cli.register_custom_action(
126
- "GenericPackageManager",
127
- ("package_name", "package_version", "file_name"),
126
+ cls_names="GenericPackageManager",
127
+ required=("package_name", "package_version", "file_name"),
128
128
  )
129
129
  @exc.on_http_error(exc.GitlabGetError)
130
130
  def download(
@@ -60,7 +60,7 @@ class ProjectPipeline(RefreshMixin, ObjectDeleteMixin, RESTObject):
60
60
  test_report_summary: "ProjectPipelineTestReportSummaryManager"
61
61
  variables: "ProjectPipelineVariableManager"
62
62
 
63
- @cli.register_custom_action("ProjectPipeline")
63
+ @cli.register_custom_action(cls_names="ProjectPipeline")
64
64
  @exc.on_http_error(exc.GitlabPipelineCancelError)
65
65
  def cancel(self, **kwargs: Any) -> Union[Dict[str, Any], requests.Response]:
66
66
  """Cancel the job.
@@ -75,7 +75,7 @@ class ProjectPipeline(RefreshMixin, ObjectDeleteMixin, RESTObject):
75
75
  path = f"{self.manager.path}/{self.encoded_id}/cancel"
76
76
  return self.manager.gitlab.http_post(path, **kwargs)
77
77
 
78
- @cli.register_custom_action("ProjectPipeline")
78
+ @cli.register_custom_action(cls_names="ProjectPipeline")
79
79
  @exc.on_http_error(exc.GitlabPipelineRetryError)
80
80
  def retry(self, **kwargs: Any) -> Union[Dict[str, Any], requests.Response]:
81
81
  """Retry the job.
@@ -139,6 +139,27 @@ class ProjectPipelineManager(RetrieveMixin, CreateMixin, DeleteMixin, RESTManage
139
139
  ProjectPipeline, CreateMixin.create(self, data, path=path, **kwargs)
140
140
  )
141
141
 
142
+ def latest(self, ref: Optional[str] = None, lazy: bool = False) -> ProjectPipeline:
143
+ """Get the latest pipeline for the most recent commit
144
+ on a specific ref in a project
145
+
146
+ Args:
147
+ ref: The branch or tag to check for the latest pipeline.
148
+ Defaults to the default branch when not specified.
149
+ Returns:
150
+ A Pipeline instance
151
+ """
152
+ data = {}
153
+ if ref:
154
+ data = {"ref": ref}
155
+ if TYPE_CHECKING:
156
+ assert self._obj_cls is not None
157
+ assert self.path is not None
158
+ server_data = self.gitlab.http_get(self.path + "/latest", query_data=data)
159
+ if TYPE_CHECKING:
160
+ assert not isinstance(server_data, requests.Response)
161
+ return self._obj_cls(self, server_data, lazy=lazy)
162
+
142
163
 
143
164
  class ProjectPipelineJob(RESTObject):
144
165
  pass
@@ -201,7 +222,7 @@ class ProjectPipelineSchedule(SaveMixin, ObjectDeleteMixin, RESTObject):
201
222
  variables: ProjectPipelineScheduleVariableManager
202
223
  pipelines: ProjectPipelineSchedulePipelineManager
203
224
 
204
- @cli.register_custom_action("ProjectPipelineSchedule")
225
+ @cli.register_custom_action(cls_names="ProjectPipelineSchedule")
205
226
  @exc.on_http_error(exc.GitlabOwnershipError)
206
227
  def take_ownership(self, **kwargs: Any) -> None:
207
228
  """Update the owner of a pipeline schedule.
@@ -219,7 +240,7 @@ class ProjectPipelineSchedule(SaveMixin, ObjectDeleteMixin, RESTObject):
219
240
  assert isinstance(server_data, dict)
220
241
  self._update_attrs(server_data)
221
242
 
222
- @cli.register_custom_action("ProjectPipelineSchedule")
243
+ @cli.register_custom_action(cls_names="ProjectPipelineSchedule")
223
244
  @exc.on_http_error(exc.GitlabPipelinePlayError)
224
245
  def play(self, **kwargs: Any) -> Dict[str, Any]:
225
246
  """Trigger a new scheduled pipeline, which runs immediately.