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