Skip to content

Commit 48b1c49

Browse files
committed
Refactored provider handler GET methods to make more easily extensible/customizable and to allow for inserting custom logic into the middle of the GET method, after the notebook data has been loaded, but before the notebook has been rendered.
1 parent 51317e6 commit 48b1c49

File tree

5 files changed

+260
-145
lines changed

5 files changed

+260
-145
lines changed

nbviewer/providers/base.py

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -263,13 +263,13 @@ def render_template(self, name, **namespace):
263263
namespace.update(self.template_namespace)
264264
template = self.get_template(name)
265265
return template.render(**namespace)
266-
266+
267267
# Wrappers to facilitate custom rendering in subclasses without having to rewrite entire GET methods
268268
# This would seem to mostly involve creating different template namespaces to enable custom logic in
269269
# extended templates, but there might be other possibilities
270270
def render_status_code_template(self, status_code, **namespace):
271271
return self.render_template('%d.html' % status_code, **namespace)
272-
272+
273273
def render_error_template(self, **namespace):
274274
return self.render_template('error.html', **namespace)
275275

@@ -630,10 +630,21 @@ def filter_formats(self, nb, raw):
630630
except Exception as err:
631631
app_log.info("failed to test %s: %s", self.request.uri, name)
632632

633+
# empty methods to be implemented by subclasses to make GET requests more modular
634+
def format_notebook_request(self, **kwargs):
635+
pass
636+
637+
def load_notebook(self, **kwargs):
638+
pass
639+
633640
# Wrappers to facilitate custom rendering in subclasses without having to rewrite entire GET methods
634641
# This would seem to mostly involve creating different template namespaces to enable custom logic in
635642
# extended templates, but there might be other possibilities
636643
def render_notebook_template(self, body, nb, download_url, json_notebook, **namespace):
644+
"""
645+
format: str, optional
646+
Rendering format (e.g., script, slides, html)
647+
"""
637648
return self.render_template(
638649
"formats/%s.html" % self.format,
639650
body=body,
@@ -658,30 +669,10 @@ def finish_notebook(self, json_notebook, download_url, msg=None,
658669
Notebook document in JSON format
659670
download_url: str
660671
URL to download the notebook document
661-
provider_url: str, optional
662-
URL to the notebook document upstream at the provider (e.g., GitHub)
663-
provider_icon: str, optional
664-
CSS classname to apply to the navbar icon linking to the provider
665-
provider_label: str, optional
666-
Text to to apply to the navbar icon linking to the provider
667672
msg: str, optional
668673
Extra information to log when rendering fails
669-
breadcrumbs: list of dict, optional
670-
Breadcrumb 'name' and 'url' to render as links at the top of the notebook page
671674
public: bool, optional
672675
True if the notebook is public and its access indexed, False if not
673-
format: str, optional
674-
Rendering format (e.g., script, slides, html)
675-
request: tornado.httputil.HTTPServerRequest, optional
676-
HTTP request that triggered notebook rendering
677-
title: str, optional
678-
Title to use as the HTML page title (i.e., text on the browser tab)
679-
executor_url: str, optional
680-
URL to execute the notebook document (e.g., Binder)
681-
executor_label: str, optional
682-
Text to apply to the navbar icon linking to the execution service
683-
executor_icon: str, optional
684-
CSS classname to apply to the navbar icon linking to the execution service
685676
"""
686677

687678
if msg is None:

nbviewer/providers/gist/handlers.py

Lines changed: 154 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,27 @@
2323

2424
from .. import _load_handler_from_location
2525

26-
2726
class GistClientMixin(GithubClientMixin):
27+
"""
28+
provider_label: str
29+
Text to to apply to the navbar icon linking to the provider
30+
provider_icon: str
31+
CSS classname to apply to the navbar icon linking to the provider
32+
executor_label: str, optional
33+
Text to apply to the navbar icon linking to the execution service
34+
executor_icon: str, optional
35+
CSS classname to apply to the navbar icon linking to the execution service
36+
"""
2837
PROVIDER_CTX = {
2938
'provider_label': 'Gist',
3039
'provider_icon': 'github-square',
3140
'executor_label': 'Binder',
3241
'executor_icon': 'icon-binder',
3342
}
34-
43+
3544
BINDER_TMPL = '{binder_base_url}/gist/{user}/{gist_id}/master'
3645
BINDER_PATH_TMPL = BINDER_TMPL+'?filepath={path}'
37-
46+
3847
def client_error_message(self, exc, url, body, msg=None):
3948
if exc.code == 403 and 'too big' in body.lower():
4049
return 400, "GitHub will not serve raw gists larger than 10MB"
@@ -50,12 +59,18 @@ class UserGistsHandler(GistClientMixin, BaseHandler):
5059
.ipynb file extension is required for listing (not for rendering).
5160
"""
5261
def render_usergists_template(self, entries, user, provider_url, prev_url,
53-
next_url, **namespace):
62+
next_url, **namespace):
63+
"""
64+
provider_url: str
65+
URL to the notebook document upstream at the provider (e.g., GitHub)
66+
executor_url: str, optional
67+
URL to execute the notebook document (e.g., Binder)
68+
"""
5469
return self.render_template("usergists.html", entries=entries, user=user,
55-
provider_url=provider_url, prev_url=prev_url,
56-
next_url=next_url, **self.PROVIDER_CTX,
57-
**namespace)
58-
70+
provider_url=provider_url, prev_url=prev_url,
71+
next_url=next_url, **self.PROVIDER_CTX,
72+
**namespace)
73+
5974
@cached
6075
@gen.coroutine
6176
def get(self, user, **namespace):
@@ -88,14 +103,17 @@ def get(self, user, **namespace):
88103

