gitlabform 4.0.4__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.4 → gitlabform-4.0.6}/PKG-INFO +22 -11
  2. {gitlabform-4.0.4 → 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.4 → gitlabform-4.0.6}/gitlabform/processors/group/group_members_processor.py +20 -7
  5. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/processors/project/branches_processor.py +9 -4
  6. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/processors/project/members_processor.py +24 -14
  7. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/processors/project/tags_processor.py +22 -8
  8. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform.egg-info/PKG-INFO +22 -11
  9. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform.egg-info/requires.txt +10 -9
  10. {gitlabform-4.0.4 → gitlabform-4.0.6}/setup.py +10 -9
  11. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/acceptance/standard/test_variables.py +9 -1
  12. gitlabform-4.0.4/gitlabform/gitlab/python_gitlab.py +0 -111
  13. {gitlabform-4.0.4 → gitlabform-4.0.6}/LICENSE +0 -0
  14. {gitlabform-4.0.4 → gitlabform-4.0.6}/README.md +0 -0
  15. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/__init__.py +0 -0
  16. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/configuration/__init__.py +0 -0
  17. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/configuration/common.py +0 -0
  18. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/configuration/core.py +0 -0
  19. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/configuration/groups.py +0 -0
  20. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/configuration/projects.py +0 -0
  21. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/configuration/transform.py +0 -0
  22. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/constants.py +0 -0
  23. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/gitlab/commits.py +0 -0
  24. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/gitlab/core.py +0 -0
  25. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/gitlab/group_badges.py +0 -0
  26. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/gitlab/group_ldap_links.py +0 -0
  27. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/gitlab/group_variables.py +0 -0
  28. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/gitlab/groups.py +0 -0
  29. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/gitlab/merge_requests.py +0 -0
  30. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/gitlab/pipelines.py +0 -0
  31. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/gitlab/project_badges.py +0 -0
  32. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/gitlab/project_deploy_keys.py +0 -0
  33. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/gitlab/project_merge_requests_approvals.py +0 -0
  34. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/gitlab/project_protected_environments.py +0 -0
  35. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/gitlab/projects.py +0 -0
  36. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/gitlab/users.py +0 -0
  37. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/gitlab/variables.py +0 -0
  38. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/lists/__init__.py +0 -0
  39. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/lists/filter.py +0 -0
  40. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/lists/groups.py +0 -0
  41. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/lists/projects.py +0 -0
  42. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/output.py +0 -0
  43. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/processors/__init__.py +0 -0
  44. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/processors/abstract_processor.py +0 -0
  45. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/processors/application/__init__.py +0 -0
  46. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/processors/application/application_settings_processor.py +0 -0
  47. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/processors/defining_keys.py +0 -0
  48. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/processors/group/__init__.py +0 -0
  49. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/processors/group/group_badges_processor.py +0 -0
  50. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/processors/group/group_labels_processor.py +0 -0
  51. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/processors/group/group_ldap_links_processor.py +0 -0
  52. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/processors/group/group_saml_links_processor.py +0 -0
  53. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/processors/group/group_settings_processor.py +0 -0
  54. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/processors/group/group_variables_processor.py +0 -0
  55. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/processors/multiple_entities_processor.py +0 -0
  56. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/processors/project/__init__.py +0 -0
  57. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/processors/project/badges_processor.py +0 -0
  58. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/processors/project/deploy_keys_processor.py +0 -0
  59. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/processors/project/files_processor.py +0 -0
  60. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/processors/project/hooks_processor.py +0 -0
  61. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/processors/project/integrations_processor.py +0 -0
  62. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/processors/project/job_token_scope_processor.py +0 -0
  63. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/processors/project/merge_requests_approval_rules.py +0 -0
  64. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/processors/project/merge_requests_approvals.py +0 -0
  65. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/processors/project/project_labels_processor.py +0 -0
  66. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/processors/project/project_processor.py +0 -0
  67. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/processors/project/project_push_rules_processor.py +0 -0
  68. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/processors/project/project_settings_processor.py +0 -0
  69. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/processors/project/resource_groups_processor.py +0 -0
  70. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/processors/project/schedules_processor.py +0 -0
  71. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/processors/project/variables_processor.py +0 -0
  72. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/processors/shared/__init__.py +0 -0
  73. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/processors/shared/protected_environments_processor.py +0 -0
  74. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/processors/single_entity_processor.py +0 -0
  75. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/processors/util/__init__.py +0 -0
  76. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/processors/util/decorators.py +0 -0
  77. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/processors/util/difference_logger.py +0 -0
  78. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/processors/util/labels_processor.py +0 -0
  79. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/run.py +0 -0
  80. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform/util.py +0 -0
  81. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform.egg-info/SOURCES.txt +0 -0
  82. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform.egg-info/dependency_links.txt +0 -0
  83. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform.egg-info/entry_points.txt +0 -0
  84. {gitlabform-4.0.4 → gitlabform-4.0.6}/gitlabform.egg-info/top_level.txt +0 -0
  85. {gitlabform-4.0.4 → gitlabform-4.0.6}/setup.cfg +0 -0
  86. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/__init__.py +0 -0
  87. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/acceptance/__init__.py +0 -0
  88. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/acceptance/conftest.py +0 -0
  89. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/acceptance/standard/__init__.py +0 -0
  90. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/acceptance/standard/test_application_settings.py +0 -0
  91. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/acceptance/standard/test_archive_project.py +0 -0
  92. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/acceptance/standard/test_badges.py +0 -0
  93. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/acceptance/standard/test_branches.py +0 -0
  94. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/acceptance/standard/test_deploy_keys.py +0 -0
  95. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/acceptance/standard/test_deploy_keys_all_projects.py +0 -0
  96. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/acceptance/standard/test_files.py +0 -0
  97. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/acceptance/standard/test_files_templates.py +0 -0
  98. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/acceptance/standard/test_group_badges.py +0 -0
  99. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/acceptance/standard/test_group_labels.py +0 -0
  100. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/acceptance/standard/test_group_members_case_insensitive.py +0 -0
  101. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/acceptance/standard/test_group_members_groups.py +0 -0
  102. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/acceptance/standard/test_group_members_users.py +0 -0
  103. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/acceptance/standard/test_group_settings.py +0 -0
  104. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/acceptance/standard/test_group_variables.py +0 -0
  105. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/acceptance/standard/test_hooks.py +0 -0
  106. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/acceptance/standard/test_integrations.py +0 -0
  107. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/acceptance/standard/test_job_token_scope.py +0 -0
  108. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/acceptance/standard/test_members.py +0 -0
  109. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/acceptance/standard/test_members_add_group.py +0 -0
  110. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/acceptance/standard/test_members_enforce.py +0 -0
  111. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/acceptance/standard/test_project_group_members_case_insensitive.py +0 -0
  112. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/acceptance/standard/test_project_labels.py +0 -0
  113. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/acceptance/standard/test_project_members_case_insensitve.py +0 -0
  114. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/acceptance/standard/test_project_settings.py +0 -0
  115. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/acceptance/standard/test_resource_groups.py +0 -0
  116. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/acceptance/standard/test_running.py +0 -0
  117. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/acceptance/standard/test_schedules.py +0 -0
  118. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/acceptance/standard/test_tags.py +0 -0
  119. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/acceptance/standard/test_token_from_config.py +0 -0
  120. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/acceptance/standard/test_transfer_project.py +0 -0
  121. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/unit/__init__.py +0 -0
  122. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/unit/configuration/__init__.py +0 -0
  123. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/unit/configuration/test_case_sensitivity.py +0 -0
  124. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/unit/configuration/test_inheritance_break_projects_and_groups.py +0 -0
  125. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/unit/configuration/test_inheritance_break_subgroups.py +0 -0
  126. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/unit/configuration/test_inheritance_break_validation.py +0 -0
  127. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/unit/configuration/test_projects_and_groups.py +0 -0
  128. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/unit/configuration/test_skip_groups_skip_projects.py +0 -0
  129. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/unit/configuration/test_subgroups.py +0 -0
  130. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/unit/configuration/test_yaml_version.py +0 -0
  131. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/unit/processors/__init__.py +0 -0
  132. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/unit/processors/test_abstract_processor.py +0 -0
  133. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/unit/processors/test_difference_logger.py +0 -0
  134. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/unit/processors/test_schedules_processor_extended_cron_pattern.py +0 -0
  135. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/unit/test_access_levels.py +0 -0
  136. {gitlabform-4.0.4 → gitlabform-4.0.6}/tests/unit/test_non_empty_configs_provider.py +0 -0
  137. {gitlabform-4.0.4 → 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.4
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
@@ -17,33 +17,44 @@ Classifier: Topic :: Software Development :: Version Control :: Git
17
17
  Requires-Python: >=3.12.0
18
18
  Description-Content-Type: text/markdown
19
19
  License-File: LICENSE
20
- Requires-Dist: certifi==2024.8.30
21
- Requires-Dist: cli-ui==0.17.2
20
+ Requires-Dist: certifi==2024.12.14
21
+ Requires-Dist: cli-ui==0.18.0
22
22
  Requires-Dist: ez-yaml==1.2.0
23
- Requires-Dist: Jinja2==3.1.4
23
+ Requires-Dist: Jinja2==3.1.5
24
24
  Requires-Dist: luddite==1.0.4
25
25
  Requires-Dist: MarkupSafe==3.0.2
26
26
  Requires-Dist: mergedeep==1.3.4
27
27
  Requires-Dist: packaging==24.2
28
- Requires-Dist: python-gitlab==5.0.0
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
32
33
  Requires-Dist: yamlpath==3.8.2
33
34
  Provides-Extra: test
34
- Requires-Dist: coverage==7.6.8; extra == "test"
35
- Requires-Dist: cryptography==43.0.3; extra == "test"
36
- Requires-Dist: deepdiff==8.0.1; extra == "test"
37
- Requires-Dist: mypy==1.13.0; extra == "test"
35
+ Requires-Dist: coverage==7.6.10; extra == "test"
36
+ Requires-Dist: cryptography==44.0.0; extra == "test"
37
+ Requires-Dist: deepdiff==8.1.1; extra == "test"
38
+ Requires-Dist: mypy==1.14.1; extra == "test"
38
39
  Requires-Dist: mypy-extensions==1.0.0; extra == "test"
39
40
  Requires-Dist: pre-commit==2.21.0; extra == "test"
40
- Requires-Dist: pytest==8.3.3; extra == "test"
41
+ Requires-Dist: pytest==8.3.4; extra == "test"
41
42
  Requires-Dist: pytest-cov==6.0.0; extra == "test"
42
43
  Requires-Dist: pytest-rerunfailures==15.0; extra == "test"
43
44
  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
@@ -1,7 +1,8 @@
1
1
  from logging import debug, warning
2
2
  from typing import Dict, Tuple
3
3
 
4
- from cli_ui import fatal
4
+ import gitlab
5
+ from cli_ui import fatal, error
5
6
 
6
7
  from gitlabform.constants import EXIT_INVALID_INPUT
7
8
  from gitlabform.gitlab import GitLab, AccessLevel
@@ -207,13 +208,13 @@ class GroupMembersProcessor(AbstractProcessor):
207
208
  )
