| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358 |
- """JupyterLab Server handlers"""
- # Copyright (c) Jupyter Development Team.
- # Distributed under the terms of the Modified BSD License.
- 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
- # -----------------------------------------------------------------------------
- # Module globals
- # -----------------------------------------------------------------------------
- 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 # noqa: B019
- def get_page_config(self) -> dict[str, Any]:
- """Construct the page config object"""
- self.application.store_id = getattr( # type:ignore[attr-defined]
- self.application, "store_id", 0
- )
- config = LabConfig()
- app: LabServerApp = self.extensionapp # type:ignore[assignment]
- settings_dir = app.app_settings_dir
- # Handle page config data.
- 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")
- # Remove the trailing slash for compatibility with html-webpack-plugin.
- 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 # type:ignore[attr-defined]
- server_root = os.path.normpath(os.path.expanduser(server_root))
- preferred_path = ""
- try:
- preferred_path = self.serverapp.contents_manager.preferred_dir
- except Exception:
- # FIXME: Remove fallback once CM.preferred_dir is ubiquitous.
- try:
- # Remove the server_root from app pref dir
- 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: # noqa: S110
- pass
- # JupyterLab relies on an unset/default path being "/"
- page_config["preferredPath"] = preferred_path or "/"
- self.application.store_id += 1 # type:ignore[attr-defined]
- mathjax_config = self.settings.get("mathjax_config", "TeX-AMS_HTML-full,Safe")
- # TODO Remove CDN usage.
- 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)
- # 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 base_url is not None and 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
- # Update the page config with the data from disk
- labextensions_path = app.extra_labextensions_path + app.labextensions_path
- recursive_update(
- page_config, get_page_config(labextensions_path, settings_dir, logger=self.log)
- )
- # modify page config with custom hook
- 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()
- # Add parameters parsed from the URL
- if mode == "doc":
- page_config["mode"] = "single-document"
- else:
- page_config["mode"] = "multiple-document"
- page_config["workspace"] = workspace
- page_config["treePath"] = tree_path
- # Write the template with the config.
- tpl = self.render_template("index.html", page_config=page_config)
- self.write(tpl)
- class NotFoundHandler(LabHandler):
- """A handler for page not found."""
- @lru_cache # noqa: B019
- def get_page_config(self) -> dict[str, Any]:
- """Get the page config."""
- # Making a copy of the page_config to ensure changes do not affect the original
- 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."""
- # Normalize directories.
- 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, "/"))
- # Normalize urls
- # Local urls should have a leading slash but no trailing slash
- 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))
- # Cache all or none of the files depending on the `cache_files` setting.
- no_cache_paths = [] if extension_app.cache_files else ["/"]
- # Handle federated lab extensions.
- 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},
- )
- )
- # Handle local settings.
- if extension_app.schemas_dir:
- # Load overrides once, rather than in each copy of the settings handler
- 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,
- }
- # Handle requests for the list of settings. Make slash optional.
- settings_path = ujoin(extension_app.settings_url, "?")
- handlers.append((settings_path, SettingsHandler, settings_config))
- # Handle requests for an individual set of settings.
- setting_path = ujoin(extension_app.settings_url, "(?P<schema_name>.+)")
- handlers.append((setting_path, SettingsHandler, settings_config))
- # Handle translations.
- # Translations requires settings as the locale source of truth is stored in it
- if extension_app.translations_api_url:
- # Handle requests for the list of language packs available.
- # Make slash optional.
- translations_path = ujoin(extension_app.translations_api_url, "?")
- handlers.append((translations_path, TranslationsHandler, settings_config))
- # Handle requests for an individual language pack.
- translations_lang_path = ujoin(extension_app.translations_api_url, "(?P<locale>.*)")
- handlers.append((translations_lang_path, TranslationsHandler, settings_config))
- # Handle saved workspaces.
- if extension_app.workspaces_dir:
- workspaces_config = {"manager": WorkspacesManager(extension_app.workspaces_dir)}
- # Handle requests for the list of workspaces. Make slash optional.
- workspaces_api_path = ujoin(extension_app.workspaces_api_url, "?")
- handlers.append((workspaces_api_path, WorkspacesHandler, workspaces_config))
- # Handle requests for an individually named workspace.
- workspace_api_path = ujoin(extension_app.workspaces_api_url, "(?P<space_name>.+)")
- handlers.append((workspace_api_path, WorkspacesHandler, workspaces_config))
- # Handle local listings.
- 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), # type:ignore[assignment]
- callback_time=callback_time,
- jitter=0.1,
- )
- ListingsHandler.pc.start() # type:ignore[attr-defined]
- handlers.append((listings_path, ListingsHandler, {}))
- # Handle local themes.
- 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,
- },
- )
- )
- # Handle licenses.
- 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)})
- )
- # Let the lab handler act as the fallthrough option instead of a 404.
- 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:]
|