89104
class GistHandler(GistClientMixin, RenderingHandler):
90105
"""render a gist notebook, or list files if a multifile gist"""
91-
@cached
106+
92107
@gen.coroutine
93-
def get(self, user, gist_id, filename=''):
108+
def parse_gist(self, user, gist_id, filename=''):
109+
94110
with self.catch_client_error():
95111
response = yield self.github_client.get_gist(gist_id)
96112

97113
gist = json.loads(response_text(response))
114+
98115
gist_id=gist['id']
116+
99117
if user is None:
100118
# redirect to /gist/user/gist_id if no user given
101119
owner_dict = gist.get('owner', {})
@@ -111,94 +129,139 @@ def get(self, user, gist_id, filename=''):
111129
return
112130

113131
files = gist['files']
132+
114133
many_files_gist = (len(files) > 1)
115134

116-
if not many_files_gist and not filename:
117-
filename = list(files.keys())[0]
135+
# user and gist_id get modified
136+
return user, gist_id, gist, files, many_files_gist
118137

119-
if filename and filename in files:
120-
file = files[filename]
121-
if (file['type'] or '').startswith('image/'):
122-
app_log.debug("Fetching raw image (%s) %s/%s: %s", file['type'], gist_id, filename, file['raw_url'])
123-
response = yield self.fetch(file['raw_url'])
124-
# use raw bytes for images:
125-
content = response.body
126-
elif file['truncated']:
127-
app_log.debug("Gist %s/%s truncated, fetching %s", gist_id, filename, file['raw_url'])
128-
response = yield self.fetch(file['raw_url'])
129-
content = response_text(response, encoding='utf-8')
138+
# Analogous to GitHubTreeHandler
139+
@gen.coroutine
140+
def tree_get(self, user, gist_id, gist, files):
141+
"""
142+
provider_url:
143+
URL to the notebook document upstream at the provider (e.g., GitHub)
144+
executor_url: str, optional
145+
URL to execute the notebook document (e.g., Binder)
146+
"""
147+
entries = []
148+
ipynbs = []
149+
others = []
150+
151+
for file in files.values():
152+
e = {}
153+
e['name'] = file['filename']
154+
if file['filename'].endswith('.ipynb'):
155+
e['url'] = quote('/%s/%s' % (gist_id, file['filename']))
156+
e['class'] = 'fa-book'
157+
ipynbs.append(e)
130158
else:
131-
content = file['content']
132-
133-
# Enable a binder navbar icon if a binder base URL is configured
134-
executor_url = self.BINDER_PATH_TMPL.format(
135-
binder_base_url=self.binder_base_url,
136-
user=user.rstrip('/'),
137-
gist_id=gist_id,
138-
path=quote(filename)
139-
) if self.binder_base_url else None
140-
141-
if not many_files_gist or filename.endswith('.ipynb'):
142-
yield self.finish_notebook(
143-
content,
144-
file['raw_url'],
145-
provider_url=gist['html_url'],
146-
executor_url=executor_url,
147-
msg="gist: %s" % gist_id,
148-
public=gist['public'],
149-
**self.PROVIDER_CTX
159+
provider_url = u"https://gist.github.com/{user}/{gist_id}#file-{clean_name}".format(
160+
user=user,
161+
gist_id=gist_id,
162+
clean_name=clean_filename(file['filename']),
150163
)
151-
else:
152-
self.set_header('Content-Type', file.get('type') or 'text/plain')
153-
# cannot redirect because of X-Frame-Content
154-
self.finish(content)
155-
return
164+
e['url'] = provider_url
165+
e['class'] = 'fa-share'
166+
others.append(e)
167+
168+
entries.extend(ipynbs)
169+
entries.extend(others)
170+
171+
# Enable a binder navbar icon if a binder base URL is configured
172+
executor_url = self.BINDER_TMPL.format(
173+
binder_base_url=self.binder_base_url,
174+
user=user.rstrip('/'),
175+
gist_id=gist_id
176+
) if self.binder_base_url else None
177+
178+
html = self.render_template(
179+
'treelist.html',
180+
entries=entries,
181+
tree_type='gist',
182+
tree_label='gists',
183+
user=user.rstrip('/'),
184+
provider_url=gist['html_url'],
185+
executor_url=executor_url,
186+
**self.PROVIDER_CTX
187+
)
188+
yield self.cache_and_finish(html)
189+
190+
# Analogous to GitHubBlobHandler
191+
@gen.coroutine
192+
def file_get(self, user, gist_id, filename, gist, many_files_gist, file):
193+
content = yield self.get_notebook_data(gist_id, filename, many_files_gist, file)
156194

157-
elif filename:
158-
raise web.HTTPError(404, "No such file in gist: %s (%s)", filename, list(files.keys()))
195+
if not content:
196+
return
197+
198+
yield self.deliver_notebook(user, gist_id, filename, gist, file, content)
199+
200+
@gen.coroutine
201+
def get_notebook_data(self, gist_id, filename, many_files_gist, file):
202+
if (file['type'] or '').startswith('image/'):
203+
app_log.debug("Fetching raw image (%s) %s/%s: %s", file['type'], gist_id, filename, file['raw_url'])
204+
response = yield self.fetch(file['raw_url'])
205+
# use raw bytes for images:
206+
content = response.body
207+
elif file['truncated']:
208+
app_log.debug("Gist %s/%s truncated, fetching %s", gist_id, filename, file['raw_url'])
209+
response = yield self.fetch(file['raw_url'])
210+
content = response_text(response, encoding='utf-8')
159211
else:
160-
entries = []
161-
ipynbs = []
162-
others = []
163-
164-
for file in files.values():
165-
e = {}
166-
e['name'] = file['filename']
167-
if file['filename'].endswith('.ipynb'):
168-
e['url'] = quote('/%s/%s' % (gist_id, file['filename']))
169-
e['class'] = 'fa-book'
170-
ipynbs.append(e)
171-
else:
172-
provider_url = u"https://gist.github.com/{user}/{gist_id}#file-{clean_name}".format(
173-
user=user,
174-
gist_id=gist_id,
175-
clean_name=clean_filename(file['filename']),
176-
)
177-
e['url'] = provider_url
178-
e['class'] = 'fa-share'
179-
others.append(e)
180-
181-
entries.extend(ipynbs)
182-
entries.extend(others)
183-
184-
# Enable a binder navbar icon if a binder base URL is configured
185-
executor_url = self.BINDER_TMPL.format(
186-
binder_base_url=self.binder_base_url,
187-
user=user.rstrip('/'),
188-
gist_id=gist_id
189-
) if self.binder_base_url else None
190-
191-
html = self.render_template(
192-
'treelist.html',
193-
entries=entries,
194-
tree_type='gist',
195-
tree_label='gists',
196-
user=user.rstrip('/'),
197-
provider_url=gist['html_url'],
198-
executor_url=executor_url,
199-
**self.PROVIDER_CTX
200-
)
201-
yield self.cache_and_finish(html)
212+
content = file['content']
213+
214+
if many_files_gist and not filename.endswith('.ipynb'):
215+
self.set_header('Content-Type', file.get('type') or 'text/plain')
216+
# cannot redirect because of X-Frame-Content
217+
self.finish(content)
218+
return
219+
220+
else:
221+
return content
222+
223+
@gen.coroutine
224+
def deliver_notebook(self, user, gist_id, filename, gist, file, content):
225+
"""
226+
provider_url: str, optional
227+
URL to the notebook document upstream at the provider (e.g., GitHub)
228+
"""
229+
# Enable a binder navbar icon if a binder base URL is configured
230+
executor_url = self.BINDER_PATH_TMPL.format(
231+
binder_base_url=self.binder_base_url,
232+
user=user.rstrip('/'),
233+
gist_id=gist_id,
234+
path=quote(filename)
235+
) if self.binder_base_url else None
236+
237+
yield self.finish_notebook(
238+
content,
239+
file['raw_url'],
240+
msg="gist: %s" % gist_id,
241+
public=gist['public'],
242+
provider_url=gist['html_url'],
243+
executor_url=executor_url,
244+
**self.PROVIDER_CTX)
245+
246+
@cached
247+
@gen.coroutine
248+
def get(self, user, gist_id, filename=''):
249+
250+
user, gist_id, gist, files, many_files_gist = yield self.parse_gist(user, gist_id, filename)
251+
252+
if many_files_gist and not filename:
253+
yield self.tree_get(user, gist_id, gist, files)
254+
255+
else:
256+
if not many_files_gist and not filename:
257+
filename = list(files.keys())[0]
258+
259+
if filename not in files:
260+
raise web.HTTPError(404, "No such file in gist: %s (%s)", filename, list(files.keys()))
261+
262+
file = files[filename]
263+
264+
yield self.file_get(user, gist_id, filename, gist, many_files_gist, file)
202265

203266

204267
class GistRedirectHandler(BaseHandler):

0 commit comments

Comments
 (0)