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