forked from jupyter/nbviewer
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathhandlers.py
363 lines (305 loc) · 12.3 KB
/
handlers.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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
import json
import os
from tornado import web
from .. import _load_handler_from_location
from ...utils import clean_filename
from ...utils import quote
from ...utils import response_text
from ...utils import url_path_join
from ..base import BaseHandler
from ..base import cached
from ..base import RenderingHandler
from ..github.handlers import GithubClientMixin
class GistClientMixin(GithubClientMixin):
# PROVIDER_CTX is a dictionary whose entries are passed as keyword arguments
# to the render_template method of the GistHandler. The following describe
# the information contained in each of these keyword arguments:
# provider_label: str
# Text to to apply to the navbar icon linking to the provider
# provider_icon: str
# CSS classname to apply to the navbar icon linking to the provider
# executor_label: str, optional
# Text to apply to the navbar icon linking to the execution service
# executor_icon: str, optional
# CSS classname to apply to the navbar icon linking to the execution service
PROVIDER_CTX = {
"provider_label": "Gist",
"provider_icon": "github-square",
"executor_label": "Binder",
"executor_icon": "icon-binder",
}
BINDER_TMPL = "{binder_base_url}/gist/{user}/{gist_id}/master"
BINDER_PATH_TMPL = BINDER_TMPL + "?filepath={path}"
def client_error_message(self, exc, url, body, msg=None):
if exc.code == 403 and "too big" in body.lower():
return 400, "GitHub will not serve raw gists larger than 10MB"
return super().client_error_message(exc, url, body, msg)
class UserGistsHandler(GistClientMixin, BaseHandler):
"""list a user's gists containing notebooks
.ipynb file extension is required for listing (not for rendering).
"""
def render_usergists_template(
self, entries, user, provider_url, prev_url, next_url, **namespace
):
"""
provider_url: str
URL to the notebook document upstream at the provider (e.g., GitHub)
executor_url: str, optional (kwarg passed into `namespace`)
URL to execute the notebook document (e.g., Binder)
"""
return self.render_template(
"usergists.html",
entries=entries,
user=user,
provider_url=provider_url,
prev_url=prev_url,
next_url=next_url,
**self.PROVIDER_CTX,
**namespace
)
@cached
async def get(self, user, **namespace):
page = self.get_argument("page", None)
params = {}
if page:
params["page"] = page
with self.catch_client_error():
response = await self.github_client.get_gists(user, params=params)
prev_url, next_url = self.get_page_links(response)
gists = json.loads(response_text(response))
entries = []
for gist in gists:
notebooks = [f for f in gist["files"] if f.endswith(".ipynb")]
if notebooks:
entries.append(
dict(
id=gist["id"],
notebooks=notebooks,
description=gist["description"] or "",
)
)
if self.github_url == "https://github.com/":
gist_base_url = "https://gist.github.com/"
else:
gist_base_url = url_path_join(self.github_url, "gist/")
provider_url = url_path_join(gist_base_url, u"{user}".format(user=user))
html = self.render_usergists_template(
entries=entries,
user=user,
provider_url=provider_url,
prev_url=prev_url,
next_url=next_url,
**namespace
)
await self.cache_and_finish(html)
class GistHandler(GistClientMixin, RenderingHandler):
"""render a gist notebook, or list files if a multifile gist"""
async def parse_gist(self, user, gist_id, filename=""):
with self.catch_client_error():
response = await self.github_client.get_gist(gist_id)
gist = json.loads(response_text(response))
gist_id = gist["id"]
if user is None:
# redirect to /gist/user/gist_id if no user given
owner_dict = gist.get("owner", {})
if owner_dict:
user = owner_dict["login"]
else:
user = "anonymous"
new_url = u"{format}/gist/{user}/{gist_id}".format(
format=self.format_prefix, user=user, gist_id=gist_id
)
if filename:
new_url = new_url + "/" + filename
self.redirect(self.from_base(new_url))
return
files = gist["files"]
many_files_gist = len(files) > 1
# user and gist_id get modified
return user, gist_id, gist, files, many_files_gist
# Analogous to GitHubTreeHandler
async def tree_get(self, user, gist_id, gist, files):
"""
user, gist_id, gist, and files are (most) of the values returned by parse_gist
"""
entries = []
ipynbs = []
others = []
for file in files.values():
e = {}
e["name"] = file["filename"]
if file["filename"].endswith(".ipynb"):
e["url"] = quote("/%s/%s" % (gist_id, file["filename"]))
e["class"] = "fa-book"
ipynbs.append(e)
else:
if self.github_url == "https://github.com/":
gist_base_url = "https://gist.github.com/"
else:
gist_base_url = url_path_join(self.github_url, "gist/")
provider_url = url_path_join(
gist_base_url,
u"{user}/{gist_id}#file-{clean_name}".format(
user=user,
gist_id=gist_id,
clean_name=clean_filename(file["filename"]),
),
)
e["url"] = provider_url
e["class"] = "fa-share"
others.append(e)
entries.extend(ipynbs)
entries.extend(others)
# Enable a binder navbar icon if a binder base URL is configured
executor_url = (
self.BINDER_TMPL.format(
binder_base_url=self.binder_base_url,
user=user.rstrip("/"),
gist_id=gist_id,
)
if self.binder_base_url
else None
)
# provider_url:
# URL to the notebook document upstream at the provider (e.g., GitHub)
# executor_url: str, optional
# URL to execute the notebook document (e.g., Binder)
html = self.render_template(
"treelist.html",
entries=entries,
tree_type="gist",
tree_label="gists",
user=user.rstrip("/"),
provider_url=gist["html_url"],
executor_url=executor_url,
**self.PROVIDER_CTX
)
await self.cache_and_finish(html)
# Analogous to GitHubBlobHandler
async def file_get(self, user, gist_id, filename, gist, many_files_gist, file):
content = await self.get_notebook_data(gist_id, filename, many_files_gist, file)
if not content:
return
await self.deliver_notebook(user, gist_id, filename, gist, file, content)
# Only called by file_get
async def get_notebook_data(self, gist_id, filename, many_files_gist, file):
"""
gist_id, filename, many_files_gist, file are all passed to file_get
"""
if (file["type"] or "").startswith("image/"):
self.log.debug(
"Fetching raw image (%s) %s/%s: %s",
file["type"],
gist_id,
filename,
file["raw_url"],
)
response = await self.fetch(file["raw_url"])
# use raw bytes for images:
content = response.body
elif file["truncated"]:
self.log.debug(
"Gist %s/%s truncated, fetching %s", gist_id, filename, file["raw_url"]
)
response = await self.fetch(file["raw_url"])
content = response_text(response, encoding="utf-8")
else:
content = file["content"]
if many_files_gist and not filename.endswith(".ipynb"):
self.set_header("Content-Type", file.get("type") or "text/plain")
# cannot redirect because of X-Frame-Content
self.finish(content)
return
else:
return content
# Only called by file_get
async def deliver_notebook(self, user, gist_id, filename, gist, file, content):
"""
user, gist_id, filename, gist, file, are the same values as those
passed into file_get, whereas content is returned from
get_notebook_data using user, gist_id, filename, gist, and file.
"""
# Enable a binder navbar icon if a binder base URL is configured
executor_url = (
self.BINDER_PATH_TMPL.format(
binder_base_url=self.binder_base_url,
user=user.rstrip("/"),
gist_id=gist_id,
path=quote(filename),
)
if self.binder_base_url
else None
)
# provider_url: str, optional
# URL to the notebook document upstream at the provider (e.g., GitHub)
await self.finish_notebook(
content,
file["raw_url"],
msg="gist: %s" % gist_id,
public=gist["public"],
provider_url=gist["html_url"],
executor_url=executor_url,
**self.PROVIDER_CTX
)
@cached
async def get(self, user, gist_id, filename=""):
"""
Encompasses both the case of a single file gist, handled by
`file_get`, as well as a many-file gist, handled by `tree_get`.
"""
parsed_gist = await self.parse_gist(user, gist_id, filename)
if parsed_gist is not None:
user, gist_id, gist, files, many_files_gist = parsed_gist
else:
return
if many_files_gist and not filename:
await self.tree_get(user, gist_id, gist, files)
else:
if not many_files_gist and not filename:
filename = list(files.keys())[0]
if filename not in files:
raise web.HTTPError(
404, "No such file in gist: %s (%s)", filename, list(files.keys())
)
file = files[filename]
await self.file_get(user, gist_id, filename, gist, many_files_gist, file)
class GistRedirectHandler(BaseHandler):
"""redirect old /<gist-id> to new /gist/<gist-id>"""
def get(self, gist_id, file=""):
new_url = "%s/gist/%s" % (self.format_prefix, gist_id)
if file:
new_url = "%s/%s" % (new_url, file)
self.log.info("Redirecting %s to %s", self.request.uri, new_url)
self.redirect(self.from_base(new_url))
def default_handlers(handlers=[], **handler_names):
"""Tornado handlers"""
gist_handler = _load_handler_from_location(handler_names["gist_handler"])
user_gists_handler = _load_handler_from_location(
handler_names["user_gists_handler"]
)
return handlers + [
(r"/gist/([^\/]+/)?([0-9]+|[0-9a-f]{20,})", gist_handler, {}),
(r"/gist/([^\/]+/)?([0-9]+|[0-9a-f]{20,})/(?:files/)?(.*)", gist_handler, {}),
(r"/([0-9]+|[0-9a-f]{20,})", GistRedirectHandler, {}),
(r"/([0-9]+|[0-9a-f]{20,})/(.*)", GistRedirectHandler, {}),
(r"/gist/([^\/]+)/?", user_gists_handler, {}),
]
def uri_rewrites(rewrites=[]):
gist_rewrites = [
(r"^([a-f0-9]+)/?$", u"/{0}"),
(r"^https?://gist.github.com/([^\/]+/)?([a-f0-9]+)/?$", u"/{1}"),
]
# github enterprise
if os.environ.get("GITHUB_API_URL", "") != "":
gist_base_url = url_path_join(
os.environ.get("GITHUB_API_URL").split("/api/v3")[0], "gist/"
)
gist_rewrites.extend(
[
# Fetching the Gist ID which is embedded in the URL, but with a different base URL
(r"^" + gist_base_url + r"([^\/]+/)?([a-f0-9]+)/?$", u"/{1}")
]
)
return gist_rewrites + rewrites