Skip to content

Commit 5ed1e01

Browse files
authored
Merge pull request #845 from krinsman/step5
Step 5
2 parents d8366d8 + 468485f commit 5ed1e01

File tree

5 files changed

+290
-145
lines changed

5 files changed

+290
-145
lines changed

nbviewer/providers/base.py

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ class BaseHandler(web.RequestHandler):
5757
"""Base Handler class with common utilities"""
5858

5959
def initialize(self, format=None, format_prefix="", **handler_settings):
60+
# format: str, optional
61+
# Rendering format (e.g. script, slides, html)
6062
self.format = format or self.default_format
6163
self.format_prefix = format_prefix
6264
self.http_client = httpclient.AsyncHTTPClient()
@@ -263,13 +265,13 @@ def render_template(self, name, **namespace):
263265
namespace.update(self.template_namespace)
264266
template = self.get_template(name)
265267
return template.render(**namespace)
266-
268+
267269
# Wrappers to facilitate custom rendering in subclasses without having to rewrite entire GET methods
268270
# This would seem to mostly involve creating different template namespaces to enable custom logic in
269271
# extended templates, but there might be other possibilities
270272
def render_status_code_template(self, status_code, **namespace):
271273
return self.render_template('%d.html' % status_code, **namespace)
272-
274+
273275
def render_error_template(self, **namespace):
274276
return self.render_template('error.html', **namespace)
275277

@@ -630,6 +632,31 @@ def filter_formats(self, nb, raw):
630632
except Exception as err:
631633
app_log.info("failed to test %s: %s", self.request.uri, name)
632634

635+
# empty methods to be implemented by subclasses to make GET requests more modular
636+
def get_notebook_data(self, **kwargs):
637+
"""
638+
Pass as kwargs variables needed to define those variables which will be necessary for
639+
the provider to find the notebook. (E.g. path for LocalHandler, user and repo for GitHub.)
640+
Return variables the provider needs to find and load the notebook. Then run custom logic
641+
in GET or pass the output of get_notebook_data immediately to deliver_notebook.
642+
643+
First part of any provider's GET method.
644+
645+
Custom logic, if applicable, is middle part of any provider's GET method, and usually
646+
is implemented or overwritten in subclasses, while get_notebook_data and deliver_notebook
647+
will often remain unchanged from the parent class (e.g. for a custom GitHub provider).
648+
"""
649+
pass
650+
651+
def deliver_notebook(self, **kwargs):
652+
"""
653+
Pass as kwargs the return values of get_notebook_data to this method. Get the JSON data
654+
from the provider to render the notebook. Finish with a call to self.finish_notebook.
655+
656+
Last part of any provider's GET method.
657+
"""
658+
pass
659+
633660
# Wrappers to facilitate custom rendering in subclasses without having to rewrite entire GET methods
634661
# This would seem to mostly involve creating different template namespaces to enable custom logic in
635662
# extended templates, but there might be other possibilities
@@ -658,30 +685,10 @@ def finish_notebook(self, json_notebook, download_url, msg=None,
658685
Notebook document in JSON format
659686
download_url: str
660687
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
667688
msg: str, optional
668689
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
671690
public: bool, optional
672691
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
685692
"""
686693

687694
if msg is None:

nbviewer/providers/gist/handlers.py

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

2424
from .. import _load_handler_from_location
2525

26-
2726
class GistClientMixin(GithubClientMixin):
27+
28+
# PROVIDER_CTX is a dictionary whose entries are passed as keyword arguments
29+
# to the render_template method of the GistHandler. The following describe
30+
# the information contained in each of these keyword arguments:
31+
# provider_label: str
32+
# Text to to apply to the navbar icon linking to the provider
33+
# provider_icon: str
34+
# CSS classname to apply to the navbar icon linking to the provider
35+
# executor_label: str, optional
36+
# Text to apply to the navbar icon linking to the execution service
37+
# executor_icon: str, optional
38+
# CSS classname to apply to the navbar icon linking to the execution service
2839
PROVIDER_CTX = {
2940
'provider_label': 'Gist',
3041
'provider_icon': 'github-square',
3142
'executor_label': 'Binder',
3243
'executor_icon': 'icon-binder',
3344
}
34-
45+
3546
BINDER_TMPL = '{binder_base_url}/gist/{user}/{gist_id}/master'
3647
BINDER_PATH_TMPL = BINDER_TMPL+'?filepath={path}'
37-
48+
3849
def client_error_message(self, exc, url, body, msg=None):
3950
if exc.code == 403 and 'too big' in body.lower():
4051
return 400, "GitHub will not serve raw gists larger than 10MB"
@@ -50,12 +61,18 @@ class UserGistsHandler(GistClientMixin, BaseHandler):
5061
.ipynb file extension is required for listing (not for rendering).
5162
"""
5263
def render_usergists_template(self, entries, user, provider_url, prev_url,
53-
next_url, **namespace):
64+
next_url, **namespace):
65+
"""
66+
provider_url: str
67+
URL to the notebook document upstream at the provider (e.g., GitHub)
68+
executor_url: str, optional (kwarg passed into `namespace`)
69+
URL to execute the notebook document (e.g., Binder)
70+
"""
5471
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-
72+
provider_url=provider_url, prev_url=prev_url,
73+
next_url=next_url, **self.PROVIDER_CTX,
74+
**namespace)
75+
5976
@cached
6077
@gen.coroutine
6178
def get(self, user, **namespace):
@@ -88,14 +105,17 @@ def get(self, user, **namespace):
88105

