Skip to content
This repository has been archived by the owner on Sep 18, 2024. It is now read-only.

JupyterLab extension #3954

Merged
merged 5 commits into from
Jul 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions dependencies/develop.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ pytest-cov
pytest-azurepipelines
coverage
ipython
jupyterlab
4 changes: 4 additions & 0 deletions nni/tools/jupyter_extension/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from . import proxy

load_jupyter_server_extension = proxy.setup
_load_jupyter_server_extension = proxy.setup
30 changes: 30 additions & 0 deletions nni/tools/jupyter_extension/management.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import json
from pathlib import Path
import shutil

from jupyter_core.paths import jupyter_config_dir, jupyter_data_dir

import nni_node

_backend_config_file = Path(jupyter_config_dir(), 'jupyter_server_config.d', 'nni.json')
_backend_config_content = {
'ServerApp': {
'jpserver_extensions': {
'nni.tools.jupyter_extension': True
}
}
}

_frontend_src = Path(nni_node.__path__[0], 'jupyter-extension')
_frontend_dst = Path(jupyter_data_dir(), 'labextensions', 'nni-jupyter-extension')

def install():
_backend_config_file.parent.mkdir(parents=True, exist_ok=True)
_backend_config_file.write_text(json.dumps(_backend_config_content))

_frontend_dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copytree(_frontend_src, _frontend_dst)

def uninstall():
_backend_config_file.unlink()
shutil.rmtree(_frontend_dst)
43 changes: 43 additions & 0 deletions nni/tools/jupyter_extension/proxy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import json
from pathlib import Path

import requests
from tornado.web import RequestHandler

def setup(server):
base_url = server.web_app.settings['base_url']
url_pattern = base_url.rstrip('/') + '/nni/(.*)'
server.web_app.add_handlers('.*$', [(url_pattern, NniProxyHandler)])

class NniProxyHandler(RequestHandler):
def get(self, path):
ports = _get_experiment_ports()
if not ports:
self.set_status(404)
return

if path == 'index':
if len(ports) > 1: # if there is more than one running experiments, show experiment list
self.redirect('experiment')
else: # if there is only one running experiment, show that experiment
self.redirect('oview')
return

r = requests.get(f'http://localhost:{ports[0]}/{path}')
self.set_status(r.status_code)
for key, value in r.headers.items():
self.add_header(key, value)
self.finish(r.content)

# TODO: post, put, etc

def set_default_headers(self):
self.clear_header('Content-Type')
self.clear_header('Date')

def _get_experiment_ports():
experiment_list_path = Path.home() / 'nni-experiments/.experiment'
if not experiment_list_path.exists():
return None
experiments = json.load(open(experiment_list_path))
return [exp['port'] for exp in experiments.values() if exp['status'] != 'STOPPED']
9 changes: 9 additions & 0 deletions nni/tools/nnictl/nnictl.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import os
import pkg_resources
from colorama import init
import nni.tools.jupyter_extension.management as jupyter_management
from .common_utils import print_error
from .launcher import create_experiment, resume_experiment, view_experiment
from .updater import update_searchspace, update_concurrency, update_duration, update_trialnum, import_data
Expand Down Expand Up @@ -278,6 +279,14 @@ def show_messsage_for_nnictl_package(args):
'the unit is second')
parser_top.set_defaults(func=monitor_experiment)

# jupyter-extension command
jupyter_parser = subparsers.add_parser('jupyter-extension', help='install or uninstall JupyterLab extension (internal preview)')
jupyter_subparsers = jupyter_parser.add_subparsers()
jupyter_install_parser = jupyter_subparsers.add_parser('install', help='install JupyterLab extension')
jupyter_install_parser.set_defaults(func=lambda _args: jupyter_management.install()) # TODO: prompt message
jupyter_uninstall_parser = jupyter_subparsers.add_parser('uninstall', help='uninstall JupyterLab extension')
jupyter_uninstall_parser.set_defaults(func=lambda _args: jupyter_management.uninstall())

