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