2222import logging
2323import os
2424import re
25+ import sys
2526import time
2627
2728from dotenv import load_dotenv
2829from github import Github
2930
30- logging .basicConfig (level = logging .DEBUG )
31-
3231load_dotenv ()
3332
34- PAT = str (os .getenv ("PAT" ))
35- ORG = str (os .environ ["ORG" ])
33+ ##
34+ # @var str PAT
35+ # @brief Personal Access Token for interacting with GitHub
36+ PAT = str (os .getenv ("PAT" , None ))
37+
38+ ##
39+ # @var str ORG
40+ # @brief name of GitHub organization to query
41+ ORG = str (os .getenv ("ORG" , None ))
3642
37- TEAM_NAME = os .environ ["TEAM_NAME" ]
43+ ##
44+ # @var str TEAM_NAME
45+ # @brief the name of the team to process
46+ TEAM_NAME = os .getenv ("TEAM_NAME" , None )
47+
48+ ##
49+ # @var str DRY_RUN
50+ # @brief when False, perform read-write operations; otherwise, read-only
3851DRY_RUN = os .getenv ("DRY_RUN" , "True" )
52+
53+ ##
54+ # @var str API_URL
55+ # @brief the URL to the API; default is GitHub.com's API
3956API_URL = os .getenv ("API_URL" , "https://api.github.com" )
57+
58+ ##
59+ # @var str USER_FILTERS
60+ # @brief a JSON object describing the filters to apply to determine membership
4061USER_FILTERS = json .loads (os .getenv ("USER_FILTERS" , "[]" ))
62+
63+ ##
64+ # @var float DELAY
65+ # @brief the number of seconds to wait between API calls
4166DELAY = float (os .getenv ("DELAY" , "3" ))
4267
68+ ##
69+ # @var int LOG_LEVEL
70+ # @brief the threshold for displaying logs; higher is quieter
71+ LOG_LEVEL = int (os .getenv ("LOGGING" , "20" ))
72+
73+ logging .basicConfig (level = LOG_LEVEL )
74+
75+ if PAT is None :
76+ logging .critical ("PAT was undefined" )
77+ sys .exit (1 )
78+
79+ if ORG is None :
80+ logging .critical ("ORG was undefined" )
81+ sys .exit (1 )
82+
83+ if TEAM_NAME is None :
84+ logging .critical ("TEAM_NAME was undefined" )
85+ sys .exit (1 )
86+
4387
4488def create_team_if_not_exists (team_name , organization ):
4589 """
@@ -259,6 +303,106 @@ def matches_regexes(member, field, key):
259303 return False
260304
261305
306+ def is_dry_run (value = DRY_RUN ):
307+ """
308+ @fn is_dry_run()
309+ @brief determine if we're running in dry run or not
310+ @details
311+ A "dry run" is one where we don't actually make any
312+ changes and it's determined by the global `DRY_RUN`.
313+ When `DRY_RUN` is set to `False` or `no`, then we'll
314+ return False (perform write operations); when it's
315+ set to `True` or `yes, we return `True` (do NOT
316+ perform write operations); if it's set to anything else,
317+ we return `True` (do NOT perform write operations).
318+
319+ By default, we return True (don't perform write
320+ operations) just to be safe.
321+ @param value the value to evaluate (default: DRY_RUN)
322+ @retval False perform write operations
323+ @retval True don't perform write operations
324+ @par Examples
325+ @code
326+ if is_dry_run():
327+ print("We won't do the thing")
328+ else:
329+ print("We're going to do the thing!!!")
330+ @endcode
331+ """
332+
333+ normalized_value = value .lower ()
334+
335+ if normalized_value in ("true" , "yes" ):
336+ return True
337+ if normalized_value in ("false" , "no" ):
338+ return False
339+
340+ return True
341+
342+
343+ def add_member_to_team (member_object , team_object ):
344+ """
345+ @fn add_member_to_team()
346+ @brief add a member to a team
347+ @details
348+ This will add a member object to a team object
349+ @param member_object the object that represents the user
350+ @param team_object the object that represents the team
351+ @retval True if the addition was successful
352+ @retval False if the addition failed
353+ @retval None otherwise (e.g., dry run)
354+ @par Examples
355+ @code
356+ add_member_to_team(wes_object, awesome_people_team_object)
357+ @endcode
358+ """
359+
360+ team_name = team_object .name
361+ member_name = member_object .login
362+
363+ logging .info ("Received member '%s' to add to team '%s'" , member_name , team_name )
364+
365+ if is_dry_run ():
366+ logging .debug ("In DRY_RUN, so not interacting with GitHub API" )
367+ return None
368+
369+ logging .debug ("Not in DRY_RUN, so interacting with GitHub API" )
370+ return team_object .add_membership (member_name )
371+
372+
373+ def remove_member_from_team (member_object , team_object ):
374+ """
375+ @fn remove_member_from_team()
376+ @brief remove a member from a team
377+ @details
378+ This will remove a member object from a team object. It's the
379+ opposite of add_member_to_team().
380+ @param member_object the object that represents the user
381+ @param team_object the object that represents the team
382+ @retval True if the removal was successful
383+ @retval False if the removal failed
384+ @retval None otherwise (e.g., dry run)
385+ @par Examples
386+ @code
387+ remove_member_from_team(bad_guy_object, winners_team_object)
388+ @endcode
389+ """
390+
391+ team_name = team_object .name
392+ member_name = member_object .login
393+
394+ logging .info (
395+ "Received member '%s' to remove from team '%s'" , member_name , team_name
396+ )
397+
398+ if is_dry_run ():
399+ logging .debug ("In DRY_RUN, so not interacting with GitHub API" )
400+ return None
401+
402+ logging .debug ("Not in DRY_RUN, so interacting with GitHub API" )
403+ return team_object .remove_member (member_name )
404+
405+
262406def main ():
263407 """
264408 @fn main()
@@ -271,36 +415,38 @@ def main():
271415
272416 github = Github (login_or_token = PAT , base_url = API_URL )
273417
274- organization = github .get_organization (ORG )
418+ organization_object = github .get_organization (ORG )
275419
276- team = create_team_if_not_exists (TEAM_NAME , organization )
420+ team_object = create_team_if_not_exists (TEAM_NAME , organization_object )
277421
278- current_org_members = get_group_logins (organization )
279- current_team_members = get_group_logins (team )
422+ current_org_members = get_group_logins (organization_object )
423+ current_team_members = get_group_logins (team_object )
424+
425+ member_count = 0
426+ member_total = len (current_org_members )
280427
281428 for member in current_org_members :
282429 member_object = github .get_user (member )
430+ member_count += 1
431+
432+ logging .info ("%i / %i: %s" , member_count , member_total , member )
433+
283434 if current_team_members [member ] and allow_user (member_object ) is False :
284435 logging .info (
285436 "'%s' is in the team but shouldn't be, so removing them" , member
286437 )
287- if not DRY_RUN .lower == "false" :
288- team .remove_membership (member )
289- else :
290- logging .info ("dry run so not removing the user" )
438+ remove_member_from_team (member_object , team_object )
291439 elif not current_team_members [member ] and allow_user (member_object ) in (
292440 None ,
293441 True ,
294442 ):
295443 logging .info ("'%s' is in the org but not the team, so adding them" , member )
296- if not DRY_RUN .lower == "false" :
297- team .add_membership (member )
298- else :
299- logging .info ("dry run so not adding the user" )
444+ add_member_to_team (member_object , team_object )
300445 else :
301- logging .debug ("No action required for '%s'" , member )
446+ logging .info ("No action required for '%s'" , member )
302447
303- time .sleep (DELAY )
448+ if member_count != member_total :
449+ time .sleep (DELAY )
304450
305451
306452if __name__ == "__main__" :
0 commit comments