diff --git a/docs/source/api.rst b/docs/source/api.rst index c3b4e58e..36e04545 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -5,4 +5,4 @@ REST API ============= .. openapi:: ../../jupyter_server_proxy/api.yml - :examples: \ No newline at end of file + :examples: diff --git a/jupyter_server_proxy/__init__.py b/jupyter_server_proxy/__init__.py index 2ace07b9..c2a2cd0a 100644 --- a/jupyter_server_proxy/__init__.py +++ b/jupyter_server_proxy/__init__.py @@ -1,10 +1,10 @@ import traitlets -from .manager import ServerProxyAppManager +from .api import setup_api_handlers from .config import ServerProxy as ServerProxyConfig from .config import get_entrypoint_server_processes, make_handlers, make_server_process from .handlers import setup_handlers -from .api import setup_api_handlers +from .manager import ServerProxyAppManager # Jupyter Extension points @@ -41,9 +41,7 @@ def _load_jupyter_server_extension(nbapp): base_url = nbapp.web_app.settings["base_url"] # Add server_proxy_manager trait to ServerApp and Instantiate a manager - nbapp.add_traits( - server_proxy_manager=traitlets.Instance(ServerProxyAppManager) - ) + nbapp.add_traits(server_proxy_manager=traitlets.Instance(ServerProxyAppManager)) manager = nbapp.server_proxy_manager = ServerProxyAppManager() serverproxy_config = ServerProxyConfig(parent=nbapp) @@ -52,7 +50,7 @@ def _load_jupyter_server_extension(nbapp): nbapp.io_loop.call_later( serverproxy_config.monitor_interval, manager.monitor, - serverproxy_config.monitor_interval + serverproxy_config.monitor_interval, ) except AttributeError: nbapp.log.debug( diff --git a/jupyter_server_proxy/api.py b/jupyter_server_proxy/api.py index e18b099f..ebe7c44a 100644 --- a/jupyter_server_proxy/api.py +++ b/jupyter_server_proxy/api.py @@ -61,8 +61,9 @@ async def delete(self, name): """Delete a server proxy by name""" if not name: raise web.HTTPError( - 403, "Please set the name of a running server proxy that " - "user wishes to terminate" + 403, + "Please set the name of a running server proxy that " + "user wishes to terminate", ) try: @@ -84,10 +85,8 @@ async def get(self, name): if name: apps = self.manager.get_server_proxy_app(name)._asdict() # If no server proxy found this will be a dict with empty values - if not apps['name']: - raise web.HTTPError( - 404, f"Server proxy {name} not found" - ) + if not apps["name"]: + raise web.HTTPError(404, f"Server proxy {name} not found") else: apps = [app._asdict() for app in self.manager.list_server_proxy_apps()] @@ -121,8 +120,8 @@ def setup_api_handlers(web_app, manager, server_processes): ( ujoin(base_url, r"server-proxy/api/servers/(?P.*)"), ServersAPIHandler, - {"manager": manager} + {"manager": manager}, ), - ] + icon_handlers + ] + + icon_handlers, ) - diff --git a/jupyter_server_proxy/api.yml b/jupyter_server_proxy/api.yml index 91bc45fb..75a6a1bc 100644 --- a/jupyter_server_proxy/api.yml +++ b/jupyter_server_proxy/api.yml @@ -124,4 +124,3 @@ components: type: string unix_socket: type: string - diff --git a/jupyter_server_proxy/config.py b/jupyter_server_proxy/config.py index d6966449..b8b1178b 100644 --- a/jupyter_server_proxy/config.py +++ b/jupyter_server_proxy/config.py @@ -11,11 +11,9 @@ from importlib.metadata import entry_points from jupyter_server.utils import url_path_join as ujoin -from traitlets import Dict, List, Tuple, Union, Int, default, observe +from traitlets import Dict, Int, List, Tuple, Union, default, observe from traitlets.config import Configurable -from jupyter_server.utils import url_path_join as ujoin - from .handlers import AddSlashHandler, NamedLocalProxyHandler, SuperviseAndProxyHandler try: @@ -351,5 +349,5 @@ def _host_whitelist_deprecated(self, change): polling the status of running servers with a frequency set by this interval. """, - config=True + config=True, ) diff --git a/jupyter_server_proxy/manager.py b/jupyter_server_proxy/manager.py index 7b055459..43dc3068 100644 --- a/jupyter_server_proxy/manager.py +++ b/jupyter_server_proxy/manager.py @@ -1,35 +1,18 @@ """Manager for jupyter server proxy""" import asyncio - from collections import namedtuple -from traitlets import List, Int -from traitlets.config import LoggingConfigurable - from jupyter_server.utils import url_path_join as ujoin - +from traitlets import Int, List +from traitlets.config import LoggingConfigurable ServerProxy = namedtuple( "ServerProxy", - [ - "name", - "url", - "cmd", - "port", - "managed", - "unix_socket" - ], - defaults=[""] * 6 -) -ServerProxyProc = namedtuple( - "ServerProxyProc", - [ - "name", - "proc" - ], - defaults=[""] * 2 + ["name", "url", "cmd", "port", "managed", "unix_socket"], + defaults=[""] * 6, ) +ServerProxyProc = namedtuple("ServerProxyProc", ["name", "proc"], defaults=[""] * 2) class ServerProxyAppManager(LoggingConfigurable): @@ -38,17 +21,12 @@ class ServerProxyAppManager(LoggingConfigurable): by jupyter server proxy. """ - server_proxy_apps = List( - help="List of server proxy apps" - ) + server_proxy_apps = List(help="List of server proxy apps") - _server_proxy_procs = List( - help="List of server proxy app proc objects" - ) + _server_proxy_procs = List(help="List of server proxy app proc objects") num_active_server_proxy_apps = Int( - 0, - help="Total number of currently running proxy apps" + 0, help="Total number of currently running proxy apps" ) def add_server_proxy_app(self, name, base_url, cmd, port, proc, unix_socket): @@ -63,7 +41,7 @@ def add_server_proxy_app(self, name, base_url, cmd, port, proc, unix_socket): cmd=" ".join(cmd), port=port, managed=True if proc else False, - unix_socket=unix_socket if unix_socket is not None else '' + unix_socket=unix_socket if unix_socket is not None else "", ) ) @@ -89,11 +67,16 @@ def del_server_proxy_app(self, name): def get_server_proxy_app(self, name): """Get a given server proxy app""" - return next((app for app in self.server_proxy_apps if app.name == name), ServerProxy()) + return next( + (app for app in self.server_proxy_apps if app.name == name), ServerProxy() + ) def _get_server_proxy_proc(self, name): """Get a given server proxy app""" - return next((app for app in self._server_proxy_procs if app.name == name), ServerProxyProc()) + return next( + (app for app in self._server_proxy_procs if app.name == name), + ServerProxyProc(), + ) def list_server_proxy_apps(self): """List all active server proxy apps""" diff --git a/jupyter_server_proxy/utils.py b/jupyter_server_proxy/utils.py index dccace08..e3ea7bea 100644 --- a/jupyter_server_proxy/utils.py +++ b/jupyter_server_proxy/utils.py @@ -1,5 +1,3 @@ -import os - from traitlets import TraitType diff --git a/labextension/schema/add-launcher-entries.json b/labextension/schema/add-launcher-entries.json index 51e61a7b..36b9ab7b 100644 --- a/labextension/schema/add-launcher-entries.json +++ b/labextension/schema/add-launcher-entries.json @@ -1,14 +1,14 @@ { - "title": "Jupyter Server Proxy", - "description": "Jupyter Server Proxy Manager settings", - "properties": { - "refreshInterval": { - "type": "number", - "title": "Auto-refresh rate", - "description": "Time to wait (in ms) in between auto refreshes of server proxy applications", - "default": 10000 - } - }, - "additionalProperties": false, - "type": "object" - } + "title": "Jupyter Server Proxy", + "description": "Jupyter Server Proxy Manager settings", + "properties": { + "refreshInterval": { + "type": "number", + "title": "Auto-refresh rate", + "description": "Time to wait (in ms) in between auto refreshes of server proxy applications", + "default": 10000 + } + }, + "additionalProperties": false, + "type": "object" +} diff --git a/labextension/src/index.ts b/labextension/src/index.ts index 7618097a..7c429db2 100644 --- a/labextension/src/index.ts +++ b/labextension/src/index.ts @@ -6,8 +6,8 @@ import { import { ILauncher } from "@jupyterlab/launcher"; import { PageConfig, URLExt } from "@jupyterlab/coreutils"; import { IRunningSessionManagers } from "@jupyterlab/running"; -import { ISettingRegistry } from '@jupyterlab/settingregistry'; -import { ITranslator, TranslationBundle } from '@jupyterlab/translation'; +import { ISettingRegistry } from "@jupyterlab/settingregistry"; +import { ITranslator, TranslationBundle } from "@jupyterlab/translation"; import { IFrame, MainAreaWidget, WidgetTracker } from "@jupyterlab/apputils"; import { ServerProxyManager } from "./manager"; import { IModel as IServerProxyModel } from "./serverproxy"; @@ -48,7 +48,7 @@ function addRunningSessionManager( managers: IRunningSessionManagers, app: JupyterFrontEnd, manager: ServerProxyManager, - trans: TranslationBundle + trans: TranslationBundle, ): void { managers.add({ name: "Server Proxy Apps", @@ -59,8 +59,9 @@ function addRunningSessionManager( shutdownAll: () => manager.shutdownAll(), refreshRunning: () => manager.refreshRunning(), runningChanged: manager.runningChanged, - shutdownAllConfirmationText: - trans.__("Are you sure you want to close all server proxy applications?") + shutdownAllConfirmationText: trans.__( + "Are you sure you want to close all server proxy applications?", + ), }); } @@ -78,7 +79,7 @@ async function activate( translator: ITranslator, sessions: IRunningSessionManagers | null, ): Promise { - const trans = translator.load('jupyter-server-proxy'); + const trans = translator.load("jupyter-server-proxy"); // Fetch configured server processes from {base_url}/server-proxy/servers-info const response = await fetch( @@ -86,7 +87,9 @@ async function activate( ); if (!response.ok) { console.log( - trans.__("Could not fetch metadata about registered servers. Make sure jupyter-server-proxy is installed."), + trans.__( + "Could not fetch metadata about registered servers. Make sure jupyter-server-proxy is installed.", + ), ); console.log(response); return; @@ -166,7 +169,10 @@ async function activate( continue; } - const url = URLExt.join(PageConfig.getBaseUrl(), server_process.launcher_entry.path_info); + const url = URLExt.join( + PageConfig.getBaseUrl(), + server_process.launcher_entry.path_info, + ); const title = server_process.launcher_entry.title; const newBrowserTab = server_process.new_browser_tab; const id = namespace + ":" + server_process.name; diff --git a/labextension/src/manager.ts b/labextension/src/manager.ts index abade12f..db1cdbcc 100644 --- a/labextension/src/manager.ts +++ b/labextension/src/manager.ts @@ -1,8 +1,8 @@ import { Signal, ISignal } from "@lumino/signaling"; -import { Poll } from '@lumino/polling'; -import { ISettingRegistry } from '@jupyterlab/settingregistry'; +import { Poll } from "@lumino/polling"; +import { ISettingRegistry } from "@jupyterlab/settingregistry"; import { ServerConnection } from "@jupyterlab/services"; -import { TranslationBundle } from '@jupyterlab/translation'; +import { TranslationBundle } from "@jupyterlab/translation"; import { listRunning, shutdown } from "./restapi"; import * as ServerProxyApp from "./serverproxy"; @@ -21,7 +21,9 @@ export class ServerProxyManager implements ServerProxyApp.IManager { this._settings = settings || null; this.serverSettings = ServerConnection.makeSettings(); - const interval = settings?.get('refreshInterval').composite as number || DEFAULT_REFRESH_INTERVAL; + const interval = + (settings?.get("refreshInterval").composite as number) || + DEFAULT_REFRESH_INTERVAL; // Start polling with exponential backoff. this._proxyPoll = new Poll({ @@ -29,8 +31,8 @@ export class ServerProxyManager implements ServerProxyApp.IManager { frequency: { interval: interval, backoff: true, - max: 300 * 1000 - } + max: 300 * 1000, + }, }); // Fire callback when settings are changed @@ -107,7 +109,9 @@ export class ServerProxyManager implements ServerProxyApp.IManager { // Shut down all models. await Promise.all( - this._names.map((name) => shutdown(name, this._trans, this.serverSettings)), + this._names.map((name) => + shutdown(name, this._trans, this.serverSettings), + ), ); // Update the list of models to clear out our state. @@ -169,7 +173,7 @@ export class ServerProxyManager implements ServerProxyApp.IManager { private _onSettingsChange(settings: ISettingRegistry.ISettings) { this._proxyPoll.frequency = { ...this._proxyPoll.frequency, - interval: settings.composite.refreshInterval as number + interval: settings.composite.refreshInterval as number, }; } diff --git a/labextension/src/restapi.ts b/labextension/src/restapi.ts index 3482108a..2b87f354 100644 --- a/labextension/src/restapi.ts +++ b/labextension/src/restapi.ts @@ -1,7 +1,7 @@ import { URLExt } from "@jupyterlab/coreutils"; -import { showDialog, Dialog } from '@jupyterlab/apputils'; +import { showDialog, Dialog } from "@jupyterlab/apputils"; import { ServerConnection } from "@jupyterlab/services"; -import { TranslationBundle } from '@jupyterlab/translation'; +import { TranslationBundle } from "@jupyterlab/translation"; import { IModel } from "./serverproxy"; /** @@ -52,12 +52,14 @@ export async function shutdown( const init = { method: "DELETE" }; const response = await ServerConnection.makeRequest(url, init, settings); if (response.status === 404) { - const msg = trans.__(`Server proxy "${name}" is not running anymore. It will be removed from this list shortly`); + const msg = trans.__( + `Server proxy "${name}" is not running anymore. It will be removed from this list shortly`, + ); console.warn(msg); void showDialog({ - title: trans.__('Warning'), + title: trans.__("Warning"), body: msg, - buttons: [Dialog.okButton({ label : 'Dismiss'})], + buttons: [Dialog.okButton({ label: "Dismiss" })], }); } else if (response.status === 403) { // This request cannot be made via JupyterLab UI and hence we just throw diff --git a/labextension/src/running.ts b/labextension/src/running.ts index 2dc9a118..f452879f 100644 --- a/labextension/src/running.ts +++ b/labextension/src/running.ts @@ -18,14 +18,14 @@ export class RunningServerProxyApp implements IRunningSessions.IRunningItem { constructor( model: IServerProxyModel, manager: ServerProxyManager, - app: JupyterFrontEnd + app: JupyterFrontEnd, ) { this._model = model; this._manager = manager; this._app = app; } open(): void { - this._app.commands.execute(CommandIDs.open, { "sp": this._model }); + this._app.commands.execute(CommandIDs.open, { sp: this._model }); } icon(): LabIcon { return ServerProxyAppIcon; diff --git a/labextension/src/serverproxy.ts b/labextension/src/serverproxy.ts index 2b43c133..29c84a6d 100644 --- a/labextension/src/serverproxy.ts +++ b/labextension/src/serverproxy.ts @@ -34,7 +34,7 @@ export interface IModel extends JSONObject { /** * Proxy app managed by jupyter-server-proxy or not. */ - readonly unix_socket: string; + readonly unix_socket: string; } /** diff --git a/tests/test_api.py b/tests/test_api.py index 0f8135ba..18dbbde5 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,16 +1,14 @@ """Tests for API endpoints""" +import json import os -import pytest import time -import json - +from http.client import HTTPConnection from typing import Tuple +import pytest from traitlets.config.loader import PyFileConfigLoader -from http.client import HTTPConnection - # use ipv4 for CI, etc. LOCALHOST = "127.0.0.1" @@ -39,7 +37,8 @@ def load_config(): """Load config file""" config_file_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), - 'resources', 'jupyter_server_config.py' + "resources", + "jupyter_server_config.py", ) cl = PyFileConfigLoader(config_file_path) return cl.load_config() @@ -47,38 +46,30 @@ def load_config(): def start_proxies(PORT, TOKEN): """Start proxy servers for testing API handlers""" - selected_servers = [ - "python-http", - "python-unix-socket-true", - "python-websocket" - ] + selected_servers = ["python-http", "python-unix-socket-true", "python-websocket"] for url in selected_servers: _ = request_get(PORT, f"/{url}/", TOKEN) return selected_servers -def test_server_proxy_info( - a_server_port_and_token: Tuple[int, str] -) -> None: +def test_server_proxy_info(a_server_port_and_token: Tuple[int, str]) -> None: """Test API endpoint of /server-proxy/api/servers-info.""" PORT, TOKEN = a_server_port_and_token config = load_config() - test_url = '/server-proxy/api/servers-info' - expected_servers = list(config['ServerProxy']['servers'].keys()) + test_url = "/server-proxy/api/servers-info" + expected_servers = list(config["ServerProxy"]["servers"].keys()) r = request_get(PORT, test_url, TOKEN) data = json.loads(r.read().decode()) - found_servers = [sp["name"] for sp in data['server_processes']] + found_servers = [sp["name"] for sp in data["server_processes"]] assert r.code == 200 assert found_servers == expected_servers -def test_get_all_server_proxy( - a_server_port_and_token: Tuple[int, str] -) -> None: +def test_get_all_server_proxy(a_server_port_and_token: Tuple[int, str]) -> None: """Test API endpoint of /server-proxy/api/servers.""" PORT, TOKEN = a_server_port_and_token expected_servers = start_proxies(PORT, TOKEN) - test_url = '/server-proxy/api/servers/' + test_url = "/server-proxy/api/servers/" r = request_get(PORT, test_url, TOKEN) data = json.loads(r.read().decode()) found_servers = [sp["name"] for sp in data] @@ -102,27 +93,25 @@ def test_get_given_server_proxy( # config = load_config() # expected_data = config['ServerProxy']['servers'][server_process_path] _ = request_get(PORT, f"/{server_process_path}/", TOKEN) - test_url = f'/server-proxy/api/servers/{server_process_path}' + test_url = f"/server-proxy/api/servers/{server_process_path}" r = request_get(PORT, test_url, TOKEN) data = json.loads(r.read().decode()) assert r.code == 200 - assert data['name'] == server_process_path - if server_process_path in ['python-http', 'python-websocket']: - assert isinstance(int(data['port']), int) - assert data['managed'] == True - assert data['unix_socket'] == '' - elif server_process_path == 'python-unix-socket-true': - assert int(data['port']) == 0 - assert 'jupyter-server-proxy' in data['unix_socket'] - assert data['managed'] == True - - -def test_get_nonexisting_server_proxy( - a_server_port_and_token: Tuple[int, str] -) -> None: + assert data["name"] == server_process_path + if server_process_path in ["python-http", "python-websocket"]: + assert isinstance(int(data["port"]), int) + assert data["managed"] == True + assert data["unix_socket"] == "" + elif server_process_path == "python-unix-socket-true": + assert int(data["port"]) == 0 + assert "jupyter-server-proxy" in data["unix_socket"] + assert data["managed"] == True + + +def test_get_nonexisting_server_proxy(a_server_port_and_token: Tuple[int, str]) -> None: """Test API non existing GET endpoint of /server-proxy/api/servers/{name}.""" PORT, TOKEN = a_server_port_and_token - test_url = '/server-proxy/api/servers/doesnotexist' + test_url = "/server-proxy/api/servers/doesnotexist" r = request_get(PORT, test_url, TOKEN) assert r.code == 404 @@ -143,7 +132,7 @@ def test_delete_given_server_proxy( _ = request_get(PORT, f"/{server_process_path}/", TOKEN) # Just give enough time for it to be added in manager if it does not exist already time.sleep(1) - test_url = f'/server-proxy/api/servers/{server_process_path}' + test_url = f"/server-proxy/api/servers/{server_process_path}" r = request_delete(PORT, test_url, TOKEN) assert r.code == 204 @@ -153,10 +142,10 @@ def test_delete_nonexisting_server_proxy( ) -> None: """Test API DELETE non existing endpoint of /server-proxy/api/servers/{name}.""" PORT, TOKEN = a_server_port_and_token - test_url = '/server-proxy/api/servers/doesnotexist' + test_url = "/server-proxy/api/servers/doesnotexist" r = request_delete(PORT, test_url, TOKEN) assert r.code == 404 # When no server name is supplied - test_url = '/server-proxy/api/servers/' + test_url = "/server-proxy/api/servers/" r = request_delete(PORT, test_url, TOKEN) assert r.code == 403