gitlabform 4.0.5__tar.gz → 4.0.6__tar.gz

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 (137) hide show
  1. {gitlabform-4.0.5 → gitlabform-4.0.6}/PKG-INFO +13 -2
  2. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/gitlab/__init__.py +9 -4
  3. gitlabform-4.0.6/gitlabform/gitlab/python_gitlab.py +254 -0
  4. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/processors/group/group_members_processor.py +9 -8
  5. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/processors/project/branches_processor.py +9 -4
  6. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/processors/project/members_processor.py +15 -17
  7. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/processors/project/tags_processor.py +22 -8
  8. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform.egg-info/PKG-INFO +13 -2
  9. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform.egg-info/requires.txt +1 -0
  10. {gitlabform-4.0.5 → gitlabform-4.0.6}/setup.py +1 -0
  11. gitlabform-4.0.5/gitlabform/gitlab/python_gitlab.py +0 -111
  12. {gitlabform-4.0.5 → gitlabform-4.0.6}/LICENSE +0 -0
  13. {gitlabform-4.0.5 → gitlabform-4.0.6}/README.md +0 -0
  14. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/__init__.py +0 -0
  15. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/configuration/__init__.py +0 -0
  16. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/configuration/common.py +0 -0
  17. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/configuration/core.py +0 -0
  18. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/configuration/groups.py +0 -0
  19. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/configuration/projects.py +0 -0
  20. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/configuration/transform.py +0 -0
  21. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/constants.py +0 -0
  22. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/gitlab/commits.py +0 -0
  23. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/gitlab/core.py +0 -0
  24. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/gitlab/group_badges.py +0 -0
  25. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/gitlab/group_ldap_links.py +0 -0
  26. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/gitlab/group_variables.py +0 -0
  27. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/gitlab/groups.py +0 -0
  28. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/gitlab/merge_requests.py +0 -0
  29. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/gitlab/pipelines.py +0 -0
  30. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/gitlab/project_badges.py +0 -0
  31. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/gitlab/project_deploy_keys.py +0 -0
  32. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/gitlab/project_merge_requests_approvals.py +0 -0
  33. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/gitlab/project_protected_environments.py +0 -0
  34. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/gitlab/projects.py +0 -0
  35. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/gitlab/users.py +0 -0
  36. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/gitlab/variables.py +0 -0
  37. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/lists/__init__.py +0 -0
  38. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/lists/filter.py +0 -0
  39. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/lists/groups.py +0 -0
  40. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/lists/projects.py +0 -0
  41. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/output.py +0 -0
  42. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/processors/__init__.py +0 -0
  43. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/processors/abstract_processor.py +0 -0
  44. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/processors/application/__init__.py +0 -0
  45. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/processors/application/application_settings_processor.py +0 -0
  46. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/processors/defining_keys.py +0 -0
  47. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/processors/group/__init__.py +0 -0
  48. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/processors/group/group_badges_processor.py +0 -0
  49. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/processors/group/group_labels_processor.py +0 -0
  50. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/processors/group/group_ldap_links_processor.py +0 -0
  51. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/processors/group/group_saml_links_processor.py +0 -0
  52. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/processors/group/group_settings_processor.py +0 -0
  53. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/processors/group/group_variables_processor.py +0 -0
  54. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/processors/multiple_entities_processor.py +0 -0
  55. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/processors/project/__init__.py +0 -0
  56. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/processors/project/badges_processor.py +0 -0
  57. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/processors/project/deploy_keys_processor.py +0 -0
  58. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/processors/project/files_processor.py +0 -0
  59. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/processors/project/hooks_processor.py +0 -0
  60. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/processors/project/integrations_processor.py +0 -0
  61. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/processors/project/job_token_scope_processor.py +0 -0
  62. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/processors/project/merge_requests_approval_rules.py +0 -0
  63. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/processors/project/merge_requests_approvals.py +0 -0
  64. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/processors/project/project_labels_processor.py +0 -0
  65. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/processors/project/project_processor.py +0 -0
  66. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/processors/project/project_push_rules_processor.py +0 -0
  67. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/processors/project/project_settings_processor.py +0 -0
  68. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/processors/project/resource_groups_processor.py +0 -0
  69. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/processors/project/schedules_processor.py +0 -0
  70. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/processors/project/variables_processor.py +0 -0
  71. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/processors/shared/__init__.py +0 -0
  72. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/processors/shared/protected_environments_processor.py +0 -0
  73. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/processors/single_entity_processor.py +0 -0
  74. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/processors/util/__init__.py +0 -0
  75. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/processors/util/decorators.py +0 -0
  76. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/processors/util/difference_logger.py +0 -0
  77. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/processors/util/labels_processor.py +0 -0
  78. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/run.py +0 -0
  79. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform/util.py +0 -0
  80. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform.egg-info/SOURCES.txt +0 -0
  81. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform.egg-info/dependency_links.txt +0 -0
  82. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform.egg-info/entry_points.txt +0 -0
  83. {gitlabform-4.0.5 → gitlabform-4.0.6}/gitlabform.egg-info/top_level.txt +0 -0
  84. {gitlabform-4.0.5 → gitlabform-4.0.6}/setup.cfg +0 -0
  85. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/__init__.py +0 -0
  86. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/acceptance/__init__.py +0 -0
  87. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/acceptance/conftest.py +0 -0
  88. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/acceptance/standard/__init__.py +0 -0
  89. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/acceptance/standard/test_application_settings.py +0 -0
  90. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/acceptance/standard/test_archive_project.py +0 -0
  91. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/acceptance/standard/test_badges.py +0 -0
  92. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/acceptance/standard/test_branches.py +0 -0
  93. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/acceptance/standard/test_deploy_keys.py +0 -0
  94. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/acceptance/standard/test_deploy_keys_all_projects.py +0 -0
  95. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/acceptance/standard/test_files.py +0 -0
  96. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/acceptance/standard/test_files_templates.py +0 -0
  97. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/acceptance/standard/test_group_badges.py +0 -0
  98. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/acceptance/standard/test_group_labels.py +0 -0
  99. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/acceptance/standard/test_group_members_case_insensitive.py +0 -0
  100. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/acceptance/standard/test_group_members_groups.py +0 -0
  101. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/acceptance/standard/test_group_members_users.py +0 -0
  102. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/acceptance/standard/test_group_settings.py +0 -0
  103. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/acceptance/standard/test_group_variables.py +0 -0
  104. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/acceptance/standard/test_hooks.py +0 -0
  105. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/acceptance/standard/test_integrations.py +0 -0
  106. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/acceptance/standard/test_job_token_scope.py +0 -0
  107. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/acceptance/standard/test_members.py +0 -0
  108. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/acceptance/standard/test_members_add_group.py +0 -0
  109. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/acceptance/standard/test_members_enforce.py +0 -0
  110. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/acceptance/standard/test_project_group_members_case_insensitive.py +0 -0
  111. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/acceptance/standard/test_project_labels.py +0 -0
  112. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/acceptance/standard/test_project_members_case_insensitve.py +0 -0
  113. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/acceptance/standard/test_project_settings.py +0 -0
  114. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/acceptance/standard/test_resource_groups.py +0 -0
  115. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/acceptance/standard/test_running.py +0 -0
  116. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/acceptance/standard/test_schedules.py +0 -0
  117. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/acceptance/standard/test_tags.py +0 -0
  118. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/acceptance/standard/test_token_from_config.py +0 -0
  119. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/acceptance/standard/test_transfer_project.py +0 -0
  120. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/acceptance/standard/test_variables.py +0 -0
  121. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/unit/__init__.py +0 -0
  122. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/unit/configuration/__init__.py +0 -0
  123. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/unit/configuration/test_case_sensitivity.py +0 -0
  124. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/unit/configuration/test_inheritance_break_projects_and_groups.py +0 -0
  125. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/unit/configuration/test_inheritance_break_subgroups.py +0 -0
  126. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/unit/configuration/test_inheritance_break_validation.py +0 -0
  127. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/unit/configuration/test_projects_and_groups.py +0 -0
  128. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/unit/configuration/test_skip_groups_skip_projects.py +0 -0
  129. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/unit/configuration/test_subgroups.py +0 -0
  130. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/unit/configuration/test_yaml_version.py +0 -0
  131. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/unit/processors/__init__.py +0 -0
  132. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/unit/processors/test_abstract_processor.py +0 -0
  133. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/unit/processors/test_difference_logger.py +0 -0
  134. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/unit/processors/test_schedules_processor_extended_cron_pattern.py +0 -0
  135. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/unit/test_access_levels.py +0 -0
  136. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/unit/test_non_empty_configs_provider.py +0 -0
  137. {gitlabform-4.0.5 → gitlabform-4.0.6}/tests/unit/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: gitlabform
