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