gitlabform 4.2.4__tar.gz → 4.2.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 (87) hide show
  1. {gitlabform-4.2.4 → gitlabform-4.2.6}/PKG-INFO +15 -15
  2. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/gitlab/python_gitlab.py +1 -0
  3. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/processors/group/group_hooks_processor.py +2 -1
  4. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/processors/group/group_members_processor.py +12 -5
  5. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/processors/project/hooks_processor.py +2 -1
  6. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/processors/project/integrations_processor.py +10 -1
  7. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/processors/project/members_processor.py +15 -30
  8. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/processors/project/schedules_processor.py +1 -1
  9. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/processors/util/labels_processor.py +34 -13
  10. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform.egg-info/PKG-INFO +15 -15
  11. gitlabform-4.2.6/gitlabform.egg-info/requires.txt +31 -0
  12. {gitlabform-4.2.4 → gitlabform-4.2.6}/pyproject.toml +15 -15
  13. gitlabform-4.2.4/gitlabform.egg-info/requires.txt +0 -31
  14. {gitlabform-4.2.4 → gitlabform-4.2.6}/LICENSE +0 -0
  15. {gitlabform-4.2.4 → gitlabform-4.2.6}/README.md +0 -0
  16. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/__init__.py +0 -0
  17. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/configuration/__init__.py +0 -0
  18. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/configuration/common.py +0 -0
  19. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/configuration/core.py +0 -0
  20. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/configuration/groups.py +0 -0
  21. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/configuration/projects.py +0 -0
  22. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/configuration/transform.py +0 -0
  23. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/constants.py +0 -0
  24. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/gitlab/__init__.py +0 -0
  25. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/gitlab/commits.py +0 -0
  26. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/gitlab/core.py +0 -0
  27. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/gitlab/group_badges.py +0 -0
  28. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/gitlab/group_ldap_links.py +0 -0
  29. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/gitlab/group_variables.py +0 -0
  30. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/gitlab/groups.py +0 -0
  31. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/gitlab/merge_requests.py +0 -0
  32. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/gitlab/pipelines.py +0 -0
  33. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/gitlab/project_badges.py +0 -0
  34. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/gitlab/project_deploy_keys.py +0 -0
  35. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/gitlab/project_merge_requests_approvals.py +0 -0
  36. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/gitlab/project_protected_environments.py +0 -0
  37. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/gitlab/project_security_settings.py +0 -0
  38. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/gitlab/projects.py +0 -0
  39. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/gitlab/variables.py +0 -0
  40. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/lists/__init__.py +0 -0
  41. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/lists/filter.py +0 -0
  42. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/lists/groups.py +0 -0
  43. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/lists/projects.py +0 -0
  44. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/output.py +0 -0
  45. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/processors/__init__.py +0 -0
  46. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/processors/abstract_processor.py +0 -0
  47. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/processors/application/__init__.py +0 -0
  48. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/processors/application/application_settings_processor.py +0 -0
  49. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/processors/defining_keys.py +0 -0
  50. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/processors/group/__init__.py +0 -0
  51. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/processors/group/group_badges_processor.py +0 -0
  52. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/processors/group/group_labels_processor.py +0 -0
  53. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/processors/group/group_ldap_links_processor.py +0 -0
  54. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/processors/group/group_push_rules_processor.py +0 -0
  55. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/processors/group/group_saml_links_processor.py +0 -0
  56. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/processors/group/group_settings_processor.py +0 -0
  57. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/processors/group/group_variables_processor.py +0 -0
  58. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/processors/multiple_entities_processor.py +0 -0
  59. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/processors/project/__init__.py +0 -0
  60. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/processors/project/badges_processor.py +0 -0
  61. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/processors/project/branches_processor.py +0 -0
  62. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/processors/project/deploy_keys_processor.py +0 -0
  63. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/processors/project/files_processor.py +0 -0
  64. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/processors/project/job_token_scope_processor.py +0 -0
  65. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/processors/project/merge_requests_approval_rules.py +0 -0
  66. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/processors/project/merge_requests_approvals.py +0 -0
  67. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/processors/project/project_labels_processor.py +0 -0
  68. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/processors/project/project_processor.py +0 -0
  69. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/processors/project/project_push_rules_processor.py +0 -0
  70. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/processors/project/project_security_settings.py +0 -0
  71. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/processors/project/project_settings_processor.py +0 -0
  72. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/processors/project/resource_groups_processor.py +0 -0
  73. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/processors/project/tags_processor.py +0 -0
  74. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/processors/project/variables_processor.py +0 -0
  75. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/processors/shared/__init__.py +0 -0
  76. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/processors/shared/protected_environments_processor.py +0 -0
  77. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/processors/single_entity_processor.py +0 -0
  78. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/processors/util/__init__.py +0 -0
  79. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/processors/util/decorators.py +0 -0
  80. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/processors/util/difference_logger.py +0 -0
  81. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/run.py +0 -0
  82. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform/util.py +0 -0
  83. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform.egg-info/SOURCES.txt +0 -0
  84. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform.egg-info/dependency_links.txt +0 -0
  85. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform.egg-info/entry_points.txt +0 -0
  86. {gitlabform-4.2.4 → gitlabform-4.2.6}/gitlabform.egg-info/top_level.txt +0 -0
  87. {gitlabform-4.2.4 → gitlabform-4.2.6}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitlabform
