diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..2390d8c8 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + groups: + github-actions: + patterns: + - "*" diff --git a/.github/workflows/check-code-style.yml b/.github/workflows/check-code-style.yml new file mode 100644 index 00000000..134b9758 --- /dev/null +++ b/.github/workflows/check-code-style.yml @@ -0,0 +1,44 @@ +name: Check code style + +on: + push: + branches: + - main + pull_request: + branches: + - main + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + check-code-style: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Checkout the branch from the PR that triggered the job + run: gh pr checkout ${{ github.event.pull_request.number }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - uses: astral-sh/setup-uv@v6 + with: + version: "latest" + python-version: 3.12 + activate-environment: true + + - name: Install dependencies + run: | + uv pip install . + uv pip install pre-commit + + - name: Run pre-commit + run: | + pre-commit run --all-files diff --git a/.github/workflows/check-typing.yml b/.github/workflows/check-typing.yml index 013933da..adf201f1 100644 --- a/.github/workflows/check-typing.yml +++ b/.github/workflows/check-typing.yml @@ -7,9 +7,6 @@ on: pull_request: branches: - main - pull_request_target: - branches: - - main workflow_dispatch: concurrency: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 43d1e2f7..fd37fec4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,9 +7,7 @@ on: pull_request: branches: - main - pull_request_target: - branches: - - main + workflow_dispatch: jobs: @@ -19,6 +17,10 @@ jobs: matrix: python-version: ['3.9', '3.10', '3.11', '3.12'] steps: + + - name: Install deps + run: | + sudo apt install libdbus-1-3 libdbus-1-dev libglib2.0-dev - name: Checkout uses: actions/checkout@v2 with: @@ -37,7 +39,9 @@ jobs: run: | uv pip install . uv pip install .[test] + uv pip install secretstorage dbus-python keyring + - uses: t1m0thyj/unlock-keyring@v1 - name: Run basic tests env: DATALAYER_TEST_TOKEN: ${{ secrets.DATALAYER_TEST_TOKEN }} diff --git a/.gitignore b/.gitignore index d35c510f..05cfa568 100644 --- a/.gitignore +++ b/.gitignore @@ -178,4 +178,6 @@ dmypy.json !**/.env !.github !.devcontainer -.env \ No newline at end of file + +# Environment variables +.env diff --git a/.licenserc.yaml b/.licenserc.yaml index 01539b00..2cad8d00 100644 --- a/.licenserc.yaml +++ b/.licenserc.yaml @@ -8,41 +8,41 @@ header: Distributed under the terms of the Modified BSD License. paths-ignore: - - '**/*.apt' - - '**/*.cedar' - - '**/*.dash' - - '**/*.fga' - - '**/*.ipynb' - - '**/*.j2' - - '**/*.json' - - '**/*.mamba' - - '**/*.md' - - '**/*.mod' - - '**/*.nblink' - - '**/*.rego' - - '**/*.sum' - - '**/*.svg' - - '**/*.template' - - '**/*.tsbuildinfo' - - '**/*.txt' - - '**/*.yaml' - - '**/*.yml' - - '**/*_key' - - '**/*_key.pub' - - '**/.*' - - '**/LICENSE.txt' - - '**/MANIFEST.in' - - '**/build' - - '**/lib' - - '**/node_modules' - - '**/schemas' - - '**/ssh/*' - - '**/static' - - '**/themes' - - '**/typings' - - '**/*.patch' - - '**/*.bundle.js' - - '**/*.map.js' - - 'LICENSE' + - "**/*.apt" + - "**/*.cedar" + - "**/*.dash" + - "**/*.fga" + - "**/*.ipynb" + - "**/*.j2" + - "**/*.json" + - "**/*.mamba" + - "**/*.md" + - "**/*.mod" + - "**/*.nblink" + - "**/*.rego" + - "**/*.sum" + - "**/*.svg" + - "**/*.template" + - "**/*.tsbuildinfo" + - "**/*.txt" + - "**/*.yaml" + - "**/*.yml" + - "**/*_key" + - "**/*_key.pub" + - "**/.*" + - "**/LICENSE.txt" + - "**/MANIFEST.in" + - "**/build" + - "**/lib" + - "**/node_modules" + - "**/schemas" + - "**/ssh/*" + - "**/static" + - "**/themes" + - "**/typings" + - "**/*.patch" + - "**/*.bundle.js" + - "**/*.map.js" + - "LICENSE" - comment: on-failure \ No newline at end of file + comment: on-failure diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..e07cab56 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,25 @@ +exclude: "^conda.recipe|dev|docs|style|src|jupyter-config|.github|.storybook$" +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-toml + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace + + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v2.2.1 + hooks: + - id: prettier + exclude_types: ["python", "jupyter", "shell", "gitignore"] + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.11.13 + hooks: + # Run the linter + - id: ruff-check + types_or: [python, pyi] + # Run the formatter + - id: ruff-format + types_or: [python, pyi] diff --git a/.storybook/main.ts b/.storybook/main.ts index bbbda12e..988c4249 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -1,20 +1,17 @@ -import type { StorybookConfig } from '@storybook/react-vite'; +import type { StorybookConfig } from "@storybook/react-vite"; const config: StorybookConfig = { - "stories": [ - "../src/**/*.mdx", - "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)" - ], - "addons": [ + stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], + addons: [ "@chromatic-com/storybook", "@storybook/addon-docs", "@storybook/addon-onboarding", "@storybook/addon-a11y", - "@storybook/addon-vitest" + "@storybook/addon-vitest", ], - "framework": { - "name": "@storybook/react-vite", - "options": {} - } + framework: { + name: "@storybook/react-vite", + options: {}, + }, }; -export default config; \ No newline at end of file +export default config; diff --git a/.storybook/preview.ts b/.storybook/preview.ts index 073582ec..3b803445 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -1,11 +1,11 @@ -import type { Preview } from '@storybook/react-vite' +import type { Preview } from "@storybook/react-vite"; const preview: Preview = { parameters: { controls: { matchers: { - color: /(background|color)$/i, - date: /Date$/i, + color: /(background|color)$/i, + date: /Date$/i, }, }, @@ -13,9 +13,9 @@ const preview: Preview = { // 'todo' - show a11y violations in the test UI only // 'error' - fail CI on a11y violations // 'off' - skip a11y checks entirely - test: 'todo' - } + test: "todo", + }, }, }; -export default preview; \ No newline at end of file +export default preview; diff --git a/.storybook/vitest.setup.ts b/.storybook/vitest.setup.ts index 44922d55..fd7ac45e 100644 --- a/.storybook/vitest.setup.ts +++ b/.storybook/vitest.setup.ts @@ -1,7 +1,7 @@ import * as a11yAddonAnnotations from "@storybook/addon-a11y/preview"; -import { setProjectAnnotations } from '@storybook/react-vite'; -import * as projectAnnotations from './preview'; +import { setProjectAnnotations } from "@storybook/react-vite"; +import * as projectAnnotations from "./preview"; // This is an important step to apply the right configuration when testing your stories. // More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations -setProjectAnnotations([a11yAddonAnnotations, projectAnnotations]); \ No newline at end of file +setProjectAnnotations([a11yAddonAnnotations, projectAnnotations]); diff --git a/conftest.py b/conftest.py index 4de75898..60ecacf3 100644 --- a/conftest.py +++ b/conftest.py @@ -3,7 +3,7 @@ import pytest -pytest_plugins = ("jupyter_server.pytest_plugin", ) +pytest_plugins = ("jupyter_server.pytest_plugin",) @pytest.fixture diff --git a/datalayer_core/__init__.py b/datalayer_core/__init__.py index 515fde90..04fe5be0 100644 --- a/datalayer_core/__init__.py +++ b/datalayer_core/__init__.py @@ -9,7 +9,17 @@ def _jupyter_server_extension_points() -> List[Dict[str, Any]]: - return [{ - "module": "datalayer_core", - "app": DatalayerExtensionApp, - }] + return [ + { + "module": "datalayer_core", + "app": DatalayerExtensionApp, + } + ] + + +__all__ = [ + "__version__", + "_jupyter_server_extension_points", + "DatalayerClient", + "DatalayerExtensionApp", +] diff --git a/datalayer_core/about/aboutapp.py b/datalayer_core/about/aboutapp.py index c686df89..1ea55101 100644 --- a/datalayer_core/about/aboutapp.py +++ b/datalayer_core/about/aboutapp.py @@ -23,7 +23,6 @@ class DatalayerAboutApp(DatalayerCLIBaseApp): _requires_auth = False - def start(self): try: super().start() diff --git a/datalayer_core/application.py b/datalayer_core/application.py index 247f5528..49a5f9f9 100644 --- a/datalayer_core/application.py +++ b/datalayer_core/application.py @@ -50,7 +50,10 @@ {"Application": {"log_level": logging.DEBUG}}, "set log level to logging.DEBUG (maximize logging output)", ), - "generate-config": ({"DatalayerApp": {"generate_config": True}}, "generate default config file"), + "generate-config": ( + {"DatalayerApp": {"generate_config": True}}, + "generate default config file", + ), "y": ( {"DatalayerApp": {"answer_yes": True}}, "Answer yes to any questions instead of prompting.", @@ -170,7 +173,7 @@ def migrate_config(self): """Migrate config/data from IPython 3""" try: # let's see if we can open the marker file # for reading and updating (writing) - f_marker = open(os.path.join(self.config_dir, "migrated"), 'r+') # noqa + f_marker = open(os.path.join(self.config_dir, "migrated"), "r+") # noqa except PermissionError: # not readable and/or writable return # so let's give up migration in such an environment except FileNotFoundError: # cannot find the marker file @@ -224,7 +227,9 @@ def load_config_file(self, suppress_errors=True): # self.raise_config_file_errors if (not suppress_errors) or self.raise_config_file_errors: raise - self.log.warning("Error loading config file: %s", config_file_name, exc_info=True) + self.log.warning( + "Error loading config file: %s", config_file_name, exc_info=True + ) # subcommand-related def _find_subcommand(self, name): diff --git a/datalayer_core/authn/__init__.py b/datalayer_core/authn/__init__.py index 815eacd6..f7d0007b 100644 --- a/datalayer_core/authn/__init__.py +++ b/datalayer_core/authn/__init__.py @@ -1,3 +1,2 @@ # Copyright (c) 2023-2025 Datalayer, Inc. # Distributed under the terms of the Modified BSD License. - diff --git a/datalayer_core/authn/apps/__init__.py b/datalayer_core/authn/apps/__init__.py index 815eacd6..f7d0007b 100644 --- a/datalayer_core/authn/apps/__init__.py +++ b/datalayer_core/authn/apps/__init__.py @@ -1,3 +1,2 @@ # Copyright (c) 2023-2025 Datalayer, Inc. # Distributed under the terms of the Modified BSD License. - diff --git a/datalayer_core/authn/apps/loginapp.py b/datalayer_core/authn/apps/loginapp.py index 0a9df5f8..e8b17eff 100644 --- a/datalayer_core/authn/apps/loginapp.py +++ b/datalayer_core/authn/apps/loginapp.py @@ -23,5 +23,7 @@ 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.run_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 550e826d..c81bc9f5 100644 --- a/datalayer_core/authn/apps/logoutapp.py +++ b/datalayer_core/authn/apps/logoutapp.py @@ -17,7 +17,6 @@ class DatalayerLogoutApp(DatalayerCLIBaseApp): _requires_auth = False - def start(self): """Start the app.""" if len(self.extra_args) > 0: # pragma: no cover diff --git a/datalayer_core/authn/apps/utils.py b/datalayer_core/authn/apps/utils.py index 6dfa3594..1f3ad2b8 100644 --- a/datalayer_core/authn/apps/utils.py +++ b/datalayer_core/authn/apps/utils.py @@ -4,6 +4,7 @@ from rich.console import Console from rich.table import Table + def display_me(me: dict, infos: dict) -> None: """Display a my profile.""" table = Table(title="Profile") @@ -17,7 +18,7 @@ def display_me(me: dict, infos: dict) -> None: me["handle_s"], me["first_name_t"], me["last_name_t"], - infos.get("run_url") + infos.get("run_url"), ) console = Console() console.print(table) diff --git a/datalayer_core/authn/apps/whoamiapp.py b/datalayer_core/authn/apps/whoamiapp.py index 01d05378..081adf3d 100644 --- a/datalayer_core/authn/apps/whoamiapp.py +++ b/datalayer_core/authn/apps/whoamiapp.py @@ -15,14 +15,13 @@ class WhoamiApp(DatalayerCLIBaseApp): datalayer whoami """ - + def get_profile(self): response = self._fetch( "{}/api/iam/v1/whoami".format(self.run_url), ) return response.json() - def start(self): """Start the app.""" if len(self.extra_args) > 0: # pragma: no cover @@ -31,7 +30,7 @@ def start(self): self.exit(1) response = self.get_profile() - infos = { + infos = { "run_url": self.run_url, } display_me(response.get("profile", {}), infos) diff --git a/datalayer_core/authn/http_server.py b/datalayer_core/authn/http_server.py index 4286d61e..180067b5 100644 --- a/datalayer_core/authn/http_server.py +++ b/datalayer_core/authn/http_server.py @@ -19,12 +19,8 @@ from socketserver import BaseRequestHandler from datalayer_core.authn.state import set_server_port -from datalayer_core.authn.keys import ( - DATALAYER_IAM_TOKEN_KEY, DATALAYER_IAM_USER_KEY -) -from datalayer_core.authn.pages import ( - LANDING_PAGE, AUTH_SUCCESS_PAGE, OAUTH_ERROR_PAGE -) +from datalayer_core.authn.keys import DATALAYER_IAM_TOKEN_KEY, DATALAYER_IAM_USER_KEY +from datalayer_core.authn.pages import LANDING_PAGE, AUTH_SUCCESS_PAGE, OAUTH_ERROR_PAGE from datalayer_core.utils.utils import find_http_port from datalayer_core.serverapplication import launch_new_instance @@ -84,14 +80,13 @@ def _save_token(self, query: str): token_key=DATALAYER_IAM_TOKEN_KEY, token=token, base_url="/", - ).encode('UTF-8', 'replace') + ).encode("UTF-8", "replace") self.send_response(HTTPStatus.OK) self.send_header("Content-type", "text/html") self.send_header("Content-Length", str(len(content))) self.end_headers() self.wfile.write(content) - def do_GET(self): parts = urllib.parse.urlsplit(self.path) if parts[2].strip("/").endswith("oauth/callback"): @@ -102,10 +97,10 @@ def do_GET(self): { "runUrl": self.server.run_url, "iamRunUrl": self.server.run_url, - "whiteLabel": False + "whiteLabel": False, } ) - ).encode('UTF-8', 'replace') + ).encode("UTF-8", "replace") self.send_response(HTTPStatus.OK) self.send_header("Content-type", "text/html") self.send_header("Content-Length", str(len(content))) @@ -114,7 +109,6 @@ def do_GET(self): else: super().do_GET() - def do_POST(self): content_length = int(self.headers["Content-Length"]) post_data = self.rfile.read(content_length) @@ -129,7 +123,6 @@ def do_POST(self): signal.raise_signal(signal.SIGINT) - def log_message(self, format, *args): message = format % args logger.debug( @@ -151,11 +144,14 @@ def __init__( bind_and_activate: bool = True, ) -> None: try: - import datalayer_ui - except: + import datalayer_ui # noqa: F401 + except Exception: print("Sorry, I can not show the login page...") - print("Check the datalayer_ui python package is available in your environment") + print( + "Check the datalayer_ui python package is available in your environment" + ) import sys + sys.exit(-1) self.run_url = run_url self.user_handle = None @@ -170,6 +166,7 @@ def server_bind(self): def finish_request(self, request, client_address): import datalayer_ui + DATALAYER_UI_PATH = Path(datalayer_ui.__file__).parent self.RequestHandlerClass( request, client_address, self, directory=str(DATALAYER_UI_PATH / "static") @@ -184,21 +181,27 @@ def get_token( server_address = ("", port or find_http_port()) port = server_address[1] - if USE_JUPYTER_SERVER_FOR_LOGIN == True: + if USE_JUPYTER_SERVER_FOR_LOGIN: set_server_port(port) - logger.info(f"Waiting for user logging, open http://localhost:{port}. Press CTRL+C to abort.\n") + logger.info( + f"Waiting for user logging, open http://localhost:{port}. Press CTRL+C to abort.\n" + ) sys.argv = [ "", - "--DatalayerExtensionApp.run_url", run_url, - "--ServerApp.disable_check_xsrf", "True", + "--DatalayerExtensionApp.run_url", + run_url, + "--ServerApp.disable_check_xsrf", + "True", ] launch_new_instance() logger.debug("Authentication finished.") -# return None if httpd.token is None else (httpd.user_handle, httpd.token) + # return None if httpd.token is None else (httpd.user_handle, httpd.token) return None else: httpd = DualStackServer(server_address, LoginRequestHandler, run_url) - logger.info(f"Waiting for user logging, open http://localhost:{port}. Press CTRL+C to abort.\n") + logger.info( + f"Waiting for user logging, open http://localhost:{port}. Press CTRL+C to abort.\n" + ) try: httpd.serve_forever() except KeyboardInterrupt: diff --git a/datalayer_core/authn/pages.py b/datalayer_core/authn/pages.py index 7c0436b0..b4bd307e 100644 --- a/datalayer_core/authn/pages.py +++ b/datalayer_core/authn/pages.py @@ -55,7 +55,7 @@ const btn = document.getElementById("return-btn") btn.addEventListener("click", () => {{ // Redirect to default page - window.location.replace('{base_url}'); + window.location.replace('{base_url}'); }}) diff --git a/datalayer_core/benchmarks/__init__.py b/datalayer_core/benchmarks/__init__.py index 815eacd6..f7d0007b 100644 --- a/datalayer_core/benchmarks/__init__.py +++ b/datalayer_core/benchmarks/__init__.py @@ -1,3 +1,2 @@ # Copyright (c) 2023-2025 Datalayer, Inc. # Distributed under the terms of the Modified BSD License. - diff --git a/datalayer_core/benchmarks/benchmarksapp.py b/datalayer_core/benchmarks/benchmarksapp.py index 7aa4cbac..77056e32 100644 --- a/datalayer_core/benchmarks/benchmarksapp.py +++ b/datalayer_core/benchmarks/benchmarksapp.py @@ -6,6 +6,7 @@ from datalayer_core.cli.base import DatalayerCLIBaseApp from datalayer_core.benchmarks.web.webapp import BenchmarksWebApp + class BenchmarksApp(DatalayerCLIBaseApp): """An application to run benchmarks.""" @@ -22,7 +23,9 @@ class BenchmarksApp(DatalayerCLIBaseApp): def start(self): try: super().start() - self.log.info(f"One of `{'` `'.join(BenchmarksApp.subcommands.keys())}` must be specified.") + self.log.info( + f"One of `{'` `'.join(BenchmarksApp.subcommands.keys())}` must be specified." + ) self.exit(1) except NoStart: pass diff --git a/datalayer_core/benchmarks/web/__init__.py b/datalayer_core/benchmarks/web/__init__.py index 815eacd6..f7d0007b 100644 --- a/datalayer_core/benchmarks/web/__init__.py +++ b/datalayer_core/benchmarks/web/__init__.py @@ -1,3 +1,2 @@ # Copyright (c) 2023-2025 Datalayer, Inc. # 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 index 0545985a..d89e63eb 100644 --- a/datalayer_core/benchmarks/web/webapp.py +++ b/datalayer_core/benchmarks/web/webapp.py @@ -24,9 +24,9 @@ def start(self): self.exit(1) self.clear_instance() sys.argv = [ - '', - '--ServerApp.disable_check_xsrf=True', - '--DatalayerExtensionApp.benchmarks=True', - f'--DatalayerExtensionApp.run_url={self.run_url}', + "", + "--ServerApp.disable_check_xsrf=True", + "--DatalayerExtensionApp.benchmarks=True", + f"--DatalayerExtensionApp.run_url={self.run_url}", ] launch_new_instance() diff --git a/datalayer_core/cli/__init__.py b/datalayer_core/cli/__init__.py index 815eacd6..f7d0007b 100644 --- a/datalayer_core/cli/__init__.py +++ b/datalayer_core/cli/__init__.py @@ -1,3 +1,2 @@ # Copyright (c) 2023-2025 Datalayer, Inc. # Distributed under the terms of the Modified BSD License. - diff --git a/datalayer_core/cli/base.py b/datalayer_core/cli/base.py index e00953f1..4d34bdb5 100644 --- a/datalayer_core/cli/base.py +++ b/datalayer_core/cli/base.py @@ -198,7 +198,7 @@ def _log_in(self) -> None: else: # Ask credentials via Browser. port = find_http_port() - if USE_JUPYTER_SERVER_FOR_LOGIN == False: + if not USE_JUPYTER_SERVER_FOR_LOGIN: self.__launch_browser(port) # Do we need to clear the instance while using raw http server? if hasattr(self, "clear_instance"): diff --git a/datalayer_core/cli/datalayer.py b/datalayer_core/cli/datalayer.py index c6b231af..e32bacae 100644 --- a/datalayer_core/cli/datalayer.py +++ b/datalayer_core/cli/datalayer.py @@ -17,8 +17,6 @@ from datalayer_core.runtimes.runtimesapp import JupyterRuntimesApp from datalayer_core.web.webapp import DatalayerWebApp -from datalayer_core.__version__ import __version__ - HERE = Path(__file__).parent @@ -36,7 +34,10 @@ class DatalayerCLI(DatalayerCLIBaseApp): "console": (RuntimesConsoleApp, RuntimesConsoleApp.description.splitlines()[0]), "envs": (EnvironmentsApp, EnvironmentsApp.description.splitlines()[0]), "run": (JupyterRuntimesApp, JupyterRuntimesApp.description.splitlines()[0]), - "runtimes": (JupyterRuntimesApp, JupyterRuntimesApp.description.splitlines()[0]), + "runtimes": ( + JupyterRuntimesApp, + JupyterRuntimesApp.description.splitlines()[0], + ), "login": (DatalayerLoginApp, DatalayerLoginApp.description.splitlines()[0]), "logout": (DatalayerLogoutApp, DatalayerLogoutApp.description.splitlines()[0]), "web": (DatalayerWebApp, DatalayerWebApp.description.splitlines()[0]), @@ -47,12 +48,15 @@ class DatalayerCLI(DatalayerCLIBaseApp): def start(self): try: super().start() - self.log.info(f"One of `{'` `'.join(DatalayerCLI.subcommands.keys())}` must be specified.") + 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/command.py b/datalayer_core/command.py index bcbeabc9..23fa79cc 100644 --- a/datalayer_core/command.py +++ b/datalayer_core/command.py @@ -59,7 +59,9 @@ def datalayer_parser() -> DatalayerParser: group = parser.add_mutually_exclusive_group(required=False) # don't use argparse's version action because it prints to stderr on py2 group.add_argument( - "--version", action="store_true", help="show the versions of core datalayer packages and exit" + "--version", + action="store_true", + help="show the versions of core datalayer packages and exit", ) subcommand_action = group.add_argument( "subcommand", type=str, nargs="?", help="the subcommand to launch" @@ -67,16 +69,26 @@ def datalayer_parser() -> DatalayerParser: # For argcomplete, supply all known subcommands subcommand_action.completer = lambda *args, **kwargs: list_subcommands() # type: ignore[attr-defined] - group.add_argument("--config-dir", action="store_true", help="show Datalayer config dir") - group.add_argument("--data-dir", action="store_true", help="show Datalayer data dir") - group.add_argument("--runtime-dir", action="store_true", help="show Datalayer runtime dir") + group.add_argument( + "--config-dir", action="store_true", help="show Datalayer config dir" + ) + group.add_argument( + "--data-dir", action="store_true", help="show Datalayer data dir" + ) + group.add_argument( + "--runtime-dir", action="store_true", help="show Datalayer runtime dir" + ) group.add_argument( "--paths", action="store_true", help="show all Datalayer paths. Add --json for machine-readable format.", ) - parser.add_argument("--json", action="store_true", help="output paths as machine-readable json") - parser.add_argument("--debug", action="store_true", help="output debug information about paths") + parser.add_argument( + "--json", action="store_true", help="output paths as machine-readable json" + ) + parser.add_argument( + "--debug", action="store_true", help="output debug information about paths" + ) return parser @@ -185,7 +197,9 @@ def _path_with_self(): for script in scripts: bindir = os.path.dirname(script) - if os.path.isdir(bindir) and os.access(script, os.X_OK): # only if it's a script + if os.path.isdir(bindir) and os.access( + script, os.X_OK + ): # only if it's a script # ensure executable's dir is on PATH # avoids missing subcommands when datalayer is run via absolute path path_list.insert(0, bindir) diff --git a/datalayer_core/config.py b/datalayer_core/config.py index dd66468a..75ff9390 100644 --- a/datalayer_core/config.py +++ b/datalayer_core/config.py @@ -11,7 +11,7 @@ class DatalayerConfig(DatalayerApp): """A Datalayer Config App. - + Run `datalayer --generate-config` to create the default config. """ diff --git a/datalayer_core/environments/environmentsapp.py b/datalayer_core/environments/environmentsapp.py index 5b97d6c3..baa5045a 100644 --- a/datalayer_core/environments/environmentsapp.py +++ b/datalayer_core/environments/environmentsapp.py @@ -4,7 +4,6 @@ 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): @@ -14,7 +13,6 @@ class EnvironmentsApp(DatalayerCLIBaseApp): _requires_auth = False - subcommands = { "list": (EnvironmentsListApp, EnvironmentsListApp.description.splitlines()[0]), "ls": (EnvironmentsListApp, EnvironmentsListApp.description.splitlines()[0]), @@ -23,7 +21,9 @@ class EnvironmentsApp(DatalayerCLIBaseApp): def start(self): try: super().start() - self.log.info(f"One of `{'` `'.join(EnvironmentsApp.subcommands.keys())}` must be specified.") + self.log.info( + f"One of `{'` `'.join(EnvironmentsApp.subcommands.keys())}` must be specified." + ) self.exit(1) except NoStart: pass diff --git a/datalayer_core/environments/list/listapp.py b/datalayer_core/environments/list/listapp.py index c806d398..47534c22 100644 --- a/datalayer_core/environments/list/listapp.py +++ b/datalayer_core/environments/list/listapp.py @@ -34,7 +34,6 @@ def add_env_to_table(table, environment): class EnvironmentsListMixin: - def _list_environments(self): """List available environments.""" response = self._fetch( @@ -65,10 +64,12 @@ def start(self): add_env_to_table(table, environment) console = Console() console.print(table) - if (len(environments) > 0): - print(f""" + if len(environments) > 0: + print(""" Create a Runtime with e.g. """) for environment in environments: - print(f"datalayer runtimes create --given-name my-runtime --credits-limit 3 {environment['name']}") + print( + f"datalayer runtimes create --given-name my-runtime --credits-limit 3 {environment['name']}" + ) print() diff --git a/datalayer_core/handlers/__init__.py b/datalayer_core/handlers/__init__.py index 815eacd6..f7d0007b 100644 --- a/datalayer_core/handlers/__init__.py +++ b/datalayer_core/handlers/__init__.py @@ -1,3 +1,2 @@ # Copyright (c) 2023-2025 Datalayer, Inc. # Distributed under the terms of the Modified BSD License. - diff --git a/datalayer_core/handlers/base.py b/datalayer_core/handlers/base.py index 5e833ce2..c74dfe3a 100644 --- a/datalayer_core/handlers/base.py +++ b/datalayer_core/handlers/base.py @@ -4,10 +4,14 @@ """Base handler.""" from jupyter_server.base.handlers import JupyterHandler -from jupyter_server.extension.handler import ExtensionHandlerMixin, ExtensionHandlerJinjaMixin +from jupyter_server.extension.handler import ( + ExtensionHandlerMixin, + ExtensionHandlerJinjaMixin, +) # pylint: disable=W0223 -class BaseTemplateHandler(ExtensionHandlerJinjaMixin, ExtensionHandlerMixin, JupyterHandler): +class BaseTemplateHandler( + ExtensionHandlerJinjaMixin, ExtensionHandlerMixin, JupyterHandler +): """The Base handler for the templates.""" - \ No newline at end of file diff --git a/datalayer_core/handlers/config/__init__.py b/datalayer_core/handlers/config/__init__.py index 815eacd6..f7d0007b 100644 --- a/datalayer_core/handlers/config/__init__.py +++ b/datalayer_core/handlers/config/__init__.py @@ -1,3 +1,2 @@ # Copyright (c) 2023-2025 Datalayer, Inc. # Distributed under the terms of the Modified BSD License. - diff --git a/datalayer_core/handlers/config/handler.py b/datalayer_core/handlers/config/handler.py index 3ac33f3c..71c3f0b8 100644 --- a/datalayer_core/handlers/config/handler.py +++ b/datalayer_core/handlers/config/handler.py @@ -7,7 +7,7 @@ import tornado -from jupyter_server.base.handlers import APIHandler, JupyterHandler +from jupyter_server.base.handlers import APIHandler from jupyter_server.extension.handler import ( ExtensionHandlerMixin, ) @@ -48,8 +48,8 @@ def get(self): { "extension": "datalayer", "version": __version__, - "settings": configuration, # TODO this is for backwards compatibility, remove at some point... - "configuration": configuration, # TODO this is for backwards compatibility, remove at some point... + "settings": configuration, # TODO this is for backwards compatibility, remove at some point... + "configuration": configuration, # TODO this is for backwards compatibility, remove at some point... } ) self.finish(res) diff --git a/datalayer_core/handlers/index/__init__.py b/datalayer_core/handlers/index/__init__.py index 815eacd6..f7d0007b 100644 --- a/datalayer_core/handlers/index/__init__.py +++ b/datalayer_core/handlers/index/__init__.py @@ -1,3 +1,2 @@ # Copyright (c) 2023-2025 Datalayer, Inc. # Distributed under the terms of the Modified BSD License. - diff --git a/datalayer_core/handlers/login/__init__.py b/datalayer_core/handlers/login/__init__.py index 815eacd6..f7d0007b 100644 --- a/datalayer_core/handlers/login/__init__.py +++ b/datalayer_core/handlers/login/__init__.py @@ -1,3 +1,2 @@ # Copyright (c) 2023-2025 Datalayer, Inc. # Distributed under the terms of the Modified BSD License. - diff --git a/datalayer_core/handlers/login/handler.py b/datalayer_core/handlers/login/handler.py index 60b49ee8..8170b4d0 100644 --- a/datalayer_core/handlers/login/handler.py +++ b/datalayer_core/handlers/login/handler.py @@ -9,14 +9,12 @@ from jupyter_server.base.handlers import APIHandler from jupyter_server.extension.handler import ExtensionHandlerMixin -from datalayer_core.__version__ import __version__ - # pylint: disable=W0223 class LoginHandler(ExtensionHandlerMixin, APIHandler): """The login handler.""" -# @tornado.web.authenticated + # @tornado.web.authenticated def post(self): """Login.""" data = json.loads(self.request.body) diff --git a/datalayer_core/handlers/service_worker/__init__.py b/datalayer_core/handlers/service_worker/__init__.py index 815eacd6..f7d0007b 100644 --- a/datalayer_core/handlers/service_worker/__init__.py +++ b/datalayer_core/handlers/service_worker/__init__.py @@ -1,3 +1,2 @@ # Copyright (c) 2023-2025 Datalayer, Inc. # Distributed under the terms of the Modified BSD License. - diff --git a/datalayer_core/handlers/service_worker/handler.py b/datalayer_core/handlers/service_worker/handler.py index a7b3f07c..3f412688 100644 --- a/datalayer_core/handlers/service_worker/handler.py +++ b/datalayer_core/handlers/service_worker/handler.py @@ -20,14 +20,17 @@ def initialize(self): # Must match the folder containing the service worker asset as specified # in the webpack.lab-config.js import datalayer_ui + DATALAYER_UI_PATH = Path(datalayer_ui.__file__).parent - extensionStatic = Path(DATALAYER_UI_PATH).parent / "datalayer_ui" / "labextension" / "static" + extensionStatic = ( + Path(DATALAYER_UI_PATH).parent / "datalayer_ui" / "labextension" / "static" + ) super().initialize(path=str(extensionStatic)) def validate_absolute_path(self, root: str, absolute_path: str) -> Optional[str]: """Only allow to serve the service worker""" # Must match the filename name set in webpack.lab-config.js - if Path(absolute_path).name != 'lite-service-worker.js': + if Path(absolute_path).name != "lite-service-worker.js": raise web.HTTPError(404) return super().validate_absolute_path(root, absolute_path) diff --git a/datalayer_core/migrate.py b/datalayer_core/migrate.py index eac81e38..528a8d12 100644 --- a/datalayer_core/migrate.py +++ b/datalayer_core/migrate.py @@ -194,7 +194,9 @@ def migrate_config(name, env): """ log = get_logger() src_base = pjoin("{profile}", "ipython_{name}_config").format(name=name, **env) - dst_base = pjoin("{datalayer_config}", "datalayer_{name}_config").format(name=name, **env) + dst_base = pjoin("{datalayer_config}", "datalayer_{name}_config").format( + name=name, **env + ) loaders = { ".py": PyFileConfigLoader, ".json": JSONFileConfigLoader, @@ -241,7 +243,9 @@ def migrate(): # write a marker to avoid re-running migration checks ensure_dir_exists(env["datalayer_config"]) - with open(os.path.join(env["datalayer_config"], "migrated"), "w", encoding="utf-8") as f: + with open( + os.path.join(env["datalayer_config"], "migrated"), "w", encoding="utf-8" + ) as f: f.write(datetime.now(tz=timezone.utc).isoformat()) return migrated diff --git a/datalayer_core/paths.py b/datalayer_core/paths.py index 859dc622..312da1c8 100644 --- a/datalayer_core/paths.py +++ b/datalayer_core/paths.py @@ -10,7 +10,6 @@ # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. - import errno import os import site @@ -86,7 +85,7 @@ def _do_i_own(path: str) -> bool: except Exception: # noqa pass - if hasattr(os, 'geteuid'): + if hasattr(os, "geteuid"): try: st = p.stat() return st.st_uid == os.geteuid() @@ -220,7 +219,9 @@ def datalayer_runtime_dir() -> str: if programdata: SYSTEM_DATALAYER_PATH = [pjoin(programdata, "datalayer_core")] else: # PROGRAMDATA is not defined by default on XP. - SYSTEM_DATALAYER_PATH = [os.path.join(sys.prefix, "share", "datalayer_core")] + SYSTEM_DATALAYER_PATH = [ + os.path.join(sys.prefix, "share", "datalayer_core") + ] else: SYSTEM_DATALAYER_PATH = [ "/usr/local/share/datalayer", @@ -256,7 +257,9 @@ def datalayer_path(*subdirs: str) -> List[str]: # highest priority is explicit environment variable if os.environ.get("DATALAYER_PATH"): - paths.extend(p.rstrip(os.sep) for p in os.environ["DATALAYER_PATH"].split(os.pathsep)) + paths.extend( + p.rstrip(os.sep) for p in os.environ["DATALAYER_PATH"].split(os.pathsep) + ) # Next is environment or user, depending on the DATALAYER_PREFER_ENV_PATH flag user = [datalayer_data_dir()] @@ -264,7 +267,9 @@ def datalayer_path(*subdirs: str) -> List[str]: # Check if site.getuserbase() exists to be compatible with virtualenv, # which often does not have this method. userbase: Optional[str] - userbase = site.getuserbase() if hasattr(site, "getuserbase") else site.USER_BASE + userbase = ( + site.getuserbase() if hasattr(site, "getuserbase") else site.USER_BASE + ) if userbase: userdir = os.path.join(userbase, "share", "datalayer_core") @@ -326,7 +331,10 @@ def datalayer_config_path() -> List[str]: # highest priority is explicit environment variable if os.environ.get("DATALAYER_CONFIG_PATH"): - paths.extend(p.rstrip(os.sep) for p in os.environ["DATALAYER_CONFIG_PATH"].split(os.pathsep)) + paths.extend( + p.rstrip(os.sep) + for p in os.environ["DATALAYER_CONFIG_PATH"].split(os.pathsep) + ) # Next is environment or user, depending on the DATALAYER_PREFER_ENV_PATH flag user = [datalayer_config_dir()] @@ -334,7 +342,9 @@ def datalayer_config_path() -> List[str]: userbase: Optional[str] # Check if site.getuserbase() exists to be compatible with virtualenv, # which often does not have this method. - userbase = site.getuserbase() if hasattr(site, "getuserbase") else site.USER_BASE + userbase = ( + site.getuserbase() if hasattr(site, "getuserbase") else site.USER_BASE + ) if userbase: userdir = os.path.join(userbase, "etc", "datalayer_core") @@ -594,7 +604,11 @@ def _win32_restrict_file_to_user_ctypes(fname: str) -> None: # noqa FILE_WRITE_ATTRIBUTES = 256 FILE_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0x1FF FILE_GENERIC_READ = ( - STANDARD_RIGHTS_READ | FILE_READ_DATA | FILE_READ_ATTRIBUTES | FILE_READ_EA | SYNCHRONIZE + STANDARD_RIGHTS_READ + | FILE_READ_DATA + | FILE_READ_ATTRIBUTES + | FILE_READ_EA + | SYNCHRONIZE ) FILE_GENERIC_WRITE = ( STANDARD_RIGHTS_WRITE @@ -725,12 +739,16 @@ def CreateWellKnownSid(WellKnownSidType): pSid = (ctypes.c_char * 1)() cbSid = wintypes.DWORD() try: - advapi32.CreateWellKnownSid(WellKnownSidType, None, pSid, ctypes.byref(cbSid)) + advapi32.CreateWellKnownSid( + WellKnownSidType, None, pSid, ctypes.byref(cbSid) + ) except OSError as e: if e.winerror != ERROR_INSUFFICIENT_BUFFER: # type:ignore[attr-defined] raise pSid = (ctypes.c_char * cbSid.value)() - advapi32.CreateWellKnownSid(WellKnownSidType, None, pSid, ctypes.byref(cbSid)) + advapi32.CreateWellKnownSid( + WellKnownSidType, None, pSid, ctypes.byref(cbSid) + ) return pSid[:] def GetUserNameEx(NameFormat): @@ -769,7 +787,9 @@ def LookupAccountName(lpSystemName, lpAccountName): raise Sid = ctypes.create_unicode_buffer("", cbSid.value) pSid = ctypes.cast(ctypes.pointer(Sid), wintypes.LPVOID) - lpReferencedDomainName = ctypes.create_unicode_buffer("", cchReferencedDomainName.value + 1) + lpReferencedDomainName = ctypes.create_unicode_buffer( + "", cchReferencedDomainName.value + 1 + ) success = advapi32.LookupAccountNameW( lpSystemName, lpAccountName, @@ -818,9 +838,13 @@ def SetFileSecurity(lpFileName, RequestedInformation, pSecurityDescriptor): # set the security of a file or directory object advapi32.SetFileSecurityW(lpFileName, RequestedInformation, pSecurityDescriptor) - def SetSecurityDescriptorDacl(pSecurityDescriptor, bDaclPresent, pDacl, bDaclDefaulted): + def SetSecurityDescriptorDacl( + pSecurityDescriptor, bDaclPresent, pDacl, bDaclDefaulted + ): # set information in a discretionary access control list (DACL) - advapi32.SetSecurityDescriptorDacl(pSecurityDescriptor, bDaclPresent, pDacl, bDaclDefaulted) + advapi32.SetSecurityDescriptorDacl( + pSecurityDescriptor, bDaclPresent, pDacl, bDaclDefaulted + ) def MakeAbsoluteSD(pSelfRelativeSecurityDescriptor): # return a security descriptor in absolute format @@ -852,7 +876,9 @@ def MakeAbsoluteSD(pSelfRelativeSecurityDescriptor): except OSError as e: if e.winerror != ERROR_INSUFFICIENT_BUFFER: # type:ignore[attr-defined] raise - pAbsoluteSecurityDescriptor = (wintypes.BYTE * lpdwAbsoluteSecurityDescriptorSize.value)() + pAbsoluteSecurityDescriptor = ( + wintypes.BYTE * lpdwAbsoluteSecurityDescriptorSize.value + )() pDaclData = (wintypes.BYTE * lpdwDaclSize.value)() pDacl = ctypes.cast(pDaclData, PACL).contents pSaclData = (wintypes.BYTE * lpdwSaclSize.value)() @@ -945,7 +971,9 @@ def get_file_mode(fname: str) -> int: ) # Use 4 octal digits since S_IMODE does the same -allow_insecure_writes = os.getenv("DATALAYER_ALLOW_INSECURE_WRITES", "false").lower() in ("true", "1") +allow_insecure_writes = os.getenv( + "DATALAYER_ALLOW_INSECURE_WRITES", "false" +).lower() in ("true", "1") @contextmanager diff --git a/datalayer_core/pydoc/replace_processor.py b/datalayer_core/pydoc/replace_processor.py index 0ee7d369..7c9b1623 100644 --- a/datalayer_core/pydoc/replace_processor.py +++ b/datalayer_core/pydoc/replace_processor.py @@ -8,8 +8,10 @@ @dataclass class Docstring: content: str + def __repr__(self): return self.content + def __str__(self): return self.content diff --git a/datalayer_core/runtimes/__init__.py b/datalayer_core/runtimes/__init__.py index e626c21f..dc07fb31 100644 --- a/datalayer_core/runtimes/__init__.py +++ b/datalayer_core/runtimes/__init__.py @@ -9,7 +9,6 @@ from datalayer_core.runtimes.terminate.terminateapp import RuntimesTerminateMixin - class RuntimesMixin( RuntimesCreateMixin, RuntimesListMixin, diff --git a/datalayer_core/runtimes/console/__init__.py b/datalayer_core/runtimes/console/__init__.py index 815eacd6..f7d0007b 100644 --- a/datalayer_core/runtimes/console/__init__.py +++ b/datalayer_core/runtimes/console/__init__.py @@ -1,3 +1,2 @@ # Copyright (c) 2023-2025 Datalayer, Inc. # Distributed under the terms of the Modified BSD License. - diff --git a/datalayer_core/runtimes/console/shell.py b/datalayer_core/runtimes/console/shell.py index dc7d1c42..fe546513 100644 --- a/datalayer_core/runtimes/console/shell.py +++ b/datalayer_core/runtimes/console/shell.py @@ -16,9 +16,7 @@ class WSTerminalInteractiveShell(ZMQTerminalInteractiveShell): manager = Instance( "datalayer_core.runtimes.manager.RuntimeManager", allow_none=True ) - client = Instance( - "datalayer_core.runtimes.client.RuntimeClient", allow_none=True - ) + client = Instance("datalayer_core.runtimes.client.RuntimeClient", allow_none=True) @default("banner") def _default_banner(self): diff --git a/datalayer_core/runtimes/create/__init__.py b/datalayer_core/runtimes/create/__init__.py index 815eacd6..f7d0007b 100644 --- a/datalayer_core/runtimes/create/__init__.py +++ b/datalayer_core/runtimes/create/__init__.py @@ -1,3 +1,2 @@ # Copyright (c) 2023-2025 Datalayer, Inc. # Distributed under the terms of the Modified BSD License. - diff --git a/datalayer_core/runtimes/create/createapp.py b/datalayer_core/runtimes/create/createapp.py index af81011c..c10f6a50 100644 --- a/datalayer_core/runtimes/create/createapp.py +++ b/datalayer_core/runtimes/create/createapp.py @@ -15,7 +15,6 @@ class RuntimesCreateMixin: - def _create_runtime(self, environment_name: str) -> dict: """Create a Runtime with the given environment name.""" body = {"type": "notebook"} @@ -26,9 +25,11 @@ def _create_runtime(self, environment_name: str) -> dict: response = self._fetch( "{}/api/iam/v1/usage/credits".format(self.run_url), method="GET" ) - + raw_credits = response.json() - self.credits_limit = get_default_credits_limit(raw_credits["reservations"], raw_credits["credits"]) + self.credits_limit = get_default_credits_limit( + raw_credits["reservations"], raw_credits["credits"] + ) self.log.warning( "The Runtime will be allowed to consumed half of your remaining credits: {:.2f} credit.".format( self.credits_limit diff --git a/datalayer_core/runtimes/exec/__init__.py b/datalayer_core/runtimes/exec/__init__.py index 815eacd6..f7d0007b 100644 --- a/datalayer_core/runtimes/exec/__init__.py +++ b/datalayer_core/runtimes/exec/__init__.py @@ -1,3 +1,2 @@ # Copyright (c) 2023-2025 Datalayer, Inc. # Distributed under the terms of the Modified BSD License. - diff --git a/datalayer_core/runtimes/list/__init__.py b/datalayer_core/runtimes/list/__init__.py index 815eacd6..f7d0007b 100644 --- a/datalayer_core/runtimes/list/__init__.py +++ b/datalayer_core/runtimes/list/__init__.py @@ -1,3 +1,2 @@ # Copyright (c) 2023-2025 Datalayer, Inc. # Distributed under the terms of the Modified BSD License. - diff --git a/datalayer_core/runtimes/list/listapp.py b/datalayer_core/runtimes/list/listapp.py index 9ff75c9c..3e0e9fc8 100644 --- a/datalayer_core/runtimes/list/listapp.py +++ b/datalayer_core/runtimes/list/listapp.py @@ -8,7 +8,6 @@ class RuntimesListMixin: - def _list_runtimes(self): """List all available runtimes.""" response = self._fetch( diff --git a/datalayer_core/runtimes/manager.py b/datalayer_core/runtimes/manager.py index 0fda64b0..10032615 100644 --- a/datalayer_core/runtimes/manager.py +++ b/datalayer_core/runtimes/manager.py @@ -81,7 +81,7 @@ def start_kernel( token=self.run_token, ) kernels = response.json().get("kernels", []) - + # If no runtime is running, let the user decide to start one from the first environment if not kernels: response_environments = fetch( @@ -98,11 +98,16 @@ def start_kernel( token=self.run_token, ) raw_credits = response_credits.json() - credits_limit = get_default_credits_limit(raw_credits["reservations"], raw_credits["credits"]) + credits_limit = get_default_credits_limit( + raw_credits["reservations"], raw_credits["credits"] + ) - user_input = input( - f"No Runtime running.\nDo you want to launch a runtime from the environment {first_environment_name} with {credits_limit:.2f} reserved credits? (yes/no) [default: yes]: " - ) or "yes" + user_input = ( + input( + f"No Runtime running.\nDo you want to launch a runtime from the environment {first_environment_name} with {credits_limit:.2f} reserved credits? (yes/no) [default: yes]: " + ) + or "yes" + ) if user_input.lower() != "yes": raise RuntimeError( "No Runtime running. Please start one Runtime using `datalayer runtimes create `." @@ -130,7 +135,7 @@ def start_kernel( token=self.run_token, ) kernels = response.json().get("kernels", []) - + kernel = kernels[0] kernel_name = kernel.get("jupyter_pod_name", "") diff --git a/datalayer_core/runtimes/pause/__init__.py b/datalayer_core/runtimes/pause/__init__.py index 815eacd6..f7d0007b 100644 --- a/datalayer_core/runtimes/pause/__init__.py +++ b/datalayer_core/runtimes/pause/__init__.py @@ -1,3 +1,2 @@ # Copyright (c) 2023-2025 Datalayer, Inc. # Distributed under the terms of the Modified BSD License. - diff --git a/datalayer_core/runtimes/pause/pauseapp.py b/datalayer_core/runtimes/pause/pauseapp.py index a0d11800..036b10ca 100644 --- a/datalayer_core/runtimes/pause/pauseapp.py +++ b/datalayer_core/runtimes/pause/pauseapp.py @@ -16,7 +16,7 @@ class RuntimesPauseApp(DatalayerCLIBaseApp): def start(self): try: super().start() - self.log.info(f"Runtime Pause - not implemented.") + self.log.info("Runtime Pause - not implemented.") except NoStart: pass self.exit(0) diff --git a/datalayer_core/runtimes/runtimesapp.py b/datalayer_core/runtimes/runtimesapp.py index 3cb53b48..b5bb409a 100644 --- a/datalayer_core/runtimes/runtimesapp.py +++ b/datalayer_core/runtimes/runtimesapp.py @@ -15,8 +15,6 @@ from datalayer_core.cli.base import DatalayerCLIBaseApp -from datalayer_core.__version__ import __version__ - class JupyterRuntimesApp(DatalayerCLIBaseApp): description = """ @@ -34,14 +32,19 @@ class JupyterRuntimesApp(DatalayerCLIBaseApp): "pause": (RuntimesPauseApp, RuntimesPauseApp.description.splitlines()[0]), "start": (RuntimesStartApp, RuntimesStartApp.description.splitlines()[0]), "stop": (RuntimesStopApp, RuntimesStopApp.description.splitlines()[0]), - "terminate": (RuntimesTerminateApp, RuntimesTerminateApp.description.splitlines()[0]), + "terminate": ( + RuntimesTerminateApp, + RuntimesTerminateApp.description.splitlines()[0], + ), "web": (RuntimesWebApp, RuntimesWebApp.description.splitlines()[0]), } def start(self): try: super().start() - self.log.info(f"One of `{'` `'.join(JupyterRuntimesApp.subcommands.keys())}` must be specified.") + self.log.info( + f"One of `{'` `'.join(JupyterRuntimesApp.subcommands.keys())}` must be specified." + ) self.exit(1) except NoStart: pass diff --git a/datalayer_core/runtimes/start/__init__.py b/datalayer_core/runtimes/start/__init__.py index 815eacd6..f7d0007b 100644 --- a/datalayer_core/runtimes/start/__init__.py +++ b/datalayer_core/runtimes/start/__init__.py @@ -1,3 +1,2 @@ # Copyright (c) 2023-2025 Datalayer, Inc. # Distributed under the terms of the Modified BSD License. - diff --git a/datalayer_core/runtimes/start/startapp.py b/datalayer_core/runtimes/start/startapp.py index 0c0f89a2..56ebfde4 100644 --- a/datalayer_core/runtimes/start/startapp.py +++ b/datalayer_core/runtimes/start/startapp.py @@ -16,7 +16,7 @@ class RuntimesStartApp(DatalayerCLIBaseApp): def start(self): try: super().start() - self.log.info(f"Runtime Start - not implemented.") + self.log.info("Runtime Start - not implemented.") except NoStart: pass self.exit(0) diff --git a/datalayer_core/runtimes/stop/__init__.py b/datalayer_core/runtimes/stop/__init__.py index 815eacd6..f7d0007b 100644 --- a/datalayer_core/runtimes/stop/__init__.py +++ b/datalayer_core/runtimes/stop/__init__.py @@ -1,3 +1,2 @@ # Copyright (c) 2023-2025 Datalayer, Inc. # Distributed under the terms of the Modified BSD License. - diff --git a/datalayer_core/runtimes/stop/stopapp.py b/datalayer_core/runtimes/stop/stopapp.py index 298f50df..9d08b5c7 100644 --- a/datalayer_core/runtimes/stop/stopapp.py +++ b/datalayer_core/runtimes/stop/stopapp.py @@ -16,7 +16,7 @@ class RuntimesStopApp(DatalayerCLIBaseApp): def start(self): try: super().start() - self.log.info(f"Runtime Stop - not implemented.") + self.log.info("Runtime Stop - not implemented.") except NoStart: pass self.exit(0) diff --git a/datalayer_core/runtimes/terminate/__init__.py b/datalayer_core/runtimes/terminate/__init__.py index 815eacd6..f7d0007b 100644 --- a/datalayer_core/runtimes/terminate/__init__.py +++ b/datalayer_core/runtimes/terminate/__init__.py @@ -1,3 +1,2 @@ # Copyright (c) 2023-2025 Datalayer, Inc. # Distributed under the terms of the Modified BSD License. - diff --git a/datalayer_core/runtimes/terminate/terminateapp.py b/datalayer_core/runtimes/terminate/terminateapp.py index d9b05192..9b66c876 100644 --- a/datalayer_core/runtimes/terminate/terminateapp.py +++ b/datalayer_core/runtimes/terminate/terminateapp.py @@ -6,8 +6,7 @@ from datalayer_core.cli.base import DatalayerCLIBaseApp -class RuntimesTerminateMixin: - +class RuntimesTerminateMixin: def _terminate_runtime(self, pod_name: str): """Terminate a Runtime with the given kernel ID.""" response = self._fetch( diff --git a/datalayer_core/runtimes/utils.py b/datalayer_core/runtimes/utils.py index a80098f0..6b58562d 100644 --- a/datalayer_core/runtimes/utils.py +++ b/datalayer_core/runtimes/utils.py @@ -49,4 +49,4 @@ def get_default_credits_limit(reservations: list[dict], credits: dict) -> float: else credits["quota"] - credits["credits"] ) available -= sum(r["credits"] for r in reservations) - return max(0.0, available * 0.5) \ No newline at end of file + return max(0.0, available * 0.5) diff --git a/datalayer_core/runtimes/web/__init__.py b/datalayer_core/runtimes/web/__init__.py index 815eacd6..f7d0007b 100644 --- a/datalayer_core/runtimes/web/__init__.py +++ b/datalayer_core/runtimes/web/__init__.py @@ -1,3 +1,2 @@ # Copyright (c) 2023-2025 Datalayer, Inc. # Distributed under the terms of the Modified BSD License. - diff --git a/datalayer_core/runtimes/web/webapp.py b/datalayer_core/runtimes/web/webapp.py index 581cbe31..f0463ec1 100644 --- a/datalayer_core/runtimes/web/webapp.py +++ b/datalayer_core/runtimes/web/webapp.py @@ -24,9 +24,9 @@ def start(self): self.exit(1) self.clear_instance() sys.argv = [ - '', - '--ServerApp.disable_check_xsrf=True', - '--DatalayerExtensionApp.kernels=True', - f'--DatalayerExtensionApp.run_url={self.run_url}', + "", + "--ServerApp.disable_check_xsrf=True", + "--DatalayerExtensionApp.kernels=True", + f"--DatalayerExtensionApp.run_url={self.run_url}", ] launch_new_instance() diff --git a/datalayer_core/sdk/__init__.py b/datalayer_core/sdk/__init__.py index 45cd1a3c..d739b06b 100644 --- a/datalayer_core/sdk/__init__.py +++ b/datalayer_core/sdk/__init__.py @@ -2,3 +2,8 @@ # Distributed under the terms of the Modified BSD License. from datalayer_core.sdk.datalayer import DatalayerClient + + +__all__ = [ + "DatalayerClient", +] diff --git a/datalayer_core/sdk/datalayer.py b/datalayer_core/sdk/datalayer.py index df6971c1..98b5c22c 100644 --- a/datalayer_core/sdk/datalayer.py +++ b/datalayer_core/sdk/datalayer.py @@ -5,6 +5,7 @@ Datalayer AI SDK - A simple SDK for AI engineers to work with Datalayer. Provides authentication, runtime creation, and code execution capabilities. """ + import os from typing import Any, Optional @@ -42,7 +43,9 @@ def __init__( token: Authentication token (can also be set via DATALAYER_TOKEN env var). """ - self.run_url = run_url.rstrip("/") or os.environ.get("DATALAYER_RUN_URL", DEFAULT_RUN_URL) + self.run_url = run_url.rstrip("/") or os.environ.get( + "DATALAYER_RUN_URL", DEFAULT_RUN_URL + ) self.token = token or os.environ.get("DATALAYER_TOKEN", None) self.user_handle = None self._kernel_client = None @@ -97,7 +100,11 @@ def create_runtime( name = f"runtime-{environment}-{os.getpid()}" runtime = Runtime( - name, environment=environment, timeout=timeout, run_url=self.run_url, token=self.token + name, + environment=environment, + timeout=timeout, + run_url=self.run_url, + token=self.token, ) return runtime @@ -155,12 +162,12 @@ def start(self) -> None: if self._runtime is None and self._kernel_client is None: self._runtime = self._create_runtime(self.environment_name) runtime = self._runtime.get("runtime") - url = runtime.get('ingress') - token = runtime.get('token') + url = runtime.get("ingress") + token = runtime.get("token") self._kernel_client = KernelClient(server_url=url, token=token) - self._kernel_client_info = runtime.get('kernel') + self._kernel_client_info = runtime.get("kernel") self._kernel_client.start() - self._pod_name = runtime.get('pod_name') + self._pod_name = runtime.get("pod_name") def stop(self) -> None: """Stop the runtime.""" diff --git a/datalayer_core/serverapplication.py b/datalayer_core/serverapplication.py index 13279214..933f09d5 100644 --- a/datalayer_core/serverapplication.py +++ b/datalayer_core/serverapplication.py @@ -4,7 +4,6 @@ """The Datalayer Core Server application.""" import os -import typing as t from traitlets import default, Bool, CInt, Instance, Unicode from traitlets.config import Configurable @@ -37,7 +36,7 @@ class DatalayerExtensionApp(ExtensionAppJinjaMixin, ExtensionApp): load_other_extensions = True static_paths = [DEFAULT_STATIC_FILES_PATH] - + template_paths = [DEFAULT_TEMPLATE_FILES_PATH] # 'run_url' can be set set and None or ' ' (empty string). @@ -57,7 +56,6 @@ class DatalayerExtensionApp(ExtensionAppJinjaMixin, ExtensionApp): webapp = Bool(False, config=True, help="""Show the webapp page.""") - class Launcher(Configurable): """Datalayer launcher configuration""" @@ -92,7 +90,6 @@ class Launcher(Configurable): def _default_launcher(self): return DatalayerExtensionApp.Launcher(parent=self, config=self.config) - class Brand(Configurable): """Datalayer launcher configuration""" @@ -162,7 +159,6 @@ class Brand(Configurable): def _default_brand(self): return DatalayerExtensionApp.Brand(parent=self, config=self.config) - def initialize_settings(self): self.serverapp.answer_yes = True if self.benchmarks: @@ -195,13 +191,13 @@ def initialize_settings(self): ) self.settings.update(**settings) - def initialize_templates(self): - self.serverapp.jinja_template_vars.update({ - "datalayer_version": __version__, - "run_url": self.run_url, - }) - + self.serverapp.jinja_template_vars.update( + { + "datalayer_version": __version__, + "run_url": self.run_url, + } + ) def initialize_handlers(self): handlers = [ @@ -211,7 +207,10 @@ def initialize_handlers(self): (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), + ( + url_path_join(self.name, "service-worker", r"([^/]+\.js)"), + ServiceWorkerHandler, + ), ] self.handlers.extend(handlers) diff --git a/datalayer_core/templates/index.html b/datalayer_core/templates/index.html index 3e092640..d075696e 100644 --- a/datalayer_core/templates/index.html +++ b/datalayer_core/templates/index.html @@ -6,7 +6,7 @@ - + Ξ Datalayer - - + +
diff --git a/datalayer_core/tests/test_cli.py b/datalayer_core/tests/test_cli.py index d844ccc6..280b869a 100644 --- a/datalayer_core/tests/test_cli.py +++ b/datalayer_core/tests/test_cli.py @@ -11,26 +11,34 @@ load_dotenv() -DATALAYER_TEST_TOKEN = os.environ['DATALAYER_TEST_TOKEN'] - - -@pytest.mark.parametrize("args,expected_output", [ - (["--version"], "1.0"), - (["--help"], "The Datalayer CLI application"), - (["about"], "About"), - (["login", '--token', DATALAYER_TEST_TOKEN], "Connected as urn:dla:iam:ext::github:226720"), - (["envs", 'list', '--token', DATALAYER_TEST_TOKEN], "Environments"), - (["envs", 'ls', '--token', DATALAYER_TEST_TOKEN], "Environments"), - (["runtimes", 'list', '--token', DATALAYER_TEST_TOKEN], "Runtimes"), - (["runtimes", 'ls', '--token', DATALAYER_TEST_TOKEN], "Runtimes"), - (["who", '--token', DATALAYER_TEST_TOKEN], "Profile"), - (["whoami", '--token', DATALAYER_TEST_TOKEN], "Profile"), - (["logout"], "\nDatalayer - Version"), -]) +DATALAYER_TEST_TOKEN = os.environ["DATALAYER_TEST_TOKEN"] + + +@pytest.mark.parametrize( + "args,expected_output", + [ + (["--version"], "1.0"), + (["--help"], "The Datalayer CLI application"), + (["about"], "About"), + ( + ["login", "--token", DATALAYER_TEST_TOKEN], + "Connected as urn:dla:iam:ext::github:226720", + ), + (["envs", "list", "--token", DATALAYER_TEST_TOKEN], "Environments"), + (["envs", "ls", "--token", DATALAYER_TEST_TOKEN], "Environments"), + (["runtimes", "list", "--token", DATALAYER_TEST_TOKEN], "Runtimes"), + (["runtimes", "ls", "--token", DATALAYER_TEST_TOKEN], "Runtimes"), + (["who", "--token", DATALAYER_TEST_TOKEN], "Profile"), + (["whoami", "--token", DATALAYER_TEST_TOKEN], "Profile"), + (["logout"], "\nDatalayer - Version"), + ], +) def test_cli(args, expected_output): """ Test the Datalayer CLI application. """ - result = run(['datalayer'] + args, capture_output=True, text=True) + result = run(["datalayer"] + args, capture_output=True, text=True) + print(result.stdout) + print(result.stderr) assert result.returncode == 0 assert expected_output in result.stdout diff --git a/datalayer_core/troubleshoot.py b/datalayer_core/troubleshoot.py index 67826804..b4f88b24 100755 --- a/datalayer_core/troubleshoot.py +++ b/datalayer_core/troubleshoot.py @@ -70,17 +70,17 @@ def main() -> None: # noqa print(f"\t{directory}") print("\nsys.executable:") - print(f'\t{environment_data["sys_exe"]}') + print(f"\t{environment_data['sys_exe']}") print("\nsys.version:") if "\n" in environment_data["sys_version"]: for data in environment_data["sys_version"].split("\n"): print(f"\t{data}") else: - print(f'\t{environment_data["sys_version"]}') + print(f"\t{environment_data['sys_version']}") print("\nplatform.platform():") - print(f'\t{environment_data["platform"]}') + print(f"\t{environment_data['platform']}") if environment_data["which"]: print("\nwhich -a datalayer:") diff --git a/datalayer_core/utils/__init__.py b/datalayer_core/utils/__init__.py index 9fa77020..f98e7fa2 100644 --- a/datalayer_core/utils/__init__.py +++ b/datalayer_core/utils/__init__.py @@ -64,7 +64,9 @@ def _external_stacklevel(internal: List[str]) -> int: normalized_internal = [str(Path(s)) for s in internal] # climb the stack frames while we see internal frames - while frame and any(s in str(Path(frame.f_code.co_filename)) for s in normalized_internal): + while frame and any( + s in str(Path(frame.f_code.co_filename)) for s in normalized_internal + ): level += 1 frame = frame.f_back @@ -119,7 +121,9 @@ def run(self, coro): name = f"{threading.current_thread().name} - runner" if self.__io_loop is None: self.__io_loop = asyncio.new_event_loop() - self.__runner_thread = threading.Thread(target=self._runner, daemon=True, name=name) + self.__runner_thread = threading.Thread( + target=self._runner, daemon=True, name=name + ) self.__runner_thread.start() fut = asyncio.run_coroutine_threadsafe(coro, self.__io_loop) return fut.result(None) diff --git a/datalayer_core/web/__init__.py b/datalayer_core/web/__init__.py index 815eacd6..f7d0007b 100644 --- a/datalayer_core/web/__init__.py +++ b/datalayer_core/web/__init__.py @@ -1,3 +1,2 @@ # Copyright (c) 2023-2025 Datalayer, Inc. # Distributed under the terms of the Modified BSD License. - diff --git a/datalayer_core/web/webapp.py b/datalayer_core/web/webapp.py index 630030dc..c2f831f5 100644 --- a/datalayer_core/web/webapp.py +++ b/datalayer_core/web/webapp.py @@ -24,9 +24,9 @@ def start(self): self.exit(1) self.clear_instance() sys.argv = [ - '', - '--ServerApp.disable_check_xsrf=True', - '--DatalayerExtensionApp.webapp=True', - f'--DatalayerExtensionApp.run_url={self.run_url}', + "", + "--ServerApp.disable_check_xsrf=True", + "--DatalayerExtensionApp.webapp=True", + f"--DatalayerExtensionApp.run_url={self.run_url}", ] launch_new_instance() diff --git a/eslint.config.js b/eslint.config.js index 1eaa69c7..3ec821f3 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -6,26 +6,29 @@ // For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format import storybook from "eslint-plugin-storybook"; -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import tseslint from 'typescript-eslint' -import { globalIgnores } from 'eslint/config' +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "typescript-eslint"; +import { globalIgnores } from "eslint/config"; -export default tseslint.config([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - js.configs.recommended, - tseslint.configs.recommended, - reactHooks.configs['recommended-latest'], - reactRefresh.configs.vite, - ], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, +export default tseslint.config( + [ + globalIgnores(["dist"]), + { + files: ["**/*.{ts,tsx}"], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs["recommended-latest"], + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, }, - }, -], storybook.configs["flat/recommended"]); + ], + storybook.configs["flat/recommended"] +); diff --git a/index.html b/index.html index 6954247c..556d6b13 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,7 @@ ~ Distributed under the terms of the Modified BSD License. --> - + diff --git a/public/vite.svg b/public/vite.svg index e7b8dfb1..ee9fadaf 100644 --- a/public/vite.svg +++ b/public/vite.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/pydoc-markdown.yml b/pydoc-markdown.yml index b0eea9db..411044ba 100644 --- a/pydoc-markdown.yml +++ b/pydoc-markdown.yml @@ -1,6 +1,6 @@ loaders: - type: python - search_path: [ datalayer_core ] + search_path: [datalayer_core] processors: - type: filter skip_empty_modules: false diff --git a/setup.py b/setup.py index fcc4952e..68074943 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ # Copyright (c) 2023-2025 Datalayer, Inc. # Distributed under the terms of the Modified BSD License. -__import__('setuptools').setup() +__import__("setuptools").setup() diff --git a/typedoc.json b/typedoc.json index 57278b5e..9273848c 100644 --- a/typedoc.json +++ b/typedoc.json @@ -1,7 +1,6 @@ { - "$schema": "https://typedoc-plugin-markdown.org/schema.json", - "entryPoints": ["./src/index.ts"], - "name": "Datalayer Core", - "plugin": ["typedoc-plugin-markdown"] - } - \ No newline at end of file + "$schema": "https://typedoc-plugin-markdown.org/schema.json", + "entryPoints": ["./src/index.ts"], + "name": "Datalayer Core", + "plugin": ["typedoc-plugin-markdown"] +} diff --git a/vite.config.ts b/vite.config.ts index 411720ff..9aaad227 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -4,39 +4,47 @@ */ /// -import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react'; +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; // https://vite.dev/config/ -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; -const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { storybookTest } from "@storybook/addon-vitest/vitest-plugin"; +const dirname = + typeof __dirname !== "undefined" + ? __dirname + : path.dirname(fileURLToPath(import.meta.url)); // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon export default defineConfig({ plugins: [react()], test: { - projects: [{ - extends: true, - plugins: [ - // The plugin will run tests for the stories defined in your Storybook config - // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest - storybookTest({ - configDir: path.join(dirname, '.storybook') - })], - test: { - name: 'storybook', - browser: { - enabled: true, - headless: true, - provider: 'playwright', - instances: [{ - browser: 'chromium' - }] + projects: [ + { + extends: true, + plugins: [ + // The plugin will run tests for the stories defined in your Storybook config + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + storybookTest({ + configDir: path.join(dirname, ".storybook"), + }), + ], + test: { + name: "storybook", + browser: { + enabled: true, + headless: true, + provider: "playwright", + instances: [ + { + browser: "chromium", + }, + ], + }, + setupFiles: [".storybook/vitest.setup.ts"], }, - setupFiles: ['.storybook/vitest.setup.ts'] - } - }] - } -}); \ No newline at end of file + }, + ], + }, +}); diff --git a/vitest.shims.d.ts b/vitest.shims.d.ts index 7c26c904..18c593e2 100644 --- a/vitest.shims.d.ts +++ b/vitest.shims.d.ts @@ -3,4 +3,4 @@ * Distributed under the terms of the Modified BSD License. */ -/// \ No newline at end of file +///