208
209
  if member_role_id_or_name:
209
210
  member_role_id_to_set = self.gl.get_member_role_id_cached(
210
- member_role_id_or_name, group.get_id()
211
+ member_role_id_or_name, group.full_path
211
212
  )
212
213
  else:
213
214
  member_role_id_to_set = None
214
215
 
215
216
  common_username = user.lower()
216
- user_id = self.gl.get_user_id(user)
217
+ user_id = self.gl.get_user_id_cached(user)
217
218
 
218
219
  if common_username in users_before:
219
220
  group_member: GroupMember = group.members.get(user_id)
@@ -273,16 +274,28 @@ class GroupMembersProcessor(AbstractProcessor):
273
274
  )
274
275
  continue
275
276
  debug("Removing user '%s' who is not configured to be a member.", user)
276
- user_id = self.gl.get_user_id(user)
277
+ try:
278
+ user_id = self.gl.get_user_id_cached(user)
279
+ except gitlab.GitlabGetError:
280
+ # User does not exist an instance level but is for whatever reason present on a Group/Project
281
+ # We should raise error into Logs but not prevent the rest of GitLabForm from executing
282
+ # This error is more likely to be prevalent in Dedicated instances; it is unlikely for a User to
283
+ # be completely deleted from gitlab.com
284
+ error(
285
+ f"Could not find User '{user}' on the Instance so can not remove User"
286
+ )
287
+ continue
288
+
277
289
  try:
