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