Package: jlab-ext-example
Server Hello WorldCreate a minimal extension with backend (i.e. server) and frontend parts.It is strongly recommended to read thebasic hello-worldexample before diving into this one.Server Hello WorldThe template folder structureFrontend PartBackend (Server) PartPackaging the ExtensionPython Package ManagerJupyterLab Extension ManagerInstalling the PackageThe template folder structureWriting a JupyterLab extension usually starts from a configurable template. It
can be downloaded with thecookiecuttertool and the following command for an extension with a server part:cookiecutterhttps://github.com/jupyterlab/extension-cookiecutter-tscookiecutterasks for some basic information that could for example be setup
like this (be careful to sethas_server_extensiontoy):author_name[]:tuto
author_email[]:tuto@help.you
labextension_name[myextension]:jlab-ext-example
python_name[jlab_ext_example]:
project_short_description[AJupyterLabextension.]:AminimalJupyterLabextensionwithbackendandfrontendparts.
has_settings[n]:
has_server_extension[n]:y
has_binder[n]:y
repository[https://github.com/github_username/jlab-ext-example]:The python name should not contain-. It is nice for user to test your extension online, so thehas_binderwas set toyes.The cookiecutter creates the directoryjlab_ext_example[or your extension name]
that looks like this:jlab_ext_example/
│# Generic Files│.gitignore
│install.json# Information retrieved by JupyterLab to help users know how to manage the extension│LICENSE# License of your code│README.md# Instructions to install and build│
├───.github
│└───workflows
│build.yml
│
├───binder
│environment.yml
│postBuild
││# Python Package Files│MANIFEST.in# Help Python to list your source files│pyproject.toml# Define dependencies for building the package│setup.py# Information about the package│
│# Backend (server) Files├───jupyter-config
│jlab_ext_example.json# Server extension enabler│
├───jlab_ext_example
│handlers.py# API handler (where things happen)│_version.py# Server extension version│__init__.py# Hook the extension in the server││# Frontend Files│.eslintignore# Code linter configuration│.eslintrc.js
│.prettierignore# Code formatter configuration│.prettierrc
│package.json# Information about the frontend package│tsconfig.json# Typescript compilation configuration│├───src
│index.ts# Actual code of the extension│handler.ts# More code used by the extension│
└───stylebase.css# CSS stylingindex.cssindex.jsThere are two major parts in the extension:A Python package for the server extension and the packagingA NPM package for the frontend extensionIn this example, you will see that the template code have been extended
to demonstrate the use of GET and POST HTTP requests.Frontend PartThe entry point for the frontend extension issrc/index.ts. The
communication with the server extension is contained in another filesrc/handler.ts. So you need to import it:// src/index.ts#L12-L12import{requestAPI}from'./handler';In theactivatefunction, the server extension is first called through
a GET request on the endpoint/jlab-ext-example/hello. The response from the server
is printed in the web browser console:// src/index.ts#L36-L42// GET requesttry{constdata=awaitrequestAPI<any>('hello');console.log(data);}catch(reason){console.error(`Error on GET /jlab-ext-example/hello.\n${reason}`);}As the server response is not instantaneous, the request is done asynchronously
using theawaitkeyword:// src/index.ts#L38-L38constdata=awaitrequestAPI<any>('hello');To use thatawaitkeyword, the function must be declared as asynchronous
using theasynckeyword:// src/index.ts#L29-L33activate:async(app:JupyterFrontEnd,palette:ICommandPalette,launcher:ILauncher|null)=>{A GET request cannot carry data from the frontend to the server. To achieve that,
you will need to execute a POST request. In this example, a POST request
is sent to the/jlab-ext-example/helloendpoint with the data{name: 'George'}:// src/index.ts#L45-L56constdataToSend={name:'George'};try{constreply=awaitrequestAPI<any>('hello',{body:JSON.stringify(dataToSend),method:'POST',});console.log(reply);}catch(reason){console.error(`Error on POST /jlab-ext-example/hello${dataToSend}.\n${reason}`);}The difference with the GET request is the use of thebodyoption to send data
and themethodoption to set the appropriate HTTP method.The data sent from the frontend to the backend can have different types. In
JupyterLab, the most common format is JSON. But JSON cannot directly be sent to
the server, it needs to be stringified to be carried over by the request.The communication logic with the server is hidden in therequestAPIfunction.
Its definition is :// src/handler.ts#L12-L37exportasyncfunctionrequestAPI<T>(endPoint='',init:RequestInit={}):Promise<T>{// Make request to Jupyter APIconstsettings=ServerConnection.makeSettings();constrequestUrl=URLExt.join(settings.baseUrl,'jlab-ext-example',endPoint);letresponse:Response;try{response=awaitServerConnection.makeRequest(requestUrl,init,settings);}catch(error){thrownewServerConnection.NetworkError(error);}constdata=awaitresponse.json();if(!response.ok){thrownewServerConnection.ResponseError(response,data.message);}returndata;First the server settings are obtained from:// src/handler.ts#L17-L17constsettings=ServerConnection.makeSettings();This requires to add@jupyterlab/servicesto the package dependencies:jlpmadd@jupyterlab/servicesThen the classServerConnectioncan be imported:// src/handler.ts#L3-L3import{ServerConnection}from'@jupyterlab/services';The next step is to build the full request URL:// src/handler.ts#L18-L21constrequestUrl=URLExt.join(settings.baseUrl,'jlab-ext-example',endPointTo concatenate the various parts, theURLExtutility is imported:// src/handler.ts#L1-L1import{URLExt}from'@jupyterlab/coreutils';This requires to add another dependency to the package:jlpmadd@jupyterlab/coreutilsYou now have all the elements to make the request:// src/handler.ts#L26-L26response=awaitServerConnection.makeRequest(requestUrl,init,settings);Finally, once the server response is obtained, its body is interpreted as
JSON. And the resulting data is returned.// src/handler.ts#L31-L37constdata=awaitresponse.json();if(!response.ok){thrownewServerConnection.ResponseError(response,data.message);}returndata;This example also showcases how you can serve static files from the server extension.// src/index.ts#L58-L79const{commands,shell}=app;constcommand=CommandIDs.get;constcategory='Extension Examples';commands.addCommand(command,{label:'Get Server Content in a IFrame Widget',caption:'Get Server Content in a IFrame Widget',execute:()=>{constwidget=newIFrameWidget();shell.add(widget,'main');},});palette.addItem({command,category:category});if(launcher){// Add launcherlauncher.add({command:command,category:category,});}Invoking the command (via the command palette or the launcher) will open a new tab with
anIFramethat will display static content fetched from the server extension.NoteIf the response is not ok (i.e. status code not in range 200-299),
aResponseErroris thrown.The response body is interpreted as JSON even in case the response is not
ok. In JupyterLab, it is a good practice in case of error on the server
side to return a response with a JSON body. It should at least define amessagekey providing nice error message for the user.Backend (Server) PartThe server part of the extension is going to be presented in this section.JupyterLab server is built on top of theTornadoPython package. To extend the server,
your extension needs to be defined as a proper Python package with some hook functions:# jlab_ext_example/__init__.pyimportjsonfrompathlibimportPathfrom.handlersimportsetup_handlersfrom._versionimport__version__HERE=Path(__file__).parent.resolve()with(HERE/"labextension"/"package.json").open()asfid:data=json.load(fid)def_jupyter_labextension_paths():return[{"src":"labextension","dest":data["name"]}]def_jupyter_server_extension_points():return[{"module":"jlab_ext_example"}]def_load_jupyter_server_extension(server_app):"""Registers the API handler to receive HTTP requests from the frontend extension.Parameters----------server_app: jupyterlab.labapp.LabAppJupyterLab application instance"""url_path="jlab-ext-example"setup_handlers(server_app.web_app,url_path)server_app.log.info(f"Registered jlab_ext_example extension at URL path /{url_path}")# For backward compatibility with the classical notebookload_jupyter_server_extension=_load_jupyter_server_extensionThe_jupyter_server_extension_pointsprovides the Python package name
to the server. But the most important one is_load_jupyter_server_extensionthat register new handlers.# jlab_ext_example/__init__.py#L29-L29setup_handlers(server_app.web_app,url_path)A handler is registered in the web application by linking an url to a class. In this
example the url isbase_server_url/jlab-ext-example/helloand the class handler isRouteHandler:# jlab_ext_example/handlers.py#L28-L34host_pattern=".*$"base_url=web_app.settings["base_url"]# Prepend the base_url so that it works in a JupyterHub settingroute_pattern=url_path_join(base_url,url_path,"hello")handlers=[(route_pattern,RouteHandler)]web_app.add_handlers(host_pattern,handlers)For Jupyter server, the handler class must inherit from theAPIHandlerand it should
implement the wanted HTTP verbs. For example, here,/jlab-ext-example/hellocan be requested
by aGETor aPOSTrequest. They will call thegetorpostmethod respectively.# jlab_ext_example/handlers.py#L11-L24classRouteHandler(APIHandler):# The following decorator should be present on all verb methods (head, get, post,# patch, put, delete, options) to ensure only authorized user can request the# Jupyter server@tornado.web.authenticateddefget(self):self.finish(json.dumps({"data":"This is /jlab-ext-example/hello endpoint!"}))@tornado.web.authenticateddefpost(self):# input_data is a dictionary with a key "name"input_data=self.get_json_body()data={"greetings":"Hello{}, enjoy JupyterLab!".format(input_data["name"])}self.finish(json.dumps(data))Security NoteThe methods to handle request likeget,post, etc.
must be decorated withtornado.web.authenticatedto ensure only
authenticated users can request the Jupyter server.Once the server has carried out the appropriate task, the handler should finish the request
by calling thefinishmethod. That method can optionally take an argument that will
become the response body of the request in the frontend.# jlab_ext_example/handlers.py#L16-L17defget(self):self.finish(json.dumps({"data":"This is /jlab-ext-example/hello endpoint!"}))In Jupyter, it is common to use JSON as format between the frontend and the backend.
But it should first be stringified to be a valid response body. This can be done usingjson.dumpson a dictionary.APOSTrequest is similar to aGETrequest except it may have a body containing data
sent by the frontend. When using JSON as communication format, you can directly use theget_json_bodyhelper method to convert the request body into a Python dictionary.# jlab_ext_example/handlers.py#L22-L23input_data=self.get_json_body()data={"greetings":"Hello{}, enjoy JupyterLab!".format(input_data["name"])}The part responsible to serve static content with aStaticFileHandlerhandler
is the following:# jlab_ext_example/handlers.py#L37-L43doc_url=url_path_join(base_url,url_path,"public")doc_dir=os.getenv("JLAB_SERVER_EXAMPLE_STATIC_DIR",os.path.join(os.path.dirname(__file__),"public"),)handlers=[("{}/(.*)".format(doc_url),StaticFileHandler,{"path":doc_dir})]web_app.add_handlers(".*$",handlers)Security NoteTheStaticFileHandleris not secured.
For enhanced security, please consider usingAuthenticatedFileHandler.NoteServer extensions can be used for different frontends (like
JupyterLab and the classical Jupyter Notebook). Some additional
documentation is available in theNotebook documentationPackaging the ExtensionPython Package ManagerIn the previous sections, the acting code has been described. But there are other files
with the sole purpose of packaging the full extension nicely to help its distribution
through package managers likepip.Note: In particular,jupyter-packagingprovides helpers to package and install JS files
with a Python package for Jupyter frontends (classical notebook,
JupyterLab,...).
As this package is a setup requirement, it needs to be specified in thepyproject.tomlto be installed bypip.Thesetup.pyfile is the entry point to describe package metadata:# setup.py"""jlab_ext_example setup"""importjsonimportsysfrompathlibimportPathimportsetuptoolsHERE=Path(__file__).parent.resolve()# The name of the projectname="jlab_ext_example"lab_path=(HERE/name.replace("-","_")/"labextension")# Representative files that should exist after a successful buildensured_targets=[str(lab_path/"package.json"),str(lab_path/"static/style.js")]labext_name="@jupyterlab-examples/server-extension"data_files_spec=[("share/jupyter/labextensions/%s"%labext_name,str(lab_path.relative_to(HERE)),"**"),("share/jupyter/labextensions/%s"%labext_name,str("."),"install.json"),("etc/jupyter/jupyter_server_config.d","jupyter-config/server-config","jlab_ext_example.json"),# For backward compatibility with notebook server("etc/jupyter/jupyter_notebook_config.d","jupyter-config/nb-config","jlab_ext_example.json"),]long_description=(HERE/"README.md").read_text()# Get the package info from package.jsonpkg_json=json.loads((HERE/"package.json").read_bytes())setup_args=dict(name=name,version=pkg_json["version"],url=pkg_json["homepage"],author=pkg_json["author"]["name"],author_email=pkg_json["author"]["email"],description=pkg_json["description"],license=pkg_json["license"],long_description=long_description,long_description_content_type="text/markdown",packages=setuptools.find_packages(),install_requires=["jupyter_server>=1.6,<2"],zip_safe=False,include_package_data=True,python_requires=">=3.6",platforms="Linux, Mac OS X, Windows",keywords=["Jupyter","JupyterLab","JupyterLab3"],classifiers=["License :: OSI Approved :: BSD License","Programming Language :: Python","Programming Language :: Python :: 3","Programming Language :: Python :: 3.6","Programming Language :: Python :: 3.7","Programming Language :: Python :: 3.8","Programming Language :: Python :: 3.9","Framework :: Jupyter",],)try:fromjupyter_packagingimport(wrap_installers,npm_builder,get_data_files)post_develop=npm_builder(build_cmd="install:extension",source_dir="src",build_dir=lab_path)setup_args["cmdclass"]=wrap_installers(post_develop=post_develop,ensured_targets=ensured_targets)setup_args["data_files"]=get_data_files(data_files_spec)exceptImportErrorase:importlogginglogging.basicConfig(format="%(levelname)s:%(message)s")logging.warning("Build tool `jupyter-packaging` is missing. Install it with pip or conda.")ifnot("--name"insys.argvor"--version"insys.argv):raiseeif__name__=="__main__":setuptools.setup(**setup_args)But in this case, it is a bit more complicated to build the frontend extension and ship it
directly with the Python package. To deploy simultaneously the frontend and the backend,
the frontend NPM package needs to be built and inserted in the Python package. This is
done using a special a dedicated builder followingPEP 517from packagejupyter-packaging. Its configuration is done inpyproject.toml:# pyproject.toml[build-system]requires=["jupyter_packaging~=0.10,<2","jupyterlab~=3.1"]build-backend="jupyter_packaging.build_api"[tool.jupyter-packaging.options]skip-if-exists=["jlab_ext_example/labextension/static/style.js"]ensured-targets=["jlab_ext_example/labextension/static/style.js","jlab_ext_example/labextension/package.json"][tool.jupyter-packaging.builder]factory="jupyter_packaging.npm_builder"[tool.jupyter-packaging.build-args]build_cmd="build:prod"npm=["jlpm"][tool.check-manifest]ignore=["jlab_ext_example/labextension/**","yarn.lock",".*","package-lock.json"]It will build the frontend NPM package through itsfactory:# pyproject.toml#L9-L14[tool.jupyter-packaging.builder]factory="jupyter_packaging.npm_builder"[tool.jupyter-packaging.build-args]build_cmd="build:prod"npm=["jlpm"]It will ensure one of the generated files isjlab_ext_example/labextension/package.json:# pyproject.toml#L7-L7ensured-targets=["jlab_ext_example/labextension/static/style.js","jlab_ext_example/labextension/package.json"]It will copy the NPM package in the Python package and force it to be copied in a place
JupyterLab is looking for frontend extensions when the Python package is installed:# setup.py#L26-L26("share/jupyter/labextensions/%s"%labext_name,str(lab_path.relative_to(HERE)),"**"),The last piece of configuration needed is the enabling of the server extension. This is
done by copying the following JSON file:// jupyter-config/server-config/jlab_ext_example.json

{
  "ServerApp": {
    "jpserver_extensions": {
      "jlab_ext_example": true
    }
  }
}in the appropriate jupyter folder (etc/jupyter/jupyter_server_config.d):# setup.py#L28-L29("etc/jupyter/jupyter_server_config.d","jupyter-config/server-config","jlab_ext_example.json"),For backward compatibility with the classical notebook, the old version of that file is copied in
(etc/jupyter/jupyter_notebook_config.d):# setup.py#L31-L32("etc/jupyter/jupyter_notebook_config.d","jupyter-config/nb-config","jlab_ext_example.json"),JupyterLab Extension ManagerThe distribution as a Python package has been described in the previous subsection. But
in JupyterLab, users have an extension manager at their disposal to find extensions. If,
like in this example, your extension needs a server extension, you should inform the
user about that dependency by adding thediscoverymetadata to yourpackage.jsonfile:// package.json#L74-L84

"jupyterlab": {
  "discovery": {
    "server": {
      "managers": [
        "pip"
      ],
      "base": {
        "name": "jlab_ext_example"
      }
    }
  },In this example, the extension requires aserverextension:// package.json#L75-L75

"discovery": {And that server extension is available throughpip:// package.json#L76-L78

"server": {
  "managers": [
    "pip"For more information on thediscoverymetadata, please refer to thedocumentation.Installing the PackageWith the packaging described above, installing the extension is done in one command once the package is published on pypi.org:# Install the server extension and# copy the frontend extension where JupyterLab can find itpipinstalljlab_ext_exampleAs developer, you might want to install the package in local editable mode.
This will shunt the installation machinery described above. Therefore the commands
to get you set are:# Install package in development modepipinstall-e.# Link your development version of the extension with JupyterLabjupyterlabextensiondevelop.--overwrite# Enable the server extensionjupyterserverextensionenablejlab_ext_example# Rebuild extension Typescript source after making changesjlpmrunbuild