# 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 / to new /gist/""" 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