1+ #!/usr/bin/env python
12"""
3+ Invite GitHub collaborators listed in a Google Sheet while obeying the
4+ 50-invite / 24-hour cap.
5+
6+ > automate_collaborator_invitations.py \
7+ --drive_url "https://docs.google.com/spreadsheets/d/1Ez5uRvOgvDMkFc9c6mI21kscTKnpiCSh4UkUh_ifLIw
8+ /edit?gid=0#gid=0" \
9+ --gh_token "$GH_PAT" \
10+ --org_name causify-ai \
11+ --repo_name tutorials
12+
13+
214Import as:
315
416import DATA605.TutorialsTask76_automate_collaborator_invitations_from_gsheet as dtacifrgs
517"""
6-
18+ import argparse
19+ import datetime
720import logging
821import subprocess
22+ import sys
923from typing import List
1024
11- # Install pygithub.
12- subprocess .run (
13- ["sudo" , "/venv/bin/pip" , "install" , "--quiet" , " pygithub" ], check = True
14- )
15- import helpers .hgoogle_file_api as hgofiapi
16- from github import Github
25+ # Install required packages and configure.
26+ packages = [
27+ "pygithub" ,
28+ "google-api-python-client" ,
29+ "oauth2client" ,
30+ "gspread" ,
31+ "ratelimit" ,
32+ ]
33+ for pkg in packages :
34+ subprocess .run (
35+ [sys .executable , "-m" , "pip" , "install" , "--quiet" , "--upgrade" , pkg ],
36+ check = True ,
37+ )
1738
1839_LOG = logging .getLogger (__name__ )
1940
20- # Globals.
21- DRIVE_URL = "https://docs.google.com/spreadsheets/d/1Ez5uRvOgvDMkFc9c6mI21kscTKnpiCSh4UkUh_ifLIw/edit?gid=0#gid=0"
22- GH_ACCESS_TOKEN = ""
23- REPO_NAME = "tutorials"
24- ORG_NAME = "causify-ai"
41+ import github
42+ import helpers .hgoogle_drive_api as hgodrapi
43+ import ratelimit
44+
45+ _INVITES_PER_WINDOW = 50
46+ _WINDOW_SECONDS = int (datetime .timedelta (hours = 24 ).total_seconds ())
2547
2648
2749def extract_usernames_from_gsheet (gsheet_url : str ) -> List [str ]:
@@ -31,15 +53,31 @@ def extract_usernames_from_gsheet(gsheet_url: str) -> List[str]:
3153 :param gsheet_url: URL of the Google Sheet
3254 :return: github usernames
3355 """
34- credentials = hgofiapi .get_credentials (
56+ credentials = hgodrapi .get_credentials (
3557 service_key_path = "/app/DATA605/google_secret.json"
3658 )
37- df = hgofiapi .read_google_file (url , credentials = credentials )
38- usernames = df ["github_username" ].tolist ()
59+ df = hgodrapi .read_google_file (gsheet_url , credentials = credentials )
60+ usernames = [
61+ user for user in df ["GitHub user" ].tolist () if user and user .strip ()
62+ ]
3963 _LOG .info ("Usernames = \n %s" , usernames )
4064 return usernames
4165
4266
67+ @ratelimit .sleep_and_retry
68+ @ratelimit .limits (calls = _INVITES_PER_WINDOW , period = _WINDOW_SECONDS )
69+ def _invite (repo , username : str , * , permission : str = "write" ) -> None :
70+ """
71+ Invite one user, limiting it to 50 invites/24h.
72+
73+ :param repo: path to repo
74+ :param username: username to add
75+ :param permission: type of permission
76+ """
77+ repo .add_to_collaborators (username , permission = permission )
78+ _LOG .info ("Invitation sent to %s" , username )
79+
80+
4381def send_invitations (
4482 usernames : List [str ],
4583 gh_access_token : str ,
@@ -54,29 +92,53 @@ def send_invitations(
5492 :param repo_url: URL of the target repository
5593 """
5694 # Initialize GitHub API.
57- g = Github (gh_access_token )
95+ gh = github . Github (gh_access_token )
5896 # Get the repository.
59- repo = g .get_repo (f"{ org_name } /{ repo_name } " )
97+ repo = gh .get_repo (f"{ org_name } /{ repo_name } " )
6098 # Send invitations.
6199 for username in usernames :
62100 try :
63- # Send invitation by adding as collaborator.
64- repo .add_to_collaborators (
65- username ,
66- # TODO Krishna: Update the permission accordingly.
67- permission = "pull" ,
101+ _invite (repo , username )
102+ except github .GithubException as exc :
103+ _LOG .error (
104+ "Failed to invite %s: %s" , username , exc .data .get ("message" )
68105 )
69- _LOG .info (f"Invitation sent to { username } " )
70- except Exception as e :
71- _LOG .error (f"Failed to invite { username } : { str (e )} " )
72106
73107
74- def main ():
75- # Extract usernames from Google Sheet.
76- usernames = extract_usernames_from_gsheet (DRIVE_URL )
77- # Send invitations.
78- send_invitations (usernames , GH_ACCESS_TOKEN , REPO_NAME , ORG_NAME )
108+ def _parse () -> argparse .Namespace :
109+ parser = argparse .ArgumentParser (
110+ description = "Invite GitHub collaborators from a Google Sheet, respecting the 50‑per‑day limit." , # noqa: E501
111+ formatter_class = argparse .ArgumentDefaultsHelpFormatter ,
112+ )
113+ parser .add_argument (
114+ "--drive_url" ,
115+ required = True ,
116+ help = "Google‑Sheet URL with a 'GitHub user' column" ,
117+ )
118+ parser .add_argument (
119+ "--gh_token" ,
120+ required = True ,
121+ help = "GitHub personal‑access token (repo scope)" ,
122+ )
123+ parser .add_argument (
124+ "--repo_name" , required = True , help = "Target repository name (without org)"
125+ )
126+ parser .add_argument (
127+ "--org_name" , required = True , help = "GitHub organisation name"
128+ )
129+ parser .add_argument (
130+ "--log_level" , type = int , default = logging .INFO , help = "Logging verbosity"
131+ )
132+ return parser .parse_args ()
133+
134+
135+ def _main (args : argparse .Namespace ) -> None :
136+ logging .basicConfig (
137+ level = args .log_level , format = "%(asctime)s %(levelname)s %(message)s"
138+ )
139+ usernames = extract_usernames_from_gsheet (args .drive_url )
140+ send_invitations (usernames , args .gh_token , args .repo_name , args .org_name )
79141
80142
81143if __name__ == "__main__" :
82- main ( )
144+ _main ( _parse () )
0 commit comments