Skip to content

Commit 4a078f5

Browse files
committed
chore: split off some general functions to separate file
1 parent cc8e27b commit 4a078f5

File tree

2 files changed

+135
-93
lines changed

2 files changed

+135
-93
lines changed

gh_org_mgr/_gh_org.py

Lines changed: 14 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@
2424
from jwt.exceptions import InvalidKeyError
2525

2626
from ._gh_api import get_github_secrets_from_env, run_graphql_query
27+
from ._helpers import (
28+
compare_two_dicts,
29+
compare_two_lists,
30+
pretty_print_dict,
31+
sluggify_teamname,
32+
)
2733
from ._stats import OrgChanges
2834

2935

@@ -64,12 +70,6 @@ class GHorg: # pylint: disable=too-many-instance-attributes, too-many-lines
6470
# --------------------------------------------------------------------------
6571
# Helper functions
6672
# --------------------------------------------------------------------------
67-
def _sluggify_teamname(self, team: str) -> str:
68-
"""Slugify a GitHub team name"""
69-
# TODO: this is very naive, no other special chars are
70-
# supported, or multiple spaces etc.
71-
return team.replace(" ", "-")
72-
7373
# amazonq-ignore-next-line
7474
def login(
7575
self, orgname: str, token: str = "", app_id: str | int = "", app_private_key: str = ""
@@ -120,88 +120,9 @@ def ratelimit(self):
120120
"Current rate limit: %s/%s (reset: %s)", core.remaining, core.limit, core.reset
121121
)
122122

123-
def pretty_print_dict(self, dictionary: dict) -> str:
124-
"""Convert a dict to a pretty-printed output"""
125-
126-
# Censor sensible fields
127-
def censor_half_string(string: str) -> str:
128-
"""Censor 50% of a string (rounded up)"""
129-
half1 = int(len(string) / 2)
130-
half2 = len(string) - half1
131-
return string[:half1] + "*" * (half2)
132-
133-
sensible_keys = ["gh_token", "gh_app_private_key"]
134-
for key in sensible_keys:
135-
if value := dictionary.get(key, ""):
136-
dictionary[key] = censor_half_string(value)
137-
138-
# Print dict nicely
139-
def pretty(d, indent=0):
140-
string = ""
141-
for key, value in d.items():
142-
string += " " * indent + str(key) + ":\n"
143-
if isinstance(value, dict):
144-
string += pretty(value, indent + 1)
145-
else:
146-
string += " " * (indent + 1) + str(value) + "\n"
147-
148-
return string
149-
150-
return pretty(dictionary)
151-
152123
def pretty_print_dataclass(self) -> str:
153124
"""Convert this dataclass to a pretty-printed output"""
154-
return self.pretty_print_dict(asdict(self))
155-
156-
def compare_two_lists(self, list1: list[str], list2: list[str]):
157-
"""
158-
Compares two lists of strings and returns a tuple containing elements
159-
missing in each list and common elements.
160-
161-
Args:
162-
list1 (list of str): The first list of strings.
163-
list2 (list of str): The second list of strings.
164-
165-
Returns:
166-
tuple: A tuple containing three lists:
167-
1. The first list contains elements in `list2` that are missing in `list1`.
168-
2. The second list contains elements that are present in both `list1` and `list2`.
169-
3. The third list contains elements in `list1` that are missing in `list2`.
170-
171-
Example:
172-
>>> list1 = ["apple", "banana", "cherry"]
173-
>>> list2 = ["banana", "cherry", "date", "fig"]
174-
>>> compare_lists(list1, list2)
175-
(['date', 'fig'], ['banana', 'cherry'], ['apple'])
176-
"""
177-
# Convert lists to sets for easier comparison
178-
set1, set2 = set(list1), set(list2)
179-
180-
# Elements in list2 that are missing in list1
181-
missing_in_list1 = list(set2 - set1)
182-
183-
# Elements present in both lists
184-
common_elements = list(set1 & set2)
185-
186-
# Elements in list1 that are missing in list2
187-
missing_in_list2 = list(set1 - set2)
188-
189-
# Return the result as a tuple
190-
return (missing_in_list1, common_elements, missing_in_list2)
191-
192-
def compare_two_dicts(self, dict1: dict, dict2: dict) -> dict[str, dict[str, str | int | None]]:
193-
"""Compares two dictionaries. Assume that the keys are the same. Output
194-
a dict with keys that have differing values"""
195-
# Create an empty dictionary to store differences
196-
differences = {}
197-
198-
# Iterate through the keys (assuming both dictionaries have the same keys)
199-
for key in dict1:
200-
# Compare the values for each key
201-
if dict1[key] != dict2[key]:
202-
differences[key] = {"dict1": dict1[key], "dict2": dict2[key]}
203-
204-
return differences
125+
return pretty_print_dict(asdict(self), sensible_keys=["gh_token", "gh_app_private_key"])
205126

