File size: 13,190 Bytes
d1ceb73 |
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 |
"""Jupyter notebook application."""
from __future__ import annotations
import os
import re
import typing as t
from pathlib import Path
from jupyter_client.utils import ensure_async # type:ignore[attr-defined]
from jupyter_core.application import base_aliases
from jupyter_core.paths import jupyter_config_dir
from jupyter_server.base.handlers import JupyterHandler
from jupyter_server.extension.handler import (
ExtensionHandlerJinjaMixin,
ExtensionHandlerMixin,
)
from jupyter_server.serverapp import flags
from jupyter_server.utils import url_escape, url_is_absolute
from jupyter_server.utils import url_path_join as ujoin
from jupyterlab.commands import ( # type:ignore[import-untyped]
get_app_dir,
get_user_settings_dir,
get_workspaces_dir,
)
from jupyterlab_server import LabServerApp
from jupyterlab_server.config import ( # type:ignore[attr-defined]
LabConfig,
get_page_config,
recursive_update,
)
from jupyterlab_server.handlers import _camelCase, is_url
from notebook_shim.shim import NotebookConfigShimMixin # type:ignore[import-untyped]
from tornado import web
from traitlets import Bool, Unicode, default
from traitlets.config.loader import Config
from ._version import __version__
HERE = Path(__file__).parent.resolve()
Flags = t.Dict[t.Union[str, t.Tuple[str, ...]], t.Tuple[t.Union[t.Dict[str, t.Any], Config], str]]
app_dir = Path(get_app_dir())
version = __version__
# mypy: disable-error-code="no-untyped-call"
class NotebookBaseHandler(ExtensionHandlerJinjaMixin, ExtensionHandlerMixin, JupyterHandler):
"""The base notebook API handler."""
@property
def custom_css(self) -> t.Any:
return self.settings.get("custom_css", True)
def get_page_config(self) -> dict[str, t.Any]:
"""Get the page config."""
config = LabConfig()
app: JupyterNotebookApp = self.extensionapp # type:ignore[assignment]
base_url = self.settings.get("base_url", "/")
page_config_data = self.settings.setdefault("page_config_data", {})
page_config = {
**page_config_data,
"appVersion": version,
"baseUrl": self.base_url,
"terminalsAvailable": self.settings.get("terminals_available", False),
"token": self.settings["token"],
"fullStaticUrl": ujoin(self.base_url, "static", self.name),
"frontendUrl": ujoin(self.base_url, "/"),
"exposeAppInBrowser": app.expose_app_in_browser,
}
server_root = self.settings.get("server_root_dir", "")
server_root = server_root.replace(os.sep, "/")
server_root = os.path.normpath(Path(server_root).expanduser())
try:
# Remove the server_root from pref dir
if self.serverapp.preferred_dir != server_root:
page_config["preferredPath"] = "/" + os.path.relpath(
self.serverapp.preferred_dir, server_root
)
else:
page_config["preferredPath"] = "/"
except Exception:
page_config["preferredPath"] = "/"
mathjax_config = self.settings.get("mathjax_config", "TeX-AMS_HTML-full,Safe")
# TODO Remove CDN usage.
mathjax_url = self.settings.get(
"mathjax_url",
"https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/MathJax.js",
)
if not url_is_absolute(mathjax_url) and not mathjax_url.startswith(self.base_url):
mathjax_url = ujoin(self.base_url, mathjax_url)
page_config.setdefault("mathjaxConfig", mathjax_config)
page_config.setdefault("fullMathjaxUrl", mathjax_url)
page_config.setdefault("jupyterConfigDir", jupyter_config_dir())
# Put all our config in page_config
for name in config.trait_names():
page_config[_camelCase(name)] = getattr(app, name)
# Add full versions of all the urls
for name in config.trait_names():
if not name.endswith("_url"):
continue
full_name = _camelCase("full_" + name)
full_url = getattr(app, name)
if not is_url(full_url):
# Relative URL will be prefixed with base_url
full_url = ujoin(base_url, full_url)
page_config[full_name] = full_url
labextensions_path = app.extra_labextensions_path + app.labextensions_path
recursive_update(
page_config,
get_page_config(
labextensions_path,
logger=self.log,
),
)
return page_config
class TreeHandler(NotebookBaseHandler):
"""A tree page handler."""
@web.authenticated
async def get(self, path: str = "") -> None:
"""
Display appropriate page for given path.
- A directory listing is shown if path is a directory
- Redirected to notebook page if path is a notebook
- Render the raw file if path is any other file
"""
path = path.strip("/")
cm = self.contents_manager
if await ensure_async(cm.dir_exists(path=path)):
if await ensure_async(cm.is_hidden(path)) and not cm.allow_hidden:
self.log.info("Refusing to serve hidden directory, via 404 Error")
raise web.HTTPError(404)
# Set treePath for routing to the directory
page_config = self.get_page_config()
page_config["treePath"] = path
tpl = self.render_template("tree.html", page_config=page_config)
return self.write(tpl)
if await ensure_async(cm.file_exists(path)):
# it's not a directory, we have redirecting to do
model = await ensure_async(cm.get(path, content=False))
if model["type"] == "notebook":
url = ujoin(self.base_url, "notebooks", url_escape(path))
else:
# Return raw content if file is not a notebook
url = ujoin(self.base_url, "files", url_escape(path))
self.log.debug("Redirecting %s to %s", self.request.path, url)
self.redirect(url)
return None
raise web.HTTPError(404)
class ConsoleHandler(NotebookBaseHandler):
"""A console page handler."""
@web.authenticated
def get(self, path: str | None = None) -> t.Any: # noqa: ARG002
"""Get the console page."""
tpl = self.render_template("consoles.html", page_config=self.get_page_config())
return self.write(tpl)
class TerminalHandler(NotebookBaseHandler):
"""A terminal page handler."""
@web.authenticated
def get(self, path: str | None = None) -> t.Any: # noqa: ARG002
"""Get the terminal page."""
tpl = self.render_template("terminals.html", page_config=self.get_page_config())
return self.write(tpl)
class FileHandler(NotebookBaseHandler):
"""A file page handler."""
@web.authenticated
def get(self, path: str | None = None) -> t.Any: # noqa: ARG002
"""Get the file page."""
tpl = self.render_template("edit.html", page_config=self.get_page_config())
return self.write(tpl)
class NotebookHandler(NotebookBaseHandler):
"""A notebook page handler."""
@web.authenticated
def get(self, path: str | None = None) -> t.Any: # noqa: ARG002
"""Get the notebook page."""
tpl = self.render_template("notebooks.html", page_config=self.get_page_config())
return self.write(tpl)
class CustomCssHandler(NotebookBaseHandler):
"""A custom CSS handler."""
@web.authenticated
def get(self) -> t.Any:
"""Get the custom css file."""
self.set_header("Content-Type", "text/css")
page_config = self.get_page_config()
custom_css_file = f"{page_config['jupyterConfigDir']}/custom/custom.css"
if not Path(custom_css_file).is_file():
static_path_root = re.match("^(.*?)static", page_config["staticDir"])
if static_path_root is not None:
custom_dir = static_path_root.groups()[0]
custom_css_file = f"{custom_dir}custom/custom.css"
with Path(custom_css_file).open() as css_f:
return self.write(css_f.read())
aliases = dict(base_aliases)
class JupyterNotebookApp(NotebookConfigShimMixin, LabServerApp): # type:ignore[misc]
"""The notebook server extension app."""
name = "notebook"
app_name = "Jupyter Notebook"
description = "Jupyter Notebook - A web-based notebook environment for interactive computing"
version = version
app_version = Unicode(version, help="The version of the application.")
extension_url = "/"
default_url = Unicode("/tree", config=True, help="The default URL to redirect to from `/`")
file_url_prefix = "/tree"
load_other_extensions = True
app_dir = app_dir
subcommands: dict[str, t.Any] = {}
expose_app_in_browser = Bool(
False,
config=True,
help="Whether to expose the global app instance to browser via window.jupyterapp",
)
custom_css = Bool(
True,
config=True,
help="""Whether custom CSS is loaded on the page.
Defaults to True and custom CSS is loaded.
""",
)
flags: Flags = flags # type:ignore[assignment]
flags["expose-app-in-browser"] = (
{"JupyterNotebookApp": {"expose_app_in_browser": True}},
"Expose the global app instance to browser via window.jupyterapp.",
)
flags["custom-css"] = (
{"JupyterNotebookApp": {"custom_css": True}},
"Load custom CSS in template html files. Default is True",
)
@default("static_dir")
def _default_static_dir(self) -> str:
return str(HERE / "static")
@default("templates_dir")
def _default_templates_dir(self) -> str:
return str(HERE / "templates")
@default("app_settings_dir")
def _default_app_settings_dir(self) -> str:
return str(app_dir / "settings")
@default("schemas_dir")
def _default_schemas_dir(self) -> str:
return str(app_dir / "schemas")
@default("themes_dir")
def _default_themes_dir(self) -> str:
return str(app_dir / "themes")
@default("user_settings_dir")
def _default_user_settings_dir(self) -> str:
return t.cast(str, get_user_settings_dir())
@default("workspaces_dir")
def _default_workspaces_dir(self) -> str:
return t.cast(str, get_workspaces_dir())
def _prepare_templates(self) -> None:
super(LabServerApp, self)._prepare_templates()
self.jinja2_env.globals.update(custom_css=self.custom_css) # type:ignore[has-type]
def server_extension_is_enabled(self, extension: str) -> bool:
"""Check if server extension is enabled."""
if self.serverapp is None:
return False
try:
extension_enabled = (
self.serverapp.extension_manager.extensions[extension].enabled is True
)
except (AttributeError, KeyError, TypeError):
extension_enabled = False
return extension_enabled
def initialize_handlers(self) -> None:
"""Initialize handlers."""
assert self.serverapp is not None # noqa: S101
page_config = self.serverapp.web_app.settings.setdefault("page_config_data", {})
nbclassic_enabled = self.server_extension_is_enabled("nbclassic")
page_config["nbclassic_enabled"] = nbclassic_enabled
# If running under JupyterHub, add more metadata.
if "hub_prefix" in self.serverapp.tornado_settings:
tornado_settings = self.serverapp.tornado_settings
hub_prefix = tornado_settings["hub_prefix"]
page_config["hubPrefix"] = hub_prefix
page_config["hubHost"] = tornado_settings["hub_host"]
page_config["hubUser"] = tornado_settings["user"]
page_config["shareUrl"] = ujoin(hub_prefix, "user-redirect")
# Assume the server_name property indicates running JupyterHub 1.0.
if hasattr(self.serverapp, "server_name"):
page_config["hubServerName"] = self.serverapp.server_name
# avoid setting API token in page config
# $JUPYTERHUB_API_TOKEN identifies the server, not the client
# but at least make sure we don't use the token
# if the serverapp set one
page_config["token"] = ""
self.handlers.append(("/tree(.*)", TreeHandler))
self.handlers.append(("/notebooks(.*)", NotebookHandler))
self.handlers.append(("/edit(.*)", FileHandler))
self.handlers.append(("/consoles/(.*)", ConsoleHandler))
self.handlers.append(("/terminals/(.*)", TerminalHandler))
self.handlers.append(("/custom/custom.css", CustomCssHandler))
super().initialize_handlers()
def initialize(self, argv: list[str] | None = None) -> None: # noqa: ARG002
"""Subclass because the ExtensionApp.initialize() method does not take arguments"""
super().initialize()
main = launch_new_instance = JupyterNotebookApp.launch_instance
if __name__ == "__main__":
main()
|