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