From 391064040a989f4fdbe422fee8765919bc457325 Mon Sep 17 00:00:00 2001 From: Jeremy Tuloup Date: Wed, 10 Feb 2021 19:28:06 +0100 Subject: [PATCH 1/4] Add support for custom handlers --- .github/workflows/build.yml | 9 ++- .gitignore | 2 +- MANIFEST.in | 5 +- .../jupyterlab_link_share.json | 7 ++ .../jupyterlab_link_share.json | 7 ++ jupyterlab-link-share/__init__.py | 19 ----- jupyterlab_link_share/__init__.py | 31 ++++++++ .../_version.py | 0 jupyterlab_link_share/handlers.py | 21 ++++++ package.json | 3 +- setup.py | 26 ++++--- src/handler.ts | 45 ++++++++++++ src/index.ts | 12 +++ yarn.lock | 73 +++++++++++++++++++ 14 files changed, 224 insertions(+), 36 deletions(-) create mode 100644 jupyter-config/jupyter_notebook_config.d/jupyterlab_link_share.json create mode 100644 jupyter-config/jupyter_server_config.d/jupyterlab_link_share.json delete mode 100644 jupyterlab-link-share/__init__.py create mode 100644 jupyterlab_link_share/__init__.py rename {jupyterlab-link-share => jupyterlab_link_share}/_version.py (100%) create mode 100644 jupyterlab_link_share/handlers.py create mode 100644 src/handler.ts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7748d89f..5bace4ff 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,8 +21,8 @@ jobs: with: python-version: '3.7' architecture: 'x64' - - + + - name: Setup pip cache uses: actions/cache@v2 with: @@ -43,7 +43,7 @@ jobs: key: yarn-${{ hashFiles('**/yarn.lock') }} restore-keys: | yarn- - + - name: Install dependencies run: python -m pip install jupyterlab - name: Build the extension @@ -53,4 +53,7 @@ jobs: python -m pip install . jupyter labextension list 2>&1 | grep -ie "jupyterlab-link-share.*OK" + jupyter server extension list 2>&1 | grep -ie "jupyterlab_link_share.*OK" + jupyter serverextension list 2>&1 | grep -ie "jupyterlab_link_share.*OK" + python -m jupyterlab.browser_check diff --git a/.gitignore b/.gitignore index b9c38c13..e9d67cf2 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ node_modules/ *.egg-info/ .ipynb_checkpoints *.tsbuildinfo -jupyterlab-link-share/labextension +jupyterlab_link_share/labextension # Created by https://www.gitignore.io/api/python # Edit at https://www.gitignore.io/?templates=python diff --git a/MANIFEST.in b/MANIFEST.in index 32fc83db..45449ab5 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,13 +1,14 @@ include LICENSE include README.md include pyproject.toml -include jupyter-config/jupyterlab-link-share.json +include jupyter-config/jupyter_server_config.d/jupyterlab_link_share.json +include jupyter-config/jupyter_notebook_config.d/jupyterlab_link_share.json include package.json include install.json include ts*.json -graft jupyterlab-link-share/labextension +graft jupyterlab_link_share/labextension # Javascript files graft src diff --git a/jupyter-config/jupyter_notebook_config.d/jupyterlab_link_share.json b/jupyter-config/jupyter_notebook_config.d/jupyterlab_link_share.json new file mode 100644 index 00000000..802d3ddb --- /dev/null +++ b/jupyter-config/jupyter_notebook_config.d/jupyterlab_link_share.json @@ -0,0 +1,7 @@ +{ + "NotebookApp": { + "nbserver_extensions": { + "jupyterlab_link_share": true + } + } +} diff --git a/jupyter-config/jupyter_server_config.d/jupyterlab_link_share.json b/jupyter-config/jupyter_server_config.d/jupyterlab_link_share.json new file mode 100644 index 00000000..c0fa4f2a --- /dev/null +++ b/jupyter-config/jupyter_server_config.d/jupyterlab_link_share.json @@ -0,0 +1,7 @@ +{ + "ServerApp": { + "jpserver_extensions": { + "jupyterlab_link_share": true + } + } +} diff --git a/jupyterlab-link-share/__init__.py b/jupyterlab-link-share/__init__.py deleted file mode 100644 index 0ef5151d..00000000 --- a/jupyterlab-link-share/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ - -import json -from pathlib import Path - -from ._version import __version__ - -HERE = Path(__file__).parent.resolve() - -with (HERE / "labextension" / "package.json").open() as fid: - data = json.load(fid) - -def _jupyter_labextension_paths(): - return [{ - "src": "labextension", - "dest": data["name"] - }] - - - diff --git a/jupyterlab_link_share/__init__.py b/jupyterlab_link_share/__init__.py new file mode 100644 index 00000000..342c8def --- /dev/null +++ b/jupyterlab_link_share/__init__.py @@ -0,0 +1,31 @@ + +import json +from pathlib import Path + +from .handlers import setup_handlers +from ._version import __version__ + +HERE = Path(__file__).parent.resolve() + +with (HERE / "labextension" / "package.json").open() as fid: + data = json.load(fid) + + +def _jupyter_labextension_paths(): + return [{ + "src": "labextension", + "dest": data["name"] + }] + + +def _jupyter_server_extension_points(): + return [{ + "module": "jupyterlab_link_share" + }] + + +def _load_jupyter_server_extension(server_app): + setup_handlers(server_app.web_app) + server_app.log.info("Registered HelloWorld extension at URL path /jupyterlab_link_share") + +load_jupyter_server_extension = _load_jupyter_server_extension diff --git a/jupyterlab-link-share/_version.py b/jupyterlab_link_share/_version.py similarity index 100% rename from jupyterlab-link-share/_version.py rename to jupyterlab_link_share/_version.py diff --git a/jupyterlab_link_share/handlers.py b/jupyterlab_link_share/handlers.py new file mode 100644 index 00000000..1e2f1735 --- /dev/null +++ b/jupyterlab_link_share/handlers.py @@ -0,0 +1,21 @@ +import json + +from jupyter_server.base.handlers import APIHandler +from jupyter_server.utils import url_path_join +import tornado + +class RouteHandler(APIHandler): + @tornado.web.authenticated + def get(self): + self.finish(json.dumps({ + "data": "This is /jupyterlab_link_share/get_example endpoint!" + })) + + +def setup_handlers(web_app): + host_pattern = ".*$" + + base_url = web_app.settings["base_url"] + route_pattern = url_path_join(base_url, "jupyterlab_link_share", "get_example") + handlers = [(route_pattern, RouteHandler)] + web_app.add_handlers(host_pattern, handlers) diff --git a/package.json b/package.json index 3a645498..95a6beda 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "@jupyterlab/apputils": "^3.0.0", "@jupyterlab/coreutils": "^5.0.0", "@jupyterlab/mainmenu": "^3.0.0", + "@jupyterlab/services": "^6.0.0", "@jupyterlab/translation": "^3.0.0", "@lumino/widgets": "^1.17.0" }, @@ -69,6 +70,6 @@ "styleModule": "style/index.js", "jupyterlab": { "extension": true, - "outputDir": "jupyterlab-link-share/labextension" + "outputDir": "jupyterlab_link_share/labextension" } } diff --git a/setup.py b/setup.py index 83cf4ee0..825394d4 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ install_npm, ensure_targets, combine_commands, - skip_if_exists + skip_if_exists, ) import setuptools @@ -17,34 +17,40 @@ # The name of the project name = "jupyterlab-link-share" +package = name.replace('-', '_') # Get our version with (HERE / "package.json").open() as f: version = json.load(f)["version"] -lab_path = (HERE / name / "labextension") +lab_path = HERE / package / "labextension" # Representative files that should exist after a successful build jstargets = [ str(lab_path / "package.json"), ] -package_data_spec = { - name: [ - "*" - ] -} +package_data_spec = {package: ["*"]} labext_name = "jupyterlab-link-share" data_files_spec = [ ("share/jupyter/labextensions/%s" % labext_name, str(lab_path), "**"), ("share/jupyter/labextensions/%s" % labext_name, str(HERE), "install.json"), + ( + "etc/jupyter/jupyter_server_config.d", + "jupyter-config/jupyter_server_config.d", + "jupyterlab_link_share.json", + ), + ( + "etc/jupyter/jupyter_notebook_config.d", + "jupyter-config/jupyter_notebook_config.d", + "jupyterlab_link_share.json", + ), ] -cmdclass = create_cmdclass("jsdeps", - package_data_spec=package_data_spec, - data_files_spec=data_files_spec +cmdclass = create_cmdclass( + "jsdeps", package_data_spec=package_data_spec, data_files_spec=data_files_spec ) js_command = combine_commands( diff --git a/src/handler.ts b/src/handler.ts new file mode 100644 index 00000000..e748242e --- /dev/null +++ b/src/handler.ts @@ -0,0 +1,45 @@ +import { URLExt } from '@jupyterlab/coreutils'; + +import { ServerConnection } from '@jupyterlab/services'; + +/** + * Call the API extension + * + * @param endPoint API REST end point for the extension + * @param init Initial values for the request + * @returns The response body interpreted as JSON + */ +export async function requestAPI( + endPoint = '', + init: RequestInit = {} +): Promise { + const settings = ServerConnection.makeSettings(); + const requestUrl = URLExt.join( + settings.baseUrl, + 'jupyterlab_link_share', + endPoint + ); + + let response: Response; + try { + response = await ServerConnection.makeRequest(requestUrl, init, settings); + } catch (error) { + throw new ServerConnection.NetworkError(error); + } + + let data: any = await response.text(); + + if (data.length > 0) { + try { + data = JSON.parse(data); + } catch (error) { + console.log('Not a JSON response body.', response); + } + } + + if (!response.ok) { + throw new ServerConnection.ResponseError(response, data.message || data); + } + + return data; +} diff --git a/src/index.ts b/src/index.ts index 448ad490..3a86da67 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,6 +18,8 @@ import { ITranslator, nullTranslator } from '@jupyterlab/translation'; import { Menu } from '@lumino/widgets'; +import { requestAPI } from './handler'; + /** * The command IDs used by the plugin. */ @@ -41,6 +43,16 @@ const plugin: JupyterFrontEndPlugin = { const { commands } = app; const trans = (translator ?? nullTranslator).load('jupyterlab'); + requestAPI('get_example') + .then((data: any) => { + console.log(data); + }) + .catch((reason: any) => { + console.error( + `The jupyterlab_link_share server extension appears to be missing.\n${reason}` + ); + }); + commands.addCommand(CommandIDs.share, { label: trans.__('Share Jupyter Server Link'), execute: async () => { diff --git a/yarn.lock b/yarn.lock index f836891c..a0d90b33 100644 --- a/yarn.lock +++ b/yarn.lock @@ -255,6 +255,19 @@ path-browserify "^1.0.0" url-parse "~1.4.7" +"@jupyterlab/coreutils@^5.0.2": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@jupyterlab/coreutils/-/coreutils-5.0.2.tgz#e1845cb05d228179babccf1a3cbfde8abe456113" + integrity sha512-ViQoYjROvzfU2PuuH9kYpbBReIZafKcmJFEEflgf2fH6IW298tXQgNcT+dQiq8FjmyEVApoQM8r2pI1d1ztVPA== + dependencies: + "@lumino/coreutils" "^1.5.3" + "@lumino/disposable" "^1.4.3" + "@lumino/signaling" "^1.4.3" + minimist "~1.2.0" + moment "^2.24.0" + path-browserify "^1.0.0" + url-parse "~1.4.7" + "@jupyterlab/docregistry@^3.0.2": version "3.0.2" resolved "https://registry.yarnpkg.com/@jupyterlab/docregistry/-/docregistry-3.0.2.tgz#d3cd111ff46778425da0d5225792412c71907e6a" @@ -298,6 +311,13 @@ dependencies: "@lumino/coreutils" "^1.5.3" +"@jupyterlab/nbformat@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@jupyterlab/nbformat/-/nbformat-3.0.2.tgz#0e7509c25a0ab994348e77332f81406296a03623" + integrity sha512-MYkUF4rkr/qhNQ2auvLYmVRwl39eIGRT1oJctCeiN2sTNtl5f1bts8KuNRhkYDOuryJUwouI1T4JsQpcE+mV6Q== + dependencies: + "@lumino/coreutils" "^1.5.3" + "@jupyterlab/observables@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@jupyterlab/observables/-/observables-4.0.1.tgz#d767fc0831a2ae32ef772492fffc77dd0ffaabe9" @@ -309,6 +329,17 @@ "@lumino/messaging" "^1.4.3" "@lumino/signaling" "^1.4.3" +"@jupyterlab/observables@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@jupyterlab/observables/-/observables-4.0.2.tgz#a76256cf3afc23ace90a9f4e8ad96c8f2f398abf" + integrity sha512-eLS0ThHEfM86eUeLHSLgjpf7BrRmgJJTTarn3FJSC0CVXVzx48Fcsvpa5mhbnWjSakzUT+LqbN4w4lFZabwz8A== + dependencies: + "@lumino/algorithm" "^1.3.3" + "@lumino/coreutils" "^1.5.3" + "@lumino/disposable" "^1.4.3" + "@lumino/messaging" "^1.4.3" + "@lumino/signaling" "^1.4.3" + "@jupyterlab/rendermime-interfaces@^3.0.2": version "3.0.2" resolved "https://registry.yarnpkg.com/@jupyterlab/rendermime-interfaces/-/rendermime-interfaces-3.0.2.tgz#0af6022045a12286ceb6cd732c2a84e322d26d5f" @@ -339,6 +370,24 @@ lodash.escape "^4.0.1" marked "^1.1.1" +"@jupyterlab/services@^6.0.0": + version "6.0.3" + resolved "https://registry.yarnpkg.com/@jupyterlab/services/-/services-6.0.3.tgz#60b65555fc1ab69065b9bee1892329b02a9ad607" + integrity sha512-jgzJPDOMIMVTr57ZRWmeWZRICyyUyumR4HPMjf6BrB6RXytL1JZ/2m9qs1dX2zReAwWHV6bGVu/kZ/Hmqk9aiA== + dependencies: + "@jupyterlab/coreutils" "^5.0.2" + "@jupyterlab/nbformat" "^3.0.2" + "@jupyterlab/observables" "^4.0.2" + "@jupyterlab/settingregistry" "^3.0.2" + "@jupyterlab/statedb" "^3.0.2" + "@lumino/algorithm" "^1.3.3" + "@lumino/coreutils" "^1.5.3" + "@lumino/disposable" "^1.4.3" + "@lumino/polling" "^1.3.3" + "@lumino/signaling" "^1.4.3" + node-fetch "^2.6.0" + ws "^7.2.0" + "@jupyterlab/services@^6.0.2": version "6.0.2" resolved "https://registry.yarnpkg.com/@jupyterlab/services/-/services-6.0.2.tgz#ca9f5f8b6c69013d9d52a15e034eb8b7935fdc5e" @@ -370,6 +419,19 @@ ajv "^6.12.3" json5 "^2.1.1" +"@jupyterlab/settingregistry@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@jupyterlab/settingregistry/-/settingregistry-3.0.2.tgz#c64343b42de96b016df766cecdb3b97c6fca62c2" + integrity sha512-M0ZPazePn5Jw8HX2H1t+I6ZgvsGYeTOonjlKoiDgkR178whYMZ7LdkTpUuX3fyEkhZ6aHpmrboXtw/H8TJiE+w== + dependencies: + "@jupyterlab/statedb" "^3.0.2" + "@lumino/commands" "^1.12.0" + "@lumino/coreutils" "^1.5.3" + "@lumino/disposable" "^1.4.3" + "@lumino/signaling" "^1.4.3" + ajv "^6.12.3" + json5 "^2.1.1" + "@jupyterlab/statedb@^3.0.1": version "3.0.1" resolved "https://registry.yarnpkg.com/@jupyterlab/statedb/-/statedb-3.0.1.tgz#8b6c757e20a8072c5276019a07c65725a9829aa9" @@ -381,6 +443,17 @@ "@lumino/properties" "^1.2.3" "@lumino/signaling" "^1.4.3" +"@jupyterlab/statedb@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@jupyterlab/statedb/-/statedb-3.0.2.tgz#6b0a99daa88d2054b42267d563c5e5588fd31371" + integrity sha512-PILxap9OT8wUPNzS5/PpCMcGwgn5nrPkDURhmpOASaDWYkCfG72UzZP5H8ZpeZ5iV+Nf8/eg5WtJP66twEKnjA== + dependencies: + "@lumino/commands" "^1.12.0" + "@lumino/coreutils" "^1.5.3" + "@lumino/disposable" "^1.4.3" + "@lumino/properties" "^1.2.3" + "@lumino/signaling" "^1.4.3" + "@jupyterlab/statusbar@^3.0.2": version "3.0.2" resolved "https://registry.yarnpkg.com/@jupyterlab/statusbar/-/statusbar-3.0.2.tgz#135ea22d62b79de3e29657abd9569297bbe39203" From b9ffa6783b30b809a6636662819d3532ac65a9cb Mon Sep 17 00:00:00 2001 From: Jeremy Tuloup Date: Wed, 10 Feb 2021 21:01:50 +0100 Subject: [PATCH 2/4] Handle multiple running servers --- jupyterlab_link_share/handlers.py | 10 +++++--- src/index.ts | 41 +++++++++++++++++-------------- 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/jupyterlab_link_share/handlers.py b/jupyterlab_link_share/handlers.py index 1e2f1735..e4e21f69 100644 --- a/jupyterlab_link_share/handlers.py +++ b/jupyterlab_link_share/handlers.py @@ -1,21 +1,23 @@ import json from jupyter_server.base.handlers import APIHandler +from jupyter_server.serverapp import list_running_servers as list_jupyter_servers from jupyter_server.utils import url_path_join +from notebook.notebookapp import list_running_servers as list_notebook_servers import tornado class RouteHandler(APIHandler): @tornado.web.authenticated def get(self): - self.finish(json.dumps({ - "data": "This is /jupyterlab_link_share/get_example endpoint!" - })) + servers = list(list_notebook_servers()) + list(list_jupyter_servers()) + servers.sort(key=lambda x: x["port"]) + self.finish(json.dumps(servers)) def setup_handlers(web_app): host_pattern = ".*$" base_url = web_app.settings["base_url"] - route_pattern = url_path_join(base_url, "jupyterlab_link_share", "get_example") + route_pattern = url_path_join(base_url, "jupyterlab_link_share", "servers") handlers = [(route_pattern, RouteHandler)] web_app.add_handlers(host_pattern, handlers) diff --git a/src/index.ts b/src/index.ts index 3a86da67..184e2a3a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,7 +16,7 @@ import { IMainMenu } from '@jupyterlab/mainmenu'; import { ITranslator, nullTranslator } from '@jupyterlab/translation'; -import { Menu } from '@lumino/widgets'; +import { Menu, Widget } from '@lumino/widgets'; import { requestAPI } from './handler'; @@ -43,27 +43,32 @@ const plugin: JupyterFrontEndPlugin = { const { commands } = app; const trans = (translator ?? nullTranslator).load('jupyterlab'); - requestAPI('get_example') - .then((data: any) => { - console.log(data); - }) - .catch((reason: any) => { - console.error( - `The jupyterlab_link_share server extension appears to be missing.\n${reason}` - ); - }); - commands.addCommand(CommandIDs.share, { label: trans.__('Share Jupyter Server Link'), execute: async () => { - const link = URLExt.normalize( - `${PageConfig.getUrl({ - workspace: PageConfig.defaultWorkspace - })}?token=${PageConfig.getToken()}` - ); + const results: { token: string }[] = await requestAPI('servers'); + + const links = results.map(server => { + return URLExt.normalize( + `${PageConfig.getUrl({ + workspace: PageConfig.defaultWorkspace + })}?token=${server.token}` + ); + }); + + const entries = document.createElement('div'); + links.map(link => { + const p = document.createElement('p'); + const a = document.createElement('a'); + a.href = link; + a.innerText = link; + p.appendChild(a); + entries.appendChild(p); + }); + const result = await showDialog({ title: trans.__('Share Jupyter Server Link'), - body: link, + body: new Widget({ node: entries }), buttons: [ Dialog.cancelButton({ label: trans.__('Cancel') }), Dialog.okButton({ @@ -73,7 +78,7 @@ const plugin: JupyterFrontEndPlugin = { ] }); if (result.button.accept) { - Clipboard.copyToSystem(link); + Clipboard.copyToSystem(links[0]); } } }); From 8a7b1822ee6e6f786ea40dfe8ef6d666d41ea6c6 Mon Sep 17 00:00:00 2001 From: Jeremy Tuloup Date: Wed, 10 Feb 2021 21:15:04 +0100 Subject: [PATCH 3/4] Sort servers by pid --- jupyterlab_link_share/handlers.py | 7 +++++-- setup.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/jupyterlab_link_share/handlers.py b/jupyterlab_link_share/handlers.py index e4e21f69..a1ccea85 100644 --- a/jupyterlab_link_share/handlers.py +++ b/jupyterlab_link_share/handlers.py @@ -1,16 +1,19 @@ import json +import tornado + from jupyter_server.base.handlers import APIHandler from jupyter_server.serverapp import list_running_servers as list_jupyter_servers from jupyter_server.utils import url_path_join from notebook.notebookapp import list_running_servers as list_notebook_servers -import tornado + class RouteHandler(APIHandler): @tornado.web.authenticated def get(self): servers = list(list_notebook_servers()) + list(list_jupyter_servers()) - servers.sort(key=lambda x: x["port"]) + # sort by pid so PID 1 is first in Docker and Binder + servers.sort(key=lambda x: x["pid"]) self.finish(json.dumps(servers)) diff --git a/setup.py b/setup.py index 825394d4..7448baee 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ cmdclass=cmdclass, packages=setuptools.find_packages(), install_requires=[ - "jupyterlab~=3.0", + "jupyterlab~=3.0" ], zip_safe=False, include_package_data=True, From a9e477d41e54f8448cd6544787c4a64ee14d4bd2 Mon Sep 17 00:00:00 2001 From: Jeremy Tuloup Date: Wed, 10 Feb 2021 23:55:42 +0100 Subject: [PATCH 4/4] Update log message --- jupyterlab_link_share/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyterlab_link_share/__init__.py b/jupyterlab_link_share/__init__.py index 342c8def..e836aa29 100644 --- a/jupyterlab_link_share/__init__.py +++ b/jupyterlab_link_share/__init__.py @@ -26,6 +26,6 @@ def _jupyter_server_extension_points(): def _load_jupyter_server_extension(server_app): setup_handlers(server_app.web_app) - server_app.log.info("Registered HelloWorld extension at URL path /jupyterlab_link_share") + server_app.log.info("Registered JupyterLab Link Share extension at URL path /jupyterlab_link_share") load_jupyter_server_extension = _load_jupyter_server_extension