mbuali's picture
Upload folder using huggingface_hub
d1ceb73 verified
"""Frontend config storage helpers."""
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
from __future__ import annotations
import json
import os
from glob import glob
from typing import Any
import json5
from jsonschema import Draft7Validator as Validator
from jsonschema import ValidationError
from jupyter_server import _tz as tz
from jupyter_server.base.handlers import APIHandler
from jupyter_server.services.config.manager import ConfigManager, recursive_update
from tornado import web
from .translation_utils import (
DEFAULT_LOCALE,
L10N_SCHEMA_NAME,
PSEUDO_LANGUAGE,
SYS_LOCALE,
is_valid_locale,
)
# The JupyterLab settings file extension.
SETTINGS_EXTENSION = ".jupyterlab-settings"
def _get_schema(
schemas_dir: str,
schema_name: str,
overrides: dict[str, Any],
labextensions_path: list[str] | None,
) -> tuple[dict[str, Any], str]:
"""Returns a dict containing a parsed and validated JSON schema."""
notfound_error = "Schema not found: %s"
parse_error = "Failed parsing schema (%s): %s"
validation_error = "Failed validating schema (%s): %s"
path = None
# Look for the setting in all of the labextension paths first
# Use the first one
if labextensions_path is not None:
ext_name, _, plugin_name = schema_name.partition(":")
for ext_path in labextensions_path:
target = os.path.join(ext_path, ext_name, "schemas", ext_name, plugin_name + ".json")
if os.path.exists(target):
schemas_dir = os.path.join(ext_path, ext_name, "schemas")
path = target
break
# Fall back on the default location
if path is None:
path = _path(schemas_dir, schema_name)
if not os.path.exists(path):
raise web.HTTPError(404, notfound_error % path)
with open(path, encoding="utf-8") as fid:
# Attempt to load the schema file.
try:
schema = json.load(fid)
except Exception as e:
name = schema_name
raise web.HTTPError(500, parse_error % (name, str(e))) from None
schema = _override(schema_name, schema, overrides)
# Validate the schema.
try:
Validator.check_schema(schema)
except Exception as e:
name = schema_name
raise web.HTTPError(500, validation_error % (name, str(e))) from None
version = _get_version(schemas_dir, schema_name)
return schema, version
def _get_user_settings(settings_dir: str, schema_name: str, schema: Any) -> dict[str, Any]:
"""
Returns a dictionary containing the raw user settings, the parsed user
settings, a validation warning for a schema, and file times.
"""
path = _path(settings_dir, schema_name, False, SETTINGS_EXTENSION)
raw = "{}"
settings = {}
warning = None
validation_warning = "Failed validating settings (%s): %s"
parse_error = "Failed loading settings (%s): %s"
last_modified = None
created = None
if os.path.exists(path):
stat = os.stat(path)
last_modified = tz.utcfromtimestamp(stat.st_mtime).isoformat()
created = tz.utcfromtimestamp(stat.st_ctime).isoformat()
with open(path, encoding="utf-8") as fid:
try: # to load and parse the settings file.
raw = fid.read() or raw
settings = json5.loads(raw)
except Exception as e:
raise web.HTTPError(500, parse_error % (schema_name, str(e))) from None
# Validate the parsed data against the schema.
if len(settings):
validator = Validator(schema)
try:
validator.validate(settings)
except ValidationError as e:
warning = validation_warning % (schema_name, str(e))
raw = "{}"
settings = {}
return dict(
raw=raw, settings=settings, warning=warning, last_modified=last_modified, created=created
)
def _get_version(schemas_dir: str, schema_name: str) -> str:
"""Returns the package version for a given schema or 'N/A' if not found."""
path = _path(schemas_dir, schema_name)
package_path = os.path.join(os.path.split(path)[0], "package.json.orig")
try: # to load and parse the package.json.orig file.
with open(package_path, encoding="utf-8") as fid:
package = json.load(fid)
return package["version"]
except Exception:
return "N/A"
def _list_settings(
schemas_dir: str,
settings_dir: str,
overrides: dict[str, Any],
extension: str = ".json",
labextensions_path: list[str] | None = None,
translator: Any = None,
ids_only: bool = False,
) -> tuple[list[Any], list[Any]]:
"""
Returns a tuple containing:
- the list of plugins, schemas, and their settings,
respecting any defaults that may have been overridden if `ids_only=False`,
otherwise a list of dict containing only the ids of plugins.
- the list of warnings that were generated when
validating the user overrides against the schemas.
"""
settings: dict[str, Any] = {}
federated_settings: dict[str, Any] = {}
warnings = []
if not os.path.exists(schemas_dir):
warnings = ["Settings directory does not exist at %s" % schemas_dir]
return ([], warnings)
schema_pattern = schemas_dir + "/**/*" + extension
schema_paths = [path for path in glob(schema_pattern, recursive=True)] # noqa: C416
schema_paths.sort()
for schema_path in schema_paths:
# Generate the schema_name used to request individual settings.
rel_path = os.path.relpath(schema_path, schemas_dir)
rel_schema_dir, schema_base = os.path.split(rel_path)
_id = schema_name = ":".join(
[rel_schema_dir, schema_base[: -len(extension)]] # Remove file extension.
).replace("\\", "/") # Normalize slashes.
if ids_only:
settings[_id] = dict(id=_id)
else:
schema, version = _get_schema(schemas_dir, schema_name, overrides, None)
if translator is not None:
schema = translator(schema)
user_settings = _get_user_settings(settings_dir, schema_name, schema)
if user_settings["warning"]:
warnings.append(user_settings.pop("warning"))
# Add the plugin to the list of settings.
settings[_id] = dict(id=_id, schema=schema, version=version, **user_settings)
if labextensions_path is not None:
schema_paths = []
for ext_dir in labextensions_path:
schema_pattern = ext_dir + "/**/schemas/**/*" + extension
schema_paths.extend(path for path in glob(schema_pattern, recursive=True))
schema_paths.sort()
for schema_path_ in schema_paths:
schema_path = schema_path_.replace(os.sep, "/")
base_dir, rel_path = schema_path.split("schemas/")
# Generate the schema_name used to request individual settings.
rel_schema_dir, schema_base = os.path.split(rel_path)
_id = schema_name = ":".join(
[rel_schema_dir, schema_base[: -len(extension)]] # Remove file extension.
).replace("\\", "/") # Normalize slashes.
# bail if we've already handled the highest federated setting
if _id in federated_settings:
continue
if ids_only:
federated_settings[_id] = dict(id=_id)
else:
schema, version = _get_schema(
schemas_dir, schema_name, overrides, labextensions_path=labextensions_path
)
user_settings = _get_user_settings(settings_dir, schema_name, schema)
if user_settings["warning"]:
warnings.append(user_settings.pop("warning"))
# Add the plugin to the list of settings.
federated_settings[_id] = dict(
id=_id, schema=schema, version=version, **user_settings
)
settings.update(federated_settings)
settings_list = [settings[key] for key in sorted(settings.keys(), reverse=True)]
return (settings_list, warnings)
def _override(
schema_name: str, schema: dict[str, Any], overrides: dict[str, Any]
) -> dict[str, Any]:
"""Override default values in the schema if necessary."""
if schema_name in overrides:
defaults = overrides[schema_name]
for key in defaults:
if key in schema["properties"]:
new_defaults = schema["properties"][key]["default"]
# If values for defaults are dicts do a recursive update
if isinstance(new_defaults, dict):
recursive_update(new_defaults, defaults[key])
else:
new_defaults = defaults[key]
schema["properties"][key]["default"] = new_defaults
else:
schema["properties"][key] = dict(default=defaults[key])
return schema
def _path(
root_dir: str, schema_name: str, make_dirs: bool = False, extension: str = ".json"
) -> str:
"""
Returns the local file system path for a schema name in the given root
directory. This function can be used to filed user overrides in addition to
schema files. If the `make_dirs` flag is set to `True` it will create the
parent directory for the calculated path if it does not exist.
"""
notfound_error = "Settings not found (%s)"
write_error = "Failed writing settings (%s): %s"
try: # to parse path, e.g. @jupyterlab/apputils-extension:themes.
package_dir, plugin = schema_name.split(":")
parent_dir = os.path.join(root_dir, package_dir)
path = os.path.join(parent_dir, plugin + extension)
except Exception:
raise web.HTTPError(404, notfound_error % schema_name) from None
if make_dirs and not os.path.exists(parent_dir):
try:
os.makedirs(parent_dir)
except Exception as e:
raise web.HTTPError(500, write_error % (schema_name, str(e))) from None
return path
def _get_overrides(app_settings_dir: str) -> tuple[dict[str, Any], str]:
"""Get overrides settings from `app_settings_dir`.
The ordering of paths is:
- {app_settings_dir}/overrides.d/*.{json,json5} (many, namespaced by package)
- {app_settings_dir}/overrides.{json,json5} (singleton, owned by the user)
"""
overrides: dict[str, Any]
error: str
overrides, error = {}, ""
overrides_d = os.path.join(app_settings_dir, "overrides.d")
# find (and sort) the conf.d overrides files
all_override_paths = sorted(
[
*(glob(os.path.join(overrides_d, "*.json"))),
*(glob(os.path.join(overrides_d, "*.json5"))),
]
)
all_override_paths += [
os.path.join(app_settings_dir, "overrides.json"),
os.path.join(app_settings_dir, "overrides.json5"),
]
for overrides_path in all_override_paths:
if not os.path.exists(overrides_path):
continue
with open(overrides_path, encoding="utf-8") as fid:
try:
if overrides_path.endswith(".json5"):
path_overrides = json5.load(fid)
else:
path_overrides = json.load(fid)
for plugin_id, config in path_overrides.items():
recursive_update(overrides.setdefault(plugin_id, {}), config)
except Exception as e:
error = e # type:ignore[assignment]
# Allow `default_settings_overrides.json` files in <jupyter_config>/labconfig dirs
# to allow layering of defaults
cm = ConfigManager(config_dir_name="labconfig")
for plugin_id, config in cm.get("default_setting_overrides").items(): # type:ignore[no-untyped-call]
recursive_update(overrides.setdefault(plugin_id, {}), config)
return overrides, error
def get_settings(
app_settings_dir: str,
schemas_dir: str,
settings_dir: str,
schema_name: str = "",
overrides: dict[str, Any] | None = None,
labextensions_path: list[str] | None = None,
translator: Any = None,
ids_only: bool = False,
) -> tuple[dict[str, Any], list[Any]]:
"""
Get settings.
Parameters
----------
app_settings_dir:
Path to applications settings.
schemas_dir: str
Path to schemas.
settings_dir:
Path to settings.
schema_name str, optional
Schema name. Default is "".
overrides: dict, optional
Settings overrides. If not provided, the overrides will be loaded
from the `app_settings_dir`. Default is None.
labextensions_path: list, optional
List of paths to federated labextensions containing their own schema files.
translator: Callable[[Dict], Dict] or None, optional
Translate a schema. It requires the schema dictionary and returns its translation
Returns
-------
tuple
The first item is a dictionary with a list of setting if no `schema_name`
was provided (only the ids if `ids_only=True`), otherwise it is a dictionary
with id, raw, scheme, settings and version keys.
The second item is a list of warnings. Warnings will either be a list of
i) strings with the warning messages or ii) `None`.
"""
result = {}
warnings = []
if overrides is None:
overrides, _error = _get_overrides(app_settings_dir)
if schema_name:
schema, version = _get_schema(schemas_dir, schema_name, overrides, labextensions_path)
if translator is not None:
schema = translator(schema)
user_settings = _get_user_settings(settings_dir, schema_name, schema)
warnings = [user_settings.pop("warning")]
result = {"id": schema_name, "schema": schema, "version": version, **user_settings}
else:
settings_list, warnings = _list_settings(
schemas_dir,
settings_dir,
overrides,
labextensions_path=labextensions_path,
translator=translator,
ids_only=ids_only,
)
result = {
"settings": settings_list,
}
return result, warnings
def save_settings(
schemas_dir: str,
settings_dir: str,
schema_name: str,
raw_settings: str,
overrides: dict[str, Any],
labextensions_path: list[str] | None = None,
) -> None:
"""
Save ``raw_settings`` settings for ``schema_name``.
Parameters
----------
schemas_dir: str
Path to schemas.
settings_dir: str
Path to settings.
schema_name str
Schema name.
raw_settings: str
Raw serialized settings dictionary
overrides: dict
Settings overrides.
labextensions_path: list, optional
List of paths to federated labextensions containing their own schema files.
"""
payload = json5.loads(raw_settings)
# Validate the data against the schema.
schema, _ = _get_schema(
schemas_dir, schema_name, overrides, labextensions_path=labextensions_path
)
validator = Validator(schema)
validator.validate(payload)
# Write the raw data (comments included) to a file.
path = _path(settings_dir, schema_name, True, SETTINGS_EXTENSION)
with open(path, "w", encoding="utf-8") as fid:
fid.write(raw_settings)
class SchemaHandler(APIHandler):
"""Base handler for handler requiring access to settings."""
def initialize(
self,
app_settings_dir: str,
schemas_dir: str,
settings_dir: str,
labextensions_path: list[str] | None,
overrides: dict[str, Any] | None = None,
**kwargs: Any,
) -> None:
"""Initialize the handler."""
super().initialize(**kwargs)
error = None
if not overrides:
overrides, error = _get_overrides(app_settings_dir)
self.overrides = overrides
self.app_settings_dir = app_settings_dir
self.schemas_dir = schemas_dir
self.settings_dir = settings_dir
self.labextensions_path = labextensions_path
if error:
overrides_warning = "Failed loading overrides: %s"
self.log.warning(overrides_warning, error)
def get_current_locale(self) -> str:
"""
Get the current locale as specified in the translation-extension settings.
Returns
-------
str
The current locale string.
Notes
-----
If the locale setting is not available or not valid, it will default to jupyterlab_server.translation_utils.DEFAULT_LOCALE.
"""
try:
settings, _ = get_settings(
self.app_settings_dir,
self.schemas_dir,
self.settings_dir,
schema_name=L10N_SCHEMA_NAME,
overrides=self.overrides,
labextensions_path=self.labextensions_path,
)
except web.HTTPError as e:
schema_warning = "Missing or misshapen translation settings schema:\n%s"
self.log.warning(schema_warning, e)
settings = {}
current_locale = settings.get("settings", {}).get("locale") or SYS_LOCALE
if current_locale == "default":
current_locale = SYS_LOCALE
if not is_valid_locale(current_locale) and current_locale != PSEUDO_LANGUAGE:
current_locale = DEFAULT_LOCALE
return current_locale