278
290
  group.members.delete(user_id)
279
- except GitlabDeleteError as error:
280
- warning(f"Member could not be deleted: ", error)
291
+ except GitlabDeleteError as delete_error:
292
+ warning(f"Member '{user}' could not be deleted: {delete_error}")
281
293
  pass
294
+
282
295
  else:
283
296
  debug("Not enforcing group members.")
284
297
 
285
- debug("Group members AFTER: %s", group.members.list(get_all=True))
298
+ debug(f"Group members AFTER: {group.members.list(get_all=True)}")
286
299
 
287
300
  @staticmethod
288
301
  def get_group_members(group) -> dict:
@@ -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,6 +1,6 @@
1
- from cli_ui import debug as verbose, warning, info
1
+ from cli_ui import debug as verbose, warning, info, error
2
2
  from cli_ui import fatal
3
- from gitlab import GitlabGetError
3
+ from gitlab import GitlabGetError, GitlabDeleteError
4
4
  from gitlab.v4.objects import Project
5
5
 
6
6
  from gitlabform.constants import EXIT_INVALID_INPUT
@@ -113,7 +113,7 @@ class MembersProcessor(AbstractProcessor):
113
113
  for user in users:
114
114
  info(f"Processing user '{user}'...")
115
115
  try:
116
- user_id = self.gl.get_user_by_username_cached(user).get_id()
116
+ user_id = self.gl.get_user_id_cached(user)
117
117
  except GitlabGetError:
118
118
  warning(f"Could not find user '{user}' in Gitlab, skipping...")
119
119
  continue
@@ -132,14 +132,9 @@ class MembersProcessor(AbstractProcessor):
132
132
  users[user]["member_role"] if "member_role" in users[user] else None
133
133
  )
134
134
  if member_role_id_or_name:
135
- # For self-managed member_roles are at an instance level, for SaaS on a Group level
136
- if self.gl.is_gitlab_saas():
137
- group_id = project.namespace["id"]
138
- else:
139
- group_id = None
140
-
135
+ group_name_and_path = project.namespace["full_path"]
141
136
  member_role_id = self.gl.get_member_role_id_cached(
142
- member_role_id_or_name, group_id
137
+ member_role_id_or_name, group_name_and_path
143
138
  )
144
139
  else:
145
140
  member_role_id = None
@@ -222,10 +217,25 @@ class MembersProcessor(AbstractProcessor):
222
217
  verbose(
223
218
  f"Removing user '{user_not_in_config}' that is not configured to be a member."
224
219
  )
225
- cached_user_id = self.gl.get_user_by_username_cached(
226
- user_not_in_config
227
- ).get_id()
228
- project.members.delete(id=cached_user_id)
220
+ try:
221
+ user_id = self.gl.get_user_id_cached(user_not_in_config)
222
+ except GitlabGetError:
223
+ # User does not exist an instance level but is for whatever reason present on a Group/Project
224
+ # We should raise error into Logs but not prevent the rest of GitLabForm from executing
225
+ # This error is more likely to be prevalent in Dedicated instances; it is unlikely for a User to
226
+ # be completely deleted from gitlab.com
227
+ warning(
228
+ f"Could not find User '{user_not_in_config}' on the Instance so can not remove User from Project '{project_and_group}'"
229
+ )
230
+ continue
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
229
239
  else:
