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