89106
class GistHandler(GistClientMixin, RenderingHandler):
90107
"""render a gist notebook, or list files if a multifile gist"""
91-
@cached
108+
92109
@gen.coroutine
93-
def get(self, user, gist_id, filename=''):
110+
def parse_gist(self, user, gist_id, filename=''):
111+
94112
with self.catch_client_error():
95113
response = yield self.github_client.get_gist(gist_id)
96114

97115
gist = json.loads(response_text(response))
116+
98117
gist_id=gist['id']
118+
99119
if user is None:
100120
# redirect to /gist/user/gist_id if no user given
101121
owner_dict = gist.get('owner', {})
@@ -111,94 +131,151 @@ def get(self, user, gist_id, filename=''):
111131
return
112132

113133
files = gist['files']
134+
114135
many_files_gist = (len(files) > 1)
115136

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

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')
140+
# Analogous to GitHubTreeHandler
141+
@gen.coroutine
142+
def tree_get(self, user, gist_id, gist, files):
143+
"""
144+
user, gist_id, gist, and files are (most) of the values returned by parse_gist
145+
"""
146+
entries = []
147+
ipynbs = []
148+
others = []
149+
150+
for file in files.values():
151+
e = {}
152+
e['name'] = file['filename']
153+
if file['filename'].endswith('.ipynb'):
154+
e['url'] = quote('/%s/%s' % (gist_id, file['filename']))
155+
e['class'] = 'fa-book'
156+
ipynbs.append(e)
130157
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
158+
provider_url = u"https://gist.github.com/{user}/{gist_id}#file-{clean_name}".format(
159+
user=user,
160+
gist_id=gist_id,
161+
clean_name=clean_filename(file['filename']),
150162
)
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
163+
e['url'] = provider_url
164+
e['class'] = 'fa-share'
165+
others.append(e)
166+
167+
entries.extend(ipynbs)
168+
entries.extend(others)
169+
170+
# Enable a binder navbar icon if a binder base URL is configured
171+
executor_url = self.BINDER_TMPL.format(
172+
binder_base_url=self.binder_base_url,
173+
user=user.rstrip('/'),
174+
gist_id=gist_id
175+
) if self.binder_base_url else None
176+
177+
# provider_url:
178+
# URL to the notebook document upstream at the provider (e.g., GitHub)
179+
# executor_url: str, optional
180+
# URL to execute the notebook document (e.g., Binder)
181+
html = self.render_template(
182+
'treelist.html',
183+
entries=entries,
184+
tree_type='gist',
185+
tree_label='gists',
186+
user=user.rstrip('/'),
187+
provider_url=gist['html_url'],
188+
executor_url=executor_url,
189+
**self.PROVIDER_CTX
190+
)
191+
yield self.cache_and_finish(html)
156192

