gitlabform 4.2.6__tar.gz → 4.3.0__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.6 → gitlabform-4.3.0}/PKG-INFO +3 -3
  2. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/processors/group/group_members_processor.py +25 -28
  3. gitlabform-4.3.0/gitlabform/processors/group/group_settings_processor.py +90 -0
  4. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/processors/project/project_settings_processor.py +62 -1
  5. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform.egg-info/PKG-INFO +3 -3
  6. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform.egg-info/requires.txt +2 -2
  7. {gitlabform-4.2.6 → gitlabform-4.3.0}/pyproject.toml +3 -3
  8. gitlabform-4.2.6/gitlabform/processors/group/group_settings_processor.py +0 -30
  9. {gitlabform-4.2.6 → gitlabform-4.3.0}/LICENSE +0 -0
  10. {gitlabform-4.2.6 → gitlabform-4.3.0}/README.md +0 -0
  11. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/__init__.py +0 -0
  12. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/configuration/__init__.py +0 -0
  13. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/configuration/common.py +0 -0
  14. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/configuration/core.py +0 -0
  15. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/configuration/groups.py +0 -0
  16. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/configuration/projects.py +0 -0
  17. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/configuration/transform.py +0 -0
  18. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/constants.py +0 -0
  19. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/gitlab/__init__.py +0 -0
  20. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/gitlab/commits.py +0 -0
  21. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/gitlab/core.py +0 -0
  22. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/gitlab/group_badges.py +0 -0
  23. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/gitlab/group_ldap_links.py +0 -0
  24. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/gitlab/group_variables.py +0 -0
  25. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/gitlab/groups.py +0 -0
  26. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/gitlab/merge_requests.py +0 -0
  27. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/gitlab/pipelines.py +0 -0
  28. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/gitlab/project_badges.py +0 -0
  29. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/gitlab/project_deploy_keys.py +0 -0
  30. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/gitlab/project_merge_requests_approvals.py +0 -0
  31. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/gitlab/project_protected_environments.py +0 -0
  32. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/gitlab/project_security_settings.py +0 -0
  33. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/gitlab/projects.py +0 -0
  34. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/gitlab/python_gitlab.py +0 -0
  35. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/gitlab/variables.py +0 -0
  36. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/lists/__init__.py +0 -0
  37. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/lists/filter.py +0 -0
  38. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/lists/groups.py +0 -0
  39. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/lists/projects.py +0 -0
  40. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/output.py +0 -0
  41. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/processors/__init__.py +0 -0
  42. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/processors/abstract_processor.py +0 -0
  43. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/processors/application/__init__.py +0 -0
  44. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/processors/application/application_settings_processor.py +0 -0
  45. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/processors/defining_keys.py +0 -0
  46. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/processors/group/__init__.py +0 -0
  47. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/processors/group/group_badges_processor.py +0 -0
  48. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/processors/group/group_hooks_processor.py +0 -0
  49. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/processors/group/group_labels_processor.py +0 -0
  50. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/processors/group/group_ldap_links_processor.py +0 -0
  51. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/processors/group/group_push_rules_processor.py +0 -0
  52. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/processors/group/group_saml_links_processor.py +0 -0
  53. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/processors/group/group_variables_processor.py +0 -0
  54. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/processors/multiple_entities_processor.py +0 -0
  55. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/processors/project/__init__.py +0 -0
  56. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/processors/project/badges_processor.py +0 -0
  57. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/processors/project/branches_processor.py +0 -0
  58. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/processors/project/deploy_keys_processor.py +0 -0
  59. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/processors/project/files_processor.py +0 -0
  60. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/processors/project/hooks_processor.py +0 -0
  61. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/processors/project/integrations_processor.py +0 -0
  62. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/processors/project/job_token_scope_processor.py +0 -0
  63. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/processors/project/members_processor.py +0 -0
  64. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/processors/project/merge_requests_approval_rules.py +0 -0
  65. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/processors/project/merge_requests_approvals.py +0 -0
  66. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/processors/project/project_labels_processor.py +0 -0
  67. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/processors/project/project_processor.py +0 -0
  68. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/processors/project/project_push_rules_processor.py +0 -0
  69. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/processors/project/project_security_settings.py +0 -0
  70. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/processors/project/resource_groups_processor.py +0 -0
  71. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/processors/project/schedules_processor.py +0 -0
  72. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/processors/project/tags_processor.py +0 -0
  73. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/processors/project/variables_processor.py +0 -0
  74. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/processors/shared/__init__.py +0 -0
  75. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/processors/shared/protected_environments_processor.py +0 -0
  76. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/processors/single_entity_processor.py +0 -0
  77. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/processors/util/__init__.py +0 -0
  78. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/processors/util/decorators.py +0 -0
  79. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/processors/util/difference_logger.py +0 -0
  80. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/processors/util/labels_processor.py +0 -0
  81. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/run.py +0 -0
  82. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform/util.py +0 -0
  83. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform.egg-info/SOURCES.txt +0 -0
  84. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform.egg-info/dependency_links.txt +0 -0
  85. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform.egg-info/entry_points.txt +0 -0
  86. {gitlabform-4.2.6 → gitlabform-4.3.0}/gitlabform.egg-info/top_level.txt +0 -0
  87. {gitlabform-4.2.6 → gitlabform-4.3.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitlabform
3
- Version: 4.2.6
3
+ Version: 4.3.0
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.6.15
23
+ Requires-Dist: certifi==2025.7.14
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
@@ -39,7 +39,7 @@ Provides-Extra: test
39
39
  Requires-Dist: coverage==7.9.2; extra == "test"
40
40
  Requires-Dist: cryptography==45.0.5; extra == "test"
41
41
  Requires-Dist: deepdiff==8.5.0; extra == "test"
42
- Requires-Dist: mypy==1.16.1; extra == "test"
42
+ Requires-Dist: mypy==1.17.0; extra == "test"
43
43
  Requires-Dist: mypy-extensions==1.1.0; extra == "test"
44
44
  Requires-Dist: pre-commit==2.21.0; extra == "test"
45
45
  Requires-Dist: pytest==8.4.1; extra == "test"
@@ -1,7 +1,6 @@
1
- from logging import debug, error
2
1
  from typing import Dict, Tuple
3
2
 
4
- from cli_ui import fatal, error, debug as verbose
3
+ from cli_ui import info, fatal, error, debug as verbose
5
4
 
6
5
  from gitlabform.constants import EXIT_INVALID_INPUT
7
6
  from gitlabform.gitlab import GitLab, AccessLevel
@@ -67,7 +66,7 @@ class GroupMembersProcessor(AbstractProcessor):
67
66
  enforce_group_members: bool,
68
67
  ):
