def initialize_settings(self): """Add settings to the tornado app.""" if self.ignore_minified_js: self.log.warning( _i18n( """The `ignore_minified_js` flag is deprecated and no longer works.""" )) self.log.warning( _i18n( """Alternatively use `%s` when working on the notebook's Javascript and LESS""" ) % 'npm run build:watch') warnings.warn( _i18n( "The `ignore_minified_js` flag is deprecated and will be removed in Notebook 6.0" ), DeprecationWarning) settings = dict( static_custom_path=self.static_custom_path, static_handler_args={ # don't cache custom.js 'no_cache_paths': [ url_path_join(self.serverapp.base_url, 'static', self.name, 'custom') ], }, ignore_minified_js=self.ignore_minified_js, mathjax_url=self.mathjax_url, mathjax_config=self.mathjax_config, nbextensions_path=self.nbextensions_path, ) self.settings.update(**settings)
class ExtensionAppJinjaMixin(HasTraits): """Use Jinja templates for HTML templates on top of an ExtensionApp.""" jinja2_options = Dict( help=_i18n("""Options to pass to the jinja2 environment for this """)).tag(config=True) def _prepare_templates(self): # Get templates defined in a subclass. self.initialize_templates() # Add templates to web app settings if extension has templates. if len(self.template_paths) > 0: self.settings.update( {"{}_template_paths".format(self.name): self.template_paths}) # Create a jinja environment for logging html templates. self.jinja2_env = Environment(loader=FileSystemLoader( self.template_paths), extensions=['jinja2.ext.i18n'], autoescape=True, **self.jinja2_options) # Add the jinja2 environment for this extension to the tornado settings. self.settings.update( {"{}_jinja2_env".format(self.name): self.jinja2_env})
def validate_security( self, app: ServerApp, ssl_options: dict | None = None, ) -> None: if self.password_required and (not self.hashed_password): self.log.critical( _i18n( "Jupyter servers are configured to only be run with a password." )) self.log.critical( _i18n("Hint: run the following command to set a password")) self.log.critical( _i18n("\t$ python -m jupyter_server.auth password")) sys.exit(1) return self.login_handler_class.validate_security(app, ssl_options)
def process_login_form(self, handler: JupyterHandler) -> User | None: """Process login form data Return authenticated User if successful, None if not. """ typed_password = handler.get_argument("password", default="") new_password = handler.get_argument("new_password", default="") user = None if not self.auth_enabled: self.log.warning( "Accepting anonymous login because auth fully disabled!") return self.generate_anonymous_user(handler) if self.passwd_check(typed_password) and not new_password: return self.generate_anonymous_user(handler) elif self.token and self.token == typed_password: user = self.generate_anonymous_user(handler) if new_password and self.allow_password_change: config_dir = handler.settings.get("config_dir", "") config_file = os.path.join(config_dir, "jupyter_server_config.json") self.hashed_password = set_password(new_password, config_file=config_file) self.log.info(_i18n(f"Wrote hashed password to {config_file}")) return user
def _update_mathjax_url(self, change): new = change['new'] if new and not self.enable_mathjax: # enable_mathjax=False overrides mathjax_url self.mathjax_url = u'' else: self.log.info(_i18n("Using MathJax: %s"), new)
class Jupyterfs(Configurable): root_manager_class = Type( config=True, default_value=LargeFileManager, help=_i18n( "the root contents manager class to use. Used by the Jupyterlab default filebrowser and elsewhere" ), klass=ContentsManager, ) resources = List( config=True, default_value=[], help=_i18n( "server-side definitions of fsspec resources for jupyter-fs"), # trait=Dict(traits={"name": Unicode, "url": Unicode}), )
def get_frontend_exporters(): from nbconvert.exporters.base import get_export_names, get_exporter # name=exporter_name, display=export_from_notebook+extension ExporterInfo = namedtuple('ExporterInfo', ['name', 'display']) default_exporters = [ ExporterInfo(name='html', display='HTML (.html)'), ExporterInfo(name='latex', display='LaTeX (.tex)'), ExporterInfo(name='markdown', display='Markdown (.md)'), ExporterInfo(name='notebook', display='Notebook (.ipynb)'), ExporterInfo(name='pdf', display='PDF via LaTeX (.pdf)'), ExporterInfo(name='rst', display='reST (.rst)'), ExporterInfo(name='script', display='Script (.txt)'), ExporterInfo(name='slides', display='Reveal.js slides (.slides.html)') ] frontend_exporters = [] for name in get_export_names(): exporter_class = get_exporter(name) exporter_instance = exporter_class() ux_name = getattr(exporter_instance, 'export_from_notebook', None) super_uxname = getattr(super(exporter_class, exporter_instance), 'export_from_notebook', None) # Ensure export_from_notebook is explicitly defined & not inherited if ux_name is not None and ux_name != super_uxname: display = _i18n('{} ({})'.format(ux_name, exporter_instance.file_extension)) frontend_exporters.append(ExporterInfo(name, display)) # Ensure default_exporters are in frontend_exporters if not already # This protects against nbconvert versions lower than 5.5 names = set(exporter.name.lower() for exporter in frontend_exporters) for exporter in default_exporters: if exporter.name not in names: frontend_exporters.append(exporter) # Protect against nbconvert 5.5.0 python_exporter = ExporterInfo(name='python', display='python (.py)') if python_exporter in frontend_exporters: frontend_exporters.remove(python_exporter) # Protect against nbconvert 5.4.x template_exporter = ExporterInfo(name='custom', display='custom (.txt)') if template_exporter in frontend_exporters: frontend_exporters.remove(template_exporter) return sorted(frontend_exporters)
class ContentsManager(LoggingConfigurable): """Base class for serving files and directories. This serves any text or binary file, as well as directories, with special handling for JSON notebook documents. Most APIs take a path argument, which is always an API-style unicode path, and always refers to a directory. - unicode, not url-escaped - '/'-separated - leading and trailing '/' will be stripped - if unspecified, path defaults to '', indicating the root path. """ root_dir = Unicode("/", config=True) allow_hidden = Bool(False, config=True, help="Allow access to hidden files") notary = Instance(sign.NotebookNotary) def _notary_default(self): return sign.NotebookNotary(parent=self) hide_globs = List( Unicode(), [ u"__pycache__", "*.pyc", "*.pyo", ".DS_Store", "*.so", "*.dylib", "*~", ], config=True, help=""" Glob patterns to hide in file and directory listings. """, ) untitled_notebook = Unicode( _i18n("Untitled"), config=True, help="The base name used when creating untitled notebooks.") untitled_file = Unicode( "untitled", config=True, help="The base name used when creating untitled files.") untitled_directory = Unicode( "Untitled Folder", config=True, help="The base name used when creating untitled directories.", ) pre_save_hook = Any( None, config=True, allow_none=True, help="""Python callable or importstring thereof To be called on a contents model prior to save. This can be used to process the structure, such as removing notebook outputs or other side effects that should not be saved. It will be called as (all arguments passed by keyword):: hook(path=path, model=model, contents_manager=self) - model: the model to be saved. Includes file contents. Modifying this dict will affect the file that is stored. - path: the API path of the save destination - contents_manager: this ContentsManager instance """, ) @validate("pre_save_hook") def _validate_pre_save_hook(self, proposal): value = proposal["value"] if isinstance(value, str): value = import_item(self.pre_save_hook) if not callable(value): raise TraitError("pre_save_hook must be callable") return value def run_pre_save_hook(self, model, path, **kwargs): """Run the pre-save hook if defined, and log errors""" if self.pre_save_hook: try: self.log.debug("Running pre-save hook on %s", path) self.pre_save_hook(model=model, path=path, contents_manager=self, **kwargs) except HTTPError: # allow custom HTTPErrors to raise, # rejecting the save with a message. raise except Exception: # unhandled errors don't prevent saving, # which could cause frustrating data loss self.log.error("Pre-save hook failed on %s", path, exc_info=True) checkpoints_class = Type(Checkpoints, config=True) checkpoints = Instance(Checkpoints, config=True) checkpoints_kwargs = Dict(config=True) @default("checkpoints") def _default_checkpoints(self): return self.checkpoints_class(**self.checkpoints_kwargs) @default("checkpoints_kwargs") def _default_checkpoints_kwargs(self): return dict( parent=self, log=self.log, ) files_handler_class = Type( FilesHandler, klass=RequestHandler, allow_none=True, config=True, help="""handler class to use when serving raw file requests. Default is a fallback that talks to the ContentsManager API, which may be inefficient, especially for large files. Local files-based ContentsManagers can use a StaticFileHandler subclass, which will be much more efficient. Access to these files should be Authenticated. """, ) files_handler_params = Dict( config=True, help="""Extra parameters to pass to files_handler_class. For example, StaticFileHandlers generally expect a `path` argument specifying the root directory from which to serve files. """, ) def get_extra_handlers(self): """Return additional handlers Default: self.files_handler_class on /files/.* """ handlers = [] if self.files_handler_class: handlers.append((r"/files/(.*)", self.files_handler_class, self.files_handler_params)) return handlers # ContentsManager API part 1: methods that must be # implemented in subclasses. def dir_exists(self, path): """Does a directory exist at the given path? Like os.path.isdir Override this method in subclasses. Parameters ---------- path : string The path to check Returns ------- exists : bool Whether the path does indeed exist. """ raise NotImplementedError def is_hidden(self, path): """Is path a hidden directory or file? Parameters ---------- path : string The path to check. This is an API path (`/` separated, relative to root dir). Returns ------- hidden : bool Whether the path is hidden. """ raise NotImplementedError def file_exists(self, path=""): """Does a file exist at the given path? Like os.path.isfile Override this method in subclasses. Parameters ---------- path : string The API path of a file to check for. Returns ------- exists : bool Whether the file exists. """ raise NotImplementedError("must be implemented in a subclass") def exists(self, path): """Does a file or directory exist at the given path? Like os.path.exists Parameters ---------- path : string The API path of a file or directory to check for. Returns ------- exists : bool Whether the target exists. """ return self.file_exists(path) or self.dir_exists(path) def get(self, path, content=True, type=None, format=None): """Get a file or directory model.""" raise NotImplementedError("must be implemented in a subclass") def save(self, model, path): """ Save a file or directory model to path. Should return the saved model with no content. Save implementations should call self.run_pre_save_hook(model=model, path=path) prior to writing any data. """ raise NotImplementedError("must be implemented in a subclass") def delete_file(self, path): """Delete the file or directory at path.""" raise NotImplementedError("must be implemented in a subclass") def rename_file(self, old_path, new_path): """Rename a file or directory.""" raise NotImplementedError("must be implemented in a subclass") # ContentsManager API part 2: methods that have useable default # implementations, but can be overridden in subclasses. def delete(self, path): """Delete a file/directory and any associated checkpoints.""" path = path.strip("/") if not path: raise HTTPError(400, "Can't delete root") self.delete_file(path) self.checkpoints.delete_all_checkpoints(path) def rename(self, old_path, new_path): """Rename a file and any checkpoints associated with that file.""" self.rename_file(old_path, new_path) self.checkpoints.rename_all_checkpoints(old_path, new_path) def update(self, model, path): """Update the file's path For use in PATCH requests, to enable renaming a file without re-uploading its contents. Only used for renaming at the moment. """ path = path.strip("/") new_path = model.get("path", path).strip("/") if path != new_path: self.rename(path, new_path) model = self.get(new_path, content=False) return model def info_string(self): return "Serving contents" def get_kernel_path(self, path, model=None): """Return the API path for the kernel KernelManagers can turn this value into a filesystem path, or ignore it altogether. The default value here will start kernels in the directory of the notebook server. FileContentsManager overrides this to use the directory containing the notebook. """ return "" def increment_filename(self, filename, path="", insert=""): """Increment a filename until it is unique. Parameters ---------- filename : unicode The name of a file, including extension path : unicode The API path of the target's directory insert : unicode The characters to insert after the base filename Returns ------- name : unicode A filename that is unique, based on the input filename. """ # Extract the full suffix from the filename (e.g. .tar.gz) path = path.strip("/") basename, dot, ext = filename.rpartition(".") if ext != "ipynb": basename, dot, ext = filename.partition(".") suffix = dot + ext for i in itertools.count(): if i: insert_i = "{}{}".format(insert, i) else: insert_i = "" name = u"{basename}{insert}{suffix}".format(basename=basename, insert=insert_i, suffix=suffix) if not self.exists(u"{}/{}".format(path, name)): break return name def validate_notebook_model(self, model): """Add failed-validation message to model""" try: validate_nb(model["content"]) except ValidationError as e: model["message"] = u"Notebook validation failed: {}:\n{}".format( e.message, json.dumps(e.instance, indent=1, default=lambda obj: "<UNKNOWN>"), ) return model def new_untitled(self, path="", type="", ext=""): """Create a new untitled file or directory in path path must be a directory File extension can be specified. Use `new` to create files with a fully specified path (including filename). """ path = path.strip("/") if not self.dir_exists(path): raise HTTPError(404, "No such directory: %s" % path) model = {} if type: model["type"] = type if ext == ".ipynb": model.setdefault("type", "notebook") else: model.setdefault("type", "file") insert = "" if model["type"] == "directory": untitled = self.untitled_directory insert = " " elif model["type"] == "notebook": untitled = self.untitled_notebook ext = ".ipynb" elif model["type"] == "file": untitled = self.untitled_file else: raise HTTPError(400, "Unexpected model type: %r" % model["type"]) name = self.increment_filename(untitled + ext, path, insert=insert) path = u"{0}/{1}".format(path, name) return self.new(model, path) def new(self, model=None, path=""): """Create a new file or directory and return its model with no content. To create a new untitled entity in a directory, use `new_untitled`. """ path = path.strip("/") if model is None: model = {} if path.endswith(".ipynb"): model.setdefault("type", "notebook") else: model.setdefault("type", "file") # no content, not a directory, so fill out new-file model if "content" not in model and model["type"] != "directory": if model["type"] == "notebook": model["content"] = new_notebook() model["format"] = "json" else: model["content"] = "" model["type"] = "file" model["format"] = "text" model = self.save(model, path) return model def copy(self, from_path, to_path=None): """Copy an existing file and return its new model. If to_path not specified, it will be the parent directory of from_path. If to_path is a directory, filename will increment `from_path-Copy#.ext`. Considering multi-part extensions, the Copy# part will be placed before the first dot for all the extensions except `ipynb`. For easier manual searching in case of notebooks, the Copy# part will be placed before the last dot. from_path must be a full path to a file. """ path = from_path.strip("/") if to_path is not None: to_path = to_path.strip("/") if "/" in path: from_dir, from_name = path.rsplit("/", 1) else: from_dir = "" from_name = path model = self.get(path) model.pop("path", None) model.pop("name", None) if model["type"] == "directory": raise HTTPError(400, "Can't copy directories") if to_path is None: to_path = from_dir if self.dir_exists(to_path): name = copy_pat.sub(u".", from_name) to_name = self.increment_filename(name, to_path, insert="-Copy") to_path = u"{0}/{1}".format(to_path, to_name) model = self.save(model, to_path) return model def log_info(self): self.log.info(self.info_string()) def trust_notebook(self, path): """Explicitly trust a notebook Parameters ---------- path : string The path of a notebook """ model = self.get(path) nb = model["content"] self.log.warning("Trusting notebook %s", path) self.notary.mark_cells(nb, True) self.check_and_sign(nb, path) def check_and_sign(self, nb, path=""): """Check for trusted cells, and sign the notebook. Called as a part of saving notebooks. Parameters ---------- nb : dict The notebook dict path : string The notebook's path (for logging) """ if self.notary.check_cells(nb): self.notary.sign(nb) else: self.log.warning("Notebook %s is not trusted", path) def mark_trusted_cells(self, nb, path=""): """Mark cells as trusted if the notebook signature matches. Called as a part of loading notebooks. Parameters ---------- nb : dict The notebook object (in current nbformat) path : string The notebook's path (for logging) """ trusted = self.notary.check_signature(nb) if not trusted: self.log.warning("Notebook %s is not trusted", path) self.notary.mark_cells(nb, trusted) def should_list(self, name): """Should this file/directory name be displayed in a listing?""" return not any(fnmatch(name, glob) for glob in self.hide_globs) # Part 3: Checkpoints API def create_checkpoint(self, path): """Create a checkpoint.""" return self.checkpoints.create_checkpoint(self, path) def restore_checkpoint(self, checkpoint_id, path): """ Restore a checkpoint. """ self.checkpoints.restore_checkpoint(self, checkpoint_id, path) def list_checkpoints(self, path): return self.checkpoints.list_checkpoints(path) def delete_checkpoint(self, checkpoint_id, path): return self.checkpoints.delete_checkpoint(checkpoint_id, path)
class ExtensionApp(JupyterApp): """Base class for configurable Jupyter Server Extension Applications. ExtensionApp subclasses can be initialized two ways: 1. Extension is listed as a jpserver_extension, and ServerApp calls its load_jupyter_server_extension classmethod. This is the classic way of loading a server extension. 2. Extension is launched directly by calling its `launch_instance` class method. This method can be set as a entry_point in the extensions setup.py """ # Subclasses should override this trait. Tells the server if # this extension allows other other extensions to be loaded # side-by-side when launched directly. load_other_extensions = True # A useful class property that subclasses can override to # configure the underlying Jupyter Server when this extension # is launched directly (using its `launch_instance` method). serverapp_config = {} # Some subclasses will likely override this trait to flip # the default value to False if they don't offer a browser # based frontend. open_browser = Bool(help="""Whether to open in a browser after starting. The specific browser used is platform dependent and determined by the python standard library `webbrowser` module, unless it is overridden using the --browser (ServerApp.browser) configuration option. """).tag(config=True) @default('open_browser') def _default_open_browser(self): return self.serverapp.config["ServerApp"].get("open_browser", True) # The extension name used to name the jupyter config # file, jupyter_{name}_config. # This should also match the jupyter subcommand used to launch # this extension from the CLI, e.g. `jupyter {name}`. name = None @classmethod def get_extension_package(cls): parts = cls.__module__.split('.') if is_namespace_package(parts[0]): # in this case the package name is `<namespace>.<package>`. return '.'.join(parts[0:2]) return parts[0] @classmethod def get_extension_point(cls): return cls.__module__ # Extension URL sets the default landing page for this extension. extension_url = "/" default_url = Unicode().tag(config=True) @default('default_url') def _default_url(self): return self.extension_url file_url_prefix = Unicode('notebooks') # Is this linked to a serverapp yet? _linked = Bool(False) # Extension can configure the ServerApp from the command-line classes = [ ServerApp, ] # A ServerApp is not defined yet, but will be initialized below. serverapp = None _log_formatter_cls = LogFormatter @default('log_level') def _default_log_level(self): return logging.INFO @default('log_format') def _default_log_format(self): """override default log format to include date & time""" return u"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s" static_url_prefix = Unicode( help="""Url where the static assets for the extension are served.""" ).tag(config=True) @default('static_url_prefix') def _default_static_url_prefix(self): static_url = "static/{name}/".format(name=self.name) return url_path_join(self.serverapp.base_url, static_url) static_paths = List(Unicode(), help="""paths to search for serving static files. This allows adding javascript/css to be available from the notebook server machine, or overriding individual files in the IPython """).tag(config=True) template_paths = List( Unicode(), help=_i18n("""Paths to search for serving jinja templates. Can be used to override templates from notebook.templates.""")).tag( config=True) settings = Dict( help=_i18n("""Settings that will passed to the server.""")).tag( config=True) handlers = List(help=_i18n("""Handlers appended to the server.""")).tag( config=True) def _config_file_name_default(self): """The default config file name.""" if not self.name: return '' return 'jupyter_{}_config'.format(self.name.replace('-', '_')) def initialize_settings(self): """Override this method to add handling of settings.""" pass def initialize_handlers(self): """Override this method to append handlers to a Jupyter Server.""" pass def initialize_templates(self): """Override this method to add handling of template files.""" pass def _prepare_config(self): """Builds a Config object from the extension's traits and passes the object to the webapp's settings as `<name>_config`. """ traits = self.class_own_traits().keys() self.extension_config = Config({t: getattr(self, t) for t in traits}) self.settings['{}_config'.format(self.name)] = self.extension_config def _prepare_settings(self): # Make webapp settings accessible to initialize_settings method webapp = self.serverapp.web_app self.settings.update(**webapp.settings) # Add static and template paths to settings. self.settings.update({ "{}_static_paths".format(self.name): self.static_paths, "{}".format(self.name): self, }) # Get setting defined by subclass using initialize_settings method. self.initialize_settings() # Update server settings with extension settings. webapp.settings.update(**self.settings) def _prepare_handlers(self): webapp = self.serverapp.web_app # Get handlers defined by extension subclass. self.initialize_handlers() # prepend base_url onto the patterns that we match new_handlers = [] for handler_items in self.handlers: # Build url pattern including base_url pattern = url_path_join(webapp.settings['base_url'], handler_items[0]) handler = handler_items[1] # Get handler kwargs, if given kwargs = {} if issubclass(handler, ExtensionHandlerMixin): kwargs['name'] = self.name try: kwargs.update(handler_items[2]) except IndexError: pass new_handler = (pattern, handler, kwargs) new_handlers.append(new_handler) # Add static endpoint for this extension, if static paths are given. if len(self.static_paths) > 0: # Append the extension's static directory to server handlers. static_url = url_path_join(self.static_url_prefix, "(.*)") # Construct handler. handler = (static_url, webapp.settings['static_handler_class'], { 'path': self.static_paths }) new_handlers.append(handler) webapp.add_handlers('.*$', new_handlers) def _prepare_templates(self): # Add templates to web app settings if extension has templates. if len(self.template_paths) > 0: self.settings.update( {"{}_template_paths".format(self.name): self.template_paths}) self.initialize_templates() def _jupyter_server_config(self): base_config = { "ServerApp": { "default_url": self.default_url, "open_browser": self.open_browser, "file_url_prefix": self.file_url_prefix } } base_config["ServerApp"].update(self.serverapp_config) return base_config def _link_jupyter_server_extension(self, serverapp): """Link the ExtensionApp to an initialized ServerApp. The ServerApp is stored as an attribute and config is exchanged between ServerApp and `self` in case the command line contains traits for the ExtensionApp or the ExtensionApp's config files have server settings. Note, the ServerApp has not initialized the Tornado Web Application yet, so do not try to affect the `web_app` attribute. """ self.serverapp = serverapp # Load config from an ExtensionApp's config files. self.load_config_file() # ServerApp's config might have picked up # config for the ExtensionApp. We call # update_config to update ExtensionApp's # traits with these values found in ServerApp's # config. # ServerApp config ---> ExtensionApp traits self.update_config(self.serverapp.config) # Use ExtensionApp's CLI parser to find any extra # args that passed through ServerApp and # now belong to ExtensionApp. self.parse_command_line(self.serverapp.extra_args) # If any config should be passed upstream to the # ServerApp, do it here. # i.e. ServerApp traits <--- ExtensionApp config self.serverapp.update_config(self.config) # Acknowledge that this extension has been linked. self._linked = True def initialize(self): """Initialize the extension app. The corresponding server app and webapp should already be initialized by this step. 1) Appends Handlers to the ServerApp, 2) Passes config and settings from ExtensionApp to the Tornado web application 3) Points Tornado Webapp to templates and static assets. """ if not self.serverapp: msg = ("This extension has no attribute `serverapp`. " "Try calling `.link_to_serverapp()` before calling " "`.initialize()`.") raise JupyterServerExtensionException(msg) self._prepare_config() self._prepare_templates() self._prepare_settings() self._prepare_handlers() def start(self): """Start the underlying Jupyter server. Server should be started after extension is initialized. """ super(ExtensionApp, self).start() # Start the server. self.serverapp.start() async def stop_extension(self): """Cleanup any resources managed by this extension.""" def stop(self): """Stop the underlying Jupyter server. """ self.serverapp.stop() self.serverapp.clear_instance() @classmethod def _load_jupyter_server_extension(cls, serverapp): """Initialize and configure this extension, then add the extension's settings and handlers to the server's web application. """ extension_manager = serverapp.extension_manager try: # Get loaded extension from serverapp. point = extension_manager.extension_points[cls.name] extension = point.app except KeyError: extension = cls() extension._link_jupyter_server_extension(serverapp) extension.initialize() return extension @classmethod def load_classic_server_extension(cls, serverapp): """Enables extension to be loaded as classic Notebook (jupyter/notebook) extension. """ extension = cls() extension.serverapp = serverapp extension.load_config_file() extension.update_config(serverapp.config) extension.parse_command_line(serverapp.extra_args) # Add redirects to get favicons from old locations in the classic notebook server extension.handlers.extend([ (r"/static/favicons/favicon.ico", RedirectHandler, { "url": url_path_join(serverapp.base_url, "static/base/images/favicon.ico") }), (r"/static/favicons/favicon-busy-1.ico", RedirectHandler, { "url": url_path_join(serverapp.base_url, "static/base/images/favicon-busy-1.ico") }), (r"/static/favicons/favicon-busy-2.ico", RedirectHandler, { "url": url_path_join(serverapp.base_url, "static/base/images/favicon-busy-2.ico") }), (r"/static/favicons/favicon-busy-3.ico", RedirectHandler, { "url": url_path_join(serverapp.base_url, "static/base/images/favicon-busy-3.ico") }), (r"/static/favicons/favicon-file.ico", RedirectHandler, { "url": url_path_join(serverapp.base_url, "static/base/images/favicon-file.ico") }), (r"/static/favicons/favicon-notebook.ico", RedirectHandler, { "url": url_path_join(serverapp.base_url, "static/base/images/favicon-notebook.ico") }), (r"/static/favicons/favicon-terminal.ico", RedirectHandler, { "url": url_path_join(serverapp.base_url, "static/base/images/favicon-terminal.ico") }), (r"/static/logo/logo.png", RedirectHandler, { "url": url_path_join(serverapp.base_url, "static/base/images/logo.png") }), ]) extension.initialize() @classmethod def initialize_server(cls, argv=[], load_other_extensions=True, **kwargs): """Creates an instance of ServerApp and explicitly sets this extension to enabled=True (i.e. superceding disabling found in other config from files). The `launch_instance` method uses this method to initialize and start a server. """ jpserver_extensions = {cls.get_extension_package(): True} find_extensions = cls.load_other_extensions if 'jpserver_extensions' in cls.serverapp_config: jpserver_extensions.update( cls.serverapp_config['jpserver_extensions']) cls.serverapp_config['jpserver_extensions'] = jpserver_extensions find_extensions = False serverapp = ServerApp.instance(jpserver_extensions=jpserver_extensions, **kwargs) serverapp.initialize( argv=argv, starter_extension=cls.name, find_extensions=find_extensions, ) return serverapp @classmethod def launch_instance(cls, argv=None, **kwargs): """Launch the extension like an application. Initializes+configs a stock server and appends the extension to the server. Then starts the server and routes to extension's landing page. """ # Handle arguments. if argv is None: args = sys.argv[1:] # slice out extension config. else: args = argv # Handle all "stops" that could happen before # continuing to launch a server+extension. subapp = _preparse_for_subcommand(cls, args) if subapp: subapp.start() return # Check for help, version, and generate-config arguments # before initializing server to make sure these # arguments trigger actions from the extension not the server. _preparse_for_stopping_flags(cls, args) serverapp = cls.initialize_server(argv=args) # Log if extension is blocking other extensions from loading. if not cls.load_other_extensions: serverapp.log.info("{ext_name} is running without loading " "other extensions.".format(ext_name=cls.name)) # Start the server. try: serverapp.start() except NoStart: pass
class PasswordIdentityProvider(IdentityProvider): hashed_password = Unicode( "", config=True, help=_i18n(""" Hashed password to use for web authentication. To generate, type in a python/IPython shell: from jupyter_server.auth import passwd; passwd() The string should be of the form type:salt:hashed-password. """), ) password_required = Bool( False, config=True, help=_i18n(""" Forces users to use a password for the Jupyter server. This is useful in a multi user environment, for instance when everybody in the LAN can access each other's machine through ssh. In such a case, serving on localhost is not secure since any user can connect to the Jupyter server via ssh. """), ) allow_password_change = Bool( True, config=True, help=_i18n(""" Allow password to be changed at login for the Jupyter server. While logging in with a token, the Jupyter server UI will give the opportunity to the user to enter a new password at the same time that will replace the token login mechanism. This can be set to False to prevent changing password from the UI/API. """), ) @default("need_token") def _need_token_default(self): return not bool(self.hashed_password) @property def login_available(self) -> bool: """Whether a LoginHandler is needed - and therefore whether the login page should be displayed.""" return self.auth_enabled @property def auth_enabled(self) -> bool: """Return whether any auth is enabled""" return bool(self.hashed_password or self.token) def passwd_check(self, password): """Check password against our stored hashed password""" return passwd_check(self.hashed_password, password) def process_login_form(self, handler: JupyterHandler) -> User | None: """Process login form data Return authenticated User if successful, None if not. """ typed_password = handler.get_argument("password", default="") new_password = handler.get_argument("new_password", default="") user = None if not self.auth_enabled: self.log.warning( "Accepting anonymous login because auth fully disabled!") return self.generate_anonymous_user(handler) if self.passwd_check(typed_password) and not new_password: return self.generate_anonymous_user(handler) elif self.token and self.token == typed_password: user = self.generate_anonymous_user(handler) if new_password and self.allow_password_change: config_dir = handler.settings.get("config_dir", "") config_file = os.path.join(config_dir, "jupyter_server_config.json") self.hashed_password = set_password(new_password, config_file=config_file) self.log.info(_i18n(f"Wrote hashed password to {config_file}")) return user def validate_security( self, app: ServerApp, ssl_options: dict | None = None, ) -> None: super().validate_security(app, ssl_options) if self.password_required and (not self.hashed_password): self.log.critical( _i18n( "Jupyter servers are configured to only be run with a password." )) self.log.critical( _i18n("Hint: run the following command to set a password")) self.log.critical( _i18n("\t$ python -m jupyter_server.auth password")) sys.exit(1)
class IdentityProvider(LoggingConfigurable): """ Interface for providing identity management and authentication. Two principle methods: - :meth:`~.IdentityProvider.get_user` returns a :class:`~.User` object for successful authentication, or None for no-identity-found. - :meth:`~.IdentityProvider.identity_model` turns a :class:`~.User` into a JSONable dict. The default is to use :py:meth:`dataclasses.asdict`, and usually shouldn't need override. Additional methods can customize authentication. .. versionadded:: 2.0 """ cookie_name = Unicode( "", config=True, help=_i18n( "Name of the cookie to set for persisting login. Default: username-${Host}." ), ) cookie_options = Dict( config=True, help=_i18n("Extra keyword arguments to pass to `set_secure_cookie`." " See tornado's set_secure_cookie docs for details."), ) secure_cookie = Bool( None, allow_none=True, config=True, help=_i18n( "Specify whether login cookie should have the `secure` property (HTTPS-only)." "Only needed when protocol-detection gives the wrong answer due to proxies." ), ) get_secure_cookie_kwargs = Dict( config=True, help=_i18n("Extra keyword arguments to pass to `get_secure_cookie`." " See tornado's get_secure_cookie docs for details."), ) token = Unicode( "<generated>", help=_i18n( """Token used for authenticating first-time connections to the server. The token can be read from the file referenced by JUPYTER_TOKEN_FILE or set directly with the JUPYTER_TOKEN environment variable. When no password is enabled, the default is to generate a new, random token. Setting to an empty string disables authentication altogether, which is NOT RECOMMENDED. Prior to 2.0: configured as ServerApp.token """), ).tag(config=True) login_handler_class = Type( default_value="jupyter_server.auth.login.LoginFormHandler", klass=web.RequestHandler, config=True, help=_i18n("The login handler class to use, if any."), ) logout_handler_class = Type( default_value="jupyter_server.auth.logout.LogoutHandler", klass=web.RequestHandler, config=True, help=_i18n("The logout handler class to use."), ) token_generated = False @default("token") def _token_default(self): if os.getenv("JUPYTER_TOKEN"): self.token_generated = False return os.environ["JUPYTER_TOKEN"] if os.getenv("JUPYTER_TOKEN_FILE"): self.token_generated = False with open(os.environ["JUPYTER_TOKEN_FILE"]) as token_file: return token_file.read() if not self.need_token: # no token if password is enabled self.token_generated = False return "" else: self.token_generated = True return binascii.hexlify(os.urandom(24)).decode("ascii") need_token = Bool(True) def get_user( self, handler: JupyterHandler) -> User | None | Awaitable[User | None]: """Get the authenticated user for a request Must return a :class:`.jupyter_server.auth.User`, though it may be a subclass. Return None if the request is not authenticated. _may_ be a coroutine """ return self._get_user(handler) # not sure how to have optional-async type signature # on base class with `async def` without splitting it into two methods async def _get_user(self, handler: JupyterHandler) -> User | None: if getattr(handler, "_jupyter_current_user", None): # already authenticated return handler._jupyter_current_user _token_user: User | None | Awaitable[User | None] = self.get_user_token( handler) if isinstance(_token_user, Awaitable): _token_user = await _token_user token_user: User | None = _token_user # need second variable name to collapse type _cookie_user = self.get_user_cookie(handler) if isinstance(_cookie_user, Awaitable): _cookie_user = await _cookie_user cookie_user: User | None = _cookie_user # prefer token to cookie if both given, # because token is always explicit user = token_user or cookie_user if user is not None and token_user is not None: # if token-authenticated, persist user_id in cookie # if it hasn't already been stored there if user != cookie_user: self.set_login_cookie(handler, user) # Record that the current request has been authenticated with a token. # Used in is_token_authenticated above. handler._token_authenticated = True if user is None: # If an invalid cookie was sent, clear it to prevent unnecessary # extra warnings. But don't do this on a request with *no* cookie, # because that can erroneously log you out (see gh-3365) cookie_name = self.get_cookie_name(handler) cookie = handler.get_cookie(cookie_name) if cookie is not None: self.log.warning( f"Clearing invalid/expired login cookie {cookie_name}") self.clear_login_cookie(handler) if not self.auth_enabled: # Completely insecure! No authentication at all. # No need to warn here, though; validate_security will have already done that. user = self.generate_anonymous_user(handler) return user def identity_model(self, user: User) -> dict: """Return a User as an Identity model""" # TODO: validate? return asdict(user) def get_handlers(self) -> list: """Return list of additional handlers for this identity provider For example, an OAuth callback handler. """ handlers = [] if self.login_available: handlers.append((r"/login", self.login_handler_class)) if self.logout_available: handlers.append((r"/logout", self.logout_handler_class)) return handlers def user_to_cookie(self, user: User) -> str: """Serialize a user to a string for storage in a cookie If overriding in a subclass, make sure to define user_from_cookie as well. Default is just the user's username. """ # default: username is enough cookie = json.dumps({ "username": user.username, "name": user.name, "display_name": user.display_name, "initials": user.initials, "color": user.color, }) return cookie def user_from_cookie(self, cookie_value: str) -> User | None: """Inverse of user_to_cookie""" user = json.loads(cookie_value) return User( user["username"], user["name"], user["display_name"], user["initials"], None, user["color"], ) def get_cookie_name(self, handler: JupyterHandler) -> str: """Return the login cookie name Uses IdentityProvider.cookie_name, if defined. Default is to generate a string taking host into account to avoid collisions for multiple servers on one hostname with different ports. """ if self.cookie_name: return self.cookie_name else: return _non_alphanum.sub("-", f"username-{handler.request.host}") def set_login_cookie(self, handler: JupyterHandler, user: User) -> None: """Call this on handlers to set the login cookie for success""" cookie_options = {} cookie_options.update(self.cookie_options) cookie_options.setdefault("httponly", True) # tornado <4.2 has a bug that considers secure==True as soon as # 'secure' kwarg is passed to set_secure_cookie secure_cookie = self.secure_cookie if secure_cookie is None: secure_cookie = handler.request.protocol == "https" if secure_cookie: cookie_options.setdefault("secure", True) cookie_options.setdefault("path", handler.base_url) cookie_name = self.get_cookie_name(handler) handler.set_secure_cookie(cookie_name, self.user_to_cookie(user), **cookie_options) def _force_clear_cookie(self, handler: JupyterHandler, name: str, path: str = "/", domain: str | None = None) -> None: """Deletes the cookie with the given name. Tornado's cookie handling currently (Jan 2018) stores cookies in a dict keyed by name, so it can only modify one cookie with a given name per response. The browser can store multiple cookies with the same name but different domains and/or paths. This method lets us clear multiple cookies with the same name. Due to limitations of the cookie protocol, you must pass the same path and domain to clear a cookie as were used when that cookie was set (but there is no way to find out on the server side which values were used for a given cookie). """ name = escape.native_str(name) expires = datetime.datetime.utcnow() - datetime.timedelta(days=365) morsel: Morsel = Morsel() morsel.set(name, "", '""') morsel["expires"] = httputil.format_timestamp(expires) morsel["path"] = path if domain: morsel["domain"] = domain handler.add_header("Set-Cookie", morsel.OutputString()) def clear_login_cookie(self, handler: JupyterHandler) -> None: """Clear the login cookie, effectively logging out the session.""" cookie_options = {} cookie_options.update(self.cookie_options) path = cookie_options.setdefault("path", handler.base_url) cookie_name = self.get_cookie_name(handler) handler.clear_cookie(cookie_name, path=path) if path and path != "/": # also clear cookie on / to ensure old cookies are cleared # after the change in path behavior. # N.B. This bypasses the normal cookie handling, which can't update # two cookies with the same name. See the method above. self._force_clear_cookie(handler, cookie_name) def get_user_cookie( self, handler: JupyterHandler) -> User | None | Awaitable[User | None]: """Get user from a cookie Calls user_from_cookie to deserialize cookie value """ _user_cookie = handler.get_secure_cookie( self.get_cookie_name(handler), **self.get_secure_cookie_kwargs, ) if not _user_cookie: return None user_cookie = _user_cookie.decode() # TODO: try/catch in case of change in config? try: return self.user_from_cookie(user_cookie) except Exception as e: # log bad cookie itself, only at debug-level self.log.debug( f"Error unpacking user from cookie: cookie={user_cookie}", exc_info=True) self.log.error(f"Error unpacking user from cookie: {e}") return None auth_header_pat = re.compile(r"(token|bearer)\s+(.+)", re.IGNORECASE) def get_token(self, handler: JupyterHandler) -> str | None: """Get the user token from a request Default: - in URL parameters: ?token=<token> - in header: Authorization: token <token> """ user_token = handler.get_argument("token", "") if not user_token: # get it from Authorization header m = self.auth_header_pat.match( handler.request.headers.get("Authorization", "")) if m: user_token = m.group(2) return user_token async def get_user_token(self, handler: JupyterHandler) -> User | None: """Identify the user based on a token in the URL or Authorization header Returns: - uuid if authenticated - None if not """ token = handler.token if not token: return None # check login token from URL argument or Authorization header user_token = self.get_token(handler) authenticated = False if user_token == token: # token-authenticated, set the login cookie self.log.debug( "Accepting token-authenticated request from %s", handler.request.remote_ip, ) authenticated = True if authenticated: # token does not correspond to user-id, # which is stored in a cookie. # still check the cookie for the user id _user = self.get_user_cookie(handler) if isinstance(_user, Awaitable): _user = await _user user: User | None = _user if user is None: user = self.generate_anonymous_user(handler) return user else: return None def generate_anonymous_user(self, handler: JupyterHandler) -> User: """Generate a random anonymous user. For use when a single shared token is used, but does not identify a user. """ user_id = uuid.uuid4().hex moon = get_anonymous_username() name = display_name = f"Anonymous {moon}" initials = f"A{moon[0]}" color = None handler.log.info( f"Generating new user for token-authenticated request: {user_id}") return User(user_id, name, display_name, initials, None, color) def should_check_origin(self, handler: JupyterHandler) -> bool: """Should the Handler check for CORS origin validation? Origin check should be skipped for token-authenticated requests. Returns: - True, if Handler must check for valid CORS origin. - False, if Handler should skip origin check since requests are token-authenticated. """ return not self.is_token_authenticated(handler) def is_token_authenticated(self, handler: JupyterHandler) -> bool: """Returns True if handler has been token authenticated. Otherwise, False. Login with a token is used to signal certain things, such as: - permit access to REST API - xsrf protection - skip origin-checks for scripts """ # ensure get_user has been called, so we know if we're token-authenticated handler.current_user # noqa return getattr(handler, "_token_authenticated", False) def validate_security( self, app: ServerApp, ssl_options: dict | None = None, ) -> None: """Check the application's security. Show messages, or abort if necessary, based on the security configuration. """ if not app.ip: warning = "WARNING: The Jupyter server is listening on all IP addresses" if ssl_options is None: app.log.warning( f"{warning} and not using encryption. This is not recommended." ) if not self.auth_enabled: app.log.warning(f"{warning} and not using authentication. " "This is highly insecure and not recommended.") else: if not self.auth_enabled: app.log.warning( "All authentication is disabled." " Anyone who can connect to this server will be able to run code." ) def process_login_form(self, handler: JupyterHandler) -> User | None: """Process login form data Return authenticated User if successful, None if not. """ typed_password = handler.get_argument("password", default="") user = None if not self.auth_enabled: self.log.warning( "Accepting anonymous login because auth fully disabled!") return self.generate_anonymous_user(handler) if self.token and self.token == typed_password: return self.user_for_token(typed_password) return user @property def auth_enabled(self): """Is authentication enabled? Should always be True, but may be False in rare, insecure cases where requests with no auth are allowed. Previously: LoginHandler.get_login_available """ return True @property def login_available(self): """Whether a LoginHandler is needed - and therefore whether the login page should be displayed.""" return self.auth_enabled @property def logout_available(self): """Whether a LogoutHandler is needed.""" return True
def _update_mathjax_config(self, change): self.log.info(_i18n("Using MathJax configuration file: %s"), change['new'])
class NotebookAppTraits(HasTraits): ignore_minified_js = Bool( False, config=True, help=_i18n( 'Deprecated: Use minified JS file or not, mainly use during dev to avoid JS recompilation' ), ) jinja_environment_options = Dict( config=True, help=_i18n( "Supply extra arguments that will be passed to Jinja environment.") ) jinja_template_vars = Dict( config=True, help=_i18n( "Extra variables to supply to jinja templates when rendering."), ) enable_mathjax = Bool( True, config=True, help="""Whether to enable MathJax for typesetting math/TeX MathJax is the javascript library Jupyter uses to render math/LaTeX. It is very large, so you may want to disable it if you have a slow internet connection, or for offline use of the notebook. When disabled, equations etc. will appear as their untransformed TeX source. """) @observe('enable_mathjax') def _update_enable_mathjax(self, change): """set mathjax url to empty if mathjax is disabled""" if not change['new']: self.mathjax_url = u'' extra_static_paths = List( Unicode(), config=True, help="""Extra paths to search for serving static files. This allows adding javascript/css to be available from the notebook server machine, or overriding individual files in the IPython""") @property def static_file_path(self): """return extra paths + the default location""" return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH] static_custom_path = List( Unicode(), help=_i18n("""Path to search for custom.js, css""")) @default('static_custom_path') def _default_static_custom_path(self): return [ os.path.join(d, 'custom') for d in (self.config_dir, DEFAULT_STATIC_FILES_PATH) ] extra_template_paths = List( Unicode(), config=True, help=_i18n("""Extra paths to search for serving jinja templates. Can be used to override templates from notebook.templates.""")) @property def template_file_path(self): """return extra paths + the default locations""" return self.extra_template_paths + DEFAULT_TEMPLATE_PATH_LIST extra_nbextensions_path = List( Unicode(), config=True, help=_i18n( """extra paths to look for Javascript notebook extensions""")) @property def nbextensions_path(self): """The path to look for Javascript notebook extensions""" path = self.extra_nbextensions_path + jupyter_path('nbextensions') # FIXME: remove IPython nbextensions path after a migration period try: from IPython.paths import get_ipython_dir except ImportError: pass else: path.append(os.path.join(get_ipython_dir(), 'nbextensions')) return path mathjax_url = Unicode("", config=True, help="""A custom url for MathJax.js. Should be in the form of a case-sensitive url to MathJax, for example: /static/components/MathJax/MathJax.js """) @property def static_url_prefix(self): """Get the static url prefix for serving static files.""" return super(NotebookAppTraits, self).static_url_prefix @default('mathjax_url') def _default_mathjax_url(self): if not self.enable_mathjax: return u'' static_url_prefix = self.static_url_prefix return url_path_join(static_url_prefix, 'components', 'MathJax', 'MathJax.js') @observe('mathjax_url') def _update_mathjax_url(self, change): new = change['new'] if new and not self.enable_mathjax: # enable_mathjax=False overrides mathjax_url self.mathjax_url = u'' else: self.log.info(_i18n("Using MathJax: %s"), new) mathjax_config = Unicode( "TeX-AMS-MML_HTMLorMML-full,Safe", config=True, help=_i18n( """The MathJax.js configuration file that is to be used.""")) @observe('mathjax_config') def _update_mathjax_config(self, change): self.log.info(_i18n("Using MathJax configuration file: %s"), change['new']) quit_button = Bool( True, config=True, help="""If True, display a button in the dashboard to quit (shutdown the notebook server).""") nbserver_extensions = Dict( {}, config=True, help=(_i18n( "Dict of Python modules to load as notebook server extensions." "Entry values can be used to enable and disable the loading of" "the extensions. The extensions will be loaded in alphabetical " "order.")))
def info_string(self): return _i18n("Serving notebooks from local directory: %s") % self.root_dir
class NotebookApp( shim.NBClassicConfigShimMixin, ExtensionAppJinjaMixin, ExtensionApp, traits.NotebookAppTraits, ): name = 'notebook' version = __version__ description = _i18n("""The Jupyter HTML Notebook. This launches a Tornado based HTML Notebook Server that serves up an HTML5/Javascript Notebook client.""" ) aliases = aliases flags = flags extension_url = "/tree" subcommands = {} default_url = Unicode("/tree").tag(config=True) # Override the default open_Browser trait in ExtensionApp, # setting it to True. open_browser = Bool(True, help="""Whether to open in a browser after starting. The specific browser used is platform dependent and determined by the python standard library `webbrowser` module, unless it is overridden using the --browser (ServerApp.browser) configuration option. """).tag(config=True) static_custom_path = List( Unicode(), help=_i18n("""Path to search for custom.js, css""")) @default('static_custom_path') def _default_static_custom_path(self): return [ os.path.join(d, 'custom') for d in (self.config_dir, DEFAULT_STATIC_FILES_PATH) ] extra_nbextensions_path = List( Unicode(), config=True, help=_i18n( """extra paths to look for Javascript notebook extensions""")) @property def nbextensions_path(self): """The path to look for Javascript notebook extensions""" path = self.extra_nbextensions_path + jupyter_path('nbextensions') # FIXME: remove IPython nbextensions path after a migration period try: from IPython.paths import get_ipython_dir except ImportError: pass else: path.append(os.path.join(get_ipython_dir(), 'nbextensions')) return path @property def static_paths(self): """Rename trait in jupyter_server.""" return self.static_file_path @property def template_paths(self): """Rename trait for Jupyter Server.""" return self.template_file_path def _prepare_templates(self): super(NotebookApp, self)._prepare_templates() # Get translations from notebook package. base_dir = os.path.dirname(notebook.__file__) nbui = gettext.translation('nbui', localedir=os.path.join( base_dir, 'notebook/i18n'), fallback=True) self.jinja2_env.install_gettext_translations(nbui, newstyle=False) def initialize_settings(self): """Add settings to the tornado app.""" if self.ignore_minified_js: self.log.warning( _i18n( """The `ignore_minified_js` flag is deprecated and no longer works.""" )) self.log.warning( _i18n( """Alternatively use `%s` when working on the notebook's Javascript and LESS""" ) % 'npm run build:watch') warnings.warn( _i18n( "The `ignore_minified_js` flag is deprecated and will be removed in Notebook 6.0" ), DeprecationWarning) settings = dict( static_custom_path=self.static_custom_path, static_handler_args={ # don't cache custom.js 'no_cache_paths': [ url_path_join(self.serverapp.base_url, 'static', self.name, 'custom') ], }, ignore_minified_js=self.ignore_minified_js, mathjax_url=self.mathjax_url, mathjax_config=self.mathjax_config, nbextensions_path=self.nbextensions_path, ) self.settings.update(**settings) def initialize_handlers(self): """Load the (URL pattern, handler) tuples for each component.""" # Order matters. The first handler to match the URL will handle the request. handlers = [] # Add a redirect from /notebooks to /edit # for opening non-ipynb files in edit mode. handlers.append((rf"/{self.file_url_prefix}/((?!.*\.ipynb($|\?)).*)", RedirectHandler, { "url": self.serverapp.base_url + "edit/{0}" })) # load extra services specified by users before default handlers for service in self.settings['extra_services']: handlers.extend(load_handlers(service)) handlers.extend(load_handlers('nbclassic.tree.handlers')) handlers.extend(load_handlers('nbclassic.notebook.handlers')) handlers.extend(load_handlers('nbclassic.edit.handlers')) # Add terminal handlers handlers.append((r"/terminals/(\w+)", TerminalHandler)) handlers.append( ( r"/nbextensions/(.*)", FileFindHandler, { 'path': self.settings['nbextensions_path'], 'no_cache_paths': ['/'], # don't cache anything in nbextensions }), ) handlers.append( ( r"/custom/(.*)", FileFindHandler, { 'path': self.settings['static_custom_path'], 'no_cache_paths': ['/'], # don't cache anything in nbextensions }), ) # Add new handlers to Jupyter server handlers. self.handlers.extend(handlers)
jupyter nbclassic # start the notebook jupyter nbclassic --certfile=mycert.pem # use SSL/TLS certificate jupyter nbclassic password # enter a password to protect the server """ #----------------------------------------------------------------------------- # Aliases and Flags #----------------------------------------------------------------------------- flags = {} aliases = {} flags['no-browser'] = ({ 'ServerApp': { 'open_browser': False } }, _i18n("Don't open the notebook in a browser after startup.")) flags['no-mathjax'] = ({ 'NotebookApp': { 'enable_mathjax': False } }, """Disable MathJax MathJax is the javascript library Jupyter uses to render math/LaTeX. It is very large, so you may want to disable it if you have a slow internet connection, or for offline use of the notebook. When disabled, equations etc. will appear as their untransformed TeX source. """) flags['allow-root'] = ({ 'ServerApp': {