You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
405 lines
14 KiB
Python
405 lines
14 KiB
Python
"""JupyterLab Server config"""
|
|
|
|
# Copyright (c) Jupyter Development Team.
|
|
# Distributed under the terms of the Modified BSD License.
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os.path as osp
|
|
from glob import iglob
|
|
from itertools import chain
|
|
from logging import Logger
|
|
from os.path import join as pjoin
|
|
from typing import Any
|
|
|
|
import json5
|
|
from jupyter_core.paths import SYSTEM_CONFIG_PATH, jupyter_config_dir, jupyter_path
|
|
from jupyter_server.services.config.manager import ConfigManager, recursive_update
|
|
from jupyter_server.utils import url_path_join as ujoin
|
|
from traitlets import Bool, HasTraits, List, Unicode, default
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Module globals
|
|
# -----------------------------------------------------------------------------
|
|
|
|
DEFAULT_TEMPLATE_PATH = osp.join(osp.dirname(__file__), "templates")
|
|
|
|
|
|
def get_package_url(data: dict[str, Any]) -> str:
|
|
"""Get the url from the extension data"""
|
|
# homepage, repository are optional
|
|
if "homepage" in data:
|
|
url = data["homepage"]
|
|
elif "repository" in data and isinstance(data["repository"], dict):
|
|
url = data["repository"].get("url", "")
|
|
else:
|
|
url = ""
|
|
return url
|
|
|
|
|
|
def get_federated_extensions(labextensions_path: list[str]) -> dict[str, Any]:
|
|
"""Get the metadata about federated extensions"""
|
|
federated_extensions = {}
|
|
for ext_dir in labextensions_path:
|
|
# extensions are either top-level directories, or two-deep in @org directories
|
|
for ext_path in chain(
|
|
iglob(pjoin(ext_dir, "[!@]*", "package.json")),
|
|
iglob(pjoin(ext_dir, "@*", "*", "package.json")),
|
|
):
|
|
with open(ext_path, encoding="utf-8") as fid:
|
|
pkgdata = json.load(fid)
|
|
if pkgdata["name"] not in federated_extensions:
|
|
data = dict(
|
|
name=pkgdata["name"],
|
|
version=pkgdata["version"],
|
|
description=pkgdata.get("description", ""),
|
|
url=get_package_url(pkgdata),
|
|
ext_dir=ext_dir,
|
|
ext_path=osp.dirname(ext_path),
|
|
is_local=False,
|
|
dependencies=pkgdata.get("dependencies", dict()),
|
|
jupyterlab=pkgdata.get("jupyterlab", dict()),
|
|
)
|
|
|
|
# Add repository info if available
|
|
if "repository" in pkgdata and "url" in pkgdata.get("repository", {}):
|
|
data["repository"] = dict(url=pkgdata.get("repository").get("url"))
|
|
|
|
install_path = osp.join(osp.dirname(ext_path), "install.json")
|
|
if osp.exists(install_path):
|
|
with open(install_path, encoding="utf-8") as fid:
|
|
data["install"] = json.load(fid)
|
|
federated_extensions[data["name"]] = data
|
|
return federated_extensions
|
|
|
|
|
|
def get_static_page_config(
|
|
app_settings_dir: str | None = None, # noqa: ARG001
|
|
logger: Logger | None = None, # noqa: ARG001
|
|
level: str = "all",
|
|
include_higher_levels: bool = False,
|
|
) -> dict[str, Any]:
|
|
"""Get the static page config for JupyterLab
|
|
|
|
Parameters
|
|
----------
|
|
logger: logger, optional
|
|
An optional logging object
|
|
level: string, optional ['all']
|
|
The level at which to get config: can be 'all', 'user', 'sys_prefix', or 'system'
|
|
"""
|
|
cm = _get_config_manager(level, include_higher_levels)
|
|
return cm.get("page_config") # type:ignore[no-untyped-call]
|
|
|
|
|
|
def load_config(path: str) -> Any:
|
|
"""Load either a json5 or a json config file.
|
|
|
|
Parameters
|
|
----------
|
|
path : str
|
|
Path to the file to be loaded
|
|
|
|
Returns
|
|
-------
|
|
Dict[Any, Any]
|
|
Dictionary of json or json5 data
|
|
"""
|
|
with open(path, encoding="utf-8") as fid:
|
|
if path.endswith(".json5"):
|
|
return json5.load(fid)
|
|
return json.load(fid)
|
|
|
|
|
|
def get_page_config(
|
|
labextensions_path: list[str], app_settings_dir: str | None = None, logger: Logger | None = None
|
|
) -> dict[str, Any]:
|
|
"""Get the page config for the application handler"""
|
|
# Build up the full page config
|
|
page_config: dict = {}
|
|
|
|
disabled_key = "disabledExtensions"
|
|
|
|
# Start with the app_settings_dir as lowest priority
|
|
if app_settings_dir:
|
|
config_paths = [
|
|
pjoin(app_settings_dir, "page_config.json5"),
|
|
pjoin(app_settings_dir, "page_config.json"),
|
|
]
|
|
for path in config_paths:
|
|
if osp.exists(path) and osp.getsize(path):
|
|
data = load_config(path)
|
|
# Convert lists to dicts
|
|
for key in [disabled_key, "deferredExtensions"]:
|
|
if key in data:
|
|
data[key] = {key: True for key in data[key]}
|
|
|
|
recursive_update(page_config, data)
|
|
break
|
|
|
|
# Get the traitlets config
|
|
static_page_config = get_static_page_config(logger=logger, level="all")
|
|
recursive_update(page_config, static_page_config)
|
|
|
|
# Handle federated extensions that disable other extensions
|
|
disabled_by_extensions_all = {}
|
|
extensions = page_config["federated_extensions"] = []
|
|
|
|
federated_exts = get_federated_extensions(labextensions_path)
|
|
|
|
# Ensure there is a disabled key
|
|
page_config.setdefault(disabled_key, {})
|
|
|
|
for _, ext_data in federated_exts.items():
|
|
if "_build" not in ext_data["jupyterlab"]:
|
|
if logger:
|
|
logger.warning("%s is not a valid extension", ext_data["name"])
|
|
continue
|
|
extbuild = ext_data["jupyterlab"]["_build"]
|
|
extension = {"name": ext_data["name"], "load": extbuild["load"]}
|
|
|
|
if "extension" in extbuild:
|
|
extension["extension"] = extbuild["extension"]
|
|
if "mimeExtension" in extbuild:
|
|
extension["mimeExtension"] = extbuild["mimeExtension"]
|
|
if "style" in extbuild:
|
|
extension["style"] = extbuild["style"]
|
|
# FIXME @experimental for plugin with no-code entrypoints.
|
|
extension["entrypoints"] = extbuild.get("entrypoints")
|
|
extensions.append(extension)
|
|
|
|
# If there is disabledExtensions metadata, consume it.
|
|
name = ext_data["name"]
|
|
|
|
if ext_data["jupyterlab"].get(disabled_key):
|
|
disabled_by_extensions_all[ext_data["name"]] = ext_data["jupyterlab"][disabled_key]
|
|
|
|
# Handle source extensions that disable other extensions
|
|
# Check for `jupyterlab`:`extensionMetadata` in the built application directory's package.json
|
|
if app_settings_dir:
|
|
app_dir = osp.dirname(app_settings_dir)
|
|
package_data_file = pjoin(app_dir, "static", "package.json")
|
|
if osp.exists(package_data_file):
|
|
with open(package_data_file, encoding="utf-8") as fid:
|
|
app_data = json.load(fid)
|
|
all_ext_data = app_data["jupyterlab"].get("extensionMetadata", {})
|
|
for ext, ext_data in all_ext_data.items():
|
|
if ext in disabled_by_extensions_all:
|
|
continue
|
|
if ext_data.get(disabled_key):
|
|
disabled_by_extensions_all[ext] = ext_data[disabled_key]
|
|
|
|
disabled_by_extensions = {}
|
|
for name in sorted(disabled_by_extensions_all):
|
|
# skip if the extension itself is disabled by other config
|
|
if page_config[disabled_key].get(name) is True:
|
|
continue
|
|
|
|
disabled_list = disabled_by_extensions_all[name]
|
|
for item in disabled_list:
|
|
disabled_by_extensions[item] = True
|
|
|
|
rollup_disabled = disabled_by_extensions
|
|
rollup_disabled.update(page_config.get(disabled_key, []))
|
|
page_config[disabled_key] = rollup_disabled
|
|
|
|
# Convert dictionaries to lists to give to the front end
|
|
for key, value in page_config.items():
|
|
if isinstance(value, dict):
|
|
page_config[key] = [subkey for subkey in value if value[subkey]]
|
|
|
|
return page_config
|
|
|
|
|
|
def write_page_config(page_config: dict[str, Any], level: str = "all") -> None:
|
|
"""Write page config to disk"""
|
|
cm = _get_config_manager(level)
|
|
cm.set("page_config", page_config) # type:ignore[no-untyped-call]
|
|
|
|
|
|
class LabConfig(HasTraits):
|
|
"""The lab application configuration object."""
|
|
|
|
app_name = Unicode("", help="The name of the application.").tag(config=True)
|
|
|
|
app_version = Unicode("", help="The version of the application.").tag(config=True)
|
|
|
|
app_namespace = Unicode("", help="The namespace of the application.").tag(config=True)
|
|
|
|
app_url = Unicode("/lab", help="The url path for the application.").tag(config=True)
|
|
|
|
app_settings_dir = Unicode("", help="The application settings directory.").tag(config=True)
|
|
|
|
extra_labextensions_path = List(
|
|
Unicode(), help="""Extra paths to look for federated JupyterLab extensions"""
|
|
).tag(config=True)
|
|
|
|
labextensions_path = List(
|
|
Unicode(), help="The standard paths to look in for federated JupyterLab extensions"
|
|
).tag(config=True)
|
|
|
|
templates_dir = Unicode("", help="The application templates directory.").tag(config=True)
|
|
|
|
static_dir = Unicode(
|
|
"",
|
|
help=(
|
|
"The optional location of local static files. "
|
|
"If given, a static file handler will be "
|
|
"added."
|
|
),
|
|
).tag(config=True)
|
|
|
|
labextensions_url = Unicode("", help="The url for federated JupyterLab extensions").tag(
|
|
config=True
|
|
)
|
|
|
|
settings_url = Unicode(help="The url path of the settings handler.").tag(config=True)
|
|
|
|
user_settings_dir = Unicode(
|
|
"", help=("The optional location of the user settings directory.")
|
|
).tag(config=True)
|
|
|
|
schemas_dir = Unicode(
|
|
"",
|
|
help=(
|
|
"The optional location of the settings "
|
|
"schemas directory. If given, a handler will "
|
|
"be added for settings."
|
|
),
|
|
).tag(config=True)
|
|
|
|
workspaces_api_url = Unicode(help="The url path of the workspaces API.").tag(config=True)
|
|
|
|
workspaces_dir = Unicode(
|
|
"",
|
|
help=(
|
|
"The optional location of the saved "
|
|
"workspaces directory. If given, a handler "
|
|
"will be added for workspaces."
|
|
),
|
|
).tag(config=True)
|
|
|
|
listings_url = Unicode(help="The listings url.").tag(config=True)
|
|
|
|
themes_url = Unicode(help="The theme url.").tag(config=True)
|
|
|
|
licenses_url = Unicode(help="The third-party licenses url.")
|
|
|
|
themes_dir = Unicode(
|
|
"",
|
|
help=(
|
|
"The optional location of the themes "
|
|
"directory. If given, a handler will be added "
|
|
"for themes."
|
|
),
|
|
).tag(config=True)
|
|
|
|
translations_api_url = Unicode(help="The url path of the translations handler.").tag(
|
|
config=True
|
|
)
|
|
|
|
tree_url = Unicode(help="The url path of the tree handler.").tag(config=True)
|
|
|
|
cache_files = Bool(
|
|
True,
|
|
help=("Whether to cache files on the server. This should be `True` except in dev mode."),
|
|
).tag(config=True)
|
|
|
|
notebook_starts_kernel = Bool(
|
|
True, help="Whether a notebook should start a kernel automatically."
|
|
).tag(config=True)
|
|
|
|
copy_absolute_path = Bool(
|
|
False,
|
|
help="Whether getting a relative (False) or absolute (True) path when copying a path.",
|
|
).tag(config=True)
|
|
|
|
@default("template_dir")
|
|
def _default_template_dir(self) -> str:
|
|
return DEFAULT_TEMPLATE_PATH
|
|
|
|
@default("labextensions_url")
|
|
def _default_labextensions_url(self) -> str:
|
|
return ujoin(self.app_url, "extensions/")
|
|
|
|
@default("labextensions_path")
|
|
def _default_labextensions_path(self) -> list[str]:
|
|
return jupyter_path("labextensions")
|
|
|
|
@default("workspaces_url")
|
|
def _default_workspaces_url(self) -> str:
|
|
return ujoin(self.app_url, "workspaces/")
|
|
|
|
@default("workspaces_api_url")
|
|
def _default_workspaces_api_url(self) -> str:
|
|
return ujoin(self.app_url, "api", "workspaces/")
|
|
|
|
@default("settings_url")
|
|
def _default_settings_url(self) -> str:
|
|
return ujoin(self.app_url, "api", "settings/")
|
|
|
|
@default("listings_url")
|
|
def _default_listings_url(self) -> str:
|
|
return ujoin(self.app_url, "api", "listings/")
|
|
|
|
@default("themes_url")
|
|
def _default_themes_url(self) -> str:
|
|
return ujoin(self.app_url, "api", "themes/")
|
|
|
|
@default("licenses_url")
|
|
def _default_licenses_url(self) -> str:
|
|
return ujoin(self.app_url, "api", "licenses/")
|
|
|
|
@default("tree_url")
|
|
def _default_tree_url(self) -> str:
|
|
return ujoin(self.app_url, "tree/")
|
|
|
|
@default("translations_api_url")
|
|
def _default_translations_api_url(self) -> str:
|
|
return ujoin(self.app_url, "api", "translations/")
|
|
|
|
|
|
def get_allowed_levels() -> list[str]:
|
|
"""
|
|
Returns the levels where configs can be stored.
|
|
"""
|
|
return ["all", "user", "sys_prefix", "system", "app", "extension"]
|
|
|
|
|
|
def _get_config_manager(level: str, include_higher_levels: bool = False) -> ConfigManager:
|
|
"""Get the location of config files for the current context
|
|
Returns the string to the environment
|
|
"""
|
|
# Delayed import since this gets monkey-patched in tests
|
|
from jupyter_core.paths import ENV_CONFIG_PATH
|
|
|
|
allowed = get_allowed_levels()
|
|
if level not in allowed:
|
|
msg = f"Page config level must be one of: {allowed}"
|
|
raise ValueError(msg)
|
|
|
|
config_name = "labconfig"
|
|
|
|
if level == "all":
|
|
return ConfigManager(config_dir_name=config_name)
|
|
|
|
paths: dict[str, list] = {
|
|
"app": [],
|
|
"system": SYSTEM_CONFIG_PATH,
|
|
"sys_prefix": [ENV_CONFIG_PATH[0]],
|
|
"user": [jupyter_config_dir()],
|
|
"extension": [],
|
|
}
|
|
|
|
levels = allowed[allowed.index(level) :] if include_higher_levels else [level]
|
|
|
|
read_config_paths, write_config_dir = [], None
|
|
|
|
for _level in levels:
|
|
for p in paths[_level]:
|
|
read_config_paths.append(osp.join(p, config_name))
|
|
if write_config_dir is None and paths[_level]: # type: ignore[redundant-expr]
|
|
write_config_dir = osp.join(paths[_level][0], config_name)
|
|
|
|
return ConfigManager(read_config_path=read_config_paths, write_config_dir=write_config_dir)
|