3
- Version: 4.2.4
3
+ Version: 4.2.6
4
4
  Summary: 🏗 Specialized configuration as a code tool for GitLab projects, groups and more using hierarchical configuration written in YAML
5
5
  Author: Greg Dubicki and Contributors
6
6
  Project-URL: Homepage, https://gitlabform.github.io/gitlabform/
@@ -20,7 +20,7 @@ Classifier: Topic :: Software Development :: Version Control :: Git
20
20
  Requires-Python: >=3.12.0
21
21
  Description-Content-Type: text/markdown
22
22
  License-File: LICENSE
23
- Requires-Dist: certifi==2025.1.31
23
+ Requires-Dist: certifi==2025.6.15
24
24
  Requires-Dist: cli-ui==0.19.0
25
25
  Requires-Dist: ez-yaml==1.2.0
26
26
  Requires-Dist: Jinja2==3.1.6
@@ -28,23 +28,23 @@ Requires-Dist: luddite==1.0.4
28
28
  Requires-Dist: MarkupSafe==3.0.2
29
29
  Requires-Dist: mergedeep==1.3.4
30
30
  Requires-Dist: packaging==25.0
31
- Requires-Dist: python-gitlab==5.6.0
32
- Requires-Dist: python-gitlab[graphql]==5.6.0
33
- Requires-Dist: requests==2.32.3
31
+ Requires-Dist: python-gitlab==6.1.0
32
+ Requires-Dist: python-gitlab[graphql]==6.1.0
33
+ Requires-Dist: requests==2.32.4
34
34
  Requires-Dist: ruamel.yaml==0.17.21
35
- Requires-Dist: types-requests==2.32.0.20250328
36
- Requires-Dist: types-setuptools==78.1.0.20250329
35
+ Requires-Dist: types-requests==2.32.4.20250611
36
+ Requires-Dist: types-setuptools==80.9.0.20250529
37
37
  Requires-Dist: yamlpath==3.8.2
38
38
  Provides-Extra: test
39
- Requires-Dist: coverage==7.8.0; extra == "test"
40
- Requires-Dist: cryptography==44.0.2; extra == "test"
41
- Requires-Dist: deepdiff==8.4.2; extra == "test"
42
- Requires-Dist: mypy==1.15.0; extra == "test"
43
- Requires-Dist: mypy-extensions==1.0.0; extra == "test"
39
+ Requires-Dist: coverage==7.9.2; extra == "test"
40
+ Requires-Dist: cryptography==45.0.5; extra == "test"
41
+ Requires-Dist: deepdiff==8.5.0; extra == "test"
42
+ Requires-Dist: mypy==1.16.1; extra == "test"
43
+ Requires-Dist: mypy-extensions==1.1.0; extra == "test"
44
44
  Requires-Dist: pre-commit==2.21.0; extra == "test"
