-
Notifications
You must be signed in to change notification settings - Fork 91
/
Copy pathgithub.py
180 lines (154 loc) · 6.86 KB
/
github.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
"""
GitHub API
"""
import sys
import os.path
import json
import logging
import pprint
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import sublime
import sublime_requests as requests
from requests.exceptions import ConnectionError
logging.basicConfig(format='%(asctime)s %(message)s')
logger = logging.getLogger()
class GitHubApi(object):
"Encapsulates the GitHub API"
PER_PAGE = 100
etags = {}
cache = {}
class UnauthorizedException(Exception):
"Raised if we get a 401 from GitHub"
pass
class OTPNeededException(Exception):
"Raised if 2FA is configured and we need a one-time password"
pass
class UnknownException(Exception):
"Raised if we get a response code we don't recognize from GitHub"
pass
class ConnectionException(Exception):
"Raised if we get a ConnectionError"
pass
class NullResponseException(Exception):
"Raised if we get an empty response (i.e., CurlSession failure)"
pass
def __init__(self, base_uri="https://api.github.com", token=None, debug=False, proxies=None, force_curl=False):
self.base_uri = base_uri
self.token = token
self.debug = debug
self.proxies = proxies
if debug:
try:
import http.client as httplib
except ImportError:
import httplib
httplib.HTTPConnection.debuglevel = 1
logger.setLevel(logging.DEBUG)
requests_log = logging.getLogger("requests.packages.urllib3")
requests_log.setLevel(logging.DEBUG)
requests_log.propagate = True
# set up requests session with the root CA cert bundle
cert_path = os.path.join(sublime.packages_path(), "sublime-github", "ca-bundle.crt")
if not os.path.isfile(cert_path):
logger.warning("Root CA cert bundle not found at %s! Not verifying requests.", cert_path)
cert_path = None
self.rsession = requests.session(verify=cert_path,
force_curl=force_curl)
def get_token(self, username, password, one_time_password=None):
auth_data = {
"scopes": ["gist"],
"note": "Sublime GitHub",
"note_url": "https://github.com/bgreenlee/sublime-github"
}
headers = {'X-GitHub-OTP': one_time_password} if one_time_password else {}
resp = self.rsession.post(self.base_uri + "/authorizations",
headers=headers,
auth=(username, password),
proxies=self.proxies,
data=json.dumps(auth_data))
if resp.status_code == requests.codes.CREATED:
logger.debug(pprint.saferepr(resp))
data = json.loads(resp.text)
return data["token"]
elif resp.status_code == requests.codes.UNAUTHORIZED:
if resp.headers['X-GitHub-OTP'].startswith('required'):
raise self.OTPNeededException()
else:
raise self.UnauthorizedException()
else:
raise self.UnknownException("%d %s" % (resp.status_code, resp.text))
def post(self, endpoint, data=None, content_type='application/json'):
return self.request('post', endpoint, data=data, content_type=content_type)
def patch(self, endpoint, data=None, content_type='application/json'):
return self.request('patch', endpoint, data=data, content_type=content_type)
def get(self, endpoint, params=None):
return self.request('get', endpoint, params=params)
def request(self, method, url, params=None, data=None, content_type=None):
if not url.startswith("http"):
url = self.base_uri + url
if data:
data = json.dumps(data)
headers = {"Authorization": "token %s" % self.token}
if content_type:
headers["Content-Type"] = content_type
# add an etag to the header if we have one
if method == 'get' and url in self.etags:
headers["If-None-Match"] = self.etags[url]
logger.debug("request: %s %s %s %s", method, url, headers, params)
try:
resp = self.rsession.request(method, url,
headers=headers,
params=params,
data=data,
proxies=self.proxies,
allow_redirects=True)
if resp is None:
raise self.NullResponseException("Empty response received.")
except ConnectionError as e:
raise self.ConnectionException(
"Connection error, please verify your internet connection: %s", e)
full_url = resp.url
logger.debug("response: %s", resp.headers)
if resp.status_code in [requests.codes.OK,
requests.codes.CREATED,
requests.codes.FOUND,
requests.codes.CONTINUE]:
if 'application/json' in resp.headers['Content-Type']:
resp_data = json.loads(resp.text)
else:
resp_data = resp.text
if method == 'get': # cache the response
etag = resp.headers['ETag']
self.etags[full_url] = etag
self.cache[etag] = resp_data
return resp_data
elif resp.status_code == requests.codes.NOT_MODIFIED:
return self.cache[resp.headers['ETag']]
elif resp.status_code == requests.codes.UNAUTHORIZED:
raise self.UnauthorizedException()
else:
raise self.UnknownException("%d %s" % (resp.status_code, resp.text))
def create_gist(self, description="", filename="", content="", public=False):
return self.post("/gists", {"description": description,
"public": public,
"files": {filename: {"content": content}}})
def get_gist(self, gist):
data = self.get("/gists/" + gist["id"])
return list(data["files"].values())[0]["content"]
def update_gist(self, gist, content):
filename = list(gist["files"].keys())[0]
return self.patch("/gists/" + gist["id"],
{"description": gist["description"],
"files": {filename: {"content": content}}})
def list_gists(self, starred=False):
page = 1
data = []
# fetch all pages
while True:
endpoint = "/gists" + ("/starred" if starred else "")
page_data = self.get(endpoint, params={'page': page, 'per_page': self.PER_PAGE})
data.extend(page_data)
if len(page_data) < self.PER_PAGE:
break
page += 1
return data