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