45
- Requires-Dist: pytest==8.3.5; extra == "test"
46
- Requires-Dist: pytest-cov==6.1.1; extra == "test"
47
- Requires-Dist: pytest-rerunfailures==15.0; extra == "test"
45
+ Requires-Dist: pytest==8.4.1; extra == "test"
46
+ Requires-Dist: pytest-cov==6.2.1; extra == "test"
47
+ Requires-Dist: pytest-rerunfailures==15.1; extra == "test"
48
48
  Requires-Dist: xkcdpass==1.20.0; extra == "test"
49
49
  Provides-Extra: docs
50
50
  Requires-Dist: mkdocs; extra == "docs"
@@ -96,6 +96,7 @@ class PythonGitlab(Gitlab):
96
96
  # Gitlab API will only ever return 0 or 1 entry when GETting using `username` attribute
97
97
  # https://docs.gitlab.com/ee/api/users.html#for-non-administrator-users
98
98
  # so will always be list[RESTObject] and never RESTObjectList from python-gitlab's api
99
+ # username list is case-insensitive on the gitlab api
99
100
  users: list[RESTObject] = self.users.list(username=username) # type: ignore
100
101
 
101
102
  if len(users) == 0:
@@ -3,6 +3,7 @@ from typing import Dict, Any, List
3
3
 
4
4
  from gitlab.base import RESTObject, RESTObjectList
5
5
  from gitlab.v4.objects import Group
6
+ from gitlab.v4.objects import GroupHook
6
7
 
7
8
  from gitlabform.gitlab import GitLab
8
9
  from gitlabform.processors.abstract_processor import AbstractProcessor
@@ -15,7 +16,7 @@ class GroupHooksProcessor(AbstractProcessor):
15
16
  def _process_configuration(self, group_path_and_name: str, configuration: dict):
16
17
  debug("Processing group hooks...")
17
18
  group: Group = self.gl.get_group_by_path_cached(group_path_and_name)
18
- group_hooks: RESTObjectList | List[RESTObject] = group.hooks.list(get_all=True)
19
+ group_hooks: list[GroupHook] = group.hooks.list(get_all=True)
19
20
 
20
21
  hooks_in_config: tuple[str, ...] = tuple(x for x in sorted(configuration["group_hooks"]) if x != "enforce")
21
22
 
@@ -1,14 +1,13 @@
1
- from logging import debug, warning
1
+ from logging import debug, error
2
2
  from typing import Dict, Tuple
3
3
 
4
- import gitlab
5
4
  from cli_ui import fatal, error, debug as verbose
6
5
 
7
6
  from gitlabform.constants import EXIT_INVALID_INPUT
8
7
  from gitlabform.gitlab import GitLab, AccessLevel
9
8
  from gitlabform.processors.abstract_processor import AbstractProcessor
10
9
  from gitlab.v4.objects import Group, GroupMember, User
11
- from gitlab import GitlabDeleteError, GitlabGetError
10
+ from gitlab import GitlabDeleteError, GitlabError, GitlabGetError
12
11
 
13
12
 
14
13
  class GroupMembersProcessor(AbstractProcessor):
@@ -102,7 +101,11 @@ class GroupMembersProcessor(AbstractProcessor):
102
101
  # to ensure that the group has the expected access level
103
102
  self._unshare(group_being_processed, share_with_group_id)
104
103
 
105
- group_being_processed.share(share_with_group_id, group_access_to_set, expires_at_to_set)
104
+ try:
105
+ group_being_processed.share(share_with_group_id, group_access_to_set, expires_at_to_set)
106
+ except GitlabError as e:
107
+ error(f"Error processing {share_with_group_path}, {e.error_message}")
108
+ raise e
106
109
 
107
110
  else:
108
111
  debug(
@@ -111,7 +114,11 @@ class GroupMembersProcessor(AbstractProcessor):
111
114
  )