157-
elif filename:
158-
raise web.HTTPError(404, "No such file in gist: %s (%s)", filename, list(files.keys()))
193+
# Analogous to GitHubBlobHandler
194+
@gen.coroutine
195+
def file_get(self, user, gist_id, filename, gist, many_files_gist, file):
196+
content = yield self.get_notebook_data(gist_id, filename, many_files_gist, file)
197+
198+
if not content:
199+
return
200+
201+
yield self.deliver_notebook(user, gist_id, filename, gist, file, content)
202+
203+
# Only called by file_get
204+
@gen.coroutine
205+
def get_notebook_data(self, gist_id, filename, many_files_gist, file):
206+
"""
207+
gist_id, filename, many_files_gist, file are all passed to file_get
208+
"""
209+
if (file['type'] or '').startswith('image/'):
210+
app_log.debug("Fetching raw image (%s) %s/%s: %s", file['type'], gist_id, filename, file['raw_url'])
211+
response = yield self.fetch(file['raw_url'])
212+
# use raw bytes for images:
213+
content = response.body
214+
elif file['truncated']:
215+
app_log.debug("Gist %s/%s truncated, fetching %s", gist_id, filename, file['raw_url'])
216+
response = yield self.fetch(file['raw_url'])
217+
content = response_text(response, encoding='utf-8')
159218
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)
219+
content = file['content']
220+
221+
if many_files_gist and not filename.endswith('.ipynb'):
222+
self.set_header('Content-Type', file.get('type') or 'text/plain')
223+
# cannot redirect because of X-Frame-Content
224+
self.finish(content)
225+
return
226+
227+
else:
228+
return content
229+
230+
# Only called by file_get
231+
@gen.coroutine
232+
def deliver_notebook(self, user, gist_id, filename, gist, file, content):
233+
"""
234+
user, gist_id, filename, gist, file, are the same values as those
235+
passed into file_get, whereas content is returned from
236+
get_notebook_data using user, gist_id, filename, gist, and file.
237+
"""
238+
# Enable a binder navbar icon if a binder base URL is configured
239+
executor_url = self.BINDER_PATH_TMPL.format(
240+
binder_base_url=self.binder_base_url,
241+
user=user.rstrip('/'),
242+
gist_id=gist_id,
243+
path=quote(filename)
244+
) if self.binder_base_url else None
245+
246+
# provider_url: str, optional
247+
# URL to the notebook document upstream at the provider (e.g., GitHub)
248+
yield self.finish_notebook(
249+
content,
250+
file['raw_url'],
251+
msg="gist: %s" % gist_id,
252+
public=gist['public'],
253+
provider_url=gist['html_url'],
254+
executor_url=executor_url,
255+
**self.PROVIDER_CTX)
256+
257+
@cached
258+
@gen.coroutine
259+
def get(self, user, gist_id, filename=''):
260+
"""
261+
Encompasses both the case of a single file gist, handled by
262+
`file_get`, as well as a many-file gist, handled by `tree_get`.
263+
"""
264+
user, gist_id, gist, files, many_files_gist = yield self.parse_gist(user, gist_id, filename)
265+
266+
if many_files_gist and not filename:
267+
yield self.tree_get(user, gist_id, gist, files)
268+
269+
else:
270+
if not many_files_gist and not filename:
271+
filename = list(files.keys())[0]
272+
273+
if filename not in files:
274+
raise web.HTTPError(404, "No such file in gist: %s (%s)", filename, list(files.keys()))
275+
276+
file = files[filename]
277+
278+
yield self.file_get(user, gist_id, filename, gist, many_files_gist, file)
202279

203280

204281
class GistRedirectHandler(BaseHandler):

0 commit comments

Comments
 (0)