3
- Version: 4.0.5
3
+ Version: 4.0.6
4
4
  Summary: 🏗 Specialized configuration as a code tool for GitLab projects, groups and more using hierarchical configuration written in YAML
5
5
  Home-page: https://gitlabform.github.io/gitlabform
6
6
  Author: Greg Dubicki and Contributors
@@ -26,6 +26,7 @@ Requires-Dist: MarkupSafe==3.0.2
26
26
  Requires-Dist: mergedeep==1.3.4
27
27
  Requires-Dist: packaging==24.2
28
28
  Requires-Dist: python-gitlab==5.3.0
29
+ Requires-Dist: python-gitlab[graphql]==5.3.0
29
30
  Requires-Dist: requests==2.32.3
30
31
  Requires-Dist: ruamel.yaml==0.17.21
31
32
  Requires-Dist: types-requests==2.32.0.20241016
@@ -44,6 +45,16 @@ Requires-Dist: xkcdpass==1.19.9; extra == "test"
44
45
  Provides-Extra: docs
45
46
  Requires-Dist: mkdocs; extra == "docs"
46
47
  Requires-Dist: mkdocs-material; extra == "docs"
48
+ Dynamic: author
49
+ Dynamic: classifier
50
+ Dynamic: description
51
+ Dynamic: description-content-type
52
+ Dynamic: home-page
53
+ Dynamic: keywords
54
+ Dynamic: provides-extra
55
+ Dynamic: requires-dist
56
+ Dynamic: requires-python
57
+ Dynamic: summary
47
58
 