112
115
 
113
116
  share_with_group_id = self.gl.get_group_id(share_with_group_path)
114
- group_being_processed.share(share_with_group_id, group_access_to_set, expires_at_to_set)
117
+ try:
118
+ group_being_processed.share(share_with_group_id, group_access_to_set, expires_at_to_set)
119
+ except GitlabError as e:
120
+ error(f"Error processing {share_with_group_path}, {e.error_message}")
121
+ raise e
115
122
 
116
123
  if enforce_group_members:
117
124
  # remove groups not configured explicitly
@@ -3,6 +3,7 @@ from typing import Dict, Any, List
3
3
 
4
4
  from gitlab.base import RESTObject, RESTObjectList
5
5
  from gitlab.v4.objects import Project
6
+ from gitlab.v4.objects import ProjectHook
6
7
 
7
8
  from gitlabform.gitlab import GitLab
8
9
  from gitlabform.processors.abstract_processor import AbstractProcessor
@@ -15,7 +16,7 @@ class HooksProcessor(AbstractProcessor):
15
16
  def _process_configuration(self, project_and_group: str, configuration: dict):
16
17
  debug("Processing hooks...")
17
18
  project: Project = self.gl.get_project_by_path_cached(project_and_group)
18
- project_hooks: RESTObjectList | List[RESTObject] = project.hooks.list(get_all=True)
19
+ project_hooks: list[ProjectHook] = project.hooks.list(get_all=True)
19
20
 
20
21
  hooks_in_config: tuple[str, ...] = tuple(x for x in sorted(configuration["hooks"]) if x != "enforce")
21
22
 
@@ -1,5 +1,6 @@
1
1
  from cli_ui import debug as verbose
2
2
 
3
+ from gitlab.exceptions import GitlabDeleteError
3
4
  from gitlab.v4.objects import Project, ProjectIntegration
4
5
  from gitlabform.gitlab import GitLab
5
6
  from gitlabform.processors.abstract_processor import AbstractProcessor
@@ -18,7 +19,15 @@ class IntegrationsProcessor(AbstractProcessor):
18
19
 
19
20
  if configured_integrations[integration].get("delete"):
20
21
  verbose(f"Deleting integration: {integration}")
21
- gl_integration.delete()
22
+ try:
23
+ gl_integration.delete()
24
+ except GitlabDeleteError as e:
25
+ # If we get a 404 the integration does not exist, so we can ignore the error
26
+ if e.response_code == 404:
27
+ verbose(f"Integration {integration} does not exist, skipping deletion.")
28
+ else:
29
+ verbose(f"Failed to delete integration {integration}: {e}")
30
+ raise
22
31
  else:
23
32
  verbose(f"Setting integration: {integration}")
24
33
  project.integrations.update(integration, configured_integrations[integration])
@@ -49,15 +49,10 @@ class MembersProcessor(AbstractProcessor):
49
49
  and expires_at == current_groups[common_group_name]["expires_at"]
50
50
  and access_level == current_groups[common_group_name]["group_access_level"]
51
51
  ):
52
- verbose(
53
- "Ignoring group '%s' as it is already a member",
54
- common_group_name,
55
- )
56
- verbose(
57
- "Current settings for '%s' are: %s" % (common_group_name, current_groups[common_group_name])
58
- )
52
+ verbose(f"Ignoring group '{common_group_name}' as it is already a member")
53
+ verbose(f"Current settings for '{common_group_name}' are: {current_groups[common_group_name]}")
59
54
  else:
60
- verbose("Setting group '%s' as a member", common_group_name)
55
+ verbose(f"Setting group '{common_group_name}' as a member")
61
56
  access = access_level
62
57
  expiry = expires_at
63
58
 
@@ -86,7 +81,6 @@ class MembersProcessor(AbstractProcessor):
86
81
  enforce_members: bool,
87
82
  keep_bots: bool,
88
83
  ):
89
-
90
84
  project: Project = self.gl.get_project_by_path_cached(project_and_group)
91
85
 
