44
55"""Dataclasses and functions for statistics"""
66
7+ import json
78from dataclasses import dataclass , field
89
10+ from ._helpers import implement_changes_into_class
11+
912
1013@dataclass
1114class TeamChanges : # pylint: disable=too-many-instance-attributes
@@ -35,12 +38,13 @@ class RepoChanges: # pylint: disable=too-many-instance-attributes
3538class OrgChanges : # pylint: disable=too-many-instance-attributes
3639 """Dataclass holding general statistics about the changes made to the organization"""
3740
41+ dry : bool = False
3842 added_owners : list [str ] = field (default_factory = list )
3943 degraded_owners : list [str ] = field (default_factory = list )
40- teams : dict [str , TeamChanges ] = field (default_factory = dict )
41- repos : dict [str , RepoChanges ] = field (default_factory = dict )
4244 members_without_team : list [str ] = field (default_factory = list )
4345 removed_members : list [str ] = field (default_factory = list )
46+ teams : dict [str , TeamChanges ] = field (default_factory = dict )
47+ repos : dict [str , RepoChanges ] = field (default_factory = dict )
4448
4549 # --------------------------------------------------------------------------
4650 # Owners
@@ -62,7 +66,7 @@ def update_team(self, team_name: str, **changes: bool | str | list[str]) -> None
6266 if team_name not in self .teams :
6367 self .teams [team_name ] = TeamChanges ()
6468
65- implement_changes (dc_object = self .teams [team_name ], ** changes )
69+ implement_changes_into_class (dc_object = self .teams [team_name ], ** changes )
6670
6771 def create_team (self , team : str ) -> None :
6872 """Team has been created"""
@@ -110,7 +114,7 @@ def update_repo(self, repo_name: str, **changes: bool | str | list[str]) -> None
110114 if repo_name not in self .teams :
111115 self .repos [repo_name ] = RepoChanges ()
112116
113- implement_changes (dc_object = self .repos [repo_name ], ** changes )
117+ implement_changes_into_class (dc_object = self .repos [repo_name ], ** changes )
114118
115119 def change_repo_team_permissions (self , repo : str , team : str , perm : str ) -> None :
116120 """Team permissions have been changed for a repo"""
@@ -128,21 +132,127 @@ def remove_repo_collaborator(self, repo: str, user: str) -> None:
128132 """Remove collaborator"""
129133 self .update_repo (repo_name = repo , removed_collaborators = user )
130134
131-
132- def implement_changes (dc_object , ** changes : bool | str | list [str ]):
133- """Smartly add changes to a dataclass object"""
134- for attribute , value in changes .items ():
135- current_value = getattr (dc_object , attribute )
136- # attribute is list
137- if isinstance (current_value , list ):
138- # input change is list
139- if isinstance (value , list ):
140- current_value .extend (value )
141- # input change is not list
142- else :
143- current_value .append (value )
144- # All other cases, bool
135+ # --------------------------------------------------------------------------
136+ # Output
137+ # --------------------------------------------------------------------------
138+ def changes_into_dict (self ) -> dict :
139+ """Convert dataclass to dict, and only use classes that are not empty/False"""
140+ changes_dict : dict [str , list [str ] | dict ] = {
141+ key : value # type: ignore
142+ for key , value in {
143+ "dry" : self .dry ,
144+ "added_owners" : self .added_owners ,
145+ "degraded_owners" : self .degraded_owners ,
146+ "members_without_team" : self .members_without_team ,
147+ "removed_members" : self .removed_members ,
148+ "teams" : self .teams ,
149+ "repos" : self .repos ,
150+ }.items ()
151+ if value # Exclude empty values
152+ }
153+
154+ team_changes : dict [str , TeamChanges ] = changes_dict .get ("teams" , {}) # type: ignore
155+ repo_changes : dict [str , RepoChanges ] = changes_dict .get ("repos" , {}) # type: ignore
156+
157+ for team , tchanges in team_changes .items ():
158+ new_changes = {
159+ key : value
160+ for key , value in tchanges .__dict__ .items ()
161+ if value # Exclude empty values
162+ }
163+ team_changes [team ] = new_changes # type: ignore
164+ changes_dict ["teams" ] = team_changes
165+
166+ for repo , rchanges in repo_changes .items ():
167+ new_changes = {
168+ key : value
169+ for key , value in rchanges .__dict__ .items ()
170+ if value # Exclude empty values
171+ }
172+ repo_changes [repo ] = new_changes # type: ignore
173+ changes_dict ["repos" ] = repo_changes
174+
175+ return changes_dict
176+
177+ def print_changes ( # pylint: disable=too-many-branches, too-many-statements
178+ self , orgname : str , output : str , dry : bool
179+ ) -> None :
180+ """Print the changes, either in pretty format or as JSON"""
181+
182+ # Add dry run information to stats dataclass
183+ self .dry = dry
184+
185+ # Output in the requested format
186+ if output == "json" :
187+ changes_dict = self .changes_into_dict ()
188+ print (json .dumps (changes_dict , indent = 2 ))
145189 else :
146- setattr (dc_object , attribute , value )
147-
148- return dc_object
190+ output = (
191+ f"#-------------------------------------{ len (orgname )* '-' } \n "
192+ f"# Changes made to GitHub organisation { orgname } \n "
193+ f"#-------------------------------------{ len (orgname )* '-' } \n \n "
194+ )
195+ if dry :
196+ output += "⚠️ Dry-run mode, no changes executed\n \n "
197+ if self .added_owners :
198+ output += f"➕ Added owners: { ', ' .join (self .added_owners )} \n "
199+ if self .degraded_owners :
200+ output += f"🔻 Degraded owners: { ', ' .join (self .degraded_owners )} \n "
201+ if self .members_without_team :
202+ output += f"⚠️ Members without team: { ', ' .join (self .members_without_team )} \n "
203+ if self .removed_members :
204+ output += (
205+ f"❌ Members removed from organisation: { ', ' .join (self .removed_members )} \n "
206+ )
207+ if self .teams :
208+ output += "\n 🤝 Team Changes:\n "
209+ for team , tchanges in self .teams .items ():
210+ output += f" 🔹 { team } :\n "
211+ if tchanges .unconfigured :
212+ output += " ⚠️ Is/was unconfigured\n "
213+ if tchanges .newly_created :
214+ output += " 🆕 Has been created\n "
215+ if tchanges .deleted :
216+ output += " ❌ Has been deleted\n "
217+ if tchanges .changed_config :
218+ output += " 🔧 Changed config:\n "
219+ for item in tchanges .changed_config :
220+ output += f" - { item } \n "
221+ if tchanges .added_members :
222+ output += " ➕ Added members:\n "
223+ for item in tchanges .added_members :
224+ output += f" - { item } \n "
225+ if tchanges .changed_members_role :
226+ output += ' 🔄 Changed members role:"'
227+ for item in tchanges .changed_members_role :
228+ output += f" - { item } \n "
229+ if tchanges .removed_members :
230+ output += " ❌ Removed members:\n "
231+ for item in tchanges .removed_members :
232+ output += f" - { item } \n "
233+ if tchanges .pending_members :
234+ output += " ⏳ Pending members:\n "
235+ for item in tchanges .pending_members :
236+ output += f" - { item } \n "
237+ if self .repos :
238+ output += "\n 📂 Repository Changes:\n "
239+ for repo , rchanges in self .repos .items ():
240+ output += f" 🔹 { repo } :\n "
241+ if rchanges .changed_permissions_for_teams :
242+ output += " 🔧 Changed permissions for teams:\n "
243+ for item in rchanges .changed_permissions_for_teams :
244+ output += f" - { item } \n "
245+ if rchanges .removed_teams :
246+ output += " ❌ Removed teams:\n "
247+ for item in rchanges .removed_teams :
248+ output += f" - { item } \n "
249+ if rchanges .unconfigured_teams_with_permissions :
250+ output += " ⚠️ Unconfigured teams with permissions:\n "
251+ for item in rchanges .unconfigured_teams_with_permissions :
252+ output += f" - { item } \n "
253+ if rchanges .removed_collaborators :
254+ output += " ❌ Removed collaborators:\n "
255+ for item in rchanges .removed_collaborators :
256+ output += f" - { item } \n "
257+
258+ print (output .strip ())
0 commit comments