gitlabform 0.0.540a0__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 (79) hide show
  1. gitlabform/__init__.py +719 -0
  2. gitlabform/configuration/__init__.py +12 -0
  3. gitlabform/configuration/common.py +19 -0
  4. gitlabform/configuration/core.py +323 -0
  5. gitlabform/configuration/groups.py +127 -0
  6. gitlabform/configuration/projects.py +73 -0
  7. gitlabform/configuration/transform.py +259 -0
  8. gitlabform/constants.py +7 -0
  9. gitlabform/gitlab/__init__.py +108 -0
  10. gitlabform/gitlab/commits.py +39 -0
  11. gitlabform/gitlab/core.py +334 -0
  12. gitlabform/gitlab/group_badges.py +50 -0
  13. gitlabform/gitlab/group_ldap_links.py +40 -0
  14. gitlabform/gitlab/groups.py +96 -0
  15. gitlabform/gitlab/merge_requests.py +57 -0
  16. gitlabform/gitlab/pipelines.py +23 -0
  17. gitlabform/gitlab/project_badges.py +52 -0
  18. gitlabform/gitlab/project_deploy_keys.py +102 -0
  19. gitlabform/gitlab/project_merge_requests_approvals.py +94 -0
  20. gitlabform/gitlab/project_protected_environments.py +37 -0
  21. gitlabform/gitlab/projects.py +151 -0
  22. gitlabform/gitlab/python_gitlab.py +251 -0
  23. gitlabform/gitlab/variables.py +47 -0
  24. gitlabform/lists/__init__.py +62 -0
  25. gitlabform/lists/filter.py +99 -0
  26. gitlabform/lists/groups.py +87 -0
  27. gitlabform/lists/projects.py +239 -0
  28. gitlabform/output.py +46 -0
  29. gitlabform/processors/__init__.py +43 -0
  30. gitlabform/processors/abstract_processor.py +187 -0
  31. gitlabform/processors/application/__init__.py +17 -0
  32. gitlabform/processors/application/application_settings_processor.py +39 -0
  33. gitlabform/processors/defining_keys.py +152 -0
  34. gitlabform/processors/group/__init__.py +48 -0
  35. gitlabform/processors/group/group_badges_processor.py +17 -0
  36. gitlabform/processors/group/group_hooks_processor.py +75 -0
  37. gitlabform/processors/group/group_labels_processor.py +28 -0
  38. gitlabform/processors/group/group_ldap_links_processor.py +16 -0
  39. gitlabform/processors/group/group_members_processor.py +287 -0
  40. gitlabform/processors/group/group_push_rules_processor.py +44 -0
  41. gitlabform/processors/group/group_saml_links_processor.py +65 -0
  42. gitlabform/processors/group/group_settings_processor.py +90 -0
  43. gitlabform/processors/group/group_variables_processor.py +26 -0
  44. gitlabform/processors/multiple_entities_processor.py +171 -0
  45. gitlabform/processors/project/__init__.py +80 -0
  46. gitlabform/processors/project/badges_processor.py +17 -0
  47. gitlabform/processors/project/branches_processor.py +514 -0
  48. gitlabform/processors/project/deploy_keys_processor.py +18 -0
  49. gitlabform/processors/project/files_processor.py +301 -0
  50. gitlabform/processors/project/hooks_processor.py +64 -0
  51. gitlabform/processors/project/integrations_processor.py +33 -0
  52. gitlabform/processors/project/job_token_scope_processor.py +216 -0
  53. gitlabform/processors/project/members_processor.py +204 -0
  54. gitlabform/processors/project/merge_requests_approval_rules.py +17 -0
  55. gitlabform/processors/project/merge_requests_approvals.py +59 -0
  56. gitlabform/processors/project/project_labels_processor.py +27 -0
  57. gitlabform/processors/project/project_processor.py +62 -0
  58. gitlabform/processors/project/project_push_rules_processor.py +52 -0
  59. gitlabform/processors/project/project_security_settings.py +66 -0
  60. gitlabform/processors/project/project_settings_processor.py +239 -0
  61. gitlabform/processors/project/project_variables_processor.py +94 -0
  62. gitlabform/processors/project/remote_mirrors_processor.py +278 -0
  63. gitlabform/processors/project/resource_groups_processor.py +48 -0
  64. gitlabform/processors/project/schedules_processor.py +208 -0
  65. gitlabform/processors/project/tags_processor.py +108 -0
  66. gitlabform/processors/shared/__init__.py +0 -0
  67. gitlabform/processors/shared/protected_environments_processor.py +20 -0
  68. gitlabform/processors/util/__init__.py +0 -0
  69. gitlabform/processors/util/decorators.py +44 -0
  70. gitlabform/processors/util/difference_logger.py +70 -0
  71. gitlabform/processors/util/labels_processor.py +120 -0
  72. gitlabform/processors/util/variables_processor.py +143 -0
  73. gitlabform/run.py +9 -0
  74. gitlabform/util.py +7 -0
  75. gitlabform-0.0.540a0.dist-info/METADATA +54 -0
  76. gitlabform-0.0.540a0.dist-info/RECORD +79 -0
  77. gitlabform-0.0.540a0.dist-info/WHEEL +4 -0
  78. gitlabform-0.0.540a0.dist-info/entry_points.txt +9 -0
  79. gitlabform-0.0.540a0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,334 @@
