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