mirror of
https://github.com/bluxmit/alnoda-workspaces.git
synced 2024-05-17 04:22:20 +12:00
808 lines
29 KiB
Python
808 lines
29 KiB
Python
|
# -----------------------------------------------------------------------------
|
||
|
# Copyright (C) Jupyter Development Team
|
||
|
#
|
||
|
# Distributed under the terms of the BSD License. The full license is in
|
||
|
# the file COPYING, distributed as part of this software.
|
||
|
# -----------------------------------------------------------------------------
|
||
|
import io
|
||
|
import json
|
||
|
import logging
|
||
|
import os
|
||
|
from concurrent.futures import ProcessPoolExecutor
|
||
|
from concurrent.futures import ThreadPoolExecutor
|
||
|
from html import escape
|
||
|
from urllib.parse import urlparse
|
||
|
|
||
|
import markdown
|
||
|
from jinja2 import Environment
|
||
|
from jinja2 import FileSystemLoader
|
||
|
from nbconvert.exporters.export import exporter_map
|
||
|
from tornado import httpserver
|
||
|
from tornado import ioloop
|
||
|
from tornado import web
|
||
|
from tornado.curl_httpclient import curl_log
|
||
|
from tornado.log import access_log
|
||
|
from tornado.log import app_log
|
||
|
from tornado.log import LogFormatter
|
||
|
from traitlets import Any
|
||
|
from traitlets import Bool
|
||
|
from traitlets import default
|
||
|
from traitlets import Dict
|
||
|
from traitlets import Int
|
||
|
from traitlets import List
|
||
|
from traitlets import Set
|
||
|
from traitlets import Unicode
|
||
|
from traitlets.config import Application
|
||
|
|
||
|
from .cache import AsyncMultipartMemcache
|
||
|
from .cache import DummyAsyncCache
|
||
|
from .cache import MockCache
|
||
|
from .cache import pylibmc
|
||
|
from .client import NBViewerAsyncHTTPClient as HTTPClientClass
|
||
|
from .formats import default_formats
|
||
|
from .handlers import init_handlers
|
||
|
from .index import NoSearch
|
||
|
from .log import log_request
|
||
|
from .providers import default_providers
|
||
|
from .providers import default_rewrites
|
||
|
from .ratelimit import RateLimiter
|
||
|
from .utils import git_info
|
||
|
from .utils import jupyter_info
|
||
|
from .utils import url_path_join
|
||
|
|
||
|
try: # Python 3.8
|
||
|
from functools import cached_property
|
||
|
except ImportError:
|
||
|
from .utils import cached_property
|
||
|
|
||
|
from jupyter_server.base.handlers import FileFindHandler as StaticFileHandler
|
||
|
|
||
|
# -----------------------------------------------------------------------------
|
||
|
# Code
|
||
|
# -----------------------------------------------------------------------------
|
||
|
|
||
|
here = os.path.dirname(__file__)
|
||
|
pjoin = os.path.join
|
||
|
|
||
|
|
||
|
def nrhead():
|
||
|
try:
|
||
|
import newrelic.agent
|
||
|
except ImportError:
|
||
|
return ""
|
||
|
return newrelic.agent.get_browser_timing_header()
|
||
|
|
||
|
|
||
|
def nrfoot():
|
||
|
try:
|
||
|
import newrelic.agent
|
||
|
except ImportError:
|
||
|
return ""
|
||
|
return newrelic.agent.get_browser_timing_footer()
|
||
|
|
||
|
|
||
|
this_dir, this_filename = os.path.split(__file__)
|
||
|
FRONTPAGE_JSON = os.path.join(this_dir, "frontpage.json")
|
||
|
|
||
|
|
||
|
class NBViewer(Application):
|
||
|
|
||
|
name = Unicode("NBViewer")
|
||
|
|
||
|
aliases = Dict(
|
||
|
{
|
||
|
"base-url": "NBViewer.base_url",
|
||
|
"binder-base-url": "NBViewer.binder_base_url",
|
||
|
"cache-expiry-max": "NBViewer.cache_expiry_max",
|
||
|
"cache-expiry-min": "NBViewer.cache_expiry_min",
|
||
|
"config-file": "NBViewer.config_file",
|
||
|
"content-security-policy": "NBViewer.content_security_policy",
|
||
|
"default-format": "NBViewer.default_format",
|
||
|
"frontpage": "NBViewer.frontpage",
|
||
|
"host": "NBViewer.host",
|
||
|
"ipywidgets-base-url": "NBViewer.ipywidgets_base_url",
|
||
|
"jupyter-js-widgets-version": "NBViewer.jupyter_js_widgets_version",
|
||
|
"jupyter-widgets-html-manager-version": "NBViewer.jupyter_widgets_html_manager_version",
|
||
|
"localfiles": "NBViewer.localfiles",
|
||
|
"log-level": "Application.log_level",
|
||
|
"mathjax-url": "NBViewer.mathjax_url",
|
||
|
"mc-threads": "NBViewer.mc_threads",
|
||
|
"port": "NBViewer.port",
|
||
|
"processes": "NBViewer.processes",
|
||
|
"provider-rewrites": "NBViewer.provider_rewrites",
|
||
|
"providers": "NBViewer.providers",
|
||
|
"proxy-host": "NBViewer.proxy_host",
|
||
|
"proxy-port": "NBViewer.proxy_port",
|
||
|
"rate-limit": "NBViewer.rate_limit",
|
||
|
"rate-limit-interval": "NBViewer.rate_limit_interval",
|
||
|
"render-timeout": "NBViewer.render_timeout",
|
||
|
"sslcert": "NBViewer.sslcert",
|
||
|
"sslkey": "NBViewer.sslkey",
|
||
|
"static-path": "NBViewer.static_path",
|
||
|
"static-url-prefix": "NBViewer.static_url_prefix",
|
||
|
"statsd-host": "NBViewer.statsd_host",
|
||
|
"statsd-port": "NBViewer.statsd_port",
|
||
|
"statsd-prefix": "NBViewer.statsd_prefix",
|
||
|
"template-path": "NBViewer.template_path",
|
||
|
"threads": "NBViewer.threads",
|
||
|
}
|
||
|
)
|
||
|
|
||
|
flags = Dict(
|
||
|
{
|
||
|
"debug": (
|
||
|
{"Application": {"log_level": logging.DEBUG}},
|
||
|
"Set log-level to debug, for the most verbose logging.",
|
||
|
),
|
||
|
"generate-config": (
|
||
|
{"NBViewer": {"generate_config": True}},
|
||
|
"Generate default config file.",
|
||
|
),
|
||
|
"localfile-any-user": (
|
||
|
{"NBViewer": {"localfile_any_user": True}},
|
||
|
"Also serve files that are not readable by 'Other' on the local file system.",
|
||
|
),
|
||
|
"localfile-follow-symlinks": (
|
||
|
{"NBViewer": {"localfile_follow_symlinks": True}},
|
||
|
"Resolve/follow symbolic links to their target file using realpath.",
|
||
|
),
|
||
|
"no-cache": ({"NBViewer": {"no_cache": True}}, "Do not cache results."),
|
||
|
"no-check-certificate": (
|
||
|
{"NBViewer": {"no_check_certificate": True}},
|
||
|
"Do not validate SSL certificates.",
|
||
|
),
|
||
|
"y": (
|
||
|
{"NBViewer": {"answer_yes": True}},
|
||
|
"Answer yes to any questions (e.g. confirm overwrite).",
|
||
|
),
|
||
|
"yes": (
|
||
|
{"NBViewer": {"answer_yes": True}},
|
||
|
"Answer yes to any questions (e.g. confirm overwrite).",
|
||
|
),
|
||
|
}
|
||
|
)
|
||
|
|
||
|
# Use this to insert custom configuration of handlers for NBViewer extensions
|
||
|
handler_settings = Dict().tag(config=True)
|
||
|
|
||
|
create_handler = Unicode(
|
||
|
default_value="nbviewer.handlers.CreateHandler",
|
||
|
help="The Tornado handler to use for creation via frontpage form.",
|
||
|
).tag(config=True)
|
||
|
custom404_handler = Unicode(
|
||
|
default_value="nbviewer.handlers.Custom404",
|
||
|
help="The Tornado handler to use for rendering 404 templates.",
|
||
|
).tag(config=True)
|
||
|
faq_handler = Unicode(
|
||
|
default_value="nbviewer.handlers.FAQHandler",
|
||
|
help="The Tornado handler to use for rendering and viewing the FAQ section.",
|
||
|
).tag(config=True)
|
||
|
gist_handler = Unicode(
|
||
|
default_value="nbviewer.providers.gist.handlers.GistHandler",
|
||
|
help="The Tornado handler to use for viewing notebooks stored as GitHub Gists",
|
||
|
).tag(config=True)
|
||
|
github_blob_handler = Unicode(
|
||
|
default_value="nbviewer.providers.github.handlers.GitHubBlobHandler",
|
||
|
help="The Tornado handler to use for viewing notebooks stored as blobs on GitHub",
|
||
|
).tag(config=True)
|
||
|
github_tree_handler = Unicode(
|
||
|
default_value="nbviewer.providers.github.handlers.GitHubTreeHandler",
|
||
|
help="The Tornado handler to use for viewing directory trees on GitHub",
|
||
|
).tag(config=True)
|
||
|
github_user_handler = Unicode(
|
||
|
default_value="nbviewer.providers.github.handlers.GitHubUserHandler",
|
||
|
help="The Tornado handler to use for viewing all of a user's repositories on GitHub.",
|
||
|
).tag(config=True)
|
||
|
index_handler = Unicode(
|
||
|
default_value="nbviewer.handlers.IndexHandler",
|
||
|
help="The Tornado handler to use for rendering the frontpage section.",
|
||
|
).tag(config=True)
|
||
|
local_handler = Unicode(
|
||
|
default_value="nbviewer.providers.local.handlers.LocalFileHandler",
|
||
|
help="The Tornado handler to use for viewing notebooks found on a local filesystem",
|
||
|
).tag(config=True)
|
||
|
url_handler = Unicode(
|
||
|
default_value="nbviewer.providers.url.handlers.URLHandler",
|
||
|
help="The Tornado handler to use for viewing notebooks accessed via URL",
|
||
|
).tag(config=True)
|
||
|
user_gists_handler = Unicode(
|
||
|
default_value="nbviewer.providers.gist.handlers.UserGistsHandler",
|
||
|
help="The Tornado handler to use for viewing directory containing all of a user's Gists",
|
||
|
).tag(config=True)
|
||
|
|
||
|
answer_yes = Bool(
|
||
|
default_value=False,
|
||
|
help="Answer yes to any questions (e.g. confirm overwrite).",
|
||
|
).tag(config=True)
|
||
|
|
||
|
# base_url specified by the user
|
||
|
base_url = Unicode(default_value="/", help="URL base for the server").tag(
|
||
|
config=True
|
||
|
)
|
||
|
|
||
|
binder_base_url = Unicode(
|
||
|
default_value="https://mybinder.org/v2",
|
||
|
help="URL base for binder notebook execution service.",
|
||
|
).tag(config=True)
|
||
|
|
||
|
cache_expiry_max = Int(
|
||
|
default_value=2 * 60 * 60, help="Maximum cache expiry (seconds)."
|
||
|
).tag(config=True)
|
||
|
|
||
|
cache_expiry_min = Int(
|
||
|
default_value=10 * 60, help="Minimum cache expiry (seconds)."
|
||
|
).tag(config=True)
|
||
|
|
||
|
client = Any().tag(config=True)
|
||
|
|
||
|
@default("client")
|
||
|
def _default_client(self):
|
||
|
client = HTTPClientClass(log=self.log)
|
||
|
client.cache = self.cache
|
||
|
return client
|
||
|
|
||
|
config_file = Unicode(
|
||
|
default_value="nbviewer_config.py", help="The config file to load."
|
||
|
).tag(config=True)
|
||
|
|
||
|
content_security_policy = Unicode(
|
||
|
default_value="connect-src 'none';",
|
||
|
help="Content-Security-Policy header setting.",
|
||
|
).tag(config=True)
|
||
|
|
||
|
default_format = Unicode(
|
||
|
default_value="html", help="Format to use for legacy / URLs."
|
||
|
).tag(config=True)
|
||
|
|
||
|
frontpage = Unicode(
|
||
|
default_value=FRONTPAGE_JSON,
|
||
|
help="Path to json file containing frontpage content.",
|
||
|
).tag(config=True)
|
||
|
|
||
|
generate_config = Bool(
|
||
|
default_value=False, help="Generate default config file."
|
||
|
).tag(config=True)
|
||
|
|
||
|
host = Unicode(help="Run on the given interface.").tag(config=True)
|
||
|
|
||
|
@default("host")
|
||
|
def _default_host(self):
|
||
|
return self.default_endpoint["host"]
|
||
|
|
||
|
index = Any().tag(config=True)
|
||
|
|
||
|
@default("index")
|
||
|
def _load_index(self):
|
||
|
if os.environ.get("NBINDEX_PORT"):
|
||
|
self.log.info("Indexing notebooks")
|
||
|
tcp_index = os.environ.get("NBINDEX_PORT")
|
||
|
index_url = tcp_index.split("tcp://")[1]
|
||
|
index_host, index_port = index_url.split(":")
|
||
|
else:
|
||
|
self.log.info("Not indexing notebooks")
|
||
|
indexer = NoSearch()
|
||
|
return indexer
|
||
|
|
||
|
ipywidgets_base_url = Unicode(
|
||
|
default_value="https://unpkg.com/", help="URL base for ipywidgets JS package."
|
||
|
).tag(config=True)
|
||
|
|
||
|
jupyter_js_widgets_version = Unicode(
|
||
|
default_value="*", help="Version specifier for jupyter-js-widgets JS package."
|
||
|
).tag(config=True)
|
||
|
|
||
|
jupyter_widgets_html_manager_version = Unicode(
|
||
|
default_value="*",
|
||
|
help="Version specifier for @jupyter-widgets/html-manager JS package.",
|
||
|
).tag(config=True)
|
||
|
|
||
|
localfile_any_user = Bool(
|
||
|
default_value=False,
|
||
|
help="Also serve files that are not readable by 'Other' on the local file system.",
|
||
|
).tag(config=True)
|
||
|
|
||
|
localfile_follow_symlinks = Bool(
|
||
|
default_value=False,
|
||
|
help="Resolve/follow symbolic links to their target file using realpath.",
|
||
|
).tag(config=True)
|
||
|
|
||
|
localfiles = Unicode(
|
||
|
default_value="",
|
||
|
help="Allow to serve local files under /localfile/* this can be a security risk.",
|
||
|
).tag(config=True)
|
||
|
|
||
|
mathjax_url = Unicode(
|
||
|
default_value="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/",
|
||
|
help="URL base for mathjax package.",
|
||
|
).tag(config=True)
|
||
|
|
||
|
# cache frontpage links for the maximum allowed time
|
||
|
max_cache_uris = Set().tag(config=True)
|
||
|
|
||
|
@default("max_cache_uris")
|
||
|
def _load_max_cache_uris(self):
|
||
|
max_cache_uris = {""}
|
||
|
for section in self.frontpage_setup["sections"]:
|
||
|
for link in section["links"]:
|
||
|
max_cache_uris.add("/" + link["target"])
|
||
|
return max_cache_uris
|
||
|
|
||
|
mc_threads = Int(
|
||
|
default_value=1, help="Number of threads to use for Async Memcache."
|
||
|
).tag(config=True)
|
||
|
|
||
|
no_cache = Bool(default_value=False, help="Do not cache results.").tag(config=True)
|
||
|
|
||
|
no_check_certificate = Bool(
|
||
|
default_value=False, help="Do not validate SSL certificates."
|
||
|
).tag(config=True)
|
||
|
|
||
|
port = Int(help="Run on the given port.").tag(config=True)
|
||
|
|
||
|
@default("port")
|
||
|
def _default_port(self):
|
||
|
return self.default_endpoint["port"]
|
||
|
|
||
|
processes = Int(
|
||
|
default_value=0, help="Use processes instead of threads for rendering."
|
||
|
).tag(config=True)
|
||
|
|
||
|
provider_rewrites = List(
|
||
|
trait=Unicode,
|
||
|
default_value=default_rewrites,
|
||
|
help="Full dotted package(s) that provide `uri_rewrites`.",
|
||
|
).tag(config=True)
|
||
|
|
||
|
providers = List(
|
||
|
trait=Unicode,
|
||
|
default_value=default_providers,
|
||
|
help="Full dotted package(s) that provide `default_handlers`.",
|
||
|
).tag(config=True)
|
||
|
|
||
|
proxy_host = Unicode(default_value="", help="The proxy URL.").tag(config=True)
|
||
|
|
||
|
proxy_port = Int(default_value=-1, help="The proxy port.").tag(config=True)
|
||
|
|
||
|
rate_limit = Int(
|
||
|
default_value=60,
|
||
|
help="Number of requests to allow in rate_limit_interval before limiting. Only requests that trigger a new render are counted.",
|
||
|
).tag(config=True)
|
||
|
|
||
|
rate_limit_interval = Int(
|
||
|
default_value=600, help="Interval (in seconds) for rate limiting."
|
||
|
).tag(config=True)
|
||
|
|
||
|
render_timeout = Int(
|
||
|
default_value=15,
|
||
|
help="Time to wait for a render to complete before showing the 'Working...' page.",
|
||
|
).tag(config=True)
|
||
|
|
||
|
sslcert = Unicode(help="Path to ssl .crt file.").tag(config=True)
|
||
|
|
||
|
sslkey = Unicode(help="Path to ssl .key file.").tag(config=True)
|
||
|
|
||
|
static_path = Unicode(
|
||
|
default_value=os.environ.get("NBVIEWER_STATIC_PATH", ""),
|
||
|
help="Custom path for loading additional static files.",
|
||
|
).tag(config=True)
|
||
|
|
||
|
static_url_prefix = Unicode(default_value="/static/").tag(config=True)
|
||
|
# Not exposed to end user for configuration, since needs to access base_url
|
||
|
_static_url_prefix = Unicode()
|
||
|
|
||
|
@default("_static_url_prefix")
|
||
|
def _load_static_url_prefix(self):
|
||
|
# Last '/' ensures that NBViewer still works regardless of whether user chooses e.g. '/static2/' or '/static2' as their custom prefix
|
||
|
return url_path_join(self._base_url, self.static_url_prefix, "/")
|
||
|
|
||
|
statsd_host = Unicode(
|
||
|
default_value="", help="Host running statsd to send metrics to."
|
||
|
).tag(config=True)
|
||
|
|
||
|
statsd_port = Int(
|
||
|
default_value=8125,
|
||
|
help="Port on which statsd is listening for metrics on statsd_host.",
|
||
|
).tag(config=True)
|
||
|
|
||
|
statsd_prefix = Unicode(
|
||
|
default_value="nbviewer",
|
||
|
help="Prefix to use for naming metrics sent to statsd.",
|
||
|
).tag(config=True)
|
||
|
|
||
|
template_path = Unicode(
|
||
|
default_value=os.environ.get("NBVIEWER_TEMPLATE_PATH", ""),
|
||
|
help="Custom template path for the nbviewer app (not rendered notebooks).",
|
||
|
).tag(config=True)
|
||
|
|
||
|
threads = Int(default_value=1, help="Number of threads to use for rendering.").tag(
|
||
|
config=True
|
||
|
)
|
||
|
|
||
|
# prefer the JupyterHub defined service prefix over the CLI
|
||
|
@cached_property
|
||
|
def _base_url(self):
|
||
|
return os.getenv("JUPYTERHUB_SERVICE_PREFIX", self.base_url)
|
||
|
|
||
|
@cached_property
|
||
|
def cache(self):
|
||
|
memcache_urls = os.environ.get(
|
||
|
"MEMCACHIER_SERVERS", os.environ.get("MEMCACHE_SERVERS")
|
||
|
)
|
||
|
# Handle linked Docker containers
|
||
|
if os.environ.get("NBCACHE_PORT"):
|
||
|
tcp_memcache = os.environ.get("NBCACHE_PORT")
|
||
|
memcache_urls = tcp_memcache.split("tcp://")[1]
|
||
|
if self.no_cache:
|
||
|
self.log.info("Not using cache")
|
||
|
cache = MockCache()
|
||
|
elif pylibmc and memcache_urls:
|
||
|
# setup memcache
|
||
|
mc_pool = ThreadPoolExecutor(self.mc_threads)
|
||
|
kwargs = dict(pool=mc_pool)
|
||
|
username = os.environ.get("MEMCACHIER_USERNAME", "")
|
||
|
password = os.environ.get("MEMCACHIER_PASSWORD", "")
|
||
|
if username and password:
|
||
|
kwargs["binary"] = True
|
||
|
kwargs["username"] = username
|
||
|
kwargs["password"] = password
|
||
|
self.log.info("Using SASL memcache")
|
||
|
else:
|
||
|
self.log.info("Using plain memcache")
|
||
|
|
||
|
cache = AsyncMultipartMemcache(memcache_urls.split(","), **kwargs)
|
||
|
else:
|
||
|
self.log.info("Using in-memory cache")
|
||
|
cache = DummyAsyncCache()
|
||
|
|
||
|
return cache
|
||
|
|
||
|
@cached_property
|
||
|
def default_endpoint(self):
|
||
|
# check if JupyterHub service options are available to use as defaults
|
||
|
if "JUPYTERHUB_SERVICE_URL" in os.environ:
|
||
|
url = urlparse(os.environ["JUPYTERHUB_SERVICE_URL"])
|
||
|
default_host, default_port = url.hostname, url.port
|
||
|
else:
|
||
|
default_host, default_port = "0.0.0.0", 5000
|
||
|
return {"host": default_host, "port": default_port}
|
||
|
|
||
|
@cached_property
|
||
|
def env(self):
|
||
|
env = Environment(loader=FileSystemLoader(self.template_paths), autoescape=True)
|
||
|
env.filters["markdown"] = markdown.markdown
|
||
|
try:
|
||
|
git_data = git_info(here)
|
||
|
except Exception as e:
|
||
|
self.log.error("Failed to get git info: %s", e)
|
||
|
git_data = {}
|
||
|
else:
|
||
|
git_data["msg"] = escape(git_data["msg"])
|
||
|
|
||
|
if self.no_cache:
|
||
|
# force Jinja2 to recompile template every time
|
||
|
env.globals.update(cache_size=0)
|
||
|
env.globals.update(
|
||
|
nrhead=nrhead,
|
||
|
nrfoot=nrfoot,
|
||
|
git_data=git_data,
|
||
|
jupyter_info=jupyter_info(),
|
||
|
len=len,
|
||
|
)
|
||
|
|
||
|
return env
|
||
|
|
||
|
@cached_property
|
||
|
def fetch_kwargs(self):
|
||
|
fetch_kwargs = dict(connect_timeout=10)
|
||
|
if self.proxy_host:
|
||
|
fetch_kwargs.update(proxy_host=self.proxy_host, proxy_port=self.proxy_port)
|
||
|
self.log.info(
|
||
|
"Using web proxy {proxy_host}:{proxy_port}." "".format(**fetch_kwargs)
|
||
|
)
|
||
|
|
||
|
if self.no_check_certificate:
|
||
|
fetch_kwargs.update(validate_cert=False)
|
||
|
self.log.info("Not validating SSL certificates")
|
||
|
|
||
|
return fetch_kwargs
|
||
|
|
||
|
@cached_property
|
||
|
def formats(self):
|
||
|
return self.configure_formats()
|
||
|
|
||
|
# load frontpage sections
|
||
|
@cached_property
|
||
|
def frontpage_setup(self):
|
||
|
with io.open(self.frontpage, "r") as f:
|
||
|
frontpage_setup = json.load(f)
|
||
|
# check if the JSON has a 'sections' field, otherwise assume it is just a list of sessions,
|
||
|
# and provide the defaults of the other fields
|
||
|
if "sections" not in frontpage_setup:
|
||
|
frontpage_setup = {
|
||
|
"title": "nbviewer",
|
||
|
"subtitle": "A simple way to share Jupyter notebooks",
|
||
|
"show_input": True,
|
||
|
"sections": frontpage_setup,
|
||
|
}
|
||
|
return frontpage_setup
|
||
|
|
||
|
# Attribute inherited from traitlets.config.Application, automatically used to style logs
|
||
|
# https://github.com/ipython/traitlets/blob/master/traitlets/config/application.py#L191
|
||
|
_log_formatter_cls = LogFormatter
|
||
|
# Need Tornado LogFormatter for color logs, keys 'color' and 'end_color' in log_format
|
||
|
|
||
|
# Observed traitlet inherited again from traitlets.config.Application
|
||
|
# https://github.com/ipython/traitlets/blob/master/traitlets/config/application.py#L177
|
||
|
@default("log_level")
|
||
|
def _log_level_default(self):
|
||
|
return logging.INFO
|
||
|
|
||
|
# Ditto the above: https://github.com/ipython/traitlets/blob/master/traitlets/config/application.py#L197
|
||
|
@default("log_format")
|
||
|
def _log_format_default(self):
|
||
|
"""override default log format to include time and color, plus to always display the log level, not just when it's high"""
|
||
|
return "%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s %(module)s:%(lineno)d]%(end_color)s %(message)s"
|
||
|
|
||
|
# For consistency with JupyterHub logs
|
||
|
@default("log_datefmt")
|
||
|
def _log_datefmt_default(self):
|
||
|
"""Exclude date from default date format"""
|
||
|
return "%Y-%m-%d %H:%M:%S"
|
||
|
|
||
|
@cached_property
|
||
|
def pool(self):
|
||
|
if self.processes:
|
||
|
pool = ProcessPoolExecutor(self.processes)
|
||
|
else:
|
||
|
pool = ThreadPoolExecutor(self.threads)
|
||
|
return pool
|
||
|
|
||
|
@cached_property
|
||
|
def rate_limiter(self):
|
||
|
rate_limiter = RateLimiter(
|
||
|
limit=self.rate_limit, interval=self.rate_limit_interval, cache=self.cache
|
||
|
)
|
||
|
return rate_limiter
|
||
|
|
||
|
@cached_property
|
||
|
def static_paths(self):
|
||
|
default_static_path = pjoin(here, "static")
|
||
|
if self.static_path:
|
||
|
self.log.info("Using custom static path {}".format(self.static_path))
|
||
|
static_paths = [self.static_path, default_static_path]
|
||
|
else:
|
||
|
static_paths = [default_static_path]
|
||
|
|
||
|
return static_paths
|
||
|
|
||
|
@cached_property
|
||
|
def template_paths(self):
|
||
|
default_template_path = pjoin(here, "templates")
|
||
|
if self.template_path:
|
||
|
self.log.info("Using custom template path {}".format(self.template_path))
|
||
|
template_paths = [self.template_path, default_template_path]
|
||
|
else:
|
||
|
template_paths = [default_template_path]
|
||
|
|
||
|
return template_paths
|
||
|
|
||
|
def configure_formats(self, formats=None):
|
||
|
"""
|
||
|
Format-specific configuration.
|
||
|
"""
|
||
|
if formats is None:
|
||
|
formats = default_formats()
|
||
|
|
||
|
# This would be better defined in a class
|
||
|
self.config.HTMLExporter.template_file = "basic"
|
||
|
self.config.SlidesExporter.template_file = "slides_reveal"
|
||
|
|
||
|
self.config.TemplateExporter.template_path = [
|
||
|
os.path.join(os.path.dirname(__file__), "templates", "nbconvert")
|
||
|
]
|
||
|
|
||
|
for key, format in formats.items():
|
||
|
exporter_cls = format.get("exporter", exporter_map[key])
|
||
|
if self.processes:
|
||
|
# can't pickle exporter instances,
|
||
|
formats[key]["exporter"] = exporter_cls
|
||
|
else:
|
||
|
formats[key]["exporter"] = exporter_cls(
|
||
|
config=self.config, log=self.log
|
||
|
)
|
||
|
|
||
|
return formats
|
||
|
|
||
|
def init_tornado_application(self):
|
||
|
# handle handlers
|
||
|
handler_names = dict(
|
||
|
create_handler=self.create_handler,
|
||
|
custom404_handler=self.custom404_handler,
|
||
|
faq_handler=self.faq_handler,
|
||
|
gist_handler=self.gist_handler,
|
||
|
github_blob_handler=self.github_blob_handler,
|
||
|
github_tree_handler=self.github_tree_handler,
|
||
|
github_user_handler=self.github_user_handler,
|
||
|
index_handler=self.index_handler,
|
||
|
local_handler=self.local_handler,
|
||
|
url_handler=self.url_handler,
|
||
|
user_gists_handler=self.user_gists_handler,
|
||
|
)
|
||
|
handler_kwargs = {
|
||
|
"handler_names": handler_names,
|
||
|
"handler_settings": self.handler_settings,
|
||
|
}
|
||
|
|
||
|
handlers = init_handlers(
|
||
|
self.formats,
|
||
|
self.providers,
|
||
|
self._base_url,
|
||
|
self.localfiles,
|
||
|
**handler_kwargs
|
||
|
)
|
||
|
|
||
|
# NBConvert config
|
||
|
self.config.NbconvertApp.fileext = "html"
|
||
|
self.config.CSSHTMLHeaderTransformer.enabled = False
|
||
|
|
||
|
# DEBUG env implies both autoreload and log-level
|
||
|
if os.environ.get("DEBUG"):
|
||
|
self.log.setLevel(logging.DEBUG)
|
||
|
|
||
|
# input traitlets to settings
|
||
|
settings = dict(
|
||
|
# Allow FileFindHandler to load static directories from e.g. a Docker container
|
||
|
allow_remote_access=True,
|
||
|
base_url=self._base_url,
|
||
|
binder_base_url=self.binder_base_url,
|
||
|
cache=self.cache,
|
||
|
cache_expiry_max=self.cache_expiry_max,
|
||
|
cache_expiry_min=self.cache_expiry_min,
|
||
|
client=self.client,
|
||
|
config=self.config,
|
||
|
content_security_policy=self.content_security_policy,
|
||
|
default_format=self.default_format,
|
||
|
fetch_kwargs=self.fetch_kwargs,
|
||
|
formats=self.formats,
|
||
|
frontpage_setup=self.frontpage_setup,
|
||
|
google_analytics_id=os.getenv("GOOGLE_ANALYTICS_ID"),
|
||
|
gzip=True,
|
||
|
hub_api_token=os.getenv("JUPYTERHUB_API_TOKEN"),
|
||
|
hub_api_url=os.getenv("JUPYTERHUB_API_URL"),
|
||
|
hub_base_url=os.getenv("JUPYTERHUB_BASE_URL"),
|
||
|
index=self.index,
|
||
|
ipywidgets_base_url=self.ipywidgets_base_url,
|
||
|
jinja2_env=self.env,
|
||
|
jupyter_js_widgets_version=self.jupyter_js_widgets_version,
|
||
|
jupyter_widgets_html_manager_version=self.jupyter_widgets_html_manager_version,
|
||
|
localfile_any_user=self.localfile_any_user,
|
||
|
localfile_follow_symlinks=self.localfile_follow_symlinks,
|
||
|
localfile_path=os.path.abspath(self.localfiles),
|
||
|
log=self.log,
|
||
|
log_function=log_request,
|
||
|
mathjax_url=self.mathjax_url,
|
||
|
max_cache_uris=self.max_cache_uris,
|
||
|
pool=self.pool,
|
||
|
provider_rewrites=self.provider_rewrites,
|
||
|
providers=self.providers,
|
||
|
rate_limiter=self.rate_limiter,
|
||
|
render_timeout=self.render_timeout,
|
||
|
static_handler_class=StaticFileHandler,
|
||
|
# FileFindHandler expects list of static paths, so self.static_path*s* is correct
|
||
|
static_path=self.static_paths,
|
||
|
static_url_prefix=self._static_url_prefix,
|
||
|
statsd_host=self.statsd_host,
|
||
|
statsd_port=self.statsd_port,
|
||
|
statsd_prefix=self.statsd_prefix,
|
||
|
)
|
||
|
|
||
|
if self.localfiles:
|
||
|
self.log.warning(
|
||
|
"Serving local notebooks in %s, this can be a security risk",
|
||
|
self.localfiles,
|
||
|
)
|
||
|
|
||
|
# create the app
|
||
|
self.tornado_application = web.Application(handlers, **settings)
|
||
|
|
||
|
def init_logging(self):
|
||
|
|
||
|
# Note that we inherit a self.log attribute from traitlets.config.Application
|
||
|
# https://github.com/ipython/traitlets/blob/master/traitlets/config/application.py#L209
|
||
|
# as well as a log_level attribute
|
||
|
# https://github.com/ipython/traitlets/blob/master/traitlets/config/application.py#L177
|
||
|
|
||
|
# This prevents double log messages because tornado use a root logger that
|
||
|
# self.log is a child of. The logging module dispatches log messages to a log
|
||
|
# and all of its ancestors until propagate is set to False.
|
||
|
self.log.propagate = False
|
||
|
|
||
|
tornado_log = logging.getLogger("tornado")
|
||
|
# hook up tornado's loggers to our app handlers
|
||
|
for log in (app_log, access_log, tornado_log, curl_log):
|
||
|
# ensure all log statements identify the application they come from
|
||
|
log.name = self.log.name
|
||
|
log.parent = self.log
|
||
|
log.propagate = True
|
||
|
log.setLevel(self.log_level)
|
||
|
|
||
|
# disable curl debug, which logs all headers, info for upstream requests, which is TOO MUCH
|
||
|
curl_log.setLevel(max(self.log_level, logging.INFO))
|
||
|
|
||
|
# Mostly copied from JupyterHub because if it isn't broken then don't fix it.
|
||
|
def write_config_file(self):
|
||
|
"""Write our default config to a .py config file"""
|
||
|
config_file_dir = os.path.dirname(os.path.abspath(self.config_file))
|
||
|
if not os.path.isdir(config_file_dir):
|
||
|
self.exit(
|
||
|
"{} does not exist. The destination directory must exist before generating config file.".format(
|
||
|
config_file_dir
|
||
|
)
|
||
|
)
|
||
|
if os.path.exists(self.config_file) and not self.answer_yes:
|
||
|
answer = ""
|
||
|
|
||
|
def ask():
|
||
|
prompt = "Overwrite %s with default config? [y/N]" % self.config_file
|
||
|
try:
|
||
|
return input(prompt).lower() or "n"
|
||
|
except KeyboardInterrupt:
|
||
|
print("") # empty line
|
||
|
return "n"
|
||
|
|
||
|
answer = ask()
|
||
|
while not answer.startswith(("y", "n")):
|
||
|
print("Please answer 'yes' or 'no'")
|
||
|
answer = ask()
|
||
|
if answer.startswith("n"):
|
||
|
self.exit("Not overwriting config file with default.")
|
||
|
|
||
|
# Inherited method from traitlets.config.Application
|
||
|
config_text = self.generate_config_file()
|
||
|
if isinstance(config_text, bytes):
|
||
|
config_text = config_text.decode("utf8")
|
||
|
print("Writing default config to: %s" % self.config_file)
|
||
|
with open(self.config_file, mode="w") as f:
|
||
|
f.write(config_text)
|
||
|
self.exit("Wrote default config file.")
|
||
|
|
||
|
def __init__(self, *args, **kwargs):
|
||
|
super().__init__(*args, **kwargs)
|
||
|
|
||
|
# parse command line with catch_config_error from traitlets.config.Application
|
||
|
super().initialize(*args, **kwargs)
|
||
|
|
||
|
if self.generate_config:
|
||
|
self.write_config_file()
|
||
|
|
||
|
# Inherited method from traitlets.config.Application
|
||
|
self.load_config_file(self.config_file)
|
||
|
self.init_logging()
|
||
|
self.init_tornado_application()
|
||
|
|
||
|
|
||
|
def main(argv=None):
|
||
|
# create and start the app
|
||
|
nbviewer = NBViewer()
|
||
|
app = nbviewer.tornado_application
|
||
|
|
||
|
# load ssl options
|
||
|
ssl_options = None
|
||
|
if nbviewer.sslcert:
|
||
|
ssl_options = {"certfile": nbviewer.sslcert, "keyfile": nbviewer.sslkey}
|
||
|
|
||
|
http_server = httpserver.HTTPServer(app, xheaders=True, ssl_options=ssl_options)
|
||
|
nbviewer.log.info(
|
||
|
"Listening on %s:%i, path %s",
|
||
|
nbviewer.host,
|
||
|
nbviewer.port,
|
||
|
app.settings["base_url"],
|
||
|
)
|
||
|
|
||
|
http_server.listen(nbviewer.port, nbviewer.host)
|
||
|
ioloop.IOLoop.current().start()
|
||
|
|
||
|
|
||
|
if __name__ == "__main__":
|
||
|
main()
|