92
86
  current_members = self._get_members_from_project(project)
@@ -97,10 +91,11 @@ class MembersProcessor(AbstractProcessor):
97
91
  for user in users:
98
92
  info(f"Processing user '{user}'...")
99
93
 
100
- user_id = self.gl.get_user_id_cached(user)
101
- if user_id is None:
94
+ gitlab_user = self.gl.get_user_by_username_cached(user)
95
+ if gitlab_user is None:
102
96
  warning(f"Could not find user '{user}' in Gitlab, skipping...")
103
97
  continue
98
+ user_id = gitlab_user.id
104
99
 
105
100
  expires_at = users[user]["expires_at"].strftime("%Y-%m-%d") if "expires_at" in users[user] else None
106
101
  access_level = users[user]["access_level"] if "access_level" in users[user] else None
@@ -128,33 +123,23 @@ class MembersProcessor(AbstractProcessor):
128
123
  and access_level == current_member.access_level
129
124
  and member_role_id == member_role_id_before
130
125
  ):
131
- verbose(
132
- "Nothing to change for user '%s' - same config now as to set.",
133
- common_username,
134
- )
135
- verbose(
136
- "Current settings for '%s' are: %s" % (common_username, current_members[common_username])
137
- )
126
+ verbose(f"Nothing to change for user '{common_username}' - same config now as to set.")
127
+ verbose(f"Current settings for '{common_username}' are: {current_members[common_username]}")
138
128
  else:
139
129
  verbose(
140
- "Editing user '%s' membership to change their access level or expires at",
141
- common_username,
130
+ f"Editing user '{common_username}' membership to change their access level or expires at",
142
131
  )
143
- update_data = {
144
- "user_id": common_username,
145
- "access_level": access_level,
146
- "member_role_id": member_role_id_before,
147
- }
148
132
 
133
+ project_member = project.members.get(id=user_id)
134
+ project_member.access_level = access_level
135
+ project_member.member_role_id = member_role_id_before
149
136
  if expires_at:
150
- update_data["expires_at"] = expires_at
151
-
152
- project.members.update(new_data=update_data)
137
+ project_member.expires_at = expires_at
138
+ project_member.save()
153
139
 
154
140
  else:
155
141
  verbose(
156
- "Adding user '%s' who previously was not a member.",
157
- common_username,
142
+ f"Adding user '{common_username}' who previously was not a member.",
158
143
  )
159
144
  create_data = {
160
145
  "user_id": user_id,
@@ -24,7 +24,7 @@ class SchedulesProcessor(AbstractProcessor):
24
24
  configured_schedules.pop("enforce")
25
25
 
26
26
  project: Project = self.gl.get_project_by_path_cached(project_and_group)
27
- existing_schedules: List[RESTObject] | RESTObjectList = project.pipelineschedules.list(get_all=True)
27
+ existing_schedules: list[ProjectPipelineSchedule] = project.pipelineschedules.list(get_all=True)
28
28
 
29
29
  schedule_ids_by_description: Dict = self._group_schedule_ids_by_description(existing_schedules)
30
30
 
@@ -1,6 +1,7 @@
1
- from cli_ui import debug as verbose, info
2
- from typing import Dict, List, Callable
1
+ from cli_ui import debug as verbose, info, warning
2
+ from typing import Dict, List, Callable, Union
3
3
 
4
+ from gitlab.base import RESTObjectList, RESTObject
4
5
  from gitlab.v4.objects import Group, Project, ProjectLabel, GroupLabel
5
6
 
6
7
 
@@ -15,7 +16,8 @@ class LabelsProcessor:
15
16
  needs_update: Callable, # self._needs_update passed from AbstractProcessor called process_labels
16
17
  ):
17
18
  # Only get Labels created directly on the project/group
18
- existing_labels = group_or_project.labels.list(get_all=True, include_ancestor_groups=False)
19
+ existing_group_labels = group_or_project.labels.list(get_all=True, include_ancestor_groups=False)
20
+ existing_group_and_parent_labels = group_or_project.labels.list(get_all=True)
19
21
  existing_label_keys: List = []
