##// END OF EJS Templates
add auth and paged request to gh_api
MinRK -
Show More
@@ -1,201 +1,238 b''
1 """Functions for Github authorisation."""
1 """Functions for Github authorisation."""
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
11 import sys
10
12
11 import requests
13 import requests
12 import getpass
14 import getpass
13 import json
15 import json
14
16
15 # Keyring stores passwords by a 'username', but we're not storing a username and
17 # Keyring stores passwords by a 'username', but we're not storing a username and
16 # password
18 # password
17 fake_username = 'ipython_tools'
19 fake_username = 'ipython_tools'
18
20
19 class Obj(dict):
21 class Obj(dict):
20 """Dictionary with attribute access to names."""
22 """Dictionary with attribute access to names."""
21 def __getattr__(self, name):
23 def __getattr__(self, name):
22 try:
24 try:
23 return self[name]
25 return self[name]
24 except KeyError:
26 except KeyError:
25 raise AttributeError(name)
27 raise AttributeError(name)
26
28
27 def __setattr__(self, name, val):
29 def __setattr__(self, name, val):
28 self[name] = val
30 self[name] = val
29
31
30 token = None
32 token = None
31 def get_auth_token():
33 def get_auth_token():
32 global token
34 global token
33
35
34 if token is not None:
36 if token is not None:
35 return token
37 return token
36
38
37 import keyring
39 import keyring
38 token = keyring.get_password('github', fake_username)
40 token = keyring.get_password('github', fake_username)
39 if token is not None:
41 if token is not None:
40 return token
42 return token
41
43
42 print("Please enter your github username and password. These are not "
44 print("Please enter your github username and password. These are not "
43 "stored, only used to get an oAuth token. You can revoke this at "
45 "stored, only used to get an oAuth token. You can revoke this at "
44 "any time on Github.")
46 "any time on Github.")
45 user = input("Username: ")
47 user = input("Username: ")
46 pw = getpass.getpass("Password: ")
48 pw = getpass.getpass("Password: ")
47
49
48 auth_request = {
50 auth_request = {
49 "scopes": [
51 "scopes": [
50 "public_repo",
52 "public_repo",
51 "gist"
53 "gist"
52 ],
54 ],
53 "note": "IPython tools",
55 "note": "IPython tools",
54 "note_url": "https://github.com/ipython/ipython/tree/master/tools",
56 "note_url": "https://github.com/ipython/ipython/tree/master/tools",
55 }
57 }
56 response = requests.post('https://api.github.com/authorizations',
58 response = requests.post('https://api.github.com/authorizations',
57 auth=(user, pw), data=json.dumps(auth_request))
59 auth=(user, pw), data=json.dumps(auth_request))
58 response.raise_for_status()
60 response.raise_for_status()
59 token = json.loads(response.text)['token']
61 token = json.loads(response.text)['token']
60 keyring.set_password('github', fake_username, token)
62 keyring.set_password('github', fake_username, token)
61 return token
63 return token
62
64
63 def make_auth_header():
65 def make_auth_header():
64 return {'Authorization': 'token ' + get_auth_token()}
66 return {'Authorization': 'token ' + get_auth_token()}
65
67
66 def post_issue_comment(project, num, body):
68 def post_issue_comment(project, num, body):
67 url = 'https://api.github.com/repos/{project}/issues/{num}/comments'.format(project=project, num=num)
69 url = 'https://api.github.com/repos/{project}/issues/{num}/comments'.format(project=project, num=num)
68 payload = json.dumps({'body': body})
70 payload = json.dumps({'body': body})
69 requests.post(url, data=payload, headers=make_auth_header())
71 requests.post(url, data=payload, headers=make_auth_header())
70
72
71 def post_gist(content, description='', filename='file', auth=False):
73 def post_gist(content, description='', filename='file', auth=False):
72 """Post some text to a Gist, and return the URL."""
74 """Post some text to a Gist, and return the URL."""
73 post_data = json.dumps({
75 post_data = json.dumps({
74 "description": description,
76 "description": description,
75 "public": True,
77 "public": True,
76 "files": {
78 "files": {
77 filename: {
79 filename: {
78 "content": content
80 "content": content
79 }
81 }
80 }
82 }
81 }).encode('utf-8')
83 }).encode('utf-8')
82
84
83 headers = make_auth_header() if auth else {}
85 headers = make_auth_header() if auth else {}
84 response = requests.post("https://api.github.com/gists", data=post_data, headers=headers)
86 response = requests.post("https://api.github.com/gists", data=post_data, headers=headers)
85 response.raise_for_status()
87 response.raise_for_status()
86 response_data = json.loads(response.text)
88 response_data = json.loads(response.text)
87 return response_data['html_url']
89 return response_data['html_url']
88
90
89 def get_pull_request(project, num):
91 def get_pull_request(project, num, auth=False):
90 """get pull request info by number
92 """get pull request info by number
91 """
93 """
92 url = "https://api.github.com/repos/{project}/pulls/{num}".format(project=project, num=num)
94 url = "https://api.github.com/repos/{project}/pulls/{num}".format(project=project, num=num)
93 response = requests.get(url)
95 if auth:
96 header = make_auth_header()
97 else:
98 header = None
99 response = requests.get(url, headers=header)
94 response.raise_for_status()
100 response.raise_for_status()
95 return json.loads(response.text, object_hook=Obj)
101 return json.loads(response.text, object_hook=Obj)
96
102
97 def get_pulls_list(project):
103 element_pat = re.compile(r'<(.+?)>')
104 rel_pat = re.compile(r'rel=[\'"](\w+)[\'"]')
105
106 def get_paged_request(url, headers=None):
107 """get a full list, handling APIv3's paging"""
108 results = []
109 while True:
110 print("fetching %s" % url, file=sys.stderr)
111 response = requests.get(url, headers=headers)
112 response.raise_for_status()
113 results.extend(response.json())
114 if 'next' in response.links:
115 url = response.links['next']['url']
116 else:
117 break
118 return results
119
120 def get_pulls_list(project, state="closed", auth=False):
98 """get pull request list
121 """get pull request list
99 """
122 """
100 url = "https://api.github.com/repos/{project}/pulls".format(project=project)
123 url = "https://api.github.com/repos/{project}/pulls?state={state}&per_page=100".format(project=project, state=state)
101 response = requests.get(url)
124 if auth:
102 response.raise_for_status()
125 headers = make_auth_header()
103 return json.loads(response.text)
126 else:
127 headers = None
128 pages = get_paged_request(url, headers=headers)
129 return pages
130
131 def get_issues_list(project, state="closed", auth=False):
132 """get pull request list
133 """
134 url = "https://api.github.com/repos/{project}/pulls?state={state}&per_page=100".format(project=project, state=state)
135 if auth:
136 headers = make_auth_header()
137 else:
138 headers = None
139 pages = get_paged_request(url, headers=headers)
140 return pages
104
141
105 # encode_multipart_formdata is from urllib3.filepost
142 # encode_multipart_formdata is from urllib3.filepost
106 # The only change is to iter_fields, to enforce S3's required key ordering
143 # The only change is to iter_fields, to enforce S3's required key ordering
107
144
108 def iter_fields(fields):
145 def iter_fields(fields):
109 fields = fields.copy()
146 fields = fields.copy()
110 for key in ('key', 'acl', 'Filename', 'success_action_status', 'AWSAccessKeyId',
147 for key in ('key', 'acl', 'Filename', 'success_action_status', 'AWSAccessKeyId',
111 'Policy', 'Signature', 'Content-Type', 'file'):
148 'Policy', 'Signature', 'Content-Type', 'file'):
112 yield (key, fields.pop(key))
149 yield (key, fields.pop(key))
113 for (k,v) in fields.items():
150 for (k,v) in fields.items():
114 yield k,v
151 yield k,v
115
152
116 def encode_multipart_formdata(fields, boundary=None):
153 def encode_multipart_formdata(fields, boundary=None):
117 """
154 """
118 Encode a dictionary of ``fields`` using the multipart/form-data mime format.
155 Encode a dictionary of ``fields`` using the multipart/form-data mime format.
119
156
120 :param fields:
157 :param fields:
121 Dictionary of fields or list of (key, value) field tuples. The key is
158 Dictionary of fields or list of (key, value) field tuples. The key is
122 treated as the field name, and the value as the body of the form-data
159 treated as the field name, and the value as the body of the form-data
123 bytes. If the value is a tuple of two elements, then the first element
160 bytes. If the value is a tuple of two elements, then the first element
124 is treated as the filename of the form-data section.
161 is treated as the filename of the form-data section.
125
162
126 Field names and filenames must be unicode.
163 Field names and filenames must be unicode.
127
164
128 :param boundary:
165 :param boundary:
129 If not specified, then a random boundary will be generated using
166 If not specified, then a random boundary will be generated using
130 :func:`mimetools.choose_boundary`.
167 :func:`mimetools.choose_boundary`.
131 """
168 """
132 # copy requests imports in here:
169 # copy requests imports in here:
133 from io import BytesIO
170 from io import BytesIO
134 from requests.packages.urllib3.filepost import (
171 from requests.packages.urllib3.filepost import (
135 choose_boundary, six, writer, b, get_content_type
172 choose_boundary, six, writer, b, get_content_type
136 )
173 )
137 body = BytesIO()
174 body = BytesIO()
138 if boundary is None:
175 if boundary is None:
139 boundary = choose_boundary()
176 boundary = choose_boundary()
140
177
141 for fieldname, value in iter_fields(fields):
178 for fieldname, value in iter_fields(fields):
142 body.write(b('--%s\r\n' % (boundary)))
179 body.write(b('--%s\r\n' % (boundary)))
143
180
144 if isinstance(value, tuple):
181 if isinstance(value, tuple):
145 filename, data = value
182 filename, data = value
146 writer(body).write('Content-Disposition: form-data; name="%s"; '
183 writer(body).write('Content-Disposition: form-data; name="%s"; '
147 'filename="%s"\r\n' % (fieldname, filename))
184 'filename="%s"\r\n' % (fieldname, filename))
148 body.write(b('Content-Type: %s\r\n\r\n' %
185 body.write(b('Content-Type: %s\r\n\r\n' %
149 (get_content_type(filename))))
186 (get_content_type(filename))))
150 else:
187 else:
151 data = value
188 data = value
152 writer(body).write('Content-Disposition: form-data; name="%s"\r\n'
189 writer(body).write('Content-Disposition: form-data; name="%s"\r\n'
153 % (fieldname))
190 % (fieldname))
154 body.write(b'Content-Type: text/plain\r\n\r\n')
191 body.write(b'Content-Type: text/plain\r\n\r\n')
155
192
156 if isinstance(data, int):
193 if isinstance(data, int):
157 data = str(data) # Backwards compatibility
194 data = str(data) # Backwards compatibility
158 if isinstance(data, six.text_type):
195 if isinstance(data, six.text_type):
159 writer(body).write(data)
196 writer(body).write(data)
160 else:
197 else:
161 body.write(data)
198 body.write(data)
162
199
163 body.write(b'\r\n')
200 body.write(b'\r\n')
164
201
165 body.write(b('--%s--\r\n' % (boundary)))
202 body.write(b('--%s--\r\n' % (boundary)))
166
203
167 content_type = b('multipart/form-data; boundary=%s' % boundary)
204 content_type = b('multipart/form-data; boundary=%s' % boundary)
168
205
169 return body.getvalue(), content_type
206 return body.getvalue(), content_type
170
207
171
208
172 def post_download(project, filename, name=None, description=""):
209 def post_download(project, filename, name=None, description=""):
173 """Upload a file to the GitHub downloads area"""
210 """Upload a file to the GitHub downloads area"""
174 if name is None:
211 if name is None:
175 name = os.path.basename(filename)
212 name = os.path.basename(filename)
176 with open(filename, 'rb') as f:
213 with open(filename, 'rb') as f:
177 filedata = f.read()
214 filedata = f.read()
178
215
179 url = "https://api.github.com/repos/{project}/downloads".format(project=project)
216 url = "https://api.github.com/repos/{project}/downloads".format(project=project)
180
217
181 payload = json.dumps(dict(name=name, size=len(filedata),
218 payload = json.dumps(dict(name=name, size=len(filedata),
182 description=description))
219 description=description))
183 response = requests.post(url, data=payload, headers=make_auth_header())
220 response = requests.post(url, data=payload, headers=make_auth_header())
184 response.raise_for_status()
221 response.raise_for_status()
185 reply = json.loads(response.content)
222 reply = json.loads(response.content)
186 s3_url = reply['s3_url']
223 s3_url = reply['s3_url']
187
224
188 fields = dict(
225 fields = dict(
189 key=reply['path'],
226 key=reply['path'],
190 acl=reply['acl'],
227 acl=reply['acl'],
191 success_action_status=201,
228 success_action_status=201,
192 Filename=reply['name'],
229 Filename=reply['name'],
193 AWSAccessKeyId=reply['accesskeyid'],
230 AWSAccessKeyId=reply['accesskeyid'],
194 Policy=reply['policy'],
231 Policy=reply['policy'],
195 Signature=reply['signature'],
232 Signature=reply['signature'],
196 file=(reply['name'], filedata),
233 file=(reply['name'], filedata),
197 )
234 )
198 fields['Content-Type'] = reply['mime_type']
235 fields['Content-Type'] = reply['mime_type']
199 data, content_type = encode_multipart_formdata(fields)
236 data, content_type = encode_multipart_formdata(fields)
200 s3r = requests.post(s3_url, data=data, headers={'Content-Type': content_type})
237 s3r = requests.post(s3_url, data=data, headers={'Content-Type': content_type})
201 return s3r
238 return s3r
General Comments 0
You need to be logged in to leave comments. Login now