99import re
1010from getpass import getpass
1111import urllib
12+ import datetime
1213
1314import requests
1415from requests .auth import HTTPBasicAuth
@@ -130,22 +131,78 @@ def GitHub_login(*, username=None, password=None, OTP=None, headers=None):
130131 if two_factor :
131132 if OTP :
132133 print (red ("Invalid authentication code" ))
134+ # For SMS, we have to make a fake request (that will fail without
135+ # the OTP) to get GitHub to send it. See https://github.com/drdoctr/doctr/pull/203
133136 auth_header = base64 .urlsafe_b64encode (bytes (username + ':' + password , 'utf8' )).decode ()
134137 login_kwargs = {'auth' : None , 'headers' : {'Authorization' : 'Basic {}' .format (auth_header )}}
135138 try :
136139 generate_GitHub_token (** login_kwargs )
137- except requests .exceptions .HTTPError :
140+ except ( requests .exceptions .HTTPError , GitHubError ) :
138141 pass
139142 print ("A two-factor authentication code is required:" , two_factor .split (';' )[1 ].strip ())
140143 OTP = input ("Authentication code: " )
141144 return GitHub_login (username = username , password = password , OTP = OTP , headers = headers )
142145
143146 raise AuthenticationFailed ("invalid username or password" )
144147
145- r . raise_for_status ( )
148+ GitHub_raise_for_status ( r )
146149 return {'auth' : auth , 'headers' : headers }
147150
148151
152+ class GitHubError (RuntimeError ):
153+ pass
154+
155+ def GitHub_raise_for_status (r ):
156+ """
157+ Call instead of r.raise_for_status() for GitHub requests
158+
159+ Checks for common GitHub response issues and prints messages for them.
160+ """
161+ # This will happen if the doctr session has been running too long and the
162+ # OTP code gathered from GitHub_login has expired.
163+
164+ # TODO: Refactor the code to re-request the OTP without exiting.
165+ if r .status_code == 401 and r .headers .get ('X-GitHub-OTP' ):
166+ raise GitHubError ("The two-factor authentication code has expired. Please run doctr configure again." )
167+ if r .status_code == 403 and r .headers .get ('X-RateLimit-Remaining' ) == '0' :
168+ reset = int (r .headers ['X-RateLimit-Reset' ])
169+ limit = int (r .headers ['X-RateLimit-Limit' ])
170+ reset_datetime = datetime .datetime .fromtimestamp (reset , datetime .timezone .utc )
171+ relative_reset_datetime = reset_datetime - datetime .datetime .now (datetime .timezone .utc )
172+ # Based on datetime.timedelta.__str__
173+ mm , ss = divmod (relative_reset_datetime .seconds , 60 )
174+ hh , mm = divmod (mm , 60 )
175+ def plural (n ):
176+ return n , abs (n ) != 1 and "s" or ""
177+
178+ s = "%d minute%s" % plural (mm )
179+ if hh :
180+ s = "%d hour%s, " % plural (hh ) + s
181+ if relative_reset_datetime .days :
182+ s = ("%d day%s, " % plural (relative_reset_datetime .days )) + s
183+ authenticated = limit >= 100
184+ message = """\
185+ Your GitHub API rate limit has been hit. GitHub allows {limit} {un}authenticated
186+ requests per hour. See {documentation_url}
187+ for more information.
188+ """ .format (limit = limit , un = "" if authenticated else "un" , documentation_url = r .json ()["documentation_url" ])
189+ if authenticated :
190+ message += """
191+ Note that GitHub's API limits are shared across all oauth applications. A
192+ common cause of hitting the rate limit is the Travis "sync account" button.
193+ """
194+ else :
195+ message += """
196+ You can get a higher API limit by authenticating. Try running doctr configure
197+ again without the --no-upload-key flag.
198+ """
199+ message += """
200+ Your rate limits will reset in {s}.\
201+ """ .format (s = s )
202+ raise GitHubError (message )
203+ r .raise_for_status ()
204+
205+
149206def GitHub_post (data , url , * , auth , headers ):
150207 """
151208 POST the data ``data`` to GitHub.
@@ -154,7 +211,7 @@ def GitHub_post(data, url, *, auth, headers):
154211
155212 """
156213 r = requests .post (url , auth = auth , headers = headers , data = json .dumps (data ))
157- r . raise_for_status ( )
214+ GitHub_raise_for_status ( r )
158215 return r .json ()
159216
160217
@@ -185,7 +242,7 @@ def generate_GitHub_token(*, note="Doctr token for pushing to gh-pages from Trav
185242def delete_GitHub_token (token_id , * , auth , headers ):
186243 """Delete a temporary GitHub token"""
187244 r = requests .delete ('https://api.github.com/authorizations/{id}' .format (id = token_id ), auth = auth , headers = headers )
188- r . raise_for_status ( )
245+ GitHub_raise_for_status ( r )
189246
190247
191248def upload_GitHub_deploy_key (deploy_repo , ssh_key , * , read_only = False ,
@@ -265,7 +322,11 @@ def check_repo_exists(deploy_repo, service='github', *, auth=None, headers=None)
265322 repo = repo ,
266323 service = service ))
267324
268- r .raise_for_status ()
325+ if service == 'github' :
326+ GitHub_raise_for_status (r )
327+ else :
328+ r .raise_for_status ()
329+
269330 private = r .json ().get ('private' , False )
270331
271332 if wiki and not private :
0 commit comments