20
22
 
21
23
  if isinstance(group_or_project, Group):
@@ -25,7 +27,7 @@ class LabelsProcessor:
25
27
 
26
28
  gitlab_labels_to_delete: List = []
27
29
 
28
- for label_to_update in existing_labels:
30
+ for label_to_update in existing_group_labels:
29
31
  label_name_in_gl = label_to_update.name
30
32
  updated_label = False
31
33
  verbose(f"Checking if {label_name_in_gl} is in Configuration to update or delete")
@@ -33,9 +35,7 @@ class LabelsProcessor:
33
35
  for key, configured_label in configured_labels.items():
34
36
  configured_label_name = configured_label.get("name")
35
37
  # Key in YAML may not match the "name" value in Gitlab or YAML, so we must match on both
36
- if (
37
- configured_label_name is not None and label_name_in_gl == configured_label_name
38
- ) or label_name_in_gl == key:
38
+ if self.configured_label_matches_gitlab_label(configured_label_name, key, label_name_in_gl):
39
39
  # label exists in GL, so update
40
40
  existing_label_keys.append(key)
41
41
  updated_label = True
@@ -65,10 +65,19 @@ class LabelsProcessor:
65
65
  self.get_label(group_or_project, label_to_delete).delete()
66
66
 
67
67
  # add new labels
68
+
68
69
  for label_key in configured_labels.keys():
69
70
  if label_key not in existing_label_keys:
70
71
  info(f"Creating new label with key: {label_key}, on {parent_object_type}")
71
- self.create_new_label(configured_labels, group_or_project, label_key, parent_object_type)
72
+ self.create_new_label(
73
+ configured_labels, group_or_project, label_key, parent_object_type, existing_group_and_parent_labels
74
+ )
75
+
76
+ @staticmethod
77
+ def configured_label_matches_gitlab_label(configured_label_name: str, key: str, label_name_in_gl: str):
78
+ return (
79
+ configured_label_name is not None and label_name_in_gl == configured_label_name
80
+ ) or label_name_in_gl == key
72
81
 
73
82
  @staticmethod
74
83
  def get_label(group_or_project, listed_label) -> GroupLabel | ProjectLabel:
@@ -88,13 +97,25 @@ class LabelsProcessor:
88
97
 
89
98
  full_label.save()
90
99
 
91
- @staticmethod
92
100
  def create_new_label(
101
+ self,
93
102
  configured_labels,
94
103
  group_or_project: Group | Project,
95
- label_name: str,
104
+ label_key: str,
96
105
  parent_object_type: str,
106
+ existing_group_and_parent_labels: Union[List[GroupLabel], List[ProjectLabel]],
97
107
  ):
98
- label = configured_labels.get(label_name)
99
- info(f"Adding {label_name} to {parent_object_type}")
100
- group_or_project.labels.create({"name": label_name, **label})
108
+ label = configured_labels.get(label_key)
109
+ configured_label_name = label.get("name")
110
+ found_existing_label = False
111
+ for existing_label in existing_group_and_parent_labels:
112
+ if self.configured_label_matches_gitlab_label(configured_label_name, label_key, existing_label.name):
113
+ warning(
114
+ f"Label {existing_label.name} already exists either in {group_or_project.name} or on Parent Group, so will not create"
115
+ )
116
+ found_existing_label = True
117
+ break
118
+
119
+ if not found_existing_label:
120
+ info(f"Adding label with key: {label_key} to {parent_object_type}")
121
+ group_or_project.labels.create({"name": label_key, **label})
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitlabform
3
- Version: 4.2.4
3
+ Version: 4.2.6
4
4
  Summary: 🏗 Specialized configuration as a code tool for GitLab projects, groups and more using hierarchical configuration written in YAML
5
5
  Author: Greg Dubicki and Contributors
6
6
  Project-URL: Homepage, https://gitlabform.github.io/gitlabform/
@@ -20,7 +20,7 @@ Classifier: Topic :: Software Development :: Version Control :: Git
20
20
  Requires-Python: >=3.12.0