69
68
  shared_with_groups_before = group_being_processed.shared_with_groups
70
- debug("Group shared with BEFORE: %s", shared_with_groups_before)
69
+ verbose("Group shared with BEFORE: %s", shared_with_groups_before)
71
70
 
72
71
  groups_before_by_group_path = dict()
73
72
  for shared_with_group in shared_with_groups_before:
@@ -87,15 +86,12 @@ class GroupMembersProcessor(AbstractProcessor):
87
86
  expires_at_before = groups_before_by_group_path[share_with_group_path]["expires_at"]
88
87
 
89
88
  if group_access_before == group_access_to_set and expires_at_before == expires_at_to_set:
90
- debug(
89
+ verbose(
91
90
  "Nothing to change for group '%s' - same config now as to set.",
92
91
  share_with_group_path,
93
92
  )
94
93
  else:
95
- debug(
96
- "Re-adding group '%s' to change their access level or expires at.",
97
- share_with_group_path,
98
- )
94
+ info(f"Re-adding group {share_with_group_path} to change their access level or expires at.")
99
95
  share_with_group_id = groups_before_by_group_path[share_with_group_path]["group_id"]
100
96
  # we will remove the group first and then re-add them,
101
97
  # to ensure that the group has the expected access level
@@ -108,9 +104,8 @@ class GroupMembersProcessor(AbstractProcessor):
108
104
  raise e
109
105
 
110
106
  else:
111
- debug(
112
- "Adding group '%s' who previously was not a member.",
113
- share_with_group_path,
107
+ verbose(
108
+ f"Adding group {share_with_group_path} who previously was not a member.",
114
109
  )
115
110
 
116
111
  share_with_group_id = self.gl.get_group_id(share_with_group_path)
@@ -124,16 +119,16 @@ class GroupMembersProcessor(AbstractProcessor):
124
119
  # remove groups not configured explicitly
125
120
  groups_not_configured = set(groups_before_by_group_path) - set(groups_to_share_with_by_path)
126
121
  for group_path in groups_not_configured:
127
- debug(
122
+ verbose(
128
123
  "Removing group '%s' who is not configured to be a member.",
129
124
  group_path,
130
125
  )
131
126
  share_with_group_id = self.gl.get_group_id(group_path)
132
127
  self._unshare(group_being_processed, share_with_group_id)
133
128
  else:
134
- debug("Not enforcing group members.")
129
+ verbose("Not enforcing group members.")
135
130
 
136
- debug(
131
+ verbose(
137
132
  "Group shared with AFTER: %s",
138
133
  group_being_processed.members.list(get_all=True),
139
134
  )
@@ -143,7 +138,7 @@ class GroupMembersProcessor(AbstractProcessor):
143
138
  try:
144
139
  group_being_processed.unshare(share_with_group_id)
145
140
  except GitlabDeleteError:
146
- debug("Group could not be unshared, likely was never shared to begin with")
141
+ info(f"Group with id {share_with_group_id} could not be unshared, likely was never shared to begin with")
147
142
  pass
148
143
 
149
144
  def _process_users(
@@ -157,7 +152,7 @@ class GroupMembersProcessor(AbstractProcessor):
157
152
  # (note: we DON'T get inherited users as we don't manage them at this level anyway)
158
153
  users_before = self.get_group_members(group)
159
154
 
160
- debug("Group members BEFORE: %s", users_before.keys())
155
+ verbose("Group members BEFORE: %s", users_before.keys())
161
156
 
162
157
  if users_to_set_by_username:
163
158
  # group users to set by access level
@@ -217,26 +212,28 @@ class GroupMembersProcessor(AbstractProcessor):
217
212
  and expires_at_before == expires_at_to_set
218
213
  and member_role_id_before == member_role_id_to_set
219
214
  ):
220
- debug(
215
+ verbose(
221
216
  "Nothing to change for user '%s' - same config now as to set.",
222
217
  common_username,
223
218
  )
224
219
  else:
225
- debug(
226
- "Editing user '%s' membership to change their access level or expires at.",
227
- common_username,
220
+ verbose(
221
+ f"Editing user {common_username} to change their access level to {access_level_to_set},"
222
+ f" expires at to {expires_at_to_set},"
223
+ f" and member_role_id to {member_role_id_to_set}."
228
224
  )
229
225
 
230
226
  group_member.access_level = access_level_to_set
231
227
  group_member.expires_at = expires_at_to_set
232
228
  group_member.member_role_id = member_role_id_to_set
233
- group_member.save()
229
+ try:
230
+ group_member.save()
231
+ except GitlabError as e:
232
+ error(f"Could not save user {common_username}, error: {e.error_message}")
233
+ raise e
234
234
 
235
235
  else:
236
- debug(
237
- "Adding user '%s' who previously was not a member.",
238
- common_username,
239
- )
236
+ verbose(f"Adding user {common_username} who previously was not a member.")
240
237
  group.members.create(
241
238
  {
242
239
  "user_id": user_id,
@@ -266,7 +263,7 @@ class GroupMembersProcessor(AbstractProcessor):
266
263
  continue
267
264
 
268
265
  if keep_bots and gl_user.bot:
269
- debug(f"Will not remove bot user '{user}' as the 'keep_bots' option is true.")
266
+ verbose(f"Will not remove bot user '{user}' as the 'keep_bots' option is true.")
270
267
  continue
271
268
 
272
269
  try:
@@ -276,9 +273,9 @@ class GroupMembersProcessor(AbstractProcessor):
276
273
  raise delete_error
277
274
 
278
275
  else:
279
- debug("Not enforcing group members.")
276
+ verbose("Not enforcing group members.")
280
277
 
281
- debug(f"Group members AFTER: {group.members.list(get_all=True)}")
278
+ verbose(f"Group members AFTER: {group.members.list(get_all=True)}")
282
279
 
283
280
  @staticmethod
284
281
  def get_group_members(group) -> dict:
@@ -0,0 +1,90 @@
1
+ import os
2
+ from logging import info, debug, warning
3
+ from typing import Dict
4
+
5
+ from gitlabform.gitlab import GitLab
6
+ from gitlab.v4.objects.groups import Group
7
+ from gitlabform.processors.abstract_processor import AbstractProcessor
8
+
9
+
10
+ class GroupSettingsProcessor(AbstractProcessor):
11
+ def __init__(self, gitlab: GitLab):
12
+ super().__init__("group_settings", gitlab)
13
+
14
+ def _process_configuration(self, group: str, configuration: Dict):
15
+ configured_group_settings = configuration.get("group_settings", {})
16
+
17
+ gitlab_group: Group = self.gl.get_group_by_path_cached(group)
18
+
19
+ # Remove avatar from config to process it last
20
+ avatar_config = configured_group_settings.pop("avatar", None)
21
+
22
+ # Process other settings first
23
+ if self._needs_update(gitlab_group.asdict(), configured_group_settings):
24
+ info(f"Updating group settings for group {gitlab_group.name}")
25
+ self.update_group_settings(gitlab_group, configured_group_settings)
26
+ else:
27
+ debug("No update needed for Group Settings")
28
+
29
+ # Process avatar last - with error handling that doesn't stop execution
30
+ if avatar_config is not None:
31
+ try:
32
+ self._process_group_avatar(gitlab_group, {"avatar": avatar_config})
33
+ except Exception as e:
34
+ warning(f"Failed to process group avatar: {e}")
35
+ raise e
36
+
37
+ @staticmethod
38
+ def update_group_settings(gitlab_group: Group, group_settings_config: dict):
39
+ for key in group_settings_config:
40
+ value = group_settings_config[key]
41
+ debug(f"Updating setting {key} to value {value}")
42
+ gitlab_group.__setattr__(key, value)
43
+ gitlab_group.save()
44
+
45
+ def _process_group_avatar(self, gitlab_group: Group, group_settings_config: dict) -> None:
46
+ """Process group avatar settings from configuration."""
47
+ debug("Processing group avatar configuration")
48
+
49
+ avatar_path = group_settings_config.get("avatar")
50
+ if avatar_path is None:
51
+ debug("No avatar configuration provided, skipping avatar processing")
52
+ return
53
+
54
+ debug(f"Avatar configuration found: {avatar_path}")
55
+
56
+ # Check current avatar status
57
+ current_avatar = getattr(gitlab_group, "avatar_url", None)
58
+
59
+ if avatar_path == "":
60
+ # Want to remove avatar
61
+ if not current_avatar:
62
+ debug("Avatar already empty, no update needed")
63
+ return
64
+ debug("Deleting group avatar")
65
+ gitlab_group.avatar = ""
66
+ gitlab_group.save()
67
+ debug("Avatar deleted successfully")
68
+ return
69
+
70
+ # Resolve relative paths to absolute paths
71
+ if not os.path.isabs(avatar_path):
72
+ # Convert relative path to absolute path relative to current working directory
73
+ avatar_path = os.path.abspath(avatar_path)
74
+ debug(f"Resolved relative path to absolute path: {avatar_path}")
75
+
76
+ # Want to set avatar from file
77
+ debug(f"Setting group avatar from file: {avatar_path}")
78
+ try:
79
+ with open(avatar_path, "rb") as avatar_file:
80
+ gitlab_group.avatar = avatar_file
81
+ gitlab_group.save()
82
+ debug("Group avatar uploaded successfully")
83
+ except FileNotFoundError:
84
+ error_msg = f"Group avatar file not found: {avatar_path}"
85
+ debug(error_msg)
86
+ raise FileNotFoundError(error_msg)
87
+ except Exception as e:
88
+ error_msg = f"Error uploading group avatar: {str(e)}"
89
+ debug(error_msg)
90
+ raise Exception(error_msg) from e
@@ -1,4 +1,5 @@
1
- from logging import debug
1
+ import os
2
+ from logging import debug, error, warning
2
3
  from typing import Callable, Dict, List
3
4
 
4
5
  from gitlab.v4.objects import Project
@@ -24,6 +25,10 @@ class ProjectSettingsProcessor(AbstractProcessor):
24
25
 
25
26
  self._process_project_topics(project_settings_in_config, project_settings_in_gitlab)
26
27
 
28
+ # Remove avatar from config to process it last
29
+ avatar_config = project_settings_in_config.pop("avatar", None)
30
+
31
+ # Process other settings first
27
32
  if self._needs_update(project_settings_in_gitlab, project_settings_in_config):
28
33
  debug("Updating project settings")
29
34
  for key, value in project_settings_in_config.items():
@@ -37,7 +42,16 @@ class ProjectSettingsProcessor(AbstractProcessor):
37
42
  else:
38
43
  debug("No update needed for project settings")
39
44
 
45
+ # Process avatar last - with error handling that doesn't stop execution
46
+ if avatar_config is not None:
47
+ try:
48
+ self._process_project_avatar(project, {"avatar": avatar_config})
49
+ except Exception as e:
50
+ warning(f"Failed to process project avatar: {e}")
51
+ raise e
52
+
40
53
  def get_project_settings(self, project_path: str):
54
+ """Get project settings from GitLab."""
41
55
  return self.gl.get_project_by_path_cached(project_path).asdict()
42
56
 
43
57
  def _print_diff(self, project_or_project_and_group: str, entity_config, diff_only_changed: bool):
@@ -94,3 +108,50 @@ class ProjectSettingsProcessor(AbstractProcessor):
94
108
  debug(f"topics after adjustment: {adjusted_project_topics_to_set}")
95
109
 
96
110
  project_settings_in_config["topics"] = adjusted_project_topics_to_set
111
+
112
+ def _process_project_avatar(self, project: Project, project_settings_in_config: dict) -> None:
113
+ """Process project avatar settings from configuration."""
114
+ debug("Processing project avatar configuration")
115
+
116
+ avatar_path = project_settings_in_config.get("avatar")
117
+ if avatar_path is None:
118
+ debug("No avatar configuration provided, skipping avatar processing")
119
+ return
120
+
121
+ debug(f"Avatar configuration found: {avatar_path}")
122
+
123
+ # Check current avatar status
124
+ current_avatar = getattr(project, "avatar_url", None)
125
+
126
+ if avatar_path == "":
127
+ # Want to remove avatar
128
+ if not current_avatar:
129
+ debug("Avatar already empty, no update needed")
130
+ return
131
+ debug("Deleting project avatar")
132
+ project.avatar = ""
133
+ project.save()
134
+ debug("Project avatar deleted successfully")
135
+ return
136
+
137
+ # Resolve relative paths to absolute paths
138
+ if not os.path.isabs(avatar_path):
139
+ # Convert relative path to absolute path relative to current working directory
140
+ avatar_path = os.path.abspath(avatar_path)
141
+ debug(f"Resolved relative path to absolute path: {avatar_path}")
142
+
143
+ # Want to set avatar from file
144
+ debug(f"Setting project avatar from file: {avatar_path}")
145
+ try:
146
+ with open(avatar_path, "rb") as avatar_file:
147
+ project.avatar = avatar_file
148
+ project.save()
149
+ debug("Project avatar uploaded successfully")
150
+ except FileNotFoundError:
151
+ error_msg = f"Project avatar file not found: {avatar_path}"
152
+ error(error_msg)
153
+ raise FileNotFoundError(error_msg)
154
+ except Exception as e:
155
+ error_msg = f"Error uploading project avatar: {str(e)}"
156
+ error(error_msg)
157
+ raise Exception(error_msg) from e
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitlabform
3
- Version: 4.2.6
3
+ Version: 4.3.0
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.6.15
23
+ Requires-Dist: certifi==2025.7.14
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
@@ -39,7 +39,7 @@ Provides-Extra: test
39
39
  Requires-Dist: coverage==7.9.2; extra == "test"
40
40
  Requires-Dist: cryptography==45.0.5; extra == "test"
41
41
  Requires-Dist: deepdiff==8.5.0; extra == "test"
42
- Requires-Dist: mypy==1.16.1; extra == "test"
42
+ Requires-Dist: mypy==1.17.0; extra == "test"
43
43
  Requires-Dist: mypy-extensions==1.1.0; extra == "test"
44
44
  Requires-Dist: pre-commit==2.21.0; extra == "test"
45
45
  Requires-Dist: pytest==8.4.1; extra == "test"
@@ -1,4 +1,4 @@
1
- certifi==2025.6.15
1
+ certifi==2025.7.14
2
2
  cli-ui==0.19.0
3
3
  ez-yaml==1.2.0
4
4
  Jinja2==3.1.6
@@ -22,7 +22,7 @@ mkdocs-material
22
22
  coverage==7.9.2
23
23
  cryptography==45.0.5
24
24
  deepdiff==8.5.0
25
- mypy==1.16.1
25
+ mypy==1.17.0
26
26
  mypy-extensions==1.1.0
27
27
  pre-commit==2.21.0
28
28
  pytest==8.4.1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gitlabform"
7
- version = "4.2.6"
7
+ version = "4.3.0"
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.6.15",
26
+ "certifi==2025.7.14",
27
27
  "cli-ui==0.19.0",
28
28
  "ez-yaml==1.2.0",
29
29
  "Jinja2==3.1.6",
@@ -55,7 +55,7 @@ test = [
55
55
  "coverage==7.9.2",
56
56
  "cryptography==45.0.5",
57
57
  "deepdiff==8.5.0",
58
- "mypy==1.16.1",
58
+ "mypy==1.17.0",
59
59
  "mypy-extensions==1.1.0",
60
60
  "pre-commit==2.21.0",
61
61
  "pytest==8.4.1",
@@ -1,30 +0,0 @@
1
- from logging import info, debug
2
- from typing import Dict
3
-
4
- from gitlabform.gitlab import GitLab
5
- from gitlab.v4.objects.groups import Group
6
- from gitlabform.processors.abstract_processor import AbstractProcessor
7
-
8
-
9
- class GroupSettingsProcessor(AbstractProcessor):
10
- def __init__(self, gitlab: GitLab):
11
- super().__init__("group_settings", gitlab)
12
-
13
- def _process_configuration(self, group: str, configuration: Dict):
14
- configured_group_settings = configuration.get("group_settings", {})
15
-
16
- gitlab_group: Group = self.gl.get_group_by_path_cached(group)
17
-
18
- if self._needs_update(gitlab_group.asdict(), configured_group_settings):
19
- info(f"Updating group settings for group {gitlab_group.name}")
20
- self.update_group_settings(gitlab_group, configured_group_settings)
21
- else:
22
- debug("No update needed for Group Settings")
23
-
24
- @staticmethod
25
- def update_group_settings(gitlab_group: Group, group_settings_config: dict):
26
- for key in group_settings_config:
27
- value = group_settings_config[key]
28
- debug(f"Updating setting {key} to value {value}")
29
- gitlab_group.__setattr__(key, value)
30
- gitlab_group.save()
File without changes
File without changes
File without changes
File without changes