From 15cf73f1b3bc86be748c407a70d19310e717dd5b Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Sun, 1 Sep 2024 17:30:03 +0200 Subject: [PATCH] CLI enhancement + Bump versions (#23) * docs: jupyterlab * fix: webpack * chore: factor envs cli * chore: use run_url * chore: benchmarks * chore: envs * chore: utils * fix: help * fix: kernel type * chore: use pathname * chore: log info * chore: datalayer run url * fix: app * fix: webpack * bump version * bump: version * chore: benchmarks * chore: bump version * bump: version * lint * cli: add example * chore: revert back to jupyterlab 4.0.9 * bump * tune: webpack * bump * fix: build * fix: build --- Makefile | 2 +- datalayer_core/authn/__main__.py | 6 +- datalayer_core/authn/apps/loginapp.py | 2 +- datalayer_core/authn/apps/logoutapp.py | 2 +- datalayer_core/authn/apps/utils.py | 21 ++++++ datalayer_core/authn/apps/whoamiapp.py | 6 +- datalayer_core/authn/http_server.py | 22 +++--- datalayer_core/benchmarks/__init__.py | 3 + datalayer_core/benchmarks/benchmarksapp.py | 29 ++++++++ datalayer_core/benchmarks/web/__init__.py | 3 + datalayer_core/benchmarks/web/webapp.py | 32 +++++++++ datalayer_core/cli/base.py | 63 ++++++++-------- datalayer_core/cli/datalayer.py | 28 ++++++-- datalayer_core/environments/__init__.py | 2 + .../environments/environmentsapp.py | 29 ++++++++ datalayer_core/environments/list/__init__.py | 2 + .../list/listapp.py} | 19 +++-- datalayer_core/kernels/console/consoleapp.py | 14 ++-- datalayer_core/kernels/console/shell.py | 2 +- datalayer_core/kernels/create/__init__.py | 3 + datalayer_core/kernels/create/createapp.py | 14 ++-- datalayer_core/kernels/exec/__init__.py | 3 + datalayer_core/kernels/exec/execapp.py | 14 ++-- datalayer_core/kernels/kernelsapp.py | 55 +++++++------- datalayer_core/kernels/list/__init__.py | 3 + datalayer_core/kernels/list/listapp.py | 11 ++- datalayer_core/kernels/manager.py | 12 ++-- datalayer_core/kernels/pause/__init__.py | 3 + datalayer_core/kernels/pause/pauseapp.py | 2 +- datalayer_core/kernels/start/__init__.py | 3 + datalayer_core/kernels/start/startapp.py | 2 +- datalayer_core/kernels/stop/__init__.py | 3 + datalayer_core/kernels/stop/stopapp.py | 2 +- datalayer_core/kernels/terminate/__init__.py | 3 + .../kernels/terminate/terminateapp.py | 4 +- datalayer_core/kernels/utils.py | 29 ++------ datalayer_core/kernels/web/__init__.py | 3 + datalayer_core/kernels/web/webapp.py | 32 +++++++++ datalayer_core/serverapplication.py | 27 +++++-- datalayer_core/web/__init__.py | 3 + datalayer_core/web/webapp.py | 32 +++++++++ docs/docs/index.mdx | 59 +++++++++++++++ package.json | 28 ++++++-- pyproject.toml | 3 +- src/DatalayerApp.tsx | 72 +++++++++++++++---- src/LoginCLIApp.tsx | 32 --------- webpack.config.js | 41 +++-------- 47 files changed, 543 insertions(+), 242 deletions(-) create mode 100644 datalayer_core/authn/apps/utils.py create mode 100644 datalayer_core/benchmarks/__init__.py create mode 100644 datalayer_core/benchmarks/benchmarksapp.py create mode 100644 datalayer_core/benchmarks/web/__init__.py create mode 100644 datalayer_core/benchmarks/web/webapp.py create mode 100644 datalayer_core/environments/__init__.py create mode 100644 datalayer_core/environments/environmentsapp.py create mode 100644 datalayer_core/environments/list/__init__.py rename datalayer_core/{kernels/envs/envssapp.py => environments/list/listapp.py} (77%) create mode 100644 datalayer_core/kernels/create/__init__.py create mode 100644 datalayer_core/kernels/exec/__init__.py create mode 100644 datalayer_core/kernels/list/__init__.py create mode 100644 datalayer_core/kernels/pause/__init__.py create mode 100644 datalayer_core/kernels/start/__init__.py create mode 100644 datalayer_core/kernels/stop/__init__.py create mode 100644 datalayer_core/kernels/terminate/__init__.py create mode 100644 datalayer_core/kernels/web/__init__.py create mode 100644 datalayer_core/kernels/web/webapp.py create mode 100644 datalayer_core/web/__init__.py create mode 100644 datalayer_core/web/webapp.py delete mode 100644 src/LoginCLIApp.tsx diff --git a/Makefile b/Makefile index d14b283..3e6f790 100755 --- a/Makefile +++ b/Makefile @@ -92,4 +92,4 @@ publish-pypi: # publish the pypi package @exec echo @exec echo twine upload ./dist/*-py3-none-any.whl @exec echo - @exec echo https://pypi.org/project/datalayer/#history + @exec echo https://pypi.org/project/datalayer-core/#history diff --git a/datalayer_core/authn/__main__.py b/datalayer_core/authn/__main__.py index 9175dc1..94c35a1 100644 --- a/datalayer_core/authn/__main__.py +++ b/datalayer_core/authn/__main__.py @@ -13,16 +13,16 @@ logger = logging.getLogger(__name__) -KERNELS_URL = "https://oss.datalayer.run" +DATALAYER_RUN_URL = "https://oss.datalayer.run" if __name__ == "__main__": from sys import argv if len(argv) == 2: - ans = get_token(KERNELS_URL, port=int(argv[1])) + ans = get_token(DATALAYER_RUN_URL, port=int(argv[1])) else: - ans = get_token(KERNELS_URL) + ans = get_token(DATALAYER_RUN_URL) if ans is not None: handle, token = ans diff --git a/datalayer_core/authn/apps/loginapp.py b/datalayer_core/authn/apps/loginapp.py index 7c9065c..e1f1066 100644 --- a/datalayer_core/authn/apps/loginapp.py +++ b/datalayer_core/authn/apps/loginapp.py @@ -23,5 +23,5 @@ def start(self): self.exit(1) if self.token and self.user_handle: - self.log.info(f"πŸŽ‰ Successfully authenticated as {self.user_handle} on {self.kernels_url}") + self.log.info(f"πŸŽ‰ Successfully authenticated as {self.user_handle} on {self.run_url}") print() diff --git a/datalayer_core/authn/apps/logoutapp.py b/datalayer_core/authn/apps/logoutapp.py index eedaeae..1fe54b3 100644 --- a/datalayer_core/authn/apps/logoutapp.py +++ b/datalayer_core/authn/apps/logoutapp.py @@ -27,7 +27,7 @@ def start(self): """ FIXME self._fetch( - "{}/api/iam/v1/logout".format(self.kernels_url), + "{}/api/iam/v1/logout".format(self.run_url), ) """ diff --git a/datalayer_core/authn/apps/utils.py b/datalayer_core/authn/apps/utils.py new file mode 100644 index 0000000..01c74db --- /dev/null +++ b/datalayer_core/authn/apps/utils.py @@ -0,0 +1,21 @@ +# Copyright (c) Datalayer Development Team. +# Distributed under the terms of the Modified BSD License. + +from rich.console import Console +from rich.table import Table + +def display_me(me: dict) -> None: + """Display a my profile.""" + table = Table(title="Profile") + table.add_column("ID", style="magenta", no_wrap=True) + table.add_column("Handle", style="cyan", no_wrap=True) + table.add_column("First name", style="green", no_wrap=True) + table.add_column("Last name", style="green", no_wrap=True) + table.add_row( + me["uid"], + me["handle_s"], + me["first_name_t"], + me["last_name_t"], + ) + console = Console() + console.print(table) diff --git a/datalayer_core/authn/apps/whoamiapp.py b/datalayer_core/authn/apps/whoamiapp.py index 544d721..090ea55 100644 --- a/datalayer_core/authn/apps/whoamiapp.py +++ b/datalayer_core/authn/apps/whoamiapp.py @@ -4,10 +4,10 @@ import warnings from datalayer_core.cli.base import DatalayerCLIBaseApp -from ...kernels.utils import display_me +from datalayer_core.authn.apps.utils import display_me -class KernelWhoamiApp(DatalayerCLIBaseApp): +class WhoamiApp(DatalayerCLIBaseApp): """An application to list the kernels.""" description = """ @@ -24,7 +24,7 @@ def start(self): self.exit(1) response = self._fetch( - "{}/api/iam/v1/whoami".format(self.kernels_url), + "{}/api/iam/v1/whoami".format(self.run_url), ) raw = response.json() display_me(raw.get("profile", {})) diff --git a/datalayer_core/authn/http_server.py b/datalayer_core/authn/http_server.py index 3a760fd..7ef8507 100644 --- a/datalayer_core/authn/http_server.py +++ b/datalayer_core/authn/http_server.py @@ -34,7 +34,9 @@ HERE = Path(__file__).parent -USE_JUPYTER_SERVER: bool = False +# Do not set it to True, the Jupyter Server +# handlers are not yet implemented. +USE_JUPYTER_SERVER_FOR_LOGIN: bool = False logger = logging.getLogger(__name__) @@ -94,12 +96,12 @@ def do_GET(self): parts = urllib.parse.urlsplit(self.path) if parts[2].strip("/").endswith("oauth/callback"): self._save_token(parts[3]) - elif parts[2] in {"/", "/login/cli"}: + elif parts[2] in {"/", "/datalayer/login/cli"}: content = LANDING_PAGE.format( config=json.dumps( { - "runUrl": self.server.kernels_url, - "iamRunUrl": self.server.kernels_url, + "runUrl": self.server.run_url, + "iamRunUrl": self.server.run_url, "whiteLabel": False } ) @@ -145,10 +147,10 @@ def __init__( self, server_address: tuple[str | bytes | bytearray, int], RequestHandlerClass: t.Callable[[t.Any, t.Any, t.Self], BaseRequestHandler], - kernels_url: str, + run_url: str, bind_and_activate: bool = True, ) -> None: - self.kernels_url = kernels_url + self.run_url = run_url self.user_handle = None self.token = None super().__init__(server_address, RequestHandlerClass, bind_and_activate) @@ -166,19 +168,19 @@ def finish_request(self, request, client_address): def get_token( - kernels_url: str, port: int | None = None, logger: logging.Logger = logger + run_url: str, port: int | None = None, logger: logging.Logger = logger ) -> tuple[str, str] | None: """Get the user handle and token.""" server_address = ("", port or find_http_port()) port = server_address[1] - if USE_JUPYTER_SERVER == True: + if USE_JUPYTER_SERVER_FOR_LOGIN == True: set_server_port(port) logger.info(f"Waiting for user logging, open http://localhost:{port}. Press CTRL+C to abort.\n") sys.argv = [ "", - "--JupyterKernelsExtensionApp.run_url", kernels_url, + "--DatalayerExtensionApp.run_url", run_url, "--ServerApp.disable_check_xsrf", "True", ] launch_new_instance() @@ -186,7 +188,7 @@ def get_token( # return None if httpd.token is None else (httpd.user_handle, httpd.token) return None else: - httpd = DualStackServer(server_address, LoginRequestHandler, kernels_url) + httpd = DualStackServer(server_address, LoginRequestHandler, run_url) logger.info(f"Waiting for user logging, open http://localhost:{port}. Press CTRL+C to abort.\n") try: httpd.serve_forever() diff --git a/datalayer_core/benchmarks/__init__.py b/datalayer_core/benchmarks/__init__.py new file mode 100644 index 0000000..89c7888 --- /dev/null +++ b/datalayer_core/benchmarks/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) Datalayer Development Team. +# Distributed under the terms of the Modified BSD License. + diff --git a/datalayer_core/benchmarks/benchmarksapp.py b/datalayer_core/benchmarks/benchmarksapp.py new file mode 100644 index 0000000..dba34bb --- /dev/null +++ b/datalayer_core/benchmarks/benchmarksapp.py @@ -0,0 +1,29 @@ +# Copyright (c) Datalayer Development Team. +# Distributed under the terms of the Modified BSD License. + +from datalayer_core.application import NoStart + +from datalayer_core.cli.base import DatalayerCLIBaseApp +from datalayer_core.benchmarks.web.webapp import BenchmarksWebApp + +class BenchmarksApp(DatalayerCLIBaseApp): + """An application to run benchmarks.""" + + description = """ + An application to run benchmarks. + """ + + _requires_auth = False + + subcommands = { + "web": (BenchmarksWebApp, BenchmarksWebApp.description.splitlines()[0]), + } + + def start(self): + try: + super().start() + self.log.info(f"One of `{'` `'.join(BenchmarksApp.subcommands.keys())}` must be specified.") + self.exit(1) + except NoStart: + pass + self.exit(0) diff --git a/datalayer_core/benchmarks/web/__init__.py b/datalayer_core/benchmarks/web/__init__.py new file mode 100644 index 0000000..89c7888 --- /dev/null +++ b/datalayer_core/benchmarks/web/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) Datalayer Development Team. +# Distributed under the terms of the Modified BSD License. + diff --git a/datalayer_core/benchmarks/web/webapp.py b/datalayer_core/benchmarks/web/webapp.py new file mode 100644 index 0000000..fd1358c --- /dev/null +++ b/datalayer_core/benchmarks/web/webapp.py @@ -0,0 +1,32 @@ +# Copyright (c) Datalayer Development Team. +# Distributed under the terms of the Modified BSD License. + +import sys + +from datalayer_core.cli.base import DatalayerCLIBaseApp +from datalayer_core.serverapplication import launch_new_instance + + +class BenchmarksWebApp(DatalayerCLIBaseApp): + """An application to show the benchmarks webapp.""" + + description = """ + An application to show the benchmarks webapp. + """ + + _requires_auth = False + + def start(self): + """Start the app.""" + if len(self.extra_args) > 1: # pragma: no cover + self.log.warning("Too many arguments were provided for kernel create.") + self.print_help() + self.exit(1) + self.clear_instance() + sys.argv = [ + '', + '--ServerApp.disable_check_xsrf=True', + '--DatalayerExtensionApp.benchmarks=True', + f'--DatalayerExtensionApp.run_url={self.run_url}', + ] + launch_new_instance() diff --git a/datalayer_core/cli/base.py b/datalayer_core/cli/base.py index e61590e..5d6f741 100644 --- a/datalayer_core/cli/base.py +++ b/datalayer_core/cli/base.py @@ -15,10 +15,10 @@ from rich.console import Console -from datalayer_core.application import DatalayerApp, base_aliases, base_flags from traitlets import Bool, Unicode -from datalayer_core.authn.http_server import get_token, USE_JUPYTER_SERVER +from datalayer_core.application import DatalayerApp, base_aliases, base_flags +from datalayer_core.authn.http_server import get_token, USE_JUPYTER_SERVER_FOR_LOGIN from datalayer_core.utils.utils import fetch, find_http_port from datalayer_core._version import __version__ @@ -28,15 +28,15 @@ datalayer_aliases = dict(base_aliases) -datalayer_aliases["kernels-url"] = "DatalayerCLIBase.kernels_url" -datalayer_aliases["token"] = "DatalayerCLIBase.token" -datalayer_aliases["external-token"] = "DatalayerCLIBase.external_token" +datalayer_aliases["run-url"] = "DatalayerCLIBaseApp.run_url" +datalayer_aliases["token"] = "DatalayerCLIBaseApp.token" +datalayer_aliases["external-token"] = "DatalayerCLIBaseApp.external_token" datalayer_flags = dict(base_flags) datalayer_flags.update( { "no-browser": ( - {"DatalayerCLIBase": {"no_browser": True}}, + {"DatalayerCLIBaseApp": {"no_browser": True}}, "Will prompt for user and password on the CLI.", ) } @@ -54,14 +54,14 @@ class DatalayerCLIBaseApp(DatalayerApp): user_handle = None - kernels_url = Unicode( + run_url = Unicode( None, allow_none=False, config=True, help="Datalayer RUN URL." ) - def _kernels_url_default(self): - return os.environ.get("DATALAYER_KERNELS_URL", "https://oss.datalayer.run") + def _run_url_default(self): + return os.environ.get("DATALAYER_RUN_URL", "https://oss.datalayer.run") token = Unicode( None, @@ -114,11 +114,11 @@ def initialize(self, argv=None): "Datalayer - Version %s - Connected as %s on %s", self.version, self.user_handle, - self.kernels_url, + self.run_url, ) console = Console() console.print() - console.print(f"Datalayer - Version [bold cyan]{self.version}[/bold cyan] - Connected as [bold yellow]{self.user_handle}[/bold yellow] on [i]{self.kernels_url}[/i]") + console.print(f"Datalayer - Version [bold cyan]{self.version}[/bold cyan] - Connected as [bold yellow]{self.user_handle}[/bold yellow] on [i]{self.run_url}[/i]") console.print() @@ -149,7 +149,7 @@ def _log_in(self) -> None: token = self.token if self.token is not None else self.external_token try: response = self._fetch( - f"{self.kernels_url}/api/iam/v1/login", + f"{self.run_url}/api/iam/v1/login", method="POST", json={"token": token}, timeout=REQUEST_TIMEOUT, @@ -158,23 +158,23 @@ def _log_in(self) -> None: self.log.debug(f"Login response {content}") ans = content["user"]["handle_s"], content["token"] except BaseException as e: - msg = f"Failed to authenticate with the Token on {self.kernels_url}" + msg = f"Failed to authenticate with the Token on {self.run_url}" self.log.debug(msg, exc_info=e) if ans is None: - self.log.critical("Failed to authenticate to %s", self.kernels_url) + self.log.critical("Failed to authenticate to %s", self.run_url) sys.exit(1) else: username, token = ans self.log.debug( "Authenticated as [%s] on [%s]", username, - self.kernels_url, + self.run_url, ) self.user_handle = username self.token = token try: import keyring - keyring.set_password(self.kernels_url, "access_token", self.token) + keyring.set_password(self.run_url, "access_token", self.token) self.log.debug("Store token with keyring %s", token) except ImportError as e: self.log.debug("Unable to import keyring.", exc_info=e) @@ -184,12 +184,12 @@ def _log_in(self) -> None: # Look for cached value. try: import keyring - stored_token = keyring.get_password(self.kernels_url, "access_token") + stored_token = keyring.get_password(self.run_url, "access_token") if stored_token: content = {} try: response = fetch( - f"{self.kernels_url}/api/iam/v1/whoami", + f"{self.run_url}/api/iam/v1/whoami", headers={ "Accept": "application/json", "Content-Type": "application/json", @@ -204,7 +204,7 @@ def _log_in(self) -> None: except requests.exceptions.HTTPError as error: if error.response.status_code == 401: # Invalidate the stored token. - self.log.debug(f"Delete invalid cached token for {self.kernels_url}") + self.log.debug(f"Delete invalid cached token for {self.run_url}") self._log_out() else: self.log.warning( @@ -234,7 +234,7 @@ def _log_in(self) -> None: credentials.pop("credentials_type") try: response = self._fetch( - f"{self.kernels_url}/api/iam/v1/login", + f"{self.run_url}/api/iam/v1/login", method="POST", json=credentials, timeout=REQUEST_TIMEOUT, @@ -243,33 +243,34 @@ def _log_in(self) -> None: ans = content["user"]["handle_s"], content["token"] except BaseException as e: if "username" in credentials: - msg = f"Failed to authenticate as {credentials['username']} on {self.kernels_url}" + msg = f"Failed to authenticate as {credentials['username']} on {self.run_url}" else: - msg = f"Failed to authenticate with the Token on {self.kernels_url}" + msg = f"Failed to authenticate with the Token on {self.run_url}" self.log.debug(msg, exc_info=e) else: # Ask credentials via Browser. port = find_http_port() - if USE_JUPYTER_SERVER == False: + if USE_JUPYTER_SERVER_FOR_LOGIN == False: self.__launch_browser(port) + # Do we need to clear the instanch while using raw http server? self.clear_instance() - ans = get_token(self.kernels_url, port, self.log) + ans = get_token(self.run_url, port, self.log) if ans is None: - self.log.critical("Failed to authenticate to %s", self.kernels_url) + self.log.critical("Failed to authenticate to %s", self.run_url) sys.exit(1) else: username, token = ans self.log.info( "Authenticated as %s on %s", username, - self.kernels_url, + self.run_url, ) self.user_handle = username self.token = token try: import keyring - keyring.set_password(self.kernels_url, "access_token", self.token) + keyring.set_password(self.run_url, "access_token", self.token) self.log.debug("Store token with keyring %s", token) except ImportError as e: self.log.debug("Unable to import keyring.", exc_info=e) @@ -321,17 +322,17 @@ def _log_out(self) -> None: self.user_handle = None try: import keyring - if keyring.get_credential(self.kernels_url, "access_token") is not None: - keyring.delete_password(self.kernels_url, "access_token") + if keyring.get_credential(self.run_url, "access_token") is not None: + keyring.delete_password(self.run_url, "access_token") except ImportError as e: self.log.debug("Unable to import keyring.", exc_info=e) - self.log.info(f"πŸ‘‹ Successfully logged out from {self.kernels_url}") + self.log.info(f"πŸ‘‹ Successfully logged out from {self.run_url}") print() def __launch_browser(self, port: int) -> None: """Launch the browser.""" - address = f"http://localhost:{port}/login/cli" + address = f"http://localhost:{port}/datalayer/login/cli" # Deferred import for environments that do not have # the webbrowser module. diff --git a/datalayer_core/cli/datalayer.py b/datalayer_core/cli/datalayer.py index 5282e49..dbc077a 100644 --- a/datalayer_core/cli/datalayer.py +++ b/datalayer_core/cli/datalayer.py @@ -3,15 +3,18 @@ from pathlib import Path +from datalayer_core.application import NoStart + +from datalayer_core.cli.base import DatalayerCLIBaseApp + from datalayer_core.authn.apps.loginapp import DatalayerLoginApp from datalayer_core.authn.apps.logoutapp import DatalayerLogoutApp -from datalayer_core.authn.apps.whoamiapp import KernelWhoamiApp - +from datalayer_core.authn.apps.whoamiapp import WhoamiApp +from datalayer_core.benchmarks.benchmarksapp import BenchmarksApp from datalayer_core.about.aboutapp import DatalayerAboutApp - +from datalayer_core.environments.environmentsapp import EnvironmentsApp from datalayer_core.kernels.kernelsapp import JupyterKernelsApp - -from datalayer_core.cli.base import DatalayerCLIBaseApp +from datalayer_core.web.webapp import DatalayerWebApp from datalayer_core._version import __version__ @@ -26,15 +29,26 @@ class DatalayerCLI(DatalayerCLIBaseApp): _requires_auth = False - subcommands = { "about": (DatalayerAboutApp, DatalayerAboutApp.description.splitlines()[0]), + "benchmarks": (BenchmarksApp, BenchmarksApp.description.splitlines()[0]), + "envs": (EnvironmentsApp, EnvironmentsApp.description.splitlines()[0]), "kernels": (JupyterKernelsApp, JupyterKernelsApp.description.splitlines()[0]), "login": (DatalayerLoginApp, DatalayerLoginApp.description.splitlines()[0]), "logout": (DatalayerLogoutApp, DatalayerLogoutApp.description.splitlines()[0]), - "whoami": (KernelWhoamiApp, KernelWhoamiApp.description.splitlines()[0]), + "web": (DatalayerWebApp, DatalayerWebApp.description.splitlines()[0]), + "who": (WhoamiApp, WhoamiApp.description.splitlines()[0]), + "whoami": (WhoamiApp, WhoamiApp.description.splitlines()[0]), } + def start(self): + try: + super().start() + self.log.info(f"One of `{'` `'.join(DatalayerCLI.subcommands.keys())}` must be specified.") + self.exit(1) + except NoStart: + pass + self.exit(0) # ----------------------------------------------------------------------------- # Main entry point diff --git a/datalayer_core/environments/__init__.py b/datalayer_core/environments/__init__.py new file mode 100644 index 0000000..7a11949 --- /dev/null +++ b/datalayer_core/environments/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Datalayer Development Team. +# Distributed under the terms of the Modified BSD License. diff --git a/datalayer_core/environments/environmentsapp.py b/datalayer_core/environments/environmentsapp.py new file mode 100644 index 0000000..09af81a --- /dev/null +++ b/datalayer_core/environments/environmentsapp.py @@ -0,0 +1,29 @@ +# Copyright (c) Datalayer Development Team. +# Distributed under the terms of the Modified BSD License. + +from datalayer_core.application import NoStart +from datalayer_core.environments.list.listapp import EnvironmentsListApp +from datalayer_core.cli.base import DatalayerCLIBaseApp +from datalayer_core._version import __version__ + + +class EnvironmentsApp(DatalayerCLIBaseApp): + description = """ + The Jupyter Kernels CLI application. + """ + + _requires_auth = False + + + subcommands = { + "list": (EnvironmentsListApp, EnvironmentsListApp.description.splitlines()[0]), + } + + def start(self): + try: + super().start() + self.log.info(f"One of `{'` `'.join(EnvironmentsApp.subcommands.keys())}` must be specified.") + self.exit(1) + except NoStart: + pass + self.exit(0) diff --git a/datalayer_core/environments/list/__init__.py b/datalayer_core/environments/list/__init__.py new file mode 100644 index 0000000..7a11949 --- /dev/null +++ b/datalayer_core/environments/list/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Datalayer Development Team. +# Distributed under the terms of the Modified BSD License. diff --git a/datalayer_core/kernels/envs/envssapp.py b/datalayer_core/environments/list/listapp.py similarity index 77% rename from datalayer_core/kernels/envs/envssapp.py rename to datalayer_core/environments/list/listapp.py index 7a03d8b..fbf05a2 100644 --- a/datalayer_core/kernels/envs/envssapp.py +++ b/datalayer_core/environments/list/listapp.py @@ -10,8 +10,8 @@ from datalayer_core.cli.base import DatalayerCLIBaseApp -def new_kernel_table(): - table = Table(title="Jupyter Environments") +def new_env_table(): + table = Table(title="Environments") table.add_column("ID", style="magenta", no_wrap=True) table.add_column("Cost per seconds", justify="right", style="red", no_wrap=True) table.add_column("Name", style="green", no_wrap=True) @@ -21,7 +21,7 @@ def new_kernel_table(): return table -def add_kernel_to_table(table, environment): +def add_env_to_table(table, environment): desc = environment["description"] table.add_row( environment["name"], @@ -33,7 +33,7 @@ def add_kernel_to_table(table, environment): ) -class KernelEnvironmentsApp(DatalayerCLIBaseApp): +class EnvironmentsListApp(DatalayerCLIBaseApp): """A Kernel application.""" description = """ @@ -49,11 +49,16 @@ def start(self): self.exit(1) response = self._fetch( - "{}/api/jupyter/v1/environments".format(self.kernels_url), + "{}/api/jupyter/v1/environments".format(self.run_url), ) content = response.json() - table = new_kernel_table() + table = new_env_table() for environment in content.get("environments", []): - add_kernel_to_table(table, environment) + add_env_to_table(table, environment) console = Console() console.print(table) + print(""" +Create a kernel with e.g. + +datalayer kernels create --given-name my-kernel --credits-limit 3 python-simple-env +""") diff --git a/datalayer_core/kernels/console/consoleapp.py b/datalayer_core/kernels/console/consoleapp.py index 43b6e0e..d3ae6d4 100644 --- a/datalayer_core/kernels/console/consoleapp.py +++ b/datalayer_core/kernels/console/consoleapp.py @@ -8,7 +8,7 @@ from traitlets.config import catch_config_error, boolean_flag -from ..._version import __version__ +from datalayer_core._version import __version__ from datalayer_core.cli.base import ( DatalayerCLIBaseApp, datalayer_aliases, @@ -62,7 +62,7 @@ aliases.update( { - "kernel": "KernelConsoleApp.kernel_name", + "kernel": "KernelsConsoleApp.kernel_name", } ) @@ -71,14 +71,14 @@ # ----------------------------------------------------------------------------- -class KernelConsoleApp(DatalayerCLIBaseApp): +class KernelsConsoleApp(DatalayerCLIBaseApp): """Start a terminal frontend to a remote kernel.""" name = "jupyter-kernels-console" version = __version__ description = """ - The Jupyter kernels terminal-based Console. + The Jupyter Kernels terminal-based Console. This launches a Console application inside a terminal. """ @@ -160,7 +160,7 @@ def init_banner(self): def init_kernel_manager(self) -> None: # Create a KernelManager. self.kernel_manager = self.kernel_manager_class( - kernels_url=self.kernels_url, + run_url=self.run_url, token=self.token, username=self.user_handle, parent=self, @@ -175,7 +175,7 @@ def init_kernel_client(self) -> None: def start(self): # JupyterApp.start dispatches on NoStart - super(KernelConsoleApp, self).start() + super(KernelsConsoleApp, self).start() try: if self.shell is None: return @@ -185,7 +185,7 @@ def start(self): self.kernel_client.stop_channels() -main = launch_new_instance = KernelConsoleApp.launch_instance +main = launch_new_instance = KernelsConsoleApp.launch_instance if __name__ == "__main__": diff --git a/datalayer_core/kernels/console/shell.py b/datalayer_core/kernels/console/shell.py index 5778d35..76b6390 100644 --- a/datalayer_core/kernels/console/shell.py +++ b/datalayer_core/kernels/console/shell.py @@ -9,7 +9,7 @@ Instance, ) -from ..._version import __version__ +from datalayer_core._version import __version__ class WSTerminalInteractiveShell(ZMQTerminalInteractiveShell): diff --git a/datalayer_core/kernels/create/__init__.py b/datalayer_core/kernels/create/__init__.py new file mode 100644 index 0000000..89c7888 --- /dev/null +++ b/datalayer_core/kernels/create/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) Datalayer Development Team. +# Distributed under the terms of the Modified BSD License. + diff --git a/datalayer_core/kernels/create/createapp.py b/datalayer_core/kernels/create/createapp.py index d49c4a9..971b791 100644 --- a/datalayer_core/kernels/create/createapp.py +++ b/datalayer_core/kernels/create/createapp.py @@ -7,15 +7,15 @@ from traitlets import Dict, Float, Unicode from datalayer_core.cli.base import DatalayerCLIBaseApp, datalayer_aliases -from ..utils import display_kernels +from datalayer_core.kernels.utils import display_kernels create_alias = dict(datalayer_aliases) -create_alias["given-name"] = "KernelCreateApp.kernel_given_name" -create_alias["credits-limit"] = "KernelCreateApp.credits_limit" +create_alias["given-name"] = "KernelsCreateApp.kernel_given_name" +create_alias["credits-limit"] = "KernelsCreateApp.credits_limit" -class KernelCreateApp(DatalayerCLIBaseApp): +class KernelsCreateApp(DatalayerCLIBaseApp): """An application to create a kernel.""" description = """ @@ -47,13 +47,13 @@ def start(self): self.print_help() self.exit(1) environment_name = self.extra_args[0] - body = {"kernel_type": "default"} + body = {"kernel_type": "notebook"} if self.kernel_given_name: body["kernel_given_name"] = self.kernel_given_name if self.credits_limit is None: response = self._fetch( - "{}/api/iam/v1/usage/credits".format(self.kernels_url), method="GET" + "{}/api/iam/v1/usage/credits".format(self.run_url), method="GET" ) raw = response.json() credits = raw["credits"] @@ -78,7 +78,7 @@ def start(self): response = self._fetch( "{}/api/jupyter/v1/environment/{}".format( - self.kernels_url, environment_name + self.run_url, environment_name ), method="POST", json=body, diff --git a/datalayer_core/kernels/exec/__init__.py b/datalayer_core/kernels/exec/__init__.py new file mode 100644 index 0000000..89c7888 --- /dev/null +++ b/datalayer_core/kernels/exec/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) Datalayer Development Team. +# Distributed under the terms of the Modified BSD License. + diff --git a/datalayer_core/kernels/exec/execapp.py b/datalayer_core/kernels/exec/execapp.py index f8ebe29..4f5fe25 100644 --- a/datalayer_core/kernels/exec/execapp.py +++ b/datalayer_core/kernels/exec/execapp.py @@ -11,7 +11,7 @@ from traitlets.config import catch_config_error, boolean_flag -from ..._version import __version__ +from datalayer_core._version import __version__ from datalayer_core.cli.base import ( DatalayerCLIBaseApp, datalayer_aliases, @@ -57,8 +57,8 @@ aliases.update( { - "kernel": "KernelExecApp.kernel_name", - "timeout": "KernelExecApp.timeout", + "kernel": "KernelsExecApp.kernel_name", + "timeout": "KernelsExecApp.timeout", } ) @@ -67,7 +67,7 @@ # ----------------------------------------------------------------------------- -class KernelExecApp(DatalayerCLIBaseApp): +class KernelsExecApp(DatalayerCLIBaseApp): """Execute a file on a IPython remote kernel.""" version = __version__ @@ -141,7 +141,7 @@ def initialize(self, argv=None): def init_kernel_manager(self) -> None: # Create a KernelManager. self.kernel_manager = self.kernel_manager_class( - kernels_url=self.kernels_url, + run_url=self.run_url, token=self.token, username=self.user_handle, parent=self, @@ -157,7 +157,7 @@ def init_kernel_client(self) -> None: def start(self): try: # JupyterApp.start dispatches on NoStart - super(KernelExecApp, self).start() + super(KernelsExecApp, self).start() if len(self.extra_args) != 3: # FIXME why is exec an args? self.log.warning("A file to execute must be provided.") self.print_help() @@ -219,7 +219,7 @@ def _get_cells(filepath: Path) -> t.Iterator[tuple[str | None, str]]: yield None, filepath.read_text(encoding="utf-8") -main = launch_new_instance = KernelExecApp.launch_instance +main = launch_new_instance = KernelsExecApp.launch_instance if __name__ == "__main__": diff --git a/datalayer_core/kernels/kernelsapp.py b/datalayer_core/kernels/kernelsapp.py index 9447c64..e510614 100644 --- a/datalayer_core/kernels/kernelsapp.py +++ b/datalayer_core/kernels/kernelsapp.py @@ -1,20 +1,17 @@ # Copyright (c) Datalayer Development Team. # Distributed under the terms of the Modified BSD License. -from pathlib import Path - -from datalayer_core.authn.apps.loginapp import DatalayerLoginApp -from datalayer_core.authn.apps.logoutapp import DatalayerLogoutApp - -from datalayer_core.kernels.console.consoleapp import KernelConsoleApp -from datalayer_core.kernels.create.createapp import KernelCreateApp -from datalayer_core.kernels.exec.execapp import KernelExecApp -from datalayer_core.kernels.list.listapp import KernelListApp -from datalayer_core.kernels.pause.pauseapp import KernelPauseApp -from datalayer_core.kernels.envs.envssapp import KernelEnvironmentsApp -from datalayer_core.kernels.start.startapp import KernelStartApp -from datalayer_core.kernels.stop.stopapp import KernelStopApp -from datalayer_core.kernels.terminate.terminateapp import KernelTerminateApp +from datalayer_core.application import NoStart + +from datalayer_core.kernels.console.consoleapp import KernelsConsoleApp +from datalayer_core.kernels.create.createapp import KernelsCreateApp +from datalayer_core.kernels.exec.execapp import KernelsExecApp +from datalayer_core.kernels.list.listapp import KernelsListApp +from datalayer_core.kernels.pause.pauseapp import KernelsPauseApp +from datalayer_core.kernels.start.startapp import KernelsStartApp +from datalayer_core.kernels.stop.stopapp import KernelsStopApp +from datalayer_core.kernels.terminate.terminateapp import KernelsTerminateApp +from datalayer_core.kernels.web.webapp import KernelsWebApp from datalayer_core.cli.base import DatalayerCLIBaseApp @@ -28,17 +25,23 @@ class JupyterKernelsApp(DatalayerCLIBaseApp): _requires_auth = False - subcommands = { - "console": (KernelConsoleApp, KernelConsoleApp.description.splitlines()[0]), - "create": (KernelCreateApp, KernelCreateApp.description.splitlines()[0]), - "exec": (KernelExecApp, KernelExecApp.description.splitlines()[0]), - "list": (KernelListApp, KernelListApp.description.splitlines()[0]), - "login": (DatalayerLoginApp, DatalayerLoginApp.description.splitlines()[0]), - "logout": (DatalayerLogoutApp, DatalayerLogoutApp.description.splitlines()[0]), - "pause": (KernelPauseApp, KernelPauseApp.description.splitlines()[0]), - "envs": (KernelEnvironmentsApp, KernelEnvironmentsApp.description.splitlines()[0]), - "start": (KernelStartApp, KernelStartApp.description.splitlines()[0]), - "stop": (KernelStopApp, KernelStopApp.description.splitlines()[0]), - "terminate": (KernelTerminateApp, KernelTerminateApp.description.splitlines()[0]), + "console": (KernelsConsoleApp, KernelsConsoleApp.description.splitlines()[0]), + "create": (KernelsCreateApp, KernelsCreateApp.description.splitlines()[0]), + "exec": (KernelsExecApp, KernelsExecApp.description.splitlines()[0]), + "list": (KernelsListApp, KernelsListApp.description.splitlines()[0]), + "pause": (KernelsPauseApp, KernelsPauseApp.description.splitlines()[0]), + "start": (KernelsStartApp, KernelsStartApp.description.splitlines()[0]), + "stop": (KernelsStopApp, KernelsStopApp.description.splitlines()[0]), + "terminate": (KernelsTerminateApp, KernelsTerminateApp.description.splitlines()[0]), + "web": (KernelsWebApp, KernelsWebApp.description.splitlines()[0]), } + + def start(self): + try: + super().start() + self.log.info(f"One of `{'` `'.join(JupyterKernelsApp.subcommands.keys())}` must be specified.") + self.exit(1) + except NoStart: + pass + self.exit(0) diff --git a/datalayer_core/kernels/list/__init__.py b/datalayer_core/kernels/list/__init__.py new file mode 100644 index 0000000..89c7888 --- /dev/null +++ b/datalayer_core/kernels/list/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) Datalayer Development Team. +# Distributed under the terms of the Modified BSD License. + diff --git a/datalayer_core/kernels/list/listapp.py b/datalayer_core/kernels/list/listapp.py index 606062f..e7d26f7 100644 --- a/datalayer_core/kernels/list/listapp.py +++ b/datalayer_core/kernels/list/listapp.py @@ -4,10 +4,10 @@ import warnings from datalayer_core.cli.base import DatalayerCLIBaseApp -from ..utils import display_kernels +from datalayer_core.kernels.utils import display_kernels -class KernelListApp(DatalayerCLIBaseApp): +class KernelsListApp(DatalayerCLIBaseApp): """An application to list the kernels.""" description = """ @@ -24,7 +24,12 @@ def start(self): self.exit(1) response = self._fetch( - "{}/api/jupyter/v1/kernels".format(self.kernels_url), + "{}/api/jupyter/v1/kernels".format(self.run_url), ) raw = response.json() display_kernels(raw.get("kernels", [])) + print(""" +Create a kernel with e.g. + +datalayer kernels create --given-name my-kernel --credits-limit 3 python-simple-env +""") diff --git a/datalayer_core/kernels/manager.py b/datalayer_core/kernels/manager.py index f8aaeeb..a6bbd14 100644 --- a/datalayer_core/kernels/manager.py +++ b/datalayer_core/kernels/manager.py @@ -13,7 +13,7 @@ from traitlets import DottedObjectName, Type from traitlets.config import LoggingConfigurable -from datalayer_core.kernels.utils import timestamp_to_local_date +from datalayer_core.kernels.utils import _timestamp_to_local_date from datalayer_core.utils.utils import fetch @@ -23,10 +23,10 @@ class KernelManager(LoggingConfigurable): """Manages a single kernel remotely.""" - def __init__(self, kernels_url: str, token: str, username: str, **kwargs): + def __init__(self, run_url: str, token: str, username: str, **kwargs): """Initialize the gateway kernel manager.""" super().__init__(**kwargs) - self.kernels_url = kernels_url + self.run_url = run_url self.token = token self.username = username self.kernel_url: str = "" @@ -116,7 +116,7 @@ def start_kernel(self, **kwargs): kernel = None if kernel_name: response = fetch( - "{}/api/jupyter/v1/kernel/{}".format(self.kernels_url, kernel_name), + "{}/api/jupyter/v1/kernel/{}".format(self.run_url, kernel_name), token=self.token, ) kernel = response.json().get("kernel") @@ -125,7 +125,7 @@ def start_kernel(self, **kwargs): "No kernel name provided. Picking the first available remote kernel…" ) response = fetch( - "{}/api/jupyter/v1/kernels".format(self.kernels_url), + "{}/api/jupyter/v1/kernels".format(self.run_url), token=self.token, ) kernels = response.json().get("kernels", []) @@ -153,7 +153,7 @@ def start_kernel(self, **kwargs): msg = f"KernelManager using existing jupyter kernel {kernel_name}" expired_at = kernel.get("expired_at") if expired_at is not None: - msg += f" expiring at {timestamp_to_local_date(expired_at)}" + msg += f" expiring at {_timestamp_to_local_date(expired_at)}" self.log.info(msg) def shutdown_kernel(self, now=False, restart=False): diff --git a/datalayer_core/kernels/pause/__init__.py b/datalayer_core/kernels/pause/__init__.py new file mode 100644 index 0000000..89c7888 --- /dev/null +++ b/datalayer_core/kernels/pause/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) Datalayer Development Team. +# Distributed under the terms of the Modified BSD License. + diff --git a/datalayer_core/kernels/pause/pauseapp.py b/datalayer_core/kernels/pause/pauseapp.py index b476003..fe85889 100644 --- a/datalayer_core/kernels/pause/pauseapp.py +++ b/datalayer_core/kernels/pause/pauseapp.py @@ -6,7 +6,7 @@ from datalayer_core.cli.base import DatalayerCLIBaseApp -class KernelPauseApp(DatalayerCLIBaseApp): +class KernelsPauseApp(DatalayerCLIBaseApp): """Kernel Pause application.""" description = """ diff --git a/datalayer_core/kernels/start/__init__.py b/datalayer_core/kernels/start/__init__.py new file mode 100644 index 0000000..89c7888 --- /dev/null +++ b/datalayer_core/kernels/start/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) Datalayer Development Team. +# Distributed under the terms of the Modified BSD License. + diff --git a/datalayer_core/kernels/start/startapp.py b/datalayer_core/kernels/start/startapp.py index 8af696a..3ce5c44 100644 --- a/datalayer_core/kernels/start/startapp.py +++ b/datalayer_core/kernels/start/startapp.py @@ -6,7 +6,7 @@ from datalayer_core.cli.base import DatalayerCLIBaseApp -class KernelStartApp(DatalayerCLIBaseApp): +class KernelsStartApp(DatalayerCLIBaseApp): """Kernel Start application.""" description = """ diff --git a/datalayer_core/kernels/stop/__init__.py b/datalayer_core/kernels/stop/__init__.py new file mode 100644 index 0000000..89c7888 --- /dev/null +++ b/datalayer_core/kernels/stop/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) Datalayer Development Team. +# Distributed under the terms of the Modified BSD License. + diff --git a/datalayer_core/kernels/stop/stopapp.py b/datalayer_core/kernels/stop/stopapp.py index 6ce2862..4ae9980 100644 --- a/datalayer_core/kernels/stop/stopapp.py +++ b/datalayer_core/kernels/stop/stopapp.py @@ -6,7 +6,7 @@ from datalayer_core.cli.base import DatalayerCLIBaseApp -class KernelStopApp(DatalayerCLIBaseApp): +class KernelsStopApp(DatalayerCLIBaseApp): """Kernel Stop application.""" description = """ diff --git a/datalayer_core/kernels/terminate/__init__.py b/datalayer_core/kernels/terminate/__init__.py new file mode 100644 index 0000000..89c7888 --- /dev/null +++ b/datalayer_core/kernels/terminate/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) Datalayer Development Team. +# Distributed under the terms of the Modified BSD License. + diff --git a/datalayer_core/kernels/terminate/terminateapp.py b/datalayer_core/kernels/terminate/terminateapp.py index 6ef5d36..eafa208 100644 --- a/datalayer_core/kernels/terminate/terminateapp.py +++ b/datalayer_core/kernels/terminate/terminateapp.py @@ -6,7 +6,7 @@ from datalayer_core.cli.base import DatalayerCLIBaseApp -class KernelTerminateApp(DatalayerCLIBaseApp): +class KernelsTerminateApp(DatalayerCLIBaseApp): """Kernel Terminate application.""" description = """ @@ -25,7 +25,7 @@ def start(self): kernel_id = self.extra_args[0] self._fetch( - "{}/api/jupyter/v1/kernel/{}".format(self.kernels_url, kernel_id), + "{}/api/jupyter/v1/kernel/{}".format(self.run_url, kernel_id), method="DELETE", ) self.log.info(f"Kernel '{kernel_id}' deleted.") diff --git a/datalayer_core/kernels/utils.py b/datalayer_core/kernels/utils.py index 702004b..0b1a2b9 100644 --- a/datalayer_core/kernels/utils.py +++ b/datalayer_core/kernels/utils.py @@ -7,13 +7,13 @@ from rich.table import Table -def timestamp_to_local_date(timestamp: str) -> str: +def _timestamp_to_local_date(timestamp: str) -> str: return ( datetime.fromtimestamp(float(timestamp), timezone.utc).astimezone().isoformat() ) -def new_kernel_table(title="Jupyter Kernel"): +def _new_kernel_table(title="Jupyter Kernel"): table = Table(title=title) table.add_column("Kernel ID", style="magenta", no_wrap=True) table.add_column("Kernel Name", style="cyan", no_wrap=True) @@ -22,37 +22,20 @@ def new_kernel_table(title="Jupyter Kernel"): return table -def add_kernel_to_table(table, kernel): +def _add_kernel_to_table(table, kernel): expired_at = kernel.get("expired_at") table.add_row( kernel["jupyter_pod_name"], kernel["kernel_given_name"], kernel["environment_name"], - "Never" if expired_at is None else timestamp_to_local_date(expired_at), + "Never" if expired_at is None else _timestamp_to_local_date(expired_at), ) def display_kernels(kernels: list) -> None: """Display a list of kernels in the console.""" - table = new_kernel_table(title="Jupyter Kernels") + table = _new_kernel_table(title="Jupyter Kernels") for kernel in kernels: - add_kernel_to_table(table, kernel) - console = Console() - console.print(table) - - -def display_me(me: dict) -> None: - """Display a my profile.""" - table = Table(title="Profile") - table.add_column("ID", style="magenta", no_wrap=True) - table.add_column("Handle", style="cyan", no_wrap=True) - table.add_column("First name", style="green", no_wrap=True) - table.add_column("Last name", style="green", no_wrap=True) - table.add_row( - me["uid"], - me["handle_s"], - me["first_name_t"], - me["last_name_t"], - ) + _add_kernel_to_table(table, kernel) console = Console() console.print(table) diff --git a/datalayer_core/kernels/web/__init__.py b/datalayer_core/kernels/web/__init__.py new file mode 100644 index 0000000..89c7888 --- /dev/null +++ b/datalayer_core/kernels/web/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) Datalayer Development Team. +# Distributed under the terms of the Modified BSD License. + diff --git a/datalayer_core/kernels/web/webapp.py b/datalayer_core/kernels/web/webapp.py new file mode 100644 index 0000000..b8daec8 --- /dev/null +++ b/datalayer_core/kernels/web/webapp.py @@ -0,0 +1,32 @@ +# Copyright (c) Datalayer Development Team. +# Distributed under the terms of the Modified BSD License. + +import sys + +from datalayer_core.cli.base import DatalayerCLIBaseApp +from datalayer_core.serverapplication import launch_new_instance + + +class KernelsWebApp(DatalayerCLIBaseApp): + """An application to show the kernels webapp.""" + + description = """ + An application to show the kernels webapp. + """ + + _requires_auth = False + + def start(self): + """Start the app.""" + if len(self.extra_args) > 1: # pragma: no cover + self.log.warning("Too many arguments were provided for kernel create.") + self.print_help() + self.exit(1) + self.clear_instance() + sys.argv = [ + '', + '--ServerApp.disable_check_xsrf=True', + '--DatalayerExtensionApp.kernels=True', + f'--DatalayerExtensionApp.run_url={self.run_url}', + ] + launch_new_instance() diff --git a/datalayer_core/serverapplication.py b/datalayer_core/serverapplication.py index 487b02d..e745a2a 100644 --- a/datalayer_core/serverapplication.py +++ b/datalayer_core/serverapplication.py @@ -36,8 +36,8 @@ class DatalayerExtensionApp(ExtensionAppJinjaMixin, ExtensionApp): static_paths = [DEFAULT_STATIC_FILES_PATH] template_paths = [DEFAULT_TEMPLATE_FILES_PATH] - # run_url can be set set and None or ' '(empty string) - # in that case, the consumer of those settings are free + # 'run_url' can be set set and None or ' ' (empty string). + # In that case, the consumer of those settings are free # to consider run_url as null. run_url = Unicode( "https://oss.datalayer.run", @@ -48,6 +48,10 @@ class DatalayerExtensionApp(ExtensionAppJinjaMixin, ExtensionApp): white_label = Bool(False, config=True, help="""Display white label content.""") + benchmarks = Bool(False, config=True, help="""Show the benchmarks page.""") + kernels = Bool(False, config=True, help="""Show the kernels page.""") + webapp = Bool(False, config=True, help="""Show the webapp page.""") + class Launcher(Configurable): """Datalayer launcher configuration""" @@ -58,7 +62,7 @@ class Launcher(Configurable): ) name = Unicode( - "Jupyter kernels", + "Jupyter Kernels", config=True, help=("Application launcher card name."), ) @@ -84,6 +88,12 @@ def _default_launcher(self): def initialize_settings(self): self.serverapp.answer_yes = True + if self.benchmarks: + self.serverapp.default_url = "/datalayer/benchmarks" + if self.kernels: + self.serverapp.default_url = "/datalayer/kernels" + if self.webapp: + self.serverapp.default_url = "/datalayer/web" port = get_server_port() if port is not None: self.serverapp.port = port @@ -107,10 +117,13 @@ def initialize_templates(self): def initialize_handlers(self): handlers = [ - ("datalayer", IndexHandler), - (url_path_join("datalayer", "config"), ConfigHandler), - (url_path_join("datalayer", "login"), LoginHandler), - (url_path_join("datalayer", "service-worker", r"([^/]+\.js)"), ServiceWorkerHandler), + ("/", IndexHandler), + (self.name, IndexHandler), + (url_path_join(self.name, "config"), ConfigHandler), + (url_path_join(self.name, "benchmarks"), IndexHandler), + (url_path_join(self.name, "kernels"), IndexHandler), + (url_path_join(self.name, "login"), LoginHandler), + (url_path_join(self.name, "service-worker", r"([^/]+\.js)"), ServiceWorkerHandler), ] self.handlers.extend(handlers) diff --git a/datalayer_core/web/__init__.py b/datalayer_core/web/__init__.py new file mode 100644 index 0000000..89c7888 --- /dev/null +++ b/datalayer_core/web/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) Datalayer Development Team. +# Distributed under the terms of the Modified BSD License. + diff --git a/datalayer_core/web/webapp.py b/datalayer_core/web/webapp.py new file mode 100644 index 0000000..29480a6 --- /dev/null +++ b/datalayer_core/web/webapp.py @@ -0,0 +1,32 @@ +# Copyright (c) Datalayer Development Team. +# Distributed under the terms of the Modified BSD License. + +import sys + +from datalayer_core.cli.base import DatalayerCLIBaseApp +from datalayer_core.serverapplication import launch_new_instance + + +class DatalayerWebApp(DatalayerCLIBaseApp): + """An application to run the webapp.""" + + description = """ + An application to run the webapp. + """ + + _requires_auth = False + + def start(self): + """Start the app.""" + if len(self.extra_args) > 1: # pragma: no cover + self.log.warning("Too many arguments were provided for kernel create.") + self.print_help() + self.exit(1) + self.clear_instance() + sys.argv = [ + '', + '--ServerApp.disable_check_xsrf=True', + '--DatalayerExtensionApp.webapp=True', + f'--DatalayerExtensionApp.run_url={self.run_url}', + ] + launch_new_instance() diff --git a/docs/docs/index.mdx b/docs/docs/index.mdx index 555d1f8..ac670e8 100644 --- a/docs/docs/index.mdx +++ b/docs/docs/index.mdx @@ -3,3 +3,62 @@ sidebar_position: 1 --- # Datalayer Core + +## JupyterLab Extensions + +Use these snippets to activate the Datalayer plugins. + +```ts +import { JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/application'; +import { doActivateDatalayerExtension } from "."; + +const plugin: JupyterFrontEndPlugin = { + id: '@datalayer/jupyter-kernels:activator', + description: 'Jupyter Kernels Activator.', + autoStart: true, + requires: [], + activate: (app: JupyterFrontEnd) => {}, + activateDatalayerExtension(app); +} + +export default plugin; +``` + +```ts +import { CommandRegistry } from "@lumino/commands"; +import { JupyterFrontEnd } from "@jupyterlab/application"; +A +const ACTIVATE_DATALAYER_EXTENSION_COMMAND = "@datalayer/jupyter-kernels:datalayer:activate"; + +export function activateDatalayerExtension(app: JupyterFrontEnd) { + try { + function doActivateDatalayerExtension( + commands: CommandRegistry, + changes: CommandRegistry.ICommandChangedArgs, + ) { + if ( + changes.type === "added" && + changes.id === ACTIVATE_DATALAYER_EXTENSION_COMMAND + ) { + commands + .execute(ACTIVATE_DATALAYER_EXTENSION_COMMAND) + .catch((reason: any) => { + console.warn("Error while activating the GPU extension", reason); + }); + } + + commands.commandChanged.disconnect(doActivateDatalayerExtension); + } + if (app.commands.hasCommand(ACTIVATE_DATALAYER_EXTENSION_COMMAND)) { + doActivateDatalayerExtension(app.commands, { + id: ACTIVATE_DATALAYER_EXTENSION_COMMAND, + type: "added", + }); + } else { + app.commands.commandChanged.connect(doActivateDatalayerExtension); + } + } catch (e) { + console.warn("Error while activating the Datalayer extension", e); + } +} +``` diff --git a/package.json b/package.json index bbfa421..6c525e1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@datalayer/core", - "version": "1.0.0", + "version": "1.0.8", "description": "Ξ Datalayer Core.", "keywords": [ "datalayer", @@ -65,7 +65,7 @@ "watch:src": "tsc -w" }, "dependencies": { - "@datalayer/run": "^0.1.15" + "@datalayer/run": "^0.2.4" }, "devDependencies": { "@babel/core": "^7.21.0", @@ -195,10 +195,6 @@ "htmlparser2": "8.0.1", "react": "18.2.0", "react-dom": "18.2.0", - "react-redux": "8.1.2", - "redux": "4.1.0", - "redux-observable": "1.2.0", - "rxjs": "6.6.0", "styled-components": "5.3.10", "typescript": "5.0.3", "webpack": "5.74.0", @@ -216,6 +212,12 @@ "access": "public" }, "jupyterlab": { + "disabledExtensions": [ + "@jupyterlab/apputils-extension:sessionDialogs", + "@jupyterlab/docmanager-extension:manager", + "@jupyterlab/running-extension:plugin", + "@jupyterlab/running-extension:sidebar" + ], "discovery": { "server": { "managers": [ @@ -226,11 +228,19 @@ } } }, - "extension": "./lib/jupyterlab/index.js", + "extension": "lib/jupyterlab/index.js", "outputDir": "datalayer_core/labextension", "schemaDir": "schema", "webpackConfig": "./webpack.lab-config.js", "sharedPackages": { + "react": { + "bundled": false, + "singleton": true + }, + "react-dom": { + "bundled": false, + "singleton": true + }, "@datalayer/icons-react": { "bundled": true, "singleton": true @@ -255,6 +265,10 @@ "bundled": true, "singleton": true }, + "@jupyterlite/pyodide-kernel": { + "bundled": false, + "singleton": true + }, "@jupyterlite/server": { "bundled": true, "singleton": true diff --git a/pyproject.toml b/pyproject.toml index e39b9d4..6e45b29 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,8 +40,7 @@ test = [ ] [project.scripts] -# dla = "datalayer_core.command:main" -# datalayer = "datalayer_core.command:main" +d = "datalayer_core.cli.datalayer:main" dla = "datalayer_core.cli.datalayer:main" datalayer = "datalayer_core.cli.datalayer:main" datalayer-config = "datalayer_core.config:main" diff --git a/src/DatalayerApp.tsx b/src/DatalayerApp.tsx index 0c45c85..1ecc194 100644 --- a/src/DatalayerApp.tsx +++ b/src/DatalayerApp.tsx @@ -1,19 +1,67 @@ -/// +/* + * Copyright (c) 2023-2024 Datalayer, Inc. + * + * Datalayer License + */ +/** + * Main entry point for the Datalayer Application. + */ +import { useState, useEffect } from 'react'; import { createRoot } from 'react-dom/client'; -import { CoreExample } from '@datalayer/run'; +import { MemoryRouter } from 'react-router-dom'; +import { RunIndex, LoginFormCLI, BenchmarksExample, KernelsExample } from '@datalayer/run'; + +import '../style/index.css'; -import "./../style/index.css"; +type ViewType = 'benchmarks' | 'kernels' | 'web' | 'login' | 'root'; + +export const DatalayerApp = (): JSX.Element => { + const pathname = window.location.pathname; + const [view, setView] = useState(); + useEffect(() => { + if (pathname === '/') { + setView('root'); + window.location.href = "/lab"; + } + if (pathname === '/datalayer/web') { + setView('web'); + } + if (pathname === '/datalayer/kernels') { + setView('kernels'); + } + else if (pathname === '/datalayer/benchmarks') { + setView('benchmarks'); + } + else if (pathname === '/datalayer/login/cli') { + setView('login'); + } + }, [pathname]) + return ( + <> + { view === 'root' && ( + <> + )} + { view === 'web' && ( + + )} + { view === 'kernels' && ( + + )} + { view === 'benchmarks' && ( + + )} + { view === 'login' && ( + + + + )} + + ); +} const div = document.createElement('div'); document.body.appendChild(div); const root = createRoot(div); -/* -if (module.hot) { - module.hot.accept('./CoreExample', () => { - const CoreExample = require('./CoreExample').default; - root.render(); - }) -} -*/ -root.render(); + +root.render(); diff --git a/src/LoginCLIApp.tsx b/src/LoginCLIApp.tsx deleted file mode 100644 index b994ae7..0000000 --- a/src/LoginCLIApp.tsx +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (c) 2023-2024 Datalayer, Inc. - * - * Datalayer License - */ - -/** - * Main entry point for Jupyter Kernels. - */ -import { createRoot } from 'react-dom/client'; -import { MemoryRouter } from 'react-router-dom'; -import { LoginFormCLI } from '@datalayer/run'; - -import '../style/index.css'; - -export const LoginCLIApp = (): JSX.Element => { - return ( - <> - - - - - ); -} - -const div = document.createElement('div'); -document.body.appendChild(div); -const root = createRoot(div); - -root.render(); diff --git a/webpack.config.js b/webpack.config.js index ada72c9..a0a7eff 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -4,13 +4,12 @@ * Datalayer License */ -const path = require('path'); +// const path = require('path'); const webpack = require('webpack'); const miniSVGDataURI = require('mini-svg-data-uri'); const HtmlWebpackPlugin = require('html-webpack-plugin'); -const BundleAnalyzerPlugin = require('webpack-bundle-analyzer') - .BundleAnalyzerPlugin; +const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; /* const shimJS = path.resolve(__dirname, 'src', 'emptyshim.js'); function shim(regExp) { @@ -22,7 +21,7 @@ const mode = IS_PRODUCTION ? 'production' : 'development'; const devtool = IS_PRODUCTION ? false : 'inline-cheap-source-map'; const minimize = IS_PRODUCTION ? true : false; const publicPath = IS_PRODUCTION - ? '/static/datalayer_core/' + ? '/static/datalayer/' // This has to remain /static/datalayer. : 'http://localhost:3063/'; const commonOptions = { @@ -30,7 +29,7 @@ const commonOptions = { devServer: { port: 3063, open: [ - 'http://localhost:3063/login/cli' + 'http://localhost:3063/datalayer/login/cli' ], https: false, server: 'http', @@ -139,7 +138,7 @@ const commonOptions = { generator: { filename: 'schema/[name][ext][query]' } - } + }, ] }, }; @@ -147,10 +146,10 @@ const commonOptions = { module.exports = [ { ...commonOptions, - entry: './src/LoginCLIApp', + entry: './src/DatalayerApp', output: { publicPath, - // filename: '[name].[contenthash].datalayer.js', + // filename: '[name].[contenthash].datalayer-core.js', filename: '[name].datalayer-core.js' }, plugins: [ @@ -167,35 +166,11 @@ module.exports = [ generateStatsFile: false }), /* - shim(/@fortawesome/), - shim(/moment/), - shim(/react-jvectormap/), - shim(/react-slick/), - shim(/react-tagsinput/), + shim(/@jupyterlite\/pyodide-kernel/), */ new HtmlWebpackPlugin({ template: './public/index.html' }) ] }, - /* - { - ...commonOptions, - entry: './src/LoginCLIApp', - output: { - path: path.resolve(__dirname, 'datalayer_core', 'login', 'static'), - filename: 'login-cli.js', - }, - plugins: [ - new webpack.ProvidePlugin({ - process: 'process/browser' - }), - new BundleAnalyzerPlugin({ - analyzerMode: IS_PRODUCTION ? 'static' : 'disabled', // server, static, json, disabled. - openAnalyzer: false, - generateStatsFile: false, - }), - ] - } - */ ]