##// END OF EJS Templates
Handle all OTP authentication methods....
Matthias Bussonnier -
Show More
@@ -1,303 +1,303 b''
1 """Functions for Github API requests."""
1 """Functions for Github API requests."""
2 from __future__ import print_function
2 from __future__ import print_function
3
3
4 try:
4 try:
5 input = raw_input
5 input = raw_input
6 except NameError:
6 except NameError:
7 pass
7 pass
8
8
9 import os
9 import os
10 import re
10 import re
11 import sys
11 import sys
12
12
13 import requests
13 import requests
14 import getpass
14 import getpass
15 import json
15 import json
16
16
17 try:
17 try:
18 import requests_cache
18 import requests_cache
19 except ImportError:
19 except ImportError:
20 print("cache not available, install `requests_cache` for caching.", file=sys.stderr)
20 print("cache not available, install `requests_cache` for caching.", file=sys.stderr)
21 else:
21 else:
22 requests_cache.install_cache("gh_api", expire_after=3600)
22 requests_cache.install_cache("gh_api", expire_after=3600)
23
23
24 # Keyring stores passwords by a 'username', but we're not storing a username and
24 # Keyring stores passwords by a 'username', but we're not storing a username and
25 # password
25 # password
26 fake_username = 'ipython_tools'
26 fake_username = 'ipython_tools'
27
27
28 class Obj(dict):
28 class Obj(dict):
29 """Dictionary with attribute access to names."""
29 """Dictionary with attribute access to names."""
30 def __getattr__(self, name):
30 def __getattr__(self, name):
31 try:
31 try:
32 return self[name]
32 return self[name]
33 except KeyError:
33 except KeyError:
34 raise AttributeError(name)
34 raise AttributeError(name)
35
35
36 def __setattr__(self, name, val):
36 def __setattr__(self, name, val):
37 self[name] = val
37 self[name] = val
38
38
39 token = None
39 token = None
40 def get_auth_token():
40 def get_auth_token():
41 global token
41 global token
42
42
43 if token is not None:
43 if token is not None:
44 return token
44 return token
45
45
46 import keyring
46 import keyring
47 token = keyring.get_password('github', fake_username)
47 token = keyring.get_password('github', fake_username)
48 if token is not None:
48 if token is not None:
49 return token
49 return token
50
50
51 print("Please enter your github username and password. These are not "
51 print("Please enter your github username and password. These are not "
52 "stored, only used to get an oAuth token. You can revoke this at "
52 "stored, only used to get an oAuth token. You can revoke this at "
53 "any time on Github.\n"
53 "any time on Github.\n"
54 "Username: ", file=sys.stderr, end='')
54 "Username: ", file=sys.stderr, end='')
55 user = input('')
55 user = input('')
56 pw = getpass.getpass("Password: ", stream=sys.stderr)
56 pw = getpass.getpass("Password: ", stream=sys.stderr)
57
57
58 auth_request = {
58 auth_request = {
59 "scopes": [
59 "scopes": [
60 "public_repo",
60 "public_repo",
61 "gist"
61 "gist"
62 ],
62 ],
63 "note": "IPython tools",
63 "note": "IPython tools",
64 "note_url": "https://github.com/ipython/ipython/tree/master/tools",
64 "note_url": "https://github.com/ipython/ipython/tree/master/tools",
65 }
65 }
66 response = requests.post('https://api.github.com/authorizations',
66 response = requests.post('https://api.github.com/authorizations',
67 auth=(user, pw), data=json.dumps(auth_request))
67 auth=(user, pw), data=json.dumps(auth_request))
68 if response.status_code == 401 and \
68 if response.status_code == 401 and \
69 response.headers.get('X-GitHub-OTP') == 'required; sms':
69 'required;' in response.headers.get('X-GitHub-OTP', ''):
70 print("Your login API resquest a SMS one time password", file=sys.stderr)
70 print("Your login API requested a one time password", file=sys.stderr)
71 sms_pw = getpass.getpass("SMS password: ", stream=sys.stderr)
71 otp = getpass.getpass("One Time Password: ", stream=sys.stderr)
72 response = requests.post('https://api.github.com/authorizations',
72 response = requests.post('https://api.github.com/authorizations',
73 auth=(user, pw),
73 auth=(user, pw),
74 data=json.dumps(auth_request),
74 data=json.dumps(auth_request),
75 headers={'X-GitHub-OTP':sms_pw})
75 headers={'X-GitHub-OTP':otp})
76 response.raise_for_status()
76 response.raise_for_status()
77 token = json.loads(response.text)['token']
77 token = json.loads(response.text)['token']
78 keyring.set_password('github', fake_username, token)
78 keyring.set_password('github', fake_username, token)
79 return token
79 return token
80
80
81 def make_auth_header():
81 def make_auth_header():
82 return {'Authorization': 'token ' + get_auth_token()}
82 return {'Authorization': 'token ' + get_auth_token()}
83
83
84 def post_issue_comment(project, num, body):
84 def post_issue_comment(project, num, body):
85 url = 'https://api.github.com/repos/{project}/issues/{num}/comments'.format(project=project, num=num)
85 url = 'https://api.github.com/repos/{project}/issues/{num}/comments'.format(project=project, num=num)
86 payload = json.dumps({'body': body})
86 payload = json.dumps({'body': body})
87 requests.post(url, data=payload, headers=make_auth_header())
87 requests.post(url, data=payload, headers=make_auth_header())
88
88
89 def post_gist(content, description='', filename='file', auth=False):
89 def post_gist(content, description='', filename='file', auth=False):
90 """Post some text to a Gist, and return the URL."""
90 """Post some text to a Gist, and return the URL."""
91 post_data = json.dumps({
91 post_data = json.dumps({
92 "description": description,
92 "description": description,
93 "public": True,
93 "public": True,
94 "files": {
94 "files": {
95 filename: {
95 filename: {
96 "content": content
96 "content": content
97 }
97 }
98 }
98 }
99 }).encode('utf-8')
99 }).encode('utf-8')
100
100
101 headers = make_auth_header() if auth else {}
101 headers = make_auth_header() if auth else {}
102 response = requests.post("https://api.github.com/gists", data=post_data, headers=headers)
102 response = requests.post("https://api.github.com/gists", data=post_data, headers=headers)
103 response.raise_for_status()
103 response.raise_for_status()
104 response_data = json.loads(response.text)
104 response_data = json.loads(response.text)
105 return response_data['html_url']
105 return response_data['html_url']
106
106
107 def get_pull_request(project, num, auth=False):
107 def get_pull_request(project, num, auth=False):
108 """get pull request info by number
108 """get pull request info by number
109 """
109 """
110 url = "https://api.github.com/repos/{project}/pulls/{num}".format(project=project, num=num)
110 url = "https://api.github.com/repos/{project}/pulls/{num}".format(project=project, num=num)
111 if auth:
111 if auth:
112 header = make_auth_header()
112 header = make_auth_header()
113 else:
113 else:
114 header = None
114 header = None
115 print("fetching %s" % url, file=sys.stderr)
115 print("fetching %s" % url, file=sys.stderr)
116 response = requests.get(url, headers=header)
116 response = requests.get(url, headers=header)
117 response.raise_for_status()
117 response.raise_for_status()
118 return json.loads(response.text, object_hook=Obj)
118 return json.loads(response.text, object_hook=Obj)
119
119
120 def get_pull_request_files(project, num, auth=False):
120 def get_pull_request_files(project, num, auth=False):
121 """get list of files in a pull request"""
121 """get list of files in a pull request"""
122 url = "https://api.github.com/repos/{project}/pulls/{num}/files".format(project=project, num=num)
122 url = "https://api.github.com/repos/{project}/pulls/{num}/files".format(project=project, num=num)
123 if auth:
123 if auth:
124 header = make_auth_header()
124 header = make_auth_header()
125 else:
125 else:
126 header = None
126 header = None
127 return get_paged_request(url, headers=header)
127 return get_paged_request(url, headers=header)
128
128
129 element_pat = re.compile(r'<(.+?)>')
129 element_pat = re.compile(r'<(.+?)>')
130 rel_pat = re.compile(r'rel=[\'"](\w+)[\'"]')
130 rel_pat = re.compile(r'rel=[\'"](\w+)[\'"]')
131
131
132 def get_paged_request(url, headers=None, **params):
132 def get_paged_request(url, headers=None, **params):
133 """get a full list, handling APIv3's paging"""
133 """get a full list, handling APIv3's paging"""
134 results = []
134 results = []
135 params.setdefault("per_page", 100)
135 params.setdefault("per_page", 100)
136 while True:
136 while True:
137 if '?' in url:
137 if '?' in url:
138 params = None
138 params = None
139 print("fetching %s" % url, file=sys.stderr)
139 print("fetching %s" % url, file=sys.stderr)
140 else:
140 else:
141 print("fetching %s with %s" % (url, params), file=sys.stderr)
141 print("fetching %s with %s" % (url, params), file=sys.stderr)
142 response = requests.get(url, headers=headers, params=params)
142 response = requests.get(url, headers=headers, params=params)
143 response.raise_for_status()
143 response.raise_for_status()
144 results.extend(response.json())
144 results.extend(response.json())
145 if 'next' in response.links:
145 if 'next' in response.links:
146 url = response.links['next']['url']
146 url = response.links['next']['url']
147 else:
147 else:
148 break
148 break
149 return results
149 return results
150
150
151 def get_pulls_list(project, auth=False, **params):
151 def get_pulls_list(project, auth=False, **params):
152 """get pull request list"""
152 """get pull request list"""
153 params.setdefault("state", "closed")
153 params.setdefault("state", "closed")
154 url = "https://api.github.com/repos/{project}/pulls".format(project=project)
154 url = "https://api.github.com/repos/{project}/pulls".format(project=project)
155 if auth:
155 if auth:
156 headers = make_auth_header()
156 headers = make_auth_header()
157 else:
157 else:
158 headers = None
158 headers = None
159 pages = get_paged_request(url, headers=headers, **params)
159 pages = get_paged_request(url, headers=headers, **params)
160 return pages
160 return pages
161
161
162 def get_issues_list(project, auth=False, **params):
162 def get_issues_list(project, auth=False, **params):
163 """get issues list"""
163 """get issues list"""
164 params.setdefault("state", "closed")
164 params.setdefault("state", "closed")
165 url = "https://api.github.com/repos/{project}/issues".format(project=project)
165 url = "https://api.github.com/repos/{project}/issues".format(project=project)
166 if auth:
166 if auth:
167 headers = make_auth_header()
167 headers = make_auth_header()
168 else:
168 else:
169 headers = None
169 headers = None
170 pages = get_paged_request(url, headers=headers, **params)
170 pages = get_paged_request(url, headers=headers, **params)
171 return pages
171 return pages
172
172
173 def get_milestones(project, auth=False, **params):
173 def get_milestones(project, auth=False, **params):
174 params.setdefault('state', 'all')
174 params.setdefault('state', 'all')
175 url = "https://api.github.com/repos/{project}/milestones".format(project=project)
175 url = "https://api.github.com/repos/{project}/milestones".format(project=project)
176 if auth:
176 if auth:
177 headers = make_auth_header()
177 headers = make_auth_header()
178 else:
178 else:
179 headers = None
179 headers = None
180 milestones = get_paged_request(url, headers=headers, **params)
180 milestones = get_paged_request(url, headers=headers, **params)
181 return milestones
181 return milestones
182
182
183 def get_milestone_id(project, milestone, auth=False, **params):
183 def get_milestone_id(project, milestone, auth=False, **params):
184 milestones = get_milestones(project, auth=auth, **params)
184 milestones = get_milestones(project, auth=auth, **params)
185 for mstone in milestones:
185 for mstone in milestones:
186 if mstone['title'] == milestone:
186 if mstone['title'] == milestone:
187 return mstone['number']
187 return mstone['number']
188 else:
188 else:
189 raise ValueError("milestone %s not found" % milestone)
189 raise ValueError("milestone %s not found" % milestone)
190
190
191 def is_pull_request(issue):
191 def is_pull_request(issue):
192 """Return True if the given issue is a pull request."""
192 """Return True if the given issue is a pull request."""
193 return bool(issue.get('pull_request', {}).get('html_url', None))
193 return bool(issue.get('pull_request', {}).get('html_url', None))
194
194
195 def get_authors(pr):
195 def get_authors(pr):
196 print("getting authors for #%i" % pr['number'], file=sys.stderr)
196 print("getting authors for #%i" % pr['number'], file=sys.stderr)
197 h = make_auth_header()
197 h = make_auth_header()
198 r = requests.get(pr['commits_url'], headers=h)
198 r = requests.get(pr['commits_url'], headers=h)
199 r.raise_for_status()
199 r.raise_for_status()
200 commits = r.json()
200 commits = r.json()
201 authors = []
201 authors = []
202 for commit in commits:
202 for commit in commits:
203 author = commit['commit']['author']
203 author = commit['commit']['author']
204 authors.append("%s <%s>" % (author['name'], author['email']))
204 authors.append("%s <%s>" % (author['name'], author['email']))
205 return authors
205 return authors
206
206
207 # encode_multipart_formdata is from urllib3.filepost
207 # encode_multipart_formdata is from urllib3.filepost
208 # The only change is to iter_fields, to enforce S3's required key ordering
208 # The only change is to iter_fields, to enforce S3's required key ordering
209
209
210 def iter_fields(fields):
210 def iter_fields(fields):
211 fields = fields.copy()
211 fields = fields.copy()
212 for key in ('key', 'acl', 'Filename', 'success_action_status', 'AWSAccessKeyId',
212 for key in ('key', 'acl', 'Filename', 'success_action_status', 'AWSAccessKeyId',
213 'Policy', 'Signature', 'Content-Type', 'file'):
213 'Policy', 'Signature', 'Content-Type', 'file'):
214 yield (key, fields.pop(key))
214 yield (key, fields.pop(key))
215 for (k,v) in fields.items():
215 for (k,v) in fields.items():
216 yield k,v
216 yield k,v
217
217
218 def encode_multipart_formdata(fields, boundary=None):
218 def encode_multipart_formdata(fields, boundary=None):
219 """
219 """
220 Encode a dictionary of ``fields`` using the multipart/form-data mime format.
220 Encode a dictionary of ``fields`` using the multipart/form-data mime format.
221
221
222 :param fields:
222 :param fields:
223 Dictionary of fields or list of (key, value) field tuples. The key is
223 Dictionary of fields or list of (key, value) field tuples. The key is
224 treated as the field name, and the value as the body of the form-data
224 treated as the field name, and the value as the body of the form-data
225 bytes. If the value is a tuple of two elements, then the first element
225 bytes. If the value is a tuple of two elements, then the first element
226 is treated as the filename of the form-data section.
226 is treated as the filename of the form-data section.
227
227
228 Field names and filenames must be unicode.
228 Field names and filenames must be unicode.
229
229
230 :param boundary:
230 :param boundary:
231 If not specified, then a random boundary will be generated using
231 If not specified, then a random boundary will be generated using
232 :func:`mimetools.choose_boundary`.
232 :func:`mimetools.choose_boundary`.
233 """
233 """
234 # copy requests imports in here:
234 # copy requests imports in here:
235 from io import BytesIO
235 from io import BytesIO
236 from requests.packages.urllib3.filepost import (
236 from requests.packages.urllib3.filepost import (
237 choose_boundary, six, writer, b, get_content_type
237 choose_boundary, six, writer, b, get_content_type
238 )
238 )
239 body = BytesIO()
239 body = BytesIO()
240 if boundary is None:
240 if boundary is None:
241 boundary = choose_boundary()
241 boundary = choose_boundary()
242
242
243 for fieldname, value in iter_fields(fields):
243 for fieldname, value in iter_fields(fields):
244 body.write(b('--%s\r\n' % (boundary)))
244 body.write(b('--%s\r\n' % (boundary)))
245
245
246 if isinstance(value, tuple):
246 if isinstance(value, tuple):
247 filename, data = value
247 filename, data = value
248 writer(body).write('Content-Disposition: form-data; name="%s"; '
248 writer(body).write('Content-Disposition: form-data; name="%s"; '
249 'filename="%s"\r\n' % (fieldname, filename))
249 'filename="%s"\r\n' % (fieldname, filename))
250 body.write(b('Content-Type: %s\r\n\r\n' %
250 body.write(b('Content-Type: %s\r\n\r\n' %
251 (get_content_type(filename))))
251 (get_content_type(filename))))
252 else:
252 else:
253 data = value
253 data = value
254 writer(body).write('Content-Disposition: form-data; name="%s"\r\n'
254 writer(body).write('Content-Disposition: form-data; name="%s"\r\n'
255 % (fieldname))
255 % (fieldname))
256 body.write(b'Content-Type: text/plain\r\n\r\n')
256 body.write(b'Content-Type: text/plain\r\n\r\n')
257
257
258 if isinstance(data, int):
258 if isinstance(data, int):
259 data = str(data) # Backwards compatibility
259 data = str(data) # Backwards compatibility
260 if isinstance(data, six.text_type):
260 if isinstance(data, six.text_type):
261 writer(body).write(data)
261 writer(body).write(data)
262 else:
262 else:
263 body.write(data)
263 body.write(data)
264
264
265 body.write(b'\r\n')
265 body.write(b'\r\n')
266
266
267 body.write(b('--%s--\r\n' % (boundary)))
267 body.write(b('--%s--\r\n' % (boundary)))
268
268
269 content_type = b('multipart/form-data; boundary=%s' % boundary)
269 content_type = b('multipart/form-data; boundary=%s' % boundary)
270
270
271 return body.getvalue(), content_type
271 return body.getvalue(), content_type
272
272
273
273
274 def post_download(project, filename, name=None, description=""):
274 def post_download(project, filename, name=None, description=""):
275 """Upload a file to the GitHub downloads area"""
275 """Upload a file to the GitHub downloads area"""
276 if name is None:
276 if name is None:
277 name = os.path.basename(filename)
277 name = os.path.basename(filename)
278 with open(filename, 'rb') as f:
278 with open(filename, 'rb') as f:
279 filedata = f.read()
279 filedata = f.read()
280
280
281 url = "https://api.github.com/repos/{project}/downloads".format(project=project)
281 url = "https://api.github.com/repos/{project}/downloads".format(project=project)
282
282
283 payload = json.dumps(dict(name=name, size=len(filedata),
283 payload = json.dumps(dict(name=name, size=len(filedata),
284 description=description))
284 description=description))
285 response = requests.post(url, data=payload, headers=make_auth_header())
285 response = requests.post(url, data=payload, headers=make_auth_header())
286 response.raise_for_status()
286 response.raise_for_status()
287 reply = json.loads(response.content)
287 reply = json.loads(response.content)
288 s3_url = reply['s3_url']
288 s3_url = reply['s3_url']
289
289
290 fields = dict(
290 fields = dict(
291 key=reply['path'],
291 key=reply['path'],
292 acl=reply['acl'],
292 acl=reply['acl'],
293 success_action_status=201,
293 success_action_status=201,
294 Filename=reply['name'],
294 Filename=reply['name'],
295 AWSAccessKeyId=reply['accesskeyid'],
295 AWSAccessKeyId=reply['accesskeyid'],
296 Policy=reply['policy'],
296 Policy=reply['policy'],
297 Signature=reply['signature'],
297 Signature=reply['signature'],
298 file=(reply['name'], filedata),
298 file=(reply['name'], filedata),
299 )
299 )
300 fields['Content-Type'] = reply['mime_type']
300 fields['Content-Type'] = reply['mime_type']
301 data, content_type = encode_multipart_formdata(fields)
301 data, content_type = encode_multipart_formdata(fields)
302 s3r = requests.post(s3_url, data=data, headers={'Content-Type': content_type})
302 s3r = requests.post(s3_url, data=data, headers={'Content-Type': content_type})
303 return s3r
303 return s3r
General Comments 0
You need to be logged in to leave comments. Login now