230
240
  verbose("Not enforcing user members.")
231
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.4
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
@@ -17,33 +17,44 @@ Classifier: Topic :: Software Development :: Version Control :: Git
17
17
  Requires-Python: >=3.12.0
18
18
  Description-Content-Type: text/markdown
19
19
  License-File: LICENSE
20
- Requires-Dist: certifi==2024.8.30
21
- Requires-Dist: cli-ui==0.17.2
20
+ Requires-Dist: certifi==2024.12.14
21
+ Requires-Dist: cli-ui==0.18.0
22
22
  Requires-Dist: ez-yaml==1.2.0
23
- Requires-Dist: Jinja2==3.1.4
23
+ Requires-Dist: Jinja2==3.1.5
24
24
  Requires-Dist: luddite==1.0.4
25
25
  Requires-Dist: MarkupSafe==3.0.2
26
26
  Requires-Dist: mergedeep==1.3.4
27
27
  Requires-Dist: packaging==24.2
28
- Requires-Dist: python-gitlab==5.0.0
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
32
33
  Requires-Dist: yamlpath==3.8.2
33
34
  Provides-Extra: test
34
- Requires-Dist: coverage==7.6.8; extra == "test"
35
- Requires-Dist: cryptography==43.0.3; extra == "test"
36
- Requires-Dist: deepdiff==8.0.1; extra == "test"
37
- Requires-Dist: mypy==1.13.0; extra == "test"
35
+ Requires-Dist: coverage==7.6.10; extra == "test"
36
+ Requires-Dist: cryptography==44.0.0; extra == "test"
37
+ Requires-Dist: deepdiff==8.1.1; extra == "test"
38
+ Requires-Dist: mypy==1.14.1; extra == "test"
38
39
  Requires-Dist: mypy-extensions==1.0.0; extra == "test"
39
40
  Requires-Dist: pre-commit==2.21.0; extra == "test"
40
- Requires-Dist: pytest==8.3.3; extra == "test"
41
+ Requires-Dist: pytest==8.3.4; extra == "test"
41
42
  Requires-Dist: pytest-cov==6.0.0; extra == "test"
42
43
  Requires-Dist: pytest-rerunfailures==15.0; extra == "test"
43
44
  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)
@@ -1,12 +1,13 @@
1
- certifi==2024.8.30
2
- cli-ui==0.17.2
1
+ certifi==2024.12.14
2
+ cli-ui==0.18.0
3
3
  ez-yaml==1.2.0
4
- Jinja2==3.1.4
4
+ Jinja2==3.1.5
5
5
  luddite==1.0.4
6
6
  MarkupSafe==3.0.2
7
7
  mergedeep==1.3.4
8
8
  packaging==24.2
9
- python-gitlab==5.0.0
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
@@ -17,13 +18,13 @@ mkdocs
17
18
  mkdocs-material
18
19
 
19
20
  [test]
20
- coverage==7.6.8
21
- cryptography==43.0.3
22
- deepdiff==8.0.1
23
- mypy==1.13.0
21
+ coverage==7.6.10
22
+ cryptography==44.0.0
23
+ deepdiff==8.1.1
24
+ mypy==1.14.1
24
25
  mypy-extensions==1.0.0
25
26
  pre-commit==2.21.0
26
- pytest==8.3.3
27
+ pytest==8.3.4
27
28
  pytest-cov==6.0.0
28
29
  pytest-rerunfailures==15.0
29
30
  xkcdpass==1.19.9