Coverage for src/types/gitlab.py: 65.79%
152 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 21:20 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 21:20 +0000
1#!/usr/bin/env python3
3# Standard libraries
4from typing import Dict, List, NamedTuple
6# Modules libraries
7from gitlab import Gitlab
8from gitlab.exceptions import GitlabGetError, GitlabHttpError, GitlabListError
9from gitlab.v4.objects import Project
11# AccessLevels class, pylint: disable=too-few-public-methods
12class AccessLevels:
13 DISABLED: str = 'disabled'
14 PRIVATE: str = 'private'
15 ENABLED: str = 'enabled'
17# MergeRequestsMethod class
18class MergeRequestsMethod:
19 ATTRIBUTE: str = 'merge_method'
20 DEFAULT: str = 'Fast-forward'
21 VALUES: Dict[str, str] = {
22 'Merge': 'merge',
23 'Semi-linear': 'rebase_merge',
24 'Fast-forward': 'ff',
25 }
27# MergeRequestsPipelines class
28class MergeRequestsPipelines:
29 ATTRIBUTE: str = 'only_allow_merge_if_pipeline_succeeds'
30 DEFAULT: bool = True
32# MergeRequestsResolved class
33class MergeRequestsResolved:
34 ATTRIBUTE: str = 'only_allow_merge_if_all_discussions_are_resolved'
35 DEFAULT: bool = True
37# MergeRequestsSquash class
38class MergeRequestsSquash:
39 ATTRIBUTE: str = 'squash_option'
40 DEFAULT: str = 'Allow'
41 VALUES: Dict[str, str] = {
42 'Do not allow': 'never',
43 'Allow': 'default_off',
44 'Encourage': 'default_on',
45 'Require': 'default_on',
46 }
48# MergeRequestsSkipped class
49class MergeRequestsSkipped:
50 ATTRIBUTE: str = 'allow_merge_on_skipped_pipeline'
51 DEFAULT: bool = True
53# ProtectionLevels class
54class ProtectionLevels:
55 NO_ONE: str = 'no-one'
56 ADMINS: str = 'admins'
57 MAINTAINERS: str = 'maintainers'
58 DEVELOPERS: str = 'developers'
60 # Default
61 @staticmethod
62 def default() -> str:
63 return ProtectionLevels.NO_ONE
65 # Names
66 @staticmethod
67 def names() -> List[str]:
68 return [
69 ProtectionLevels.NO_ONE,
70 ProtectionLevels.ADMINS,
71 ProtectionLevels.MAINTAINERS,
72 ProtectionLevels.DEVELOPERS,
73 ]
75# RolesCreateProjects class
76class RolesCreateProjects:
77 NO_ONE: str = 'noone'
78 OWNER: str = 'owner'
79 MAINTAINER: str = 'maintainer'
80 DEVELOPER: str = 'developer'
81 ADMINISTRATOR: str = 'administrator'
83 # Default
84 @staticmethod
85 def default() -> str:
86 return RolesCreateProjects.DEVELOPER
88 # Names
89 @staticmethod
90 def names() -> List[str]:
91 return [
92 RolesCreateProjects.NO_ONE,
93 RolesCreateProjects.OWNER,
94 RolesCreateProjects.MAINTAINER,
95 RolesCreateProjects.DEVELOPER,
96 RolesCreateProjects.ADMINISTRATOR,
97 ]
99# RolesCreateSubgroups class
100class RolesCreateSubgroups:
101 OWNER: str = 'owner'
102 MAINTAINER: str = 'maintainer'
104 # Default
105 @staticmethod
106 def default() -> str:
107 return RolesCreateSubgroups.MAINTAINER
109 # Names
110 @staticmethod
111 def names() -> List[str]:
112 return [
113 RolesCreateSubgroups.OWNER,
114 RolesCreateSubgroups.MAINTAINER,
115 ]
117# Visibility class, pylint: disable=too-few-public-methods
118class Visibility:
119 PRIVATE: str = 'private'
120 INTERNAL: str = 'internal'
121 PUBLIC: str = 'public'
123# ProjectFeatureLevel class, pylint: disable=too-few-public-methods
124class ProjectFeatureLevel(NamedTuple):
125 key: str
126 settings: Dict[str, str] = {
127 Visibility.PRIVATE: AccessLevels.PRIVATE,
128 Visibility.INTERNAL: AccessLevels.ENABLED,
129 Visibility.PUBLIC: AccessLevels.ENABLED,
130 }
132# ProjectFeatures class, pylint: disable=too-few-public-methods
133class ProjectFeatures:
135 # Members
136 cache_keys: List[str] = []
137 cache_names: List[str] = []
139 # Feature
140 class Feature(NamedTuple):
142 # Variables
143 name: str
144 access_level: List[ProjectFeatureLevel] = []
145 enabled: List[str] = []
146 tests: List[str] = []
148 # Get
149 @staticmethod
150 def get(key: str) -> Feature:
152 # Get feature object
153 feature = getattr(ProjectFeatures, key)
154 assert isinstance(feature, ProjectFeatures.Feature)
155 return feature
157 # Keys
158 @staticmethod
159 def keys() -> List[str]:
161 # Evaluate keys
162 if ProjectFeatures.cache_keys:
163 return ProjectFeatures.cache_keys
165 # Evaluate keys
166 ProjectFeatures.cache_keys = [
167 key for key in ProjectFeatures.__dict__
168 if isinstance(getattr(ProjectFeatures, key), ProjectFeatures.Feature)
169 ]
170 return ProjectFeatures.cache_keys
172 # Names
173 @staticmethod
174 def names() -> List[str]:
176 # Evaluate names
177 if ProjectFeatures.cache_names:
178 return ProjectFeatures.cache_names
180 # Evaluate names
181 ProjectFeatures.cache_names = [
182 ProjectFeatures.get(key).name for key in ProjectFeatures.__dict__
183 if isinstance(getattr(ProjectFeatures, key), ProjectFeatures.Feature)
184 ]
185 return ProjectFeatures.cache_names
187 # Project test feature, pylint: disable=too-many-branches
188 @staticmethod
189 def test(
190 gitlab: Gitlab,
191 project: Project,
192 tests: List[str],
193 ) -> bool:
195 # Avoid without tests
196 if not tests:
197 return False
199 # Iterate through tests
200 try:
201 for test in tests:
203 # Handle CI/CD test
204 if test == 'CI_CD':
205 test_ci_jobs = project.jobs.list(get_all=False)
206 test_ci_files = any(
207 'name' in item and item['name'] == '.gitlab-ci.yml'
208 for item in project.repository_tree(get_all=True) \
209 )
210 test_ci_token_access: bool = False
211 try:
212 test_ci_token_access = any(
213 'path_with_namespace' in item \
214 and isinstance(item, dict) \
215 and item['path_with_namespace'] != project.path_with_namespace \
216 for item in gitlab.http_get(
217 f'/projects/{project.id}/job_token_scope/allowlist',
218 get_all=True,
219 ) \
220 )
221 except GitlabHttpError:
222 test_ci_token_access = False
223 assert (test_ci_jobs or test_ci_files or test_ci_token_access)
225 # Handle commits test
226 elif test == 'COMMITS':
227 assert project.commits.list(get_all=False)
229 # Handle environments test
230 elif test == 'ENVIRONMENTS':
231 assert project.environments.list(get_all=False)
233 # Handle forks test
234 elif test == 'FORKS':
235 assert project.forks.list(get_all=False)
237 # Handle Git LFS test
238 elif test == 'GIT_LFS':
239 assert project.statistics['lfs_objects_size'] > 0
241 # Handle issues test
242 elif test == 'ISSUES':
243 assert project.issues.list(get_all=False)
245 # Handle merge requests test
246 elif test == 'MERGE_REQUESTS':
247 assert (project.mergerequests.list(get_all=False)
248 or len(project.branches.list(get_all=False)) > 1)
250 # Handle packages test
251 elif test == 'PACKAGES':
252 assert project.packages.list(get_all=False)
254 # Handle pages test
255 elif test == 'PAGES':
256 test_pages_jobs = any(
257 job.name == 'pages' for job in project.jobs.list(get_all=True))
258 assert test_pages_jobs
260 # Handle releases test
261 elif test == 'RELEASES':
262 assert project.releases.list(get_all=False)
264 # Handle repositories test
265 elif test == 'REPOSITORIES':
266 assert project.repositories.list(get_all=False)
268 # Handle snippets test
269 elif test == 'SNIPPETS':
270 assert project.snippets.list(get_all=False)
272 # Handle wiki test
273 elif test == 'WIKI':
274 assert project.wikis.list(get_all=False)
276 # Handle failed test
277 except (AssertionError, GitlabGetError, GitlabListError):
278 return False
280 # Result
281 return True
283 # Issues
284 ISSUES = Feature(
285 name='Issues',
286 access_level=[
287 ProjectFeatureLevel('issues_access_level'),
288 ],
289 enabled=[
290 'issues_enabled',
291 ],
292 tests=[
293 'ISSUES',
294 ],
295 )
297 # Merge requests
298 MERGE_REQUESTS = Feature(
299 name='Merge requests',
300 access_level=[
301 ProjectFeatureLevel('merge_requests_access_level'),
302 ],
303 enabled=[
304 'merge_requests_enabled',
305 ],
306 tests=[
307 'MERGE_REQUESTS',
308 ],
309 )
311 # Forks
312 FORKS = Feature(
313 name='Forks',
314 access_level=[
315 ProjectFeatureLevel('forking_access_level'),
316 ],
317 tests=[
318 'FORKS',
319 ],
320 )
322 # Git LFS
323 GIT_LFS = Feature(
324 name='Git LFS',
325 enabled=[
326 'lfs_enabled',
327 ],
328 tests=[
329 'GIT_LFS',
330 ],
331 )
333 # CI/CD
334 CI_CD = Feature(
335 name='CI/CD',
336 access_level=[
337 ProjectFeatureLevel('builds_access_level'),
338 ],
339 enabled=[
340 'jobs_enabled',
341 ],
342 tests=[
343 'CI_CD',
344 ],
345 )
347 # Repository
348 REPOSITORY = Feature(
349 name='Repository',
350 access_level=[
351 ProjectFeatureLevel(
352 'repository_access_level',
353 {
354 Visibility.PRIVATE: AccessLevels.ENABLED,
355 Visibility.INTERNAL: AccessLevels.ENABLED,
356 Visibility.PUBLIC: AccessLevels.ENABLED,
357 },
358 ),
359 ProjectFeatureLevel('merge_requests_access_level'),
360 ProjectFeatureLevel('forking_access_level'),
361 ProjectFeatureLevel('builds_access_level'),
362 ],
363 enabled=[
364 'lfs_enabled',
365 'jobs_enabled',
366 ],
367 tests=[
368 'COMMITS',
369 ],
370 )
372 # Container registry
373 CONTAINER_REGISTRY = Feature(
374 name='Container registry',
375 enabled=[
376 'container_registry_enabled',
377 ],
378 access_level=[
379 ProjectFeatureLevel('container_registry_access_level'),
380 ],
381 tests=[
382 'REPOSITORIES',
383 ],
384 )
386 # Analytics
387 ANALYTICS = Feature(
388 name='Analytics',
389 access_level=[
390 ProjectFeatureLevel('analytics_access_level'),
391 ],
392 tests=[],
393 )
395 # Security and Compliance
396 SECURITY_AND_COMPLIANCE = Feature(
397 name='Security and Compliance',
398 access_level=[
399 ProjectFeatureLevel(
400 'security_and_compliance_access_level', {
401 Visibility.PRIVATE: AccessLevels.PRIVATE,
402 Visibility.INTERNAL: AccessLevels.PRIVATE,
403 Visibility.PUBLIC: AccessLevels.PRIVATE,
404 }),
405 ],
406 tests=[],
407 )
409 # Wiki
410 WIKI = Feature(
411 name='Wiki',
412 enabled=[
413 'wiki_enabled',
414 ],
415 access_level=[
416 ProjectFeatureLevel('wiki_access_level'),
417 ],
418 tests=[
419 'WIKI',
420 ],
421 )
423 # Snippets
424 SNIPPETS = Feature(
425 name='Snippets',
426 enabled=[
427 'snippets_enabled',
428 ],
429 access_level=[
430 ProjectFeatureLevel('snippets_access_level'),
431 ],
432 tests=[
433 'SNIPPETS',
434 ],
435 )
437 # Package registry
438 PACKAGE_REGISTRY = Feature(
439 name='Package registry',
440 enabled=[
441 'packages_enabled',
442 ],
443 tests=[
444 'PACKAGES',
445 ],
446 )
448 # Model experiments
449 MODEL_EXPERIMENTS = Feature(
450 name='Model experiments',
451 access_level=[
452 ProjectFeatureLevel('model_experiments_access_level'),
453 ],
454 tests=[],
455 )
457 # Model registry
458 MODEL_REGISTRY = Feature(
459 name='Model registry',
460 access_level=[
461 ProjectFeatureLevel('model_registry_access_level'),
462 ],
463 tests=[],
464 )
466 # Pages
467 PAGES = Feature(
468 name='Pages',
469 access_level=[
470 ProjectFeatureLevel(
471 'pages_access_level', {
472 Visibility.PRIVATE: AccessLevels.PRIVATE,
473 Visibility.INTERNAL: AccessLevels.PRIVATE,
474 Visibility.PUBLIC: AccessLevels.ENABLED,
475 })
476 ],
477 tests=[
478 'PAGES',
479 ],
480 )
482 # Monitor
483 MONITOR = Feature(
484 name='Monitor',
485 access_level=[
486 ProjectFeatureLevel('monitor_access_level'),
487 ],
488 tests=[],
489 )
491 # Environments
492 ENVIRONMENTS = Feature(
493 name='Environments',
494 access_level=[
495 ProjectFeatureLevel('environments_access_level'),
496 ],
497 tests=[
498 'ENVIRONMENTS',
499 ],
500 )
502 # Feature flags
503 FEATURE_FLAGS = Feature(
504 name='Feature flags',
505 access_level=[
506 ProjectFeatureLevel('feature_flags_access_level'),
507 ],
508 tests=[],
509 )
511 # Infrastructure
512 INFRASTRUCTURE = Feature(
513 name='Infrastructure',
514 access_level=[
515 ProjectFeatureLevel('infrastructure_access_level'),
516 ],
517 tests=[],
518 )
520 # Releases
521 RELEASES = Feature(
522 name='Releases',
523 access_level=[
524 ProjectFeatureLevel('releases_access_level'),
525 ],
526 tests=[
527 'RELEASES',
528 ],
529 )
531 # Service Desk
532 SERVICE_DESK = Feature(
533 name='Service Desk',
534 enabled=[
535 'service_desk_enabled',
536 ],
537 tests=[],
538 )
540 # Auto DevOps
541 AUTO_DEVOPS = Feature(
542 name='Auto DevOps',
543 enabled=[
544 'auto_devops_enabled',
545 ],
546 tests=[],
547 )