206127
def _resolve_gh_username(self, username: str, teamname: str) -> NamedUser | None:
207128
"""Turn a username into a proper GitHub user object"""
@@ -296,7 +217,7 @@ def sync_org_owners(self, dry: bool = False, force: bool = False) -> None:
296217
return
297218

298219
# Get differences between the current and configured owners
299-
owners_remove, owners_ok, owners_add = self.compare_two_lists(
220+
owners_remove, owners_ok, owners_add = compare_two_lists(
300221
self.configured_org_owners, [user.login for user in self.current_org_owners]
301222
)
302223
# Compare configured (lower-cased) owners with lower-cased list of current owners
@@ -375,7 +296,7 @@ def create_missing_teams(self, dry: bool = False):
375296
for team, attributes in self.configured_teams.items():
376297
if team not in existent_team_names:
377298
if parent := attributes.get("parent"): # type: ignore
378-
parent_id = self.org.get_team_by_slug(self._sluggify_teamname(parent)).id
299+
parent_id = self.org.get_team_by_slug(sluggify_teamname(parent)).id
379300

380301
logging.info("Creating team '%s' with parent ID '%s'", team, parent_id)
381302
self.stats.create_team(team)
@@ -417,7 +338,7 @@ def _prepare_team_config_for_sync(
417338
# team coming from config, and valid string
418339
elif isinstance(parent, str) and parent:
419340
team_config["parent_team_id"] = self.org.get_team_by_slug(
420-
self._sluggify_teamname(parent)
341+
sluggify_teamname(parent)
421342
).id
422343
# empty from string, so probably default value
423344
elif isinstance(parent, str) and not parent:
@@ -478,7 +399,7 @@ def sync_current_teams_settings(self, dry: bool = False) -> None:
478399
)
479400

480401
# Compare settings and update if necessary
481-
if differences := self.compare_two_dicts(configured_team_configs, current_team_configs):
402+
if differences := compare_two_dicts(configured_team_configs, current_team_configs):
482403
# Log differences
483404
logging.info(
484405
"Team settings for '%s' differ from the configuration. Updating them:",
@@ -497,7 +418,7 @@ def sync_current_teams_settings(self, dry: bool = False) -> None:
497418
logging.critical(
498419
"Team '%s' settings could not be edited. Error: \n%s",
499420
team.name,
500-
self.pretty_print_dict(exc.data),
421+
pretty_print_dict(exc.data),
501422
)
502423
sys.exit(1)
503424
else:
@@ -772,7 +693,7 @@ def _create_perms_changelist_for_teams(
772693

773694
# Convert team name to Team object
774695
try:
775-
team = self.org.get_team_by_slug(self._sluggify_teamname(team_name))
696+
team = self.org.get_team_by_slug(sluggify_teamname(team_name))
776697
# Team not found, probably because a new team should be created, but it's a dry-run
777698
except UnknownObjectException:
778699
logging.debug(
@@ -795,7 +716,7 @@ def _create_perms_changelist_for_teams(
795716
attributes={
796717
"id": 0,
797718
"name": team_name,
798-
"slug": self._sluggify_teamname(team_name),
719+
"slug": sluggify_teamname(team_name),
799720
},
800721
completed=True, # Mark as fully initialized
801722
)

gh_org_mgr/_helpers.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# SPDX-FileCopyrightText: 2025 DB Systel GmbH
2+
#
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
"""Helper functions"""
6+
7+
from dataclasses import asdict
8+
9+
10+
def sluggify_teamname(team: str) -> str:
11+
"""Slugify a GitHub team name"""
12+
# TODO: this is very naive, no other special chars are
13+
# supported, or multiple spaces etc.
14+
return team.replace(" ", "-")
15+
16+
17+
def compare_two_lists(list1: list[str], list2: list[str]):
18+
"""
19+
Compares two lists of strings and returns a tuple containing elements
20+
missing in each list and common elements.
21+
22+
Args:
23+
list1 (list of str): The first list of strings.
24+
list2 (list of str): The second list of strings.
25+
26+
Returns:
27+
tuple: A tuple containing three lists:
28+
1. The first list contains elements in `list2` that are missing in `list1`.
29+
2. The second list contains elements that are present in both `list1` and `list2`.
30+
3. The third list contains elements in `list1` that are missing in `list2`.
31+
32+
Example:
33+
>>> list1 = ["apple", "banana", "cherry"]
34+
>>> list2 = ["banana", "cherry", "date", "fig"]
35+
>>> compare_lists(list1, list2)
36+
(['date', 'fig'], ['banana', 'cherry'], ['apple'])
37+
"""
38+
# Convert lists to sets for easier comparison
39+
set1, set2 = set(list1), set(list2)
40+
41+
# Elements in list2 that are missing in list1
42+
missing_in_list1 = list(set2 - set1)
43+
44+
# Elements present in both lists
45+
common_elements = list(set1 & set2)
46+
47+
# Elements in list1 that are missing in list2
48+
missing_in_list2 = list(set1 - set2)
49+
50+
# Return the result as a tuple
51+
return (missing_in_list1, common_elements, missing_in_list2)
52+
53+
54+
def compare_two_dicts(dict1: dict, dict2: dict) -> dict[str, dict[str, str | int | None]]:
55+
"""Compares two dictionaries. Assume that the keys are the same. Output
56+
a dict with keys that have differing values"""
57+
# Create an empty dictionary to store differences
58+
differences = {}
59+
60+
# Iterate through the keys (assuming both dictionaries have the same keys)
61+
for key in dict1:
62+
# Compare the values for each key
63+
if dict1[key] != dict2[key]:
64+
differences[key] = {"dict1": dict1[key], "dict2": dict2[key]}
65+
66+
return differences
67+
68+
69+
def pretty_print_dict(dictionary: dict, sensible_keys: None | list[str] = None) -> str:
70+
"""Convert a dict to a pretty-printed output"""
71+
72+
# Censor sensible fields
73+
def censor_half_string(string: str) -> str:
74+
"""Censor 50% of a string (rounded up)"""
75+
half1 = int(len(string) / 2)
76+
half2 = len(string) - half1
77+
return string[:half1] + "*" * (half2)
78+
79+
if sensible_keys is None:
80+
sensible_keys = []
81+
for key in sensible_keys:
82+
if value := dictionary.get(key, ""):
83+
dictionary[key] = censor_half_string(value)
84+
85+
# Print dict nicely
86+
def pretty(d, indent=0):
87+
string = ""
88+
for key, value in d.items():
89+
string += " " * indent + str(key) + ":\n"
90+
if isinstance(value, dict):
91+
string += pretty(value, indent + 1)
92+
else:
93+
string += " " * (indent + 1) + str(value) + "\n"
94+
95+
return string
96+
97+
return pretty(dictionary)
98+
99+
100+
def pretty_print_dataclass(dc):
101+
"""Convert dataclass to a pretty-printed output"""
102+
pretty_print_dict(asdict(dc))
103+
104+
105+
def implement_changes_into_class(dc_object, **changes: bool | str | list[str]):
106+
"""Smartly add changes to a (data)class object"""
107+
for attribute, value in changes.items():
108+
current_value = getattr(dc_object, attribute)
109+
# attribute is list
110+
if isinstance(current_value, list):
111+
# input change is list
112+
if isinstance(value, list):
113+
current_value.extend(value)
114+
# input change is not list
115+
else:
116+
current_value.append(value)
117+
# All other cases, bool
118+
else:
119+
setattr(dc_object, attribute, value)
120+
121+
return dc_object

0 commit comments

Comments
 (0)