Skip to content

Commit ffc6b70

Browse files
committed
New version of github script from IPython
1 parent bef47b4 commit ffc6b70

File tree

2 files changed

+413
-90
lines changed

2 files changed

+413
-90
lines changed

tools/gh_api.py

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

0 commit comments

Comments
 (0)