|
"""JupyterLab Server handlers""" |
|
|
|
|
|
|
|
from __future__ import annotations |
|
|
|
import os |
|
import pathlib |
|
import warnings |
|
from functools import lru_cache |
|
from typing import TYPE_CHECKING, Any |
|
from urllib.parse import urlparse |
|
|
|
from jupyter_server.base.handlers import FileFindHandler, JupyterHandler |
|
from jupyter_server.extension.handler import ExtensionHandlerJinjaMixin, ExtensionHandlerMixin |
|
from jupyter_server.utils import url_path_join as ujoin |
|
from tornado import template, web |
|
|
|
from .config import LabConfig, get_page_config, recursive_update |
|
from .licenses_handler import LicensesHandler, LicensesManager |
|
from .listings_handler import ListingsHandler, fetch_listings |
|
from .settings_handler import SettingsHandler |
|
from .settings_utils import _get_overrides |
|
from .themes_handler import ThemesHandler |
|
from .translations_handler import TranslationsHandler |
|
from .workspaces_handler import WorkspacesHandler, WorkspacesManager |
|
|
|
if TYPE_CHECKING: |
|
from .app import LabServerApp |
|
|
|
|
|
|
|
|
|
MASTER_URL_PATTERN = ( |
|
r"/(?P<mode>{}|doc)(?P<workspace>/workspaces/[a-zA-Z0-9\-\_]+)?(?P<tree>/tree/.*)?" |
|
) |
|
|
|
DEFAULT_TEMPLATE = template.Template( |
|
""" |
|
<!DOCTYPE html> |
|
<html> |
|
<head> |
|
<meta charset="utf-8"> |
|
<title>Error</title> |
|
</head> |
|
<body> |
|
<h2>Cannot find template: "{{name}}"</h2> |
|
<p>In "{{path}}"</p> |
|
</body> |
|
</html> |
|
""" |
|
) |
|
|
|
|
|
def is_url(url: str) -> bool: |
|
"""Test whether a string is a full url (e.g. https://nasa.gov) |
|
|
|
https://stackoverflow.com/a/52455972 |
|
""" |
|
try: |
|
result = urlparse(url) |
|
return all([result.scheme, result.netloc]) |
|
except ValueError: |
|
return False |
|
|
|
|
|
class LabHandler(ExtensionHandlerJinjaMixin, ExtensionHandlerMixin, JupyterHandler): |
|
"""Render the JupyterLab View.""" |
|
|
|
@lru_cache |
|
def get_page_config(self) -> dict[str, Any]: |
|
"""Construct the page config object""" |
|
self.application.store_id = getattr( |
|
self.application, "store_id", 0 |
|
) |
|
config = LabConfig() |
|
app: LabServerApp = self.extensionapp |
|
settings_dir = app.app_settings_dir |
|
|
|
page_config = self.settings.setdefault("page_config_data", {}) |
|
terminals = self.settings.get("terminals_available", False) |
|
server_root = self.settings.get("server_root_dir", "") |
|
server_root = server_root.replace(os.sep, "/") |
|
base_url = self.settings.get("base_url") |
|
|
|
|
|
full_static_url = self.static_url_prefix.rstrip("/") |
|
page_config.setdefault("fullStaticUrl", full_static_url) |
|
|
|
page_config.setdefault("terminalsAvailable", terminals) |
|
page_config.setdefault("ignorePlugins", []) |
|
page_config.setdefault("serverRoot", server_root) |
|
page_config["store_id"] = self.application.store_id |
|
|
|
server_root = os.path.normpath(os.path.expanduser(server_root)) |
|
preferred_path = "" |
|
try: |
|
preferred_path = self.serverapp.contents_manager.preferred_dir |
|
except Exception: |
|
|
|
try: |
|
|
|
if self.serverapp.preferred_dir and self.serverapp.preferred_dir != server_root: |
|
preferred_path = ( |
|
pathlib.Path(self.serverapp.preferred_dir) |
|
.relative_to(server_root) |
|
.as_posix() |
|
) |
|
except Exception: |
|
pass |
|
|
|
page_config["preferredPath"] = preferred_path or "/" |
|
|
|
self.application.store_id += 1 |
|
|
|
mathjax_config = self.settings.get("mathjax_config", "TeX-AMS_HTML-full,Safe") |
|
|
|
mathjax_url = self.mathjax_url |
|
if not mathjax_url: |
|
mathjax_url = "https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/MathJax.js" |
|
|
|
page_config.setdefault("mathjaxConfig", mathjax_config) |
|
page_config.setdefault("fullMathjaxUrl", mathjax_url) |
|
|
|
|
|
for name in config.trait_names(): |
|
page_config[_camelCase(name)] = getattr(app, name) |
|
|
|
|
|
for name in config.trait_names(): |
|
if not name.endswith("_url"): |
|
continue |
|
full_name = _camelCase("full_" + name) |
|
full_url = getattr(app, name) |
|
if base_url is not None and not is_url(full_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, settings_dir, logger=self.log) |
|
) |
|
|
|
|
|
page_config_hook = self.settings.get("page_config_hook", None) |
|
if page_config_hook: |
|
page_config = page_config_hook(self, page_config) |
|
|
|
return page_config |
|
|
|
@web.authenticated |
|
@web.removeslash |
|
def get( |
|
self, mode: str | None = None, workspace: str | None = None, tree: str | None = None |
|
) -> None: |
|
"""Get the JupyterLab html page.""" |
|
workspace = "default" if workspace is None else workspace.replace("/workspaces/", "") |
|
tree_path = "" if tree is None else tree.replace("/tree/", "") |
|
|
|
page_config = self.get_page_config() |
|
|
|
|
|
if mode == "doc": |
|
page_config["mode"] = "single-document" |
|
else: |
|
page_config["mode"] = "multiple-document" |
|
page_config["workspace"] = workspace |
|
page_config["treePath"] = tree_path |
|
|
|
|
|
tpl = self.render_template("index.html", page_config=page_config) |
|
self.write(tpl) |
|
|
|
|
|
class NotFoundHandler(LabHandler): |
|
"""A handler for page not found.""" |
|
|
|
@lru_cache |
|
def get_page_config(self) -> dict[str, Any]: |
|
"""Get the page config.""" |
|
|
|
page_config = super().get_page_config().copy() |
|
page_config["notFoundUrl"] = self.request.path |
|
return page_config |
|
|
|
|
|
def add_handlers(handlers: list[Any], extension_app: LabServerApp) -> None: |
|
"""Add the appropriate handlers to the web app.""" |
|
|
|
for name in LabConfig.class_trait_names(): |
|
if not name.endswith("_dir"): |
|
continue |
|
value = getattr(extension_app, name) |
|
setattr(extension_app, name, value.replace(os.sep, "/")) |
|
|
|
|
|
|
|
for name in LabConfig.class_trait_names(): |
|
if not name.endswith("_url"): |
|
continue |
|
value = getattr(extension_app, name) |
|
if is_url(value): |
|
continue |
|
if not value.startswith("/"): |
|
value = "/" + value |
|
if value.endswith("/"): |
|
value = value[:-1] |
|
setattr(extension_app, name, value) |
|
|
|
url_pattern = MASTER_URL_PATTERN.format(extension_app.app_url.replace("/", "")) |
|
handlers.append((url_pattern, LabHandler)) |
|
|
|
|
|
no_cache_paths = [] if extension_app.cache_files else ["/"] |
|
|
|
|
|
labextensions_path = extension_app.extra_labextensions_path + extension_app.labextensions_path |
|
labextensions_url = ujoin(extension_app.labextensions_url, "(.*)") |
|
handlers.append( |
|
( |
|
labextensions_url, |
|
FileFindHandler, |
|
{"path": labextensions_path, "no_cache_paths": no_cache_paths}, |
|
) |
|
) |
|
|
|
|
|
if extension_app.schemas_dir: |
|
|
|
overrides, error = _get_overrides(extension_app.app_settings_dir) |
|
|
|
if error: |
|
overrides_warning = "Failed loading overrides: %s" |
|
extension_app.log.warning(overrides_warning, error) |
|
|
|
settings_config: dict[str, Any] = { |
|
"app_settings_dir": extension_app.app_settings_dir, |
|
"schemas_dir": extension_app.schemas_dir, |
|
"settings_dir": extension_app.user_settings_dir, |
|
"labextensions_path": labextensions_path, |
|
"overrides": overrides, |
|
} |
|
|
|
|
|
settings_path = ujoin(extension_app.settings_url, "?") |
|
handlers.append((settings_path, SettingsHandler, settings_config)) |
|
|
|
|
|
setting_path = ujoin(extension_app.settings_url, "(?P<schema_name>.+)") |
|
handlers.append((setting_path, SettingsHandler, settings_config)) |
|
|
|
|
|
|
|
if extension_app.translations_api_url: |
|
|
|
|
|
translations_path = ujoin(extension_app.translations_api_url, "?") |
|
handlers.append((translations_path, TranslationsHandler, settings_config)) |
|
|
|
|
|
translations_lang_path = ujoin(extension_app.translations_api_url, "(?P<locale>.*)") |
|
handlers.append((translations_lang_path, TranslationsHandler, settings_config)) |
|
|
|
|
|
if extension_app.workspaces_dir: |
|
workspaces_config = {"manager": WorkspacesManager(extension_app.workspaces_dir)} |
|
|
|
|
|
workspaces_api_path = ujoin(extension_app.workspaces_api_url, "?") |
|
handlers.append((workspaces_api_path, WorkspacesHandler, workspaces_config)) |
|
|
|
|
|
workspace_api_path = ujoin(extension_app.workspaces_api_url, "(?P<space_name>.+)") |
|
handlers.append((workspace_api_path, WorkspacesHandler, workspaces_config)) |
|
|
|
|
|
|
|
settings_config = extension_app.settings.get("config", {}).get("LabServerApp", {}) |
|
blocked_extensions_uris: str = settings_config.get("blocked_extensions_uris", "") |
|
allowed_extensions_uris: str = settings_config.get("allowed_extensions_uris", "") |
|
|
|
if (blocked_extensions_uris) and (allowed_extensions_uris): |
|
warnings.warn( |
|
"Simultaneous blocked_extensions_uris and allowed_extensions_uris is not supported. Please define only one of those.", |
|
stacklevel=2, |
|
) |
|
import sys |
|
|
|
sys.exit(-1) |
|
|
|
ListingsHandler.listings_refresh_seconds = settings_config.get( |
|
"listings_refresh_seconds", 60 * 60 |
|
) |
|
ListingsHandler.listings_request_opts = settings_config.get("listings_request_options", {}) |
|
listings_url = ujoin(extension_app.listings_url) |
|
listings_path = ujoin(listings_url, "(.*)") |
|
|
|
if blocked_extensions_uris: |
|
ListingsHandler.blocked_extensions_uris = set(blocked_extensions_uris.split(",")) |
|
if allowed_extensions_uris: |
|
ListingsHandler.allowed_extensions_uris = set(allowed_extensions_uris.split(",")) |
|
|
|
fetch_listings(None) |
|
|
|
if ( |
|
len(ListingsHandler.blocked_extensions_uris) > 0 |
|
or len(ListingsHandler.allowed_extensions_uris) > 0 |
|
): |
|
from tornado import ioloop |
|
|
|
callback_time = ListingsHandler.listings_refresh_seconds * 1000 |
|
ListingsHandler.pc = ioloop.PeriodicCallback( |
|
lambda: fetch_listings(None), |
|
callback_time=callback_time, |
|
jitter=0.1, |
|
) |
|
ListingsHandler.pc.start() |
|
|
|
handlers.append((listings_path, ListingsHandler, {})) |
|
|
|
|
|
if extension_app.themes_dir: |
|
themes_url = extension_app.themes_url |
|
themes_path = ujoin(themes_url, "(.*)") |
|
handlers.append( |
|
( |
|
themes_path, |
|
ThemesHandler, |
|
{ |
|
"themes_url": themes_url, |
|
"path": extension_app.themes_dir, |
|
"labextensions_path": labextensions_path, |
|
"no_cache_paths": no_cache_paths, |
|
}, |
|
) |
|
) |
|
|
|
|
|
if extension_app.licenses_url: |
|
licenses_url = extension_app.licenses_url |
|
licenses_path = ujoin(licenses_url, "(.*)") |
|
handlers.append( |
|
(licenses_path, LicensesHandler, {"manager": LicensesManager(parent=extension_app)}) |
|
) |
|
|
|
|
|
fallthrough_url = ujoin(extension_app.app_url, r".*") |
|
handlers.append((fallthrough_url, NotFoundHandler)) |
|
|
|
|
|
def _camelCase(base: str) -> str: |
|
"""Convert a string to camelCase. |
|
https://stackoverflow.com/a/20744956 |
|
""" |
|
output = "".join(x for x in base.title() if x.isalpha()) |
|
return output[0].lower() + output[1:] |
|
|