48
59
  [![version](https://badge.fury.io/gh/gitlabform%2Fgitlabform.svg)](https://badge.fury.io/gh/gitlabform%2Fgitlabform)
49
60
  ![release date](https://img.shields.io/github/release-date/gitlabform/gitlabform)
@@ -2,6 +2,8 @@ import enum
2
2
 
3
3
  from typing import List
4
4
 
5
+ from gitlab import GraphQL
6
+
5
7
  from gitlabform.gitlab.commits import GitLabCommits
6
8
  from gitlabform.gitlab.group_badges import GitLabGroupBadges
7
9
  from gitlabform.gitlab.group_ldap_links import GitLabGroupLDAPLinks
@@ -71,14 +73,17 @@ class GitlabWrapper:
71
73
  timeout = gitlabform.timeout
72
74
  session = gitlabform.session
73
75
 
76
+ graphql = GraphQL(url=url, token=token)
77
+
74
78
  self._gitlab: PythonGitlab = PythonGitlab(
75
- url,
76
- token,
79
+ url=url,
80
+ private_token=token,
77
81
  ssl_verify=ssl_verify,
82
+ timeout=timeout,
78
83
  api_version="4",
79
- session=session,
80
84
  retry_transient_errors=True,
81
- timeout=timeout,
85
+ graphql=graphql,
86
+ session=session,
82
87
  )
83
88
 
84
89
  def get_gitlab(self):
@@ -0,0 +1,254 @@
1
+ import functools
2
+ from typing import Union, Any, Optional, Dict, List
3
+
4
+ import gitlab.const
5
+ from gitlab import Gitlab, GitlabGetError, GraphQL
6
+ from gitlab.base import RESTObject
7
+ from gitlab.v4.objects import Group, Project
8
+
9
+ from cli_ui import debug as verbose
10
+
11
+
12
+ # Extends the python-gitlab class to add convenience wrappers for common functionality used within gitlabform
13
+ class PythonGitlab(Gitlab):
14
+ def __init__(
15
+ self,
16
+ graphql: GraphQL,
17
+ url: Optional[str] = None,
18
+ private_token: Optional[str] = None,
19
+ oauth_token: Optional[str] = None,
20
+ job_token: Optional[str] = None,
21
+ ssl_verify: Union[bool, str] = True,
22
+ http_username: Optional[str] = None,
23
+ http_password: Optional[str] = None,
24
+ timeout: Optional[float] = None,
25
+ api_version: str = "4",
26
+ per_page: Optional[int] = None,
27
+ pagination: Optional[str] = None,
28
+ order_by: Optional[str] = None,
29
+ user_agent: str = gitlab.const.USER_AGENT,
30
+ retry_transient_errors: bool = False,
31
+ keep_base_url: bool = False,
32
+ **kwargs: Any,
33
+ ) -> None:
34
+ super().__init__(
35
+ url,
36
+ private_token,
37
+ oauth_token,
38
+ job_token,
39
+ ssl_verify,
40
+ http_username,
41
+ http_password,
42
+ timeout,
43
+ api_version,
44
+ per_page,
45
+ pagination,
46
+ order_by,
47
+ user_agent,
48
+ retry_transient_errors,
49
+ keep_base_url,
50
+ **kwargs,
51
+ )
52
+ # Python Gitlab GraphQL interface
53
+ # https://python-gitlab.readthedocs.io/en/stable/api-usage-graphql.html
54
+ self.graphql = graphql
55
+
56
+ @functools.lru_cache()
57
+ def get_user_id_cached(self, username) -> int:
58
+ user = self._get_user_by_username_cached(username)
59
+ return user.id
60
+
61
+ def get_group_id(self, groupname) -> int:
62
+ group = self.get_group_by_path_cached(groupname)
63
+ return group.id
64
+
65
+ def get_project_id(self, name) -> int:
66
+ project = self.get_project_by_path_cached(name)
67
+ return project.id
68
+
69
+ @functools.lru_cache()
70
+ def get_project_by_path_cached(self, name: str, lazy: bool = False) -> Project:
71
+ project: Project = self.projects.get(name, lazy)
72
+ if project:
73
+ return project
74
+
75
+ raise GitlabGetError("No project found when getting '%s'" % name, 404)
76
+
77
+ @functools.lru_cache()
78
+ def get_group_by_path_cached(self, groupname: str) -> Group:
79
+ group: Group = self.groups.get(groupname)
80
+ if group:
81
+ return group
82
+
83
+ raise GitlabGetError("No group found when getting '%s'" % groupname, 404)
84
+
85
+ # Uses "LIST" to get a user by username, to get the full User object, call get using the user's id
86
+ @functools.lru_cache()
87
+ def _get_user_by_username_cached(self, username: str) -> RESTObject:
88
+ # Gitlab API will only ever return 0 or 1 entry when GETting using `username` attribute
89
+ # https://docs.gitlab.com/ee/api/users.html#for-non-administrator-users
90
+ # so will always be list[RESTObject] and never RESTObjectList from python-gitlab's api
91
+ users: list[RESTObject] = self.users.list(username=username) # type: ignore
92
+
93
+ if len(users) == 0:
94
+ raise GitlabGetError(
95
+ "No users found when searching for username '%s'" % username, 404
96
+ )
97
+
98
+ return users[0]
99
+
100
+ @functools.lru_cache()
101
+ def _get_member_roles_from_group_cached(
102
+ self, group_full_path: str
103
+ ) -> List[Dict[str, str]]:
104
+ """Query GraphQL using Python Gitlab
105
+ https://python-gitlab.readthedocs.io/en/stable/api-usage-graphql.html
106
+
107
+ GET Member_Role via REST Api needs "Owner" on Gitlab.com or "Admin" on Dedicated
108
+ List of custom roles can be retrieved via GraphQL endpoint with "Guest+"
109
+ https://gitlab.com/gitlab-org/gitlab/-/issues/511919#note_2287581884
110
+
111
+ https://docs.gitlab.com/ee/api/graphql/reference/index.html#groupmemberroles
112
+ """
113
+
114
+ if group_full_path is None:
115
+ raise GitlabGetError(
116
+ "Group Path must be provided when getting member roles",
117
+ 404,
118
+ )
119
+
120
+ query = (
121
+ """
122
+ {
123
+ group(fullPath: \""""
124
+ + group_full_path
125
+ + """\") {
126
+ memberRoles {
127
+ nodes {
128
+ id
129
+ name
130
+ }
131
+ }
132
+ }
133
+ }
134
+ """
135
+ )
136
+ verbose(
137
+ f"Executing graphQl query to get Member Roles for Group '{group_full_path}'"
138
+ )
139
+ result = self.graphql.execute(query)
140
+
141
+ # Validate Group / MemberRoles exist
142
+ if (
143
+ result["group"] is not None
144
+ and result["group"]["memberRoles"] is not None
145
+ and result["group"]["memberRoles"]["nodes"] is not None
146
+ ):
147
+ member_role_nodes = result["group"]["memberRoles"]["nodes"]
148
+ else:
149
+ raise GitlabGetError(f"Failed to get Member Roles from Group: {query}")
150
+
151
+ return self._convert_result_to_member_roles(member_role_nodes)
152
+
153
+ @functools.lru_cache()
154
+ def _get_member_roles_from_instance_cached(self) -> List[Dict[str, str]]:
155
+ """Query GraphQL using Python Gitlab
156
+ https://python-gitlab.readthedocs.io/en/stable/api-usage-graphql.html
157
+
158
+ GET Member_Role via REST Api needs "Admin" on Dedicated/Self-Hosted
159
+
160
+ On Self-Hosted/Dedicated it is only possible to create Member Roles at an instance level, so we query at that
161
+ level and cache the result in order to save API calls when processing multiple Groups
162
+
163
+ List of custom roles can be retrieved via GraphQL endpoint with "Guest+"
164
+ https://gitlab.com/gitlab-org/gitlab/-/issues/511919#note_2287581884
165
+
166
+ https://docs.gitlab.com/ee/api/graphql/reference/index.html#groupmemberroles
167
+ """
168
+
169
+ query = """
170
+ {
171
+ memberRoles {
172
+ edges {
173
+ node {
174
+ id
175
+ name
176
+ }
177
+ }
178
+ }
179
+ }
180
+ """
181
+ result = self.graphql.execute(query)
182
+
183
+ if (
184
+ result["memberRoles"] is not None
185
+ and result["memberRoles"]["edges"] is not None
186
+ ):
187
+ member_role_edges = result["memberRoles"]["edges"]
188
+ else:
189
+ raise GitlabGetError(f"Failed to get Member Roles from instance: {query}")
190
+
191
+ member_role_nodes = []
192
+
193
+ for edge in member_role_edges:
194
+ member_role_nodes.append(edge["node"])
195
+
196
+ return self._convert_result_to_member_roles(member_role_nodes)
197
+
198
+ @staticmethod
199
+ def _convert_result_to_member_roles(
200
+ member_role_nodes: List[Dict[str, str]]
201
+ ) -> List[Dict[str, str]]:
202
+ # Unwrap Ids from GraphQl Gids
203
+ member_role_id_prefix = "gid://gitlab/MemberRole/"
204
+
205
+ member_roles = []
206
+ for node in member_role_nodes:
207
+ member_role_id = node["id"]
208
+ member_role_id = member_role_id.replace(member_role_id_prefix, "")
209
+ member_roles.append({"id": member_role_id, "name": node["name"]})
210
+
211
+ return member_roles
212
+
213
+ @staticmethod
214
+ def _get_member_role_from_member_roles(
215
+ name_or_id: Union[int, str], member_roles: List[Dict[str, str]]
216
+ ) -> Dict[str, str]:
217
+ for member_role in member_roles:
218
+ member_role_id: str = member_role["id"]
219
+ member_role_name: str = member_role["name"]
220
+ if int(member_role_id) == name_or_id:
221
+ return member_role
222
+ elif member_role_name.lower() == str(name_or_id).lower():
223
+ return member_role
224
+
225
+ # Failed to find member role so throw an exception explaining such to user
226
+ raise GitlabGetError(
227
+ f"Member Role with name or id {name_or_id} could not be found",
228
+ 404,
229
+ )
230
+
231
+ def get_member_role_id_cached(
232
+ self, name_or_id: Union[int, str], group_full_path: str
233
+ ) -> int:
234
+ """
235
+ GETs a member role id set in the config by the Name of the Member Role (by calling out to API) or by Id
236
+
237
+ Get member_roles calls themselves are Cached, the logic in this method otherwise performs no API calls,
238
+ so we should not cache the result of this specific method, otherwise we may fill the cache with duplicate data
239
+ """
240
+ if type(name_or_id) is int:
241
+ # Already supplied as an id so no need to go get it from API
242
+ return name_or_id
243
+
244
+ if self._is_gitlab_saas():
245
+ member_roles = self._get_member_roles_from_group_cached(group_full_path)
246
+ else:
247
+ member_roles = self._get_member_roles_from_instance_cached()
248
+
249
+ member_role = self._get_member_role_from_member_roles(name_or_id, member_roles)
250
+
251
+ return int(member_role["id"])
252
+
253
+ def _is_gitlab_saas(self) -> bool:
254
+ return self.url == gitlab.const.DEFAULT_URL
@@ -208,13 +208,13 @@ class GroupMembersProcessor(AbstractProcessor):
208
208
  )
209
209
  if member_role_id_or_name:
210
210
  member_role_id_to_set = self.gl.get_member_role_id_cached(
211
- member_role_id_or_name, group.get_id()
211
+ member_role_id_or_name, group.full_path
212
212
  )
213
213
  else:
214
214
  member_role_id_to_set = None
215
215
 
216
216
  common_username = user.lower()
217
- user_id = self.gl.get_user_id(user)
217
+ user_id = self.gl.get_user_id_cached(user)
218
218
 
219
219
  if common_username in users_before:
220
220
  group_member: GroupMember = group.members.get(user_id)
@@ -275,12 +275,7 @@ class GroupMembersProcessor(AbstractProcessor):
275
275
  continue
276
276
  debug("Removing user '%s' who is not configured to be a member.", user)
277
277
  try:
278
- user_id = self.gl.get_user_id(user)
279
- try:
280
- group.members.delete(user_id)
281
- except GitlabDeleteError as delete_error:
282
- warning(f"Member '{user}' could not be deleted: {delete_error}")
283
- pass
278
+ user_id = self.gl.get_user_id_cached(user)
284
279
  except gitlab.GitlabGetError:
285
280
  # User does not exist an instance level but is for whatever reason present on a Group/Project
286
281
  # We should raise error into Logs but not prevent the rest of GitLabForm from executing
@@ -289,6 +284,12 @@ class GroupMembersProcessor(AbstractProcessor):
289
284
  error(
290
285
  f"Could not find User '{user}' on the Instance so can not remove User"
291
286
  )
287
+ continue
288
+
289
+ try:
290
+ group.members.delete(user_id)
291
+ except GitlabDeleteError as delete_error:
292
+ warning(f"Member '{user}' could not be deleted: {delete_error}")
292
293
  pass
293
294
 
294
295
  else:
@@ -1,12 +1,14 @@
1
+ from logging import debug
1
2
  from typing import Optional
2
- from logging import debug, info
3
- from cli_ui import warning, fatal
4
- from gitlab.v4.objects import Project, ProjectProtectedBranch
3
+ from cli_ui import warning, fatal, debug as verbose
5
4
  from gitlab import (
6
5
  GitlabGetError,
7
6
  GitlabDeleteError,
8
7
  GitlabCreateError,
9
8
  )
9
+ from gitlab.v4.objects import Project, ProjectProtectedBranch
10
+
11
+ from gitlabform import gitlab
10
12
  from gitlabform.constants import EXIT_INVALID_INPUT, EXIT_PROCESSING_ERROR
11
13
  from gitlabform.gitlab import GitLab
12
14
  from gitlabform.processors.abstract_processor import AbstractProcessor
@@ -123,13 +125,16 @@ class BranchesProcessor(AbstractProcessor):
123
125
  or group name but GitLab only supports their id. This method will transform the
124
126
  config by replacing them with ids.
125
127
  """
128
+ verbose("Transforming User and Group names in Branch configuration to Ids")
126
129
 
127
130
  for key in branch_config:
128
131
  if isinstance(branch_config[key], list):
129
132
  for item in branch_config[key]:
130
133
  if isinstance(item, dict):
131
134
  if "user" in item:
132
- item["user_id"] = self.gl.get_user_id(item.pop("user"))
135
+ item["user_id"] = self.gl.get_user_id_cached(
136
+ item.pop("user")
137
+ )
133
138
  elif "group" in item:
134
139
  item["group_id"] = self.gl.get_group_id(item.pop("group"))
135
140
 
@@ -1,7 +1,6 @@
1
- import gitlab
2
1
  from cli_ui import debug as verbose, warning, info, error
3
2
  from cli_ui import fatal
4
- from gitlab import GitlabGetError
3
+ from gitlab import GitlabGetError, GitlabDeleteError
5
4
  from gitlab.v4.objects import Project
6
5
 
7
6
  from gitlabform.constants import EXIT_INVALID_INPUT
@@ -114,7 +113,7 @@ class MembersProcessor(AbstractProcessor):
114
113
  for user in users:
115
114
  info(f"Processing user '{user}'...")
116
115
  try:
117
- user_id = self.gl.get_user_by_username_cached(user).get_id()
116
+ user_id = self.gl.get_user_id_cached(user)
118
117
  except GitlabGetError:
119
118
  warning(f"Could not find user '{user}' in Gitlab, skipping...")
120
119
  continue
@@ -133,14 +132,9 @@ class MembersProcessor(AbstractProcessor):
133
132
  users[user]["member_role"] if "member_role" in users[user] else None
134
133
  )
135
134
  if member_role_id_or_name:
136
- # For self-managed member_roles are at an instance level, for SaaS on a Group level
137
- if self.gl.is_gitlab_saas():
138
- group_id = project.namespace["id"]
139
- else:
140
- group_id = None
141
-
135
+ group_name_and_path = project.namespace["full_path"]
142
136
  member_role_id = self.gl.get_member_role_id_cached(
143
- member_role_id_or_name, group_id
137
+ member_role_id_or_name, group_name_and_path
144
138
  )
145
139
  else:
146
140
  member_role_id = None
@@ -224,20 +218,24 @@ class MembersProcessor(AbstractProcessor):
224
218
  f"Removing user '{user_not_in_config}' that is not configured to be a member."
225
219
  )
226
220
  try:
227
- cached_user_id = self.gl.get_user_by_username_cached(
228
- user_not_in_config
229
- ).get_id()
230
- project.members.delete(id=cached_user_id)
231
- except gitlab.GitlabGetError:
221
+ user_id = self.gl.get_user_id_cached(user_not_in_config)
222
+ except GitlabGetError:
232
223
  # User does not exist an instance level but is for whatever reason present on a Group/Project
233
224
  # We should raise error into Logs but not prevent the rest of GitLabForm from executing
234
225
  # This error is more likely to be prevalent in Dedicated instances; it is unlikely for a User to
235
226
  # be completely deleted from gitlab.com
236
- error(
227
+ warning(
237
228
  f"Could not find User '{user_not_in_config}' on the Instance so can not remove User from Project '{project_and_group}'"
238
229
  )
239
- pass
230
+ continue
240
231
 
232
+ try:
233
+ project.members.delete(id=user_id)
234
+ except GitlabDeleteError as delete_error:
235
+ error(
236
+ f"Member '{user_not_in_config}' could not be deleted: {delete_error}"
237
+ )
238
+ raise delete_error
241
239
  else:
242
240
  verbose("Not enforcing user members.")
243
241
 
@@ -1,5 +1,5 @@
1
1
  from logging import debug
2
- from cli_ui import warning, fatal
2
+ from cli_ui import warning, fatal, error
3
3
 
4
4
  from gitlabform.constants import EXIT_PROCESSING_ERROR
5
5
  from gitlabform.gitlab import GitLab
@@ -35,16 +35,30 @@ class TagsProcessor(AbstractProcessor):
35
35
  elif "user_id" in config:
36
36
  user_ids.add(config["user_id"])
37
37
  elif "user" in config:
38
- gitlab_user = self.gl.get_user_by_username_cached(
39
- config["user"]
40
- )
41
- user_ids.add(gitlab_user.get_id())
38
+ try:
39
+ user_id = self.gl.get_user_id_cached(config["user"])
40
+ except GitlabGetError as e:
41
+ error(
42
+ f"Could not find User '{config["user"]}' on the Instance, cannot Protect "
43
+ f"Tag with them"
44
+ )
45
+ raise e
46
+
47
+ user_ids.add(user_id)
42
48
  elif "group_id" in config:
43
49
  group_ids.add(config["group_id"])
44
50
  elif "group" in config:
45
- gitlab_group = self.gl.get_group_by_path_cached(
46
- config["group"]
47
- )
51
+ try:
52
+ gitlab_group = self.gl.get_group_by_path_cached(
53
+ config["group"]
54
+ )
55
+ except GitlabGetError as e:
56
+ error(
57
+ f"Could not find Group '{config["group"]}' on the Instance, cannot Protect "
58
+ f"Tag with them"
59
+ )
60
+ raise e
61
+
48
62
  group_ids.add(gitlab_group.get_id())
49
63
 
50
64
  for val in access_levels:
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: gitlabform
3
- Version: 4.0.5
3
+ Version: 4.0.6
4
4
  Summary: 🏗 Specialized configuration as a code tool for GitLab projects, groups and more using hierarchical configuration written in YAML
5
5
  Home-page: https://gitlabform.github.io/gitlabform
6
6
  Author: Greg Dubicki and Contributors
@@ -26,6 +26,7 @@ Requires-Dist: MarkupSafe==3.0.2
26
26
  Requires-Dist: mergedeep==1.3.4
27
27
  Requires-Dist: packaging==24.2
28
28
  Requires-Dist: python-gitlab==5.3.0
29
+ Requires-Dist: python-gitlab[graphql]==5.3.0
29
30
  Requires-Dist: requests==2.32.3
30
31
  Requires-Dist: ruamel.yaml==0.17.21
31
32
  Requires-Dist: types-requests==2.32.0.20241016
@@ -44,6 +45,16 @@ Requires-Dist: xkcdpass==1.19.9; extra == "test"
44
45
  Provides-Extra: docs
45
46
  Requires-Dist: mkdocs; extra == "docs"
46
47
  Requires-Dist: mkdocs-material; extra == "docs"
48
+ Dynamic: author
49
+ Dynamic: classifier
50
+ Dynamic: description
51
+ Dynamic: description-content-type
52
+ Dynamic: home-page
53
+ Dynamic: keywords
54
+ Dynamic: provides-extra
55
+ Dynamic: requires-dist
56
+ Dynamic: requires-python
57
+ Dynamic: summary
47
58
 
48
59
  [![version](https://badge.fury.io/gh/gitlabform%2Fgitlabform.svg)](https://badge.fury.io/gh/gitlabform%2Fgitlabform)
49
60
  ![release date](https://img.shields.io/github/release-date/gitlabform/gitlabform)
@@ -7,6 +7,7 @@ MarkupSafe==3.0.2
7
7
  mergedeep==1.3.4
8
8
  packaging==24.2
9
9
  python-gitlab==5.3.0
10
+ python-gitlab[graphql]==5.3.0
10
11
  requests==2.32.3
11
12
  ruamel.yaml==0.17.21
12
13
  types-requests==2.32.0.20241016
@@ -49,6 +49,7 @@ setup(
49
49
  "mergedeep==1.3.4",
50
50
  "packaging==24.2",
51
51
  "python-gitlab==5.3.0",
52
+ "python-gitlab[graphql]==5.3.0",
52
53
  "requests==2.32.3",
53
54
  "ruamel.yaml==0.17.21",
54
55
  "types-requests==2.32.0.20241016",
@@ -1,111 +0,0 @@
1
- import functools
2
- from typing import Union
3
-
4
- from cli_ui import debug
5
-
6
- import gitlab.const
7
- from gitlab import Gitlab, GitlabGetError
8
- from gitlab.base import RESTObject
9
- from gitlab.v4.objects import Group, Project
10
-
11
-
12
- # Extends the python-gitlab class to add convenience wrappers for common functionality used within gitlabform
13
- class PythonGitlab(Gitlab):
14
- def get_user_id(self, username) -> int:
15
- user = self.get_user_by_username_cached(username)
16
- return user.id
17
-
18
- def get_group_id(self, groupname) -> int:
19
- group = self.get_group_by_path_cached(groupname)
20
- return group.id
21
-
22
- def get_project_id(self, name) -> int:
23
- project = self.get_project_by_path_cached(name)
24
- return project.id
25
-
26
- @functools.lru_cache()
27
- def get_project_by_path_cached(self, name: str, lazy: bool = False) -> Project:
28
- project: Project = self.projects.get(name, lazy)
29
- if project:
30
- return project
31
-
32
- raise GitlabGetError("No project found when getting '%s'" % name, 404)
33
-
34
- @functools.lru_cache()
35
- def get_group_by_path_cached(self, groupname: str) -> Group:
36
- group: Group = self.groups.get(groupname)
37
- if group:
38
- return group
39
-
40
- raise GitlabGetError("No group found when getting '%s'" % groupname, 404)
41
-
42
- # Uses "LIST" to get a user by username, to get the full User object, call get using the user's id
43
- @functools.lru_cache()
44
- def get_user_by_username_cached(self, username: str) -> RESTObject:
45
- # Gitlab API will only ever return 0 or 1 entry when GETting using `username` attribute
46
- # https://docs.gitlab.com/ee/api/users.html#for-non-administrator-users
47
- # so will always be list[RESTObject] and never RESTObjectList from python-gitlab's api
48
- users: list[RESTObject] = self.users.list(username=username) # type: ignore
49
-
50
- if len(users) == 0:
51
- raise GitlabGetError(
52
- "No users found when searching for username '%s'" % username, 404
53
- )
54
-
55
- return users[0]
56
-
57
- @functools.lru_cache()
58
- def get_member_roles_cached(self, group_id: Union[int, None]):
59
- """Python-Gitlab does not natively support Member Roles yet
60
- use https://python-gitlab.readthedocs.io/en/stable/api-levels.html#lower-level-api-http-methods
61
- to directly invoke member_roles GET endpoint(s) in GitLab
62
- https://docs.gitlab.com/ee/api/member_roles.html
63
- """
64
- if self.is_gitlab_saas():
65
- if group_id is None:
66
- raise GitlabGetError(
67
- "Group Id must be provided when getting member roles on GitLab SaaS",
68
- 404,
69
- )
70
- # SAAS
71
- path = f"/groups/{group_id}/member_roles"
72
- else:
73
- # Self-Managed & Dedicated
74
- path = f"/member_roles"
75
-
76
- debug(f"Retrieving member roles from: {path}")
77
- return self.http_get(path)
78
-
79
- @functools.lru_cache()
80
- def get_member_role_cached(
81
- self, name_or_id: Union[int, str], group_id: Union[int, None]
82
- ):
83
- member_roles = self.get_member_roles_cached(group_id)
84
- for member_role in member_roles:
85
- if member_role["id"] == name_or_id:
86
- return member_role
87
- elif (
88
- type(name_or_id) == str
89
- and member_role["name"].lower() == name_or_id.lower()
90
- ):
91
- return member_role
92
-
93
- # Failed to find member role so throw an exception explaining such to user
94
- raise GitlabGetError(
95
- f"Member Role with name or id {name_or_id} could not be found",
96
- 404,
97
- )
98
-
99
- @functools.lru_cache()
100
- def get_member_role_id_cached(
101
- self, name_or_id: Union[int, str], group_id: Union[int, None]
102
- ) -> int:
103
- if type(name_or_id) is int:
104
- # Already supplied as an id so no need to go get it from API
105
- return name_or_id
106
- member_role = self.get_member_role_cached(name_or_id, group_id)
107
- return member_role["id"]
108
-
109
- @functools.lru_cache()
110
- def is_gitlab_saas(self) -> bool:
111
- return self.url == gitlab.const.DEFAULT_URL
File without changes
File without changes
File without changes
File without changes
File without changes