1
+ import functools
2
+ import os
3
+ import re
4
+ from logging import debug, info, warning
5
+ from typing import Union
6
+ from urllib import parse
7
+
8
+ from packaging import version
9
+
10
+ from importlib.metadata import version as package_version
11
+ import requests
12
+
13
+ # noinspection PyPackageRequirements
14
+ import urllib3
15
+ from requests.adapters import HTTPAdapter
16
+
17
+ # noinspection PyPackageRequirements
18
+ from urllib3.util.retry import Retry
19
+
20
+ from gitlabform.configuration import Configuration
21
+ from gitlabform.util import to_str
22
+
23
+
24
+ class GitLabCore:
25
+ def __init__(self, config_path=None, config_string=None):
26
+ self.configuration = Configuration(config_path, config_string)
27
+
28
+ default_gitlab_config = {
29
+ "url": os.getenv("GITLAB_URL"),
30
+ "token": os.getenv("GITLAB_TOKEN"),
31
+ "ssl_verify": True,
32
+ "timeout": 10,
33
+ "max_retries": 3,
34
+ "backoff_factor": 0.25,
35
+ "retry_transient_errors": True,
36
+ }
37
+ gitlab_config_from_file = self.configuration.get("gitlab", {})
38
+ self.gitlab_config = {**default_gitlab_config, **gitlab_config_from_file}
39
+
40
+ self.session = requests.Session()
41
+
42
+ retries_status_forcelist = []
43
+ if self.gitlab_config["retry_transient_errors"]:
44
+ # 429 Too Many Requests is included to handle rate limiting
45
+ # Ideally we would like to handle Rate Limiting retrys based on 'Retry-After' header
46
+ # As done in python-gitlab: https://github.com/python-gitlab/python-gitlab/blob/main/docs/api-usage-advanced.rst#rate-limits
47
+ # 5xx status codes are included to retry after transient server errors
48
+ retries_status_forcelist = [429, 500, 502, 503, 504] + list(range(520, 531))
49
+
50
+ retries = Retry(
51
+ total=self.gitlab_config["max_retries"],
52
+ backoff_factor=self.gitlab_config["backoff_factor"],
53
+ status_forcelist=retries_status_forcelist,
54
+ )
55
+
56
+ self.session.mount("http://", HTTPAdapter(max_retries=retries))
57
+ self.session.mount("https://", HTTPAdapter(max_retries=retries))
58
+
59
+ self.session.verify = self.gitlab_config["ssl_verify"]
60
+ if not self.gitlab_config["ssl_verify"]:
61
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
62
+
63
+ self.gitlabform_version = package_version("gitlabform")
64
+ self.requests_version = package_version("requests")
65
+ self.session.headers.update(
66
+ {
67
+ "private-token": self.gitlab_config["token"],
68
+ "authorization": f"Bearer {self.gitlab_config['token']}",
69
+ "user-agent": f"GitLabForm/{self.gitlabform_version} (python-requests/{self.requests_version})",
70
+ }
71
+ )
72
+
73
+ try:
74
+ version_response = self._make_requests_to_api("version")
75
+ info(
76
+ f"Connected to GitLab version: {version_response['version']} ({version_response['revision']}), Enterprise Edition: {version_response['enterprise']}"
77
+ )
78
+ self.version = version_response["version"]
79
+ self.enterprise = version_response["enterprise"]
80
+
81
+ if self.is_version_less_than("16"):
82
+ warning(
83
+ f"Support for GitLab version {self.version} is Deprecated. See Requirements: https://gitlabform.github.io/gitlabform/requirements/"
84
+ )
85
+
86
+ current_user = self._make_requests_to_api("user")
87
+ if current_user.get("is_admin", False):
88
+ self.admin = True
89
+ else:
90
+ self.admin = False
91
+ info(f"Connected as: {current_user['username']}, admin: {'yes' if self.admin else 'no'}")
92
+ if not self.admin:
93
+ warning("Connected as non-admin. You may encounter permission issues.")
94
+
95
+ except Exception as e:
96
+ raise TestRequestFailedException(e)
97
+
98
+ def get_configuration(self):
99
+ return self.configuration
100
+
101
+ def get_project(self, project_and_group_or_id):
102
+ return self._make_requests_to_api("projects/%s", project_and_group_or_id)
103
+
104
+ def is_version_at_least(self, min_version: Union[str, version.Version]) -> bool:
105
+ """
106
+ Check if GitLab server version is at least the specified version
107
+
108
+ Args:
109
+ min_version: Version string like "15.4.0" or "16.0", or a Version object
110
+
111
+ Returns:
112
+ bool: True if server version is >= min_version, False otherwise
113
+ """
114
+ current_version = "0.0.0"
115
+
116
+ if self.version != "unknown":
117
+ sem_ver_regex = "^\d*\.\d*\.\d*"
118
+ # Get the pure semantic version from self.version, for example Gitlab will return 18.2.1-ee
119
+ match = re.search(sem_ver_regex, self.version)
120
+ if match:
121
+ current_version = match.group()
122
+
123
+ if isinstance(min_version, str):
124
+ min_version = version.parse(min_version)
125
+
126
+ return version.parse(current_version) >= min_version
127
+
128
+ def is_version_less_than(self, max_version: Union[str, version.Version]) -> bool:
129
+ """
130
+ Check if GitLab server version is less than the specified version
131
+ Args:
132
+ max_version: Version string like "15.4.0" or "16.0", or a Version object
133
+
134
+ Returns:
135
+ bool: True if server version is < max_version, False otherwise
136
+ """
137
+
138
+ return not self.is_version_at_least(max_version)
139
+
140
+ @functools.lru_cache()
141
+ def _get_user_id(self, username: str) -> int:
142
+ users = self._make_requests_to_api("users?username=%s", username, "GET")
143
+
144
+ # this API endpoint is for lookup, not search, so 'username' has to be full and exact username
145
+ # also it's not possible to get more than 1 user as a result
146
+
147
+ if len(users) == 0:
148
+ raise NotFoundException("No users found when searching for username '%s'" % username)
149
+
150
+ return int(users[0]["id"])
151
+
152
+ @functools.lru_cache()
153
+ def _get_group_id(self, path) -> int:
154
+ group = self._make_requests_to_api("groups/%s", path, "GET")
155
+ return int(group["id"])
156
+
157
+ @functools.lru_cache()
158
+ def _get_protected_branch_id(self, project_and_group_name, branch) -> int:
159
+ branch = self._make_requests_to_api("projects/%s/protected_branches/%s", (project_and_group_name, branch))
160
+ return int(branch["id"])
161
+
162
+ @functools.lru_cache()
163
+ def _get_project_id(self, project_and_group):
164
+ # This is a NEW workaround for https://github.com/gitlabhq/gitlabhq/issues/8290
165
+ result = self.get_project(project_and_group)
166
+ return str(result["id"])
167
+
168
+ def _make_requests_to_api(
169
+ self,
170
+ path_as_format_string,
171
+ args=None,
172
+ method="GET",
173
+ data=None,
174
+ expected_codes=200,
175
+ json=None,
176
+ ):
177
+ """
178
+ Makes an HTTP request or requests to the GitLab API endpoint. More than one request is made automatically
179
+ if the endpoint is paginated. (See underlying method for authentication, retries, timeout etc.)
180
+
181
+ :param path_as_format_string: path with parts to be replaced by values from `args` replaced by '%s'
182
+ (aka the old-style Python string formatting, see:
183
+ https://docs.python.org/2/library/stdtypes.html#string-formatting )
184
+ :param args: single element or a tuple of values to put under '%s's in `path_as_format_string`
185
+ :param method: uppercase string of a HTTP method name, like 'GET' or 'PUT'
186
+ :param data: dict with data to be 'PUT'ted or 'POST'ed
187
+ :param expected_codes: a single HTTP code (like: 200) or a list of accepted HTTP codes
188
+ - if the call to the API will return other code an exception will be thrown
189
+ :param json: alternatively to `dict` you can set this to a string that can be parsed as JSON that will
190
+ be used as data to be 'PUT'ted or 'POST'ed
191
+ :return: data returned by the endpoint, as a JSON object. If the API is paginated, it returns JSONs with
192
+ arrays of objects and then this method returns JSON with a single array that contains all of those
193
+ objects.
194
+ """
195
+ if method != "GET":
196
+ response = self._make_request_to_api(path_as_format_string, args, method, data, expected_codes, json)
197
+ return response.json()
198
+ else:
199
+ if "?" in path_as_format_string:
200
+ path_as_format_string += "&per_page=100"
201
+ else:
202
+ path_as_format_string += "?per_page=100"
203
+
204
+ first_response = self._make_request_to_api(path_as_format_string, args, method, data, expected_codes, json)
205
+ results = first_response.json()
206
+
207
+ # In newer versions of GitLab the 'x-total-pages' may not be available
208
+ # anymore, see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/43159
209
+ # so let's use the 'x-next-page' header instead
210
+
211
+ response = first_response
212
+ while True:
213
+ if "x-next-page" in response.headers and response.headers["x-next-page"]:
214
+ next_page = response.headers["x-next-page"]
215
+ response = self._make_request_to_api(
216
+ path_as_format_string + "&page=" + str(next_page),
217
+ args,
218
+ method,
219
+ data,
220
+ expected_codes,
221
+ json,
222
+ )
223
+ results += response.json()
224
+ else:
225
+ break
226
+
227
+ return results
228
+
229
+ def _make_request_to_api(self, path_as_format_string, args, method, dict_data, expected_codes, json_data):
230
+ """
231
+ Makes a single request to the GitLab API. Takes care of the authentication, basic error processing,
232
+ retries, timeout etc.
233
+
234
+ :param for the params description please see `_make_requests_to_api()`
235
+ :return: data returned by the endpoint, as a JSON object.
236
+ """
237
+
238
+ expected_codes = self._listify(expected_codes)
239
+
240
+ if dict_data and json_data:
241
+ raise Exception("You need to pass the data either as dict (dict_data) or JSON (json_data), not both!")
242
+
243
+ url = f"{self.gitlab_config['url']}/api/v4/{self._format_with_url_encoding(path_as_format_string, args)}"
244
+ if dict_data:
245
+ response = self.session.request(method, url, data=dict_data, timeout=self.gitlab_config["timeout"])
246
+ debug(f"===> data = {to_str(dict_data)}")
247
+ elif json_data:
248
+ response = self.session.request(method, url, json=json_data, timeout=self.gitlab_config["timeout"])
249
+ debug(f"===> json = {to_str(json_data)}")
250
+ else:
251
+ response = self.session.request(method, url, timeout=self.gitlab_config["timeout"])
252
+
253
+ if response.status_code in expected_codes:
254
+ # if we accept error responses then they will likely not contain a JSON body
255
+ # so fake it to fix further calls to response.json()
256
+ if response.status_code == 204 or (400 <= response.status_code <= 499):
257
+ response.json = lambda: {}
258
+ else:
259
+ if response.status_code == 404:
260
+ raise NotFoundException(f"Resource with url='{url}' not found (HTTP 404)!")
261
+ else:
262
+ if dict_data:
263
+ data_output = f"data='{to_str(dict_data)}' "
264
+ elif json_data:
265
+ data_output = f"json='{to_str(json_data)}' "
266
+ else:
267
+ data_output = ""
268
+
269
+ raise UnexpectedResponseException(
270
+ f"Request url='{url}', method={method}, {data_output}failed -"
271
+ f" expected code(s) {str(expected_codes)},"
272
+ f" got code {response.status_code} & body: '{response.text}'",
273
+ response.status_code,
274
+ response.text,
275
+ )
276
+ if response.json():
277
+ debug(f"<--- json = {to_str(response.json())}")
278
+ else:
279
+ debug(f"<--- json = (empty))")
280
+ return response
281
+
282
+ @staticmethod
283
+ def _format_with_url_encoding(format_string, single_arg_or_args_tuple):
284
+ # we want to URL-encode all the args, but not the path itself which looks like "/foo/%s/bar"
285
+ # because '/'s here are NOT to be URL-encoded
286
+
287
+ if not single_arg_or_args_tuple:
288
+ # there are no params, so the format_string is the URL
289
+ return format_string
290
+ else:
291
+ if type(single_arg_or_args_tuple) == tuple:
292
+ # URL-encode each arg in the tuple and return it as tuple too
293
+ url_encoded_args = ()
294
+ for arg in single_arg_or_args_tuple:
295
+ url_encoded_args += (parse.quote_plus(str(arg)),)
296
+ else:
297
+ # URL-encode single arg
298
+ url_encoded_args = parse.quote_plus(str(single_arg_or_args_tuple))
299
+
300
+ return format_string % url_encoded_args
301
+
302
+ @staticmethod
303
+ def _listify(expected_codes):
304
+ if isinstance(expected_codes, int):
305
+ return [expected_codes]
306
+ else:
307
+ return expected_codes
308
+
309
+
310
+ class TestRequestFailedException(Exception):
311
+ def __init__(self, underlying: Exception):
312
+ self.underlying = underlying
313
+
314
+
315
+ class NotFoundException(Exception):
316
+ pass
317
+
318
+
319
+ class TimeoutWaitingForDeletion(Exception):
320
+ pass
321
+
322
+
323
+ class InvalidParametersException(Exception):
324
+ pass
325
+
326
+
327
+ class UnexpectedResponseException(Exception):
328
+ def __init__(self, message: str, response_status_code: int, response_text: str):
329
+ self.message: str = message
330
+ self.response_status_code: int = response_status_code
331
+ self.response_text: str = response_text
332
+
333
+ def __str__(self):
334
+ return self.message
@@ -0,0 +1,50 @@
1
+ from gitlabform.gitlab.groups import GitLabGroups
2
+
3
+
4
+ class GitLabGroupBadges(GitLabGroups):
5
+ def get_group_badges(self, group_path):
6
+ # (unlike the one for project badges) this endpoint returns ONLY group badges
7
+ return self._make_requests_to_api(
8
+ "groups/%s/badges",
9
+ group_path,
10
+ expected_codes=200,
11
+ )
12
+
13
+ def add_group_badge(
14
+ self,
15
+ group_path,
16
+ badge_in_config,
17
+ ):
18
+ return self._make_requests_to_api(
19
+ "groups/%s/badges",
20
+ group_path,
21
+ method="POST",
22
+ data=badge_in_config,
23
+ expected_codes=201,
24
+ )
25
+
26
+ def edit_group_badge(
27
+ self,
28
+ group_path,
29
+ badge_in_gitlab,
30
+ badge_in_config,
31
+ ):
32
+ return self._make_requests_to_api(
33
+ "groups/%s/badges/%s",
34
+ (group_path, badge_in_gitlab["id"]),
35
+ method="PUT",
36
+ data=badge_in_config,
37
+ )
38
+
39
+ def delete_group_badge(
40
+ self,
41
+ group_path,
42
+ badge_in_gitlab,
43
+ ):
44
+ # 404 means it is already removed, so let's accept it for idempotency
45
+ return self._make_requests_to_api(
46
+ "groups/%s/badges/%s",
47
+ (group_path, badge_in_gitlab["id"]),
48
+ method="DELETE",
49
+ expected_codes=[200, 204, 404],
50
+ )
@@ -0,0 +1,40 @@
1
+ from gitlabform.gitlab.core import NotFoundException, InvalidParametersException
2
+ from gitlabform.gitlab.groups import GitLabGroups
3
+
4
+
5
+ class GitLabGroupLDAPLinks(GitLabGroups):
6
+ def get_ldap_group_links(self, group):
7
+ group_id = self.get_group_id_case_insensitive(group)
8
+ return self._make_requests_to_api("groups/%s/ldap_group_links", group_id, expected_codes=[200, 404])
9
+
10
+ def add_ldap_group_link(self, group, data):
11
+ group_id = self.get_group_id_case_insensitive(group)
12
+ data["id"] = group_id
13
+
14
+ try:
15
+ return self._make_requests_to_api(
16
+ "groups/%s/ldap_group_links",
17
+ group_id,
18
+ method="POST",
19
+ data=data,
20
+ expected_codes=[200, 201],
21
+ )
22
+ # this is a GitLab API bug - it returns 404 here instead of 400 for bad requests...
23
+ except NotFoundException:
24
+ raise InvalidParametersException(f"Invalid parameters for a Group LDAP link for group {group}: {data}")
25
+
26
+ def delete_ldap_group_link(self, group, data):
27
+ if "group_access" in data:
28
+ del data["group_access"]
29
+
30
+ group_id = self.get_group_id_case_insensitive(group)
31
+ data["id"] = group_id
32
+
33
+ # 404 means that the LDAP group link is already removed, so let's accept it for idempotency
34
+ self._make_requests_to_api(
35
+ "groups/%s/ldap_group_links",
36
+ group_id,
37
+ method="DELETE",
38
+ data=data,
39
+ expected_codes=[204, 404],
40
+ )
@@ -0,0 +1,96 @@
1
+ import functools
2
+
3
+ from gitlabform.gitlab.core import GitLabCore, NotFoundException
4
+
5
+
6
+ class GitLabGroups(GitLabCore):
7
+ @functools.lru_cache()
8
+ def get_group_id_case_insensitive(self, some_string):
9
+ # Cache the mapping from some_string -> id, as that won't change during our run.
10
+ return self.get_group_case_insensitive(some_string)["id"]
11
+
12
+ def get_group_case_insensitive(self, some_string):
13
+ # maybe "foo/bar" is some group's path
14
+
15
+ try:
16
+ # try with exact case
17
+ return self.get_group(some_string)
18
+ except NotFoundException:
19
+ # try case insensitive
20
+ groups = self._make_requests_to_api(
21
+ "groups?search=%s",
22
+ some_string.lower(),
23
+ method="GET",
24
+ )
25
+
26
+ for group in groups:
27
+ if group["full_path"].lower() == some_string.lower():
28
+ return group
29
+ raise NotFoundException(f"Group/subgroup with path '{some_string}' not found.")
30
+
31
+ def get_group(self, name):
32
+ return self._make_requests_to_api("groups/%s", name)
33
+
34
+ def get_group_descendants(self, group_id_or_path):
35
+ return self._make_requests_to_api("groups/%s/descendant_groups", group_id_or_path)
36
+
37
+ def get_groups(self):
38
+ """
39
+ :return: sorted list of groups
40
+ """
41
+
42
+ if self.admin:
43
+ query = "all_available=true"
44
+ else:
45
+ # as a non-admin it's pointless to get groups with a lower role than Reporter
46
+ # - it's the minimal role that is needed to manage something (f.e. group labels)
47
+ query = f"min_access_level=20"
48
+
49
+ result = self._make_requests_to_api(f"groups?{query}")
50
+
51
+ return sorted(map(lambda x: x["full_path"], result))
52
+
53
+ def get_projects(self, group, include_archived=False, only_names=True):
54
+ """
55
+ :param group: group name
56
+ :param include_archived: set to True if archived projects should also be returned
57
+ :param only_names: set to False to get the whole project objects
58
+ :return: sorted list of strings "group/project_name". Note that only projects from "group" namespace are
59
+ returned, so if "group" (= members of this group) is also a member of some projects, they won't be
60
+ returned here.
61
+ """
62
+ try:
63
+ # there are 3 states of the "archived" flag: true, false, undefined
64
+ # we use the last 2
65
+ if include_archived:
66
+ query1 = "include_subgroups=true"
67
+ else:
68
+ query1 = "include_subgroups=true&archived=false"
69
+
70
+ if self.admin:
71
+ query2 = "all_available=true"
72
+ else:
73
+ # it's pointless to get projects with a lower role than Reporter
74
+ # - it's the minimal role that is needed to manage something (f.e. labels)
75
+ query2 = f"min_access_level=20"
76
+
77
+ projects = self._make_requests_to_api(f"groups/%s/projects?{query1}&{query2}", group)
78
+ except NotFoundException:
79
+ projects = []
80
+
81
+ project_and_groups_in_group_namespace = [
82
+ project for project in projects if project["path_with_namespace"].startswith(group + "/")
83
+ ]
84
+
85
+ if only_names:
86
+ return sorted(
87
+ map(
88
+ lambda x: x["path_with_namespace"],
89
+ project_and_groups_in_group_namespace,
90
+ )
91
+ )
92
+ else:
93
+ return sorted(
94
+ project_and_groups_in_group_namespace,
95
+ key=lambda x: x["path_with_namespace"],
96
+ )
@@ -0,0 +1,57 @@
1
+ from gitlabform.gitlab.core import GitLabCore
2
+
3
+
4
+ class GitLabMergeRequests(GitLabCore):
5
+ def create_mr(
6
+ self,
7
+ project_and_group_name,
8
+ source_branch,
9
+ target_branch,
10
+ title,
11
+ description=None,
12
+ ):
13
+ data = {
14
+ "id": project_and_group_name,
15
+ "source_branch": source_branch,
16
+ "target_branch": target_branch,
17
+ "title": title,
18
+ "description": description,
19
+ }
20
+ return self._make_requests_to_api(
21
+ "projects/%s/merge_requests",
22
+ project_and_group_name,
23
+ method="POST",
24
+ data=data,
25
+ expected_codes=201,
26
+ )
27
+
28
+ def accept_mr(self, project_and_group_name, mr_iid):
29
+ return self._make_requests_to_api(
30
+ "projects/%s/merge_requests/%s/merge",
31
+ (project_and_group_name, mr_iid),
32
+ method="PUT",
33
+ )
34
+
35
+ def update_mr(self, project_and_group_name, mr_iid, data):
36
+ self._make_requests_to_api(
37
+ "projects/%s/merge_requests/%s",
38
+ (project_and_group_name, mr_iid),
39
+ method="PUT",
40
+ data=data,
41
+ )
42
+
43
+ def get_mrs(self, project_and_group_name):
44
+ """
45
+ :param project_and_group_name: like 'group/project'
46
+ :return: get all *open* MRs in given project
47
+ """
48
+ return self._make_requests_to_api(
49
+ "projects/%s/merge_requests?scope=all&state=opened",
50
+ project_and_group_name,
51
+ )
52
+
53
+ def get_mr(self, project_and_group_name, mr_iid):
54
+ return self._make_requests_to_api("projects/%s/merge_requests/%s", (project_and_group_name, mr_iid))
55
+
56
+ def get_mr_approvals(self, project_and_group_name, mr_iid):
57
+ return self._make_requests_to_api("projects/%s/merge_requests/%s/approvals", (project_and_group_name, mr_iid))
@@ -0,0 +1,23 @@
1
+ from gitlabform.gitlab.core import GitLabCore
2
+
3
+
4
+ class GitLabPipelines(GitLabCore):
5
+ def get_pipelines(self, project_and_group_name, branch):
6
+ pipelines = self._make_requests_to_api(
7
+ "projects/%s/pipelines?ref=%s",
8
+ (project_and_group_name, branch),
9
+ )
10
+ return pipelines
11
+
12
+ def get_pipeline(self, project_and_group_name, pipeline_id):
13
+ pipeline = self._make_requests_to_api("/projects/%s/pipelines/%s", (project_and_group_name, pipeline_id))
14
+ return pipeline
15
+
16
+ def retry_pipeline(self, project_and_group_name, pipeline_id):
17
+ pipeline = self._make_requests_to_api(
18
+ "projects/%s/pipelines/%s/retry",
19
+ (project_and_group_name, pipeline_id),
20
+ method="POST",
21
+ expected_codes=[200, 201],
22
+ )
23
+ return pipeline
@@ -0,0 +1,52 @@
1
+ from gitlabform.gitlab.projects import GitLabProjects
2
+
3
+
4
+ class GitLabProjectBadges(GitLabProjects):
5
+ def get_project_badges(self, project_and_group_name):
6
+ badges = self._make_requests_to_api(
7
+ "projects/%s/badges",
8
+ project_and_group_name,
9
+ expected_codes=200,
10
+ )
11
+ # according to the docs_new this endpoint returns also the group badges
12
+ # but we want only the project badges here
13
+ return [badge for badge in badges if badge["kind"] == "project"]
14
+
15
+ def add_project_badge(
16
+ self,
17
+ project_and_group_name,
18
+ badge_in_config,
19
+ ):
20
+ return self._make_requests_to_api(
21
+ "projects/%s/badges",
22
+ project_and_group_name,
23
+ method="POST",
24
+ data=badge_in_config,
25
+ expected_codes=201,
26
+ )
27
+
28
+ def edit_project_badge(
29
+ self,
30
+ project_and_group_name,
31
+ badge_in_gitlab,
32
+ badge_in_config,
33
+ ):
34
+ return self._make_requests_to_api(
35
+ "projects/%s/badges/%s",
36
+ (project_and_group_name, badge_in_gitlab["id"]),
37
+ method="PUT",
38
+ data=badge_in_config,
39
+ )
40
+
41
+ def delete_project_badge(
42
+ self,
43
+ project_and_group_name,
44
+ badge_in_gitlab,
45
+ ):
46
+ # 404 means it is already removed, so let's accept it for idempotency
47
+ return self._make_requests_to_api(
48
+ "projects/%s/badges/%s",
49
+ (project_and_group_name, badge_in_gitlab["id"]),
50
+ method="DELETE",
51
+ expected_codes=[200, 204, 404],
52
+ )