args = parser.parse_args()
args.func(args)

Expand Down
2 changes: 1 addition & 1 deletion pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,4 @@ ignore-patterns=test*
# List of members which are set dynamically and missed by pylint inference
generated-members=numpy.*,torch.*,tensorflow.*,pycuda.*,tensorrt.*

ignored-modules=tensorflow,_winapi,msvcrt,tensorrt,pycuda
ignored-modules=tensorflow,_winapi,msvcrt,tensorrt,pycuda,nni_node
8 changes: 8 additions & 0 deletions setup_ts.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,10 @@ def compile_ts():
_yarn('ts/nasui')
_yarn('ts/nasui', 'build')

_print('Building JupyterLab extension')
_yarn('ts/jupyter_extension')
_yarn('ts/jupyter_extension', 'build')


def symlink_nni_node():
"""
Expand All @@ -172,6 +176,8 @@ def symlink_nni_node():
_symlink('ts/nasui/build', 'nni_node/nasui/build')
_symlink('ts/nasui/server.js', 'nni_node/nasui/server.js')

_symlink('ts/jupyter_extension/dist', 'nni_node/jupyter-extension')


def copy_nni_node(version):
"""
Expand Down Expand Up @@ -205,6 +211,8 @@ def copy_nni_node(version):
shutil.copytree('ts/nasui/build', 'nni_node/nasui/build')
shutil.copyfile('ts/nasui/server.js', 'nni_node/nasui/server.js')

shutil.copytree('ts/jupyter_extension/dist', 'nni_node/jupyter-extension')


_yarn_env = dict(os.environ)
# `Path('nni_node').resolve()` does not work on Windows if the directory not exists
Expand Down
2 changes: 2 additions & 0 deletions ts/jupyter_extension/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/build
/nni
20 changes: 20 additions & 0 deletions ts/jupyter_extension/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "nni-jupyter-extension",
"version": "999.0.0-developing",
"license": "MIT",
"scripts": {
"build": "tsc && jupyter labextension build ."
},
"dependencies": {
"@jupyterlab/application": "^3.0.11",
"@jupyterlab/launcher": "^3.0.9"
},
"devDependencies": {
"@jupyterlab/builder": "^3.0.9"
},
"jupyterlab": {
"extension": true,
"outputDir": "dist"
},
"main": "build/index.js"
}
55 changes: 55 additions & 0 deletions ts/jupyter_extension/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/application';
import { ICommandPalette, IFrame } from '@jupyterlab/apputils';
import { PageConfig } from '@jupyterlab/coreutils';
import { ILauncher } from '@jupyterlab/launcher';

class NniWidget extends IFrame {
constructor() {
super({
sandbox: [
'allow-same-origin',
'allow-scripts',
]
});
this.url = PageConfig.getBaseUrl() + 'nni/index';
this.id = 'nni';
this.title.label = 'NNI';
this.title.closable = true;
}
}

async function activate(app: JupyterFrontEnd, palette: ICommandPalette, launcher: ILauncher | null) {
console.log('nni extension is activated');
const { commands, shell } = app;
const command = 'nni';
const category = 'Other';

commands.addCommand(command, {
label: 'NNI',
caption: 'NNI',
iconClass: (args) => (args.isPalette ? null : 'jp-Launcher-kernelIcon'),
execute: () => {
shell.add(new NniWidget(), 'main');
}
});

palette.addItem({ command, category });

if (launcher) {
launcher.add({
command,
category,
kernelIconUrl: '/nni/icon.png' // FIXME: this field only works for "Notebook" category
});
}
}

const extension: JupyterFrontEndPlugin<void> = {
id: 'nni',
autoStart: true,
optional: [ILauncher],
requires: [ICommandPalette],
activate,
};

export default extension;
14 changes: 14 additions & 0 deletions ts/jupyter_extension/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"jsx": "react",
"module": "esnext",
"moduleResolution": "node",
"outDir": "build",
"rootDir": "src",
"target": "es2017"
},
"include": [
"src/*"
]
}
Loading