21
21
  Description-Content-Type: text/markdown
22
22
  License-File: LICENSE
23
- Requires-Dist: certifi==2025.1.31
23
+ Requires-Dist: certifi==2025.6.15
24
24
  Requires-Dist: cli-ui==0.19.0
25
25
  Requires-Dist: ez-yaml==1.2.0
26
26
  Requires-Dist: Jinja2==3.1.6
@@ -28,23 +28,23 @@ Requires-Dist: luddite==1.0.4
28
28
  Requires-Dist: MarkupSafe==3.0.2
29
29
  Requires-Dist: mergedeep==1.3.4
30
30
  Requires-Dist: packaging==25.0
31
- Requires-Dist: python-gitlab==5.6.0
32
- Requires-Dist: python-gitlab[graphql]==5.6.0
33
- Requires-Dist: requests==2.32.3
31
+ Requires-Dist: python-gitlab==6.1.0
32
+ Requires-Dist: python-gitlab[graphql]==6.1.0
33
+ Requires-Dist: requests==2.32.4
34
34
  Requires-Dist: ruamel.yaml==0.17.21
35
- Requires-Dist: types-requests==2.32.0.20250328
36
- Requires-Dist: types-setuptools==78.1.0.20250329
35
+ Requires-Dist: types-requests==2.32.4.20250611
36
+ Requires-Dist: types-setuptools==80.9.0.20250529
37
37
  Requires-Dist: yamlpath==3.8.2
38
38
  Provides-Extra: test
39
- Requires-Dist: coverage==7.8.0; extra == "test"
40
- Requires-Dist: cryptography==44.0.2; extra == "test"
41
- Requires-Dist: deepdiff==8.4.2; extra == "test"
42
- Requires-Dist: mypy==1.15.0; extra == "test"
43
- Requires-Dist: mypy-extensions==1.0.0; extra == "test"
39
+ Requires-Dist: coverage==7.9.2; extra == "test"
40
+ Requires-Dist: cryptography==45.0.5; extra == "test"
41
+ Requires-Dist: deepdiff==8.5.0; extra == "test"
42
+ Requires-Dist: mypy==1.16.1; extra == "test"
43
+ Requires-Dist: mypy-extensions==1.1.0; extra == "test"
44
44
  Requires-Dist: pre-commit==2.21.0; extra == "test"
45
- Requires-Dist: pytest==8.3.5; extra == "test"
46
- Requires-Dist: pytest-cov==6.1.1; extra == "test"
47
- Requires-Dist: pytest-rerunfailures==15.0; extra == "test"
45
+ Requires-Dist: pytest==8.4.1; extra == "test"
46
+ Requires-Dist: pytest-cov==6.2.1; extra == "test"
47
+ Requires-Dist: pytest-rerunfailures==15.1; extra == "test"
48
48
  Requires-Dist: xkcdpass==1.20.0; extra == "test"
49
49
  Provides-Extra: docs
50
50
  Requires-Dist: mkdocs; extra == "docs"
@@ -0,0 +1,31 @@
1
+ certifi==2025.6.15
2
+ cli-ui==0.19.0
3
+ ez-yaml==1.2.0
4
+ Jinja2==3.1.6
5
+ luddite==1.0.4
6
+ MarkupSafe==3.0.2
7
+ mergedeep==1.3.4
8
+ packaging==25.0
9
+ python-gitlab==6.1.0
10
+ python-gitlab[graphql]==6.1.0
11
+ requests==2.32.4
12
+ ruamel.yaml==0.17.21
13
+ types-requests==2.32.4.20250611
14
+ types-setuptools==80.9.0.20250529
15
+ yamlpath==3.8.2
16
+
17
+ [docs]
18
+ mkdocs
19
+ mkdocs-material
20
+
21
+ [test]
22
+ coverage==7.9.2
23
+ cryptography==45.0.5
24
+ deepdiff==8.5.0
25
+ mypy==1.16.1
26
+ mypy-extensions==1.1.0
27
+ pre-commit==2.21.0
28
+ pytest==8.4.1
29
+ pytest-cov==6.2.1
30
+ pytest-rerunfailures==15.1
31
+ xkcdpass==1.20.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gitlabform"
7
- version = "4.2.4"
7
+ version = "4.2.6"
8
8
  authors = [{ name = "Greg Dubicki and Contributors" }]
9
9
  description = "🏗 Specialized configuration as a code tool for GitLab projects, groups and more using hierarchical configuration written in YAML"
10
10
  keywords = ["cli", "yaml", "gitlab", "configuration-as-code"]
@@ -23,7 +23,7 @@ classifiers = [
23
23
  ]
24
24
  requires-python = ">=3.12.0"
25
25
  dependencies = [
26
- "certifi==2025.1.31",
26
+ "certifi==2025.6.15",
27
27
  "cli-ui==0.19.0",
28
28
  "ez-yaml==1.2.0",
29
29
  "Jinja2==3.1.6",
@@ -31,12 +31,12 @@ dependencies = [
31
31
  "MarkupSafe==3.0.2",
32
32
  "mergedeep==1.3.4",
33
33
  "packaging==25.0",
34
- "python-gitlab==5.6.0",
35
- "python-gitlab[graphql]==5.6.0",
36
- "requests==2.32.3",
34
+ "python-gitlab==6.1.0",
35
+ "python-gitlab[graphql]==6.1.0",
36
+ "requests==2.32.4",
37
37
  "ruamel.yaml==0.17.21",
38
- "types-requests==2.32.0.20250328",
39
- "types-setuptools==78.1.0.20250329",
38
+ "types-requests==2.32.4.20250611",
39
+ "types-setuptools==80.9.0.20250529",
40
40
  "yamlpath==3.8.2",
41
41
  ]
42
42
 
@@ -52,15 +52,15 @@ content-type = "text/markdown"
52
52
 
53
53
  [project.optional-dependencies]
54
54
  test = [
55
- "coverage==7.8.0",
56
- "cryptography==44.0.2",
57
- "deepdiff==8.4.2",
58
- "mypy==1.15.0",
59
- "mypy-extensions==1.0.0",
55
+ "coverage==7.9.2",
56
+ "cryptography==45.0.5",
57
+ "deepdiff==8.5.0",
58
+ "mypy==1.16.1",
59
+ "mypy-extensions==1.1.0",
60
60
  "pre-commit==2.21.0",
61
- "pytest==8.3.5",
62
- "pytest-cov==6.1.1",
63
- "pytest-rerunfailures==15.0",
61
+ "pytest==8.4.1",
62
+ "pytest-cov==6.2.1",
63
+ "pytest-rerunfailures==15.1",
64
64
  "xkcdpass==1.20.0",
65
65
  ]
66
66
  docs = ["mkdocs", "mkdocs-material"]
@@ -1,31 +0,0 @@
1
- certifi==2025.1.31
2
- cli-ui==0.19.0
3
- ez-yaml==1.2.0
4
- Jinja2==3.1.6
5
- luddite==1.0.4
6
- MarkupSafe==3.0.2
7
- mergedeep==1.3.4
8
- packaging==25.0
9
- python-gitlab==5.6.0
10
- python-gitlab[graphql]==5.6.0
11
- requests==2.32.3
12
- ruamel.yaml==0.17.21
13
- types-requests==2.32.0.20250328
14
- types-setuptools==78.1.0.20250329
15
- yamlpath==3.8.2
16
-
17
- [docs]
18
- mkdocs
19
- mkdocs-material
20
-
21
- [test]
22
- coverage==7.8.0
23
- cryptography==44.0.2
24
- deepdiff==8.4.2
25
- mypy==1.15.0
26
- mypy-extensions==1.0.0
27
- pre-commit==2.21.0
28
- pytest==8.3.5
29
- pytest-cov==6.1.1
30
- pytest-rerunfailures==15.0
31
- xkcdpass==1.20.0
File without changes
File without changes
File without changes
File without changes