Repository: OKUA1/juvio Branch: main Commit: adb74f5cf9f7 Files: 43 Total size: 75.3 KB Directory structure: gitextract_jjcr1hib/ ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── juvio/ │ ├── __init__.py │ ├── content_manager.py │ ├── converter.py │ ├── kernel_launcher.py │ ├── kernelspec/ │ │ └── kernel.json │ ├── labextension/ │ │ ├── package.json │ │ └── static/ │ │ ├── 509.8b21d569ebbb8f039bdd.js │ │ ├── 728.864a8d7732d3e14d284a.js │ │ ├── remoteEntry.cb75fcc929fa00484f36.js │ │ ├── style.js │ │ └── third-party-licenses.json │ ├── server_extension.py │ └── shared.py ├── juvio_frontend/ │ ├── .gitignore │ ├── .prettierignore │ ├── .yarnrc.yml │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── RELEASE.md │ ├── babel.config.js │ ├── install.json │ ├── jest.config.js │ ├── package.json │ ├── pyproject.toml │ ├── setup.py │ ├── src/ │ │ ├── __tests__/ │ │ │ └── juvio_frontend.spec.ts │ │ └── index.ts │ ├── style/ │ │ ├── base.css │ │ ├── index.css │ │ └── index.js │ ├── tsconfig.json │ ├── tsconfig.test.json │ └── ui-tests/ │ ├── README.md │ ├── jupyter_server_test_config.py │ ├── package.json │ ├── playwright.config.js │ └── tests/ │ └── juvio_frontend.spec.ts └── pyproject.toml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # UV # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. #uv.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control #poetry.lock # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. #pdm.lock # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it # in version control. # https://pdm.fming.dev/latest/usage/project/#working-with-version-control .pdm.toml .pdm-python .pdm-build/ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # PyCharm # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ # Ruff stuff: .ruff_cache/ # PyPI configuration file .pypirc ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2025 Oleh Kostromin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: MANIFEST.in ================================================ include LICENSE include README.md include pyproject.toml # Include all files from the kernelspec and labextension directories # Use the correct relative paths recursive-include juvio/kernelspec * recursive-include juvio/labextension * # Exclude unnecessary files global-exclude *.py[cod] global-exclude __pycache__ global-exclude *.so global-exclude .git* global-exclude .ipynb_checkpoints global-exclude node_modules ================================================ FILE: README.md ================================================
logo
# **Juvio**: reproducible, dependency-aware, and Git-friendly Jupyter Notebooks. ## 🚀 What It Does - 💡 **Inline Dependency Management** Install packages right from the notebook: ```python %juvio install numpy pandas ``` Dependencies are saved directly in the notebook as metadata (PEP 723-style), like: ```python # /// script # requires-python = "==3.10.17" # dependencies = [ # "numpy==2.2.5", # "pandas==2.2.3" # ] # /// ``` - ⚙️ **Automatic Environment Setup** When the notebook is opened, Juvio installs the dependencies automatically in an ephemeral virtual environment (using `uv`), ensuring that the notebook runs with the correct versions of the packages and Python. - 📁 **Git-Friendly Format** Notebooks are converted on the fly to a script-style format using `# %%` markers, making diffs and version control painless: ```python # %% %juvio install numpy # %% import numpy as np # %% arr = np.array([1, 2, 3]) print(arr) # %% ``` ## 🧑‍💻 How to Use > [!WARNING] > This project is currently in **early beta**. It may contain bugs and is subject to change. > Please [open an issue](../../issues) to report problems or suggest improvements. **1. Install Juvio:** ```bash pip install juvio jupyter labextension enable juvio-frontend ``` **2. Make sure you have uv installed:** https://docs.astral.sh/uv/getting-started/installation/ **3. Start JupyterLab and create a Juvio Notebook.** **4. Install necessary packages in the notebook and run your code** ```python %juvio install ... ``` Dependencies are tracked, environments are reproducible, and your notebook stays Git-clean ✨ **Known issue:** If you experience the error "Notebook does not appear to be JSON", try to lauch the jupyterlab with an additional argument: ```bash jupyter lab --ServerApp.jpserver_extensions="{'juvio': True}" ``` ## Why Use Juvio? - No additional lock or requirements files are needed - Guaranteed reproducibility - Cleaner Git diffs ## Powered By - `uv` – ultra-fast Python package management - `PEP 723` – Python inline dependency standards - `jupytext`-like format for easy version control ================================================ FILE: juvio/__init__.py ================================================ from .server_extension import _load_jupyter_server_extension def _jupyter_server_extension_points(): return [{"module": "juvio.server_extension"}] # def _jupyter_labextension_paths(): # return [ # { # "src": "labextension", # "dest": "juvio", # } # ] ================================================ FILE: juvio/content_manager.py ================================================ import nbformat import inspect import aiofiles from juvio.converter import JuvioConverter from juvio.shared import FileLock, AsyncFileLock import os def _is_juvio_file(path): return path.endswith(".juvio") def juvio_get_sync(self, path, content=True, type=None, format=None, **kwargs): api_path = path.replace("\\", "/").lstrip("/") full_path = self._get_os_path(api_path) if not _is_juvio_file(api_path): return self._original_get( api_path, content=content, type=type, format=format, **kwargs ) model = self._original_get(api_path, content=False, **kwargs) if not content: model["type"] = "notebook" return model try: with FileLock(full_path): with open(full_path, "r", encoding="utf-8") as f: text = f.read() nb = JuvioConverter.convert_script_to_notebook(text) nbformat.validate(nb) model.update(type="notebook", format="json", content=nb) return model except Exception as e: from tornado.web import HTTPError raise HTTPError(500, f"Error while reading {api_path}: {e}") def juvio_save_sync(self, model, path, **kwargs): from datetime import datetime def format_timestamp(timestamp): return datetime.utcfromtimestamp(timestamp).strftime("%Y-%m-%dT%H:%M:%S.%fZ") model_type = model.get("type") model_content = model.get("content") api_path = path.replace("\\", "/").lstrip("/") full_path = self._get_os_path(api_path) os.makedirs(os.path.dirname(full_path), exist_ok=True) if not _is_juvio_file(api_path) or model_type != "notebook": return self._original_save(model, api_path, **kwargs) try: with FileLock(full_path): pass # ensures lock before opening if model_content is not None: if isinstance(model_content, dict): nb = nbformat.from_dict(model_content) else: nb = model_content nbformat.validate(nb) with open(full_path, "r", encoding="utf-8") as f: existing = f.read() metadata = JuvioConverter._generate_metadata( **JuvioConverter.extract_metadata(existing) ) text = JuvioConverter.convert_notebook_to_script(nb, metadata) with open(full_path, "w", encoding="utf-8") as f: f.write(text) else: with open(full_path, "w", encoding="utf-8") as f: f.write(JuvioConverter.create()) stat = os.stat(full_path) return { "name": os.path.basename(api_path), "path": api_path, "type": "notebook", "format": None, "content": None, "created": format_timestamp(stat.st_ctime), "last_modified": format_timestamp(stat.st_mtime), "writable": True, "mimetype": None, } except Exception as e: from tornado.web import HTTPError raise HTTPError(500, f"Error while saving {api_path}: {e}") async def juvio_get_async(self, path, content=True, type=None, format=None, **kwargs): api_path = path.replace("\\", "/").lstrip("/") full_path = self._get_os_path(api_path) if not _is_juvio_file(api_path): return await self._original_get( api_path, content=content, type=type, format=format, **kwargs ) model = await self._original_get(api_path, content=False, **kwargs) if not content: model["type"] = "notebook" return model try: async with AsyncFileLock(full_path): pass # ensures lock before opening async with aiofiles.open(full_path, "r", encoding="utf-8") as f: text = await f.read() nb = JuvioConverter.convert_script_to_notebook(text) nbformat.validate(nb) model.update(type="notebook", format="json", content=nb) return model except Exception as e: from tornado.web import HTTPError raise HTTPError(500, f"Error while reading {api_path}: {e}") async def juvio_save_async(self, model, path, **kwargs): from datetime import datetime def format_timestamp(timestamp): return datetime.utcfromtimestamp(timestamp).strftime("%Y-%m-%dT%H:%M:%S.%fZ") model_type = model.get("type") model_content = model.get("content") api_path = path.replace("\\", "/").lstrip("/") full_path = self._get_os_path(api_path) os.makedirs(os.path.dirname(full_path), exist_ok=True) if not _is_juvio_file(api_path) or model_type != "notebook": return await self._original_save(model, api_path, **kwargs) try: async with AsyncFileLock(full_path): pass # ensures lock before opening if model_content is not None: if isinstance(model_content, dict): nb = nbformat.from_dict(model_content) else: nb = model_content nbformat.validate(nb) async with aiofiles.open(full_path, "r", encoding="utf-8") as f: existing = await f.read() metadata = JuvioConverter._generate_metadata( **JuvioConverter.extract_metadata(existing) ) text = JuvioConverter.convert_notebook_to_script(nb, metadata) async with aiofiles.open(full_path, "w", encoding="utf-8") as f: await f.write(text) else: async with aiofiles.open(full_path, "w", encoding="utf-8") as f: await f.write(JuvioConverter.create()) stat = os.stat(full_path) return { "name": os.path.basename(api_path), "path": api_path, "type": "notebook", "format": None, "content": None, "created": format_timestamp(stat.st_ctime), "last_modified": format_timestamp(stat.st_mtime), "writable": True, "mimetype": None, } except Exception as e: from tornado.web import HTTPError raise HTTPError(500, f"Error while saving {api_path}: {e}") def juvio_rename_file(self, old_path, new_path): old_api_path = old_path.replace("\\", "/").lstrip("/") new_api_path = new_path.replace("\\", "/").lstrip("/") full_path = self._get_os_path(new_api_path) result = self._original_rename_file(old_api_path, new_api_path) if _is_juvio_file(new_api_path): with FileLock(full_path): if os.path.getsize(full_path) == 0: with open(full_path, "w", encoding="utf-8") as f: f.write(JuvioConverter.create()) return result async def rename_file(self, old_path, new_path): old_api_path = old_path.replace("\\", "/").lstrip("/") new_api_path = new_path.replace("\\", "/").lstrip("/") full_path = self._get_os_path(new_api_path) result = await self._original_rename_file(old_api_path, new_api_path) async with AsyncFileLock(full_path): if os.path.getsize(full_path) == 0: async with aiofiles.open(full_path, "w", encoding="utf-8") as f: await f.write(JuvioConverter.create()) return result def create_juvio_contents_manager_class(base_manager_class): is_async = inspect.iscoroutinefunction(base_manager_class.get) if is_async: class AsyncJuvioContentsManager(base_manager_class): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._original_get = super().get self._original_save = super().save self._original_rename_file = super().rename_file async def get(self, path, content=True, type=None, format=None, **kwargs): return await juvio_get_async( self, path, content=content, type=type, format=format, **kwargs ) async def save(self, model, path, **kwargs): return await juvio_save_async(self, model, path, **kwargs) async def rename_file(self, old_path, new_path): return await rename_file(self, old_path, new_path) return AsyncJuvioContentsManager else: class JuvioContentsManager(base_manager_class): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._original_get = super().get self._original_save = super().save self._original_rename_file = super().rename_file def get(self, path, content=True, type=None, format=None, **kwargs): return juvio_get_sync( self, path, content=content, type=type, format=format, **kwargs ) def save(self, model, path, **kwargs): return juvio_save_sync(self, model, path, **kwargs) def rename_file(self, old_path, new_path): return juvio_rename_file(self, old_path, new_path) return JuvioContentsManager ================================================ FILE: juvio/converter.py ================================================ import sys import nbformat if sys.version_info >= (3, 11): import tomllib else: raise ImportError("Python 3.11+ is required for tomllib support.") class JuvioConverter: @staticmethod def _parse_metadata(metadata_lines): toml_text = "\n".join(metadata_lines) data = tomllib.loads(toml_text) if "requires-python" in data: version = data["requires-python"] # Clean up operators like >=, ~=, etc. for prefix in (">=", "~=", "==", ">", "<=", "<"): if version.startswith(prefix): version = version[len(prefix) :].strip() break data["python_version"] = version data.pop("requires-python", None) return data @staticmethod def _generate_metadata(python_version, dependencies): lines = [] lines.append(f'requires-python = "=={python_version}"') if dependencies: lines.append("dependencies = [") for dep in dependencies: lines.append(f' "{dep}",') lines.append("]") else: lines.append("dependencies = []") return lines @staticmethod def convert_script_to_notebook(text: str): nb = nbformat.v4.new_notebook() nb.metadata.kernelspec = { "name": "juvio", "language": "python", "display_name": "Juvio", } nb.metadata.language_info = { "name": "python", "version": "3.10", "mimetype": "text/x-python", "codemirror_mode": {"name": "ipython", "version": 3}, "pygments_lexer": "ipython3", "nbconvert_exporter": "python", "file_extension": ".py", } lines = text.splitlines() metadata_lines = [] body_lines = [] in_metadata = False for line in lines: if line.startswith("# ///"): in_metadata = not in_metadata elif in_metadata: if line.strip().startswith("#"): metadata_lines.append(line.strip("# ").rstrip()) else: body_lines.append(line) lines = body_lines cells = [] cell_source = [] cell_type = "code" for line in lines: if line.startswith("# %%"): if cell_source: if cell_type == "markdown": cleaned = [ l[2:] if l.startswith("# ") else l.lstrip("#") for l in cell_source ] cells.append(nbformat.v4.new_markdown_cell("\n".join(cleaned))) else: cells.append(nbformat.v4.new_code_cell("\n".join(cell_source))) cell_source = [] if "markdown" in line.lower(): cell_type = "markdown" else: cell_type = "code" else: cell_source.append(line) if cell_source: if cell_type == "markdown": cleaned = [ l[2:] if l.startswith("# ") else l.lstrip("#") for l in cell_source ] cells.append(nbformat.v4.new_markdown_cell("\n".join(cleaned))) else: cells.append(nbformat.v4.new_code_cell("\n".join(cell_source))) if not cells: cells.append(nbformat.v4.new_code_cell("")) nb.cells = cells return nb @staticmethod def convert_notebook_to_script(nb, dep_metadata: list[str] | None = None): dep_metadata = dep_metadata or [] lines = [] if dep_metadata: lines.append("# /// script") for line in dep_metadata: lines.append("# " + line) lines.append("# ///") for cell in nb.cells: if cell.cell_type == "code": lines.append("# %%") lines.append(cell.source.rstrip() if cell.source else "") elif cell.cell_type == "markdown": lines.append("# %% markdown") if cell.source: for line in cell.source.rstrip().splitlines(): lines.append( "# " + line if not line.startswith("#") else "# " + line ) else: lines.append("") return "\n".join(lines) + "\n" @staticmethod def create(python_version="3.10", dependencies=None): if dependencies is None: dependencies = [] lines = [] lines.append("# /// script") metadata_block = JuvioConverter._generate_metadata(python_version, dependencies) for line in metadata_block: lines.append("# " + line) lines.append("# ///") lines.append("# %%") lines.append("") return "\n".join(lines) + "\n" @staticmethod def extract_metadata(text: str): lines = text.splitlines() metadata_lines = [] in_metadata = False for line in lines: if line.startswith("# ///"): in_metadata = not in_metadata elif in_metadata: if line.strip().startswith("#"): metadata_lines.append(line.strip("# ").rstrip()) if metadata_lines: return JuvioConverter._parse_metadata(metadata_lines) else: return { "python_version": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}", "dependencies": [], } ================================================ FILE: juvio/kernel_launcher.py ================================================ import os import sys import platform from juvio.converter import JuvioConverter current_path = os.path.dirname(os.path.abspath(__file__)) shared_path = os.path.join(current_path, "shared.py") def cmd( python_version=None, deps=None, ): head = [ "uv", "run", "--no-project", "--isolated", "--exact", "--with", "ipykernel", ] head.append("--python") if python_version is not None: head.append(python_version) else: head.append(f"{sys.version_info.major}.{sys.version_info.minor}") if deps is not None and len(deps) > 0: for dep in deps: head.append("--with") head.append(f"{dep}") else: head.append("-n") head.extend(["--", "python", shared_path]) head += sys.argv[1:] return head def main(): notebook_path = os.environ.get("JPY_SESSION_NAME", None) if notebook_path is None: print("No notebook path found in environment variable JPY_SESSION_NAME.") sys.exit(1) with open(notebook_path, "r", encoding="utf-8") as f: text = f.read() metadata = JuvioConverter.extract_metadata(text) if platform.system() == "Windows": import subprocess proc = subprocess.Popen( cmd( python_version=metadata.get("python_version", None), deps=metadata.get("dependencies", []), ) ) proc.wait() sys.exit(proc.returncode) else: os.execvp( "uv", cmd( python_version=metadata.get("python_version", None), deps=metadata.get("dependencies", []), ), ) ================================================ FILE: juvio/kernelspec/kernel.json ================================================ { "argv": [ "juvio-kernel-launcher", "-f", "{connection_file}" ], "name": "juvio", "display_name": "Juvio", "language": "python" } ================================================ FILE: juvio/labextension/package.json ================================================ { "name": "juvio_frontend", "version": "0.1.0", "description": "Juvio jupyterlab extension.", "keywords": [ "jupyter", "jupyterlab", "jupyterlab-extension" ], "homepage": "-", "bugs": { "url": "-/issues" }, "license": "BSD-3-Clause", "author": { "name": "Oleh Kostromin", "email": "oleh@beastbyte.ai" }, "files": [ "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}" ], "main": "lib/index.js", "types": "lib/index.d.ts", "style": "style/index.css", "repository": { "type": "git", "url": "-.git" }, "workspaces": [ "ui-tests" ], "scripts": { "build": "jlpm build:lib && jlpm build:labextension:dev", "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension", "build:labextension": "jupyter labextension build .", "build:labextension:dev": "jupyter labextension build --development True .", "build:lib": "tsc --sourceMap", "build:lib:prod": "tsc", "clean": "jlpm clean:lib", "clean:lib": "rimraf lib tsconfig.tsbuildinfo", "clean:lintcache": "rimraf .eslintcache .stylelintcache", "clean:labextension": "rimraf juvio_frontend/labextension juvio_frontend/_version.py", "clean:all": "jlpm clean:lib && jlpm clean:labextension && jlpm clean:lintcache", "eslint": "jlpm eslint:check --fix", "eslint:check": "eslint . --cache --ext .ts,.tsx", "install:extension": "jlpm build", "lint": "jlpm stylelint && jlpm prettier && jlpm eslint", "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check", "prettier": "jlpm prettier:base --write --list-different", "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", "prettier:check": "jlpm prettier:base --check", "stylelint": "jlpm stylelint:check --fix", "stylelint:check": "stylelint --cache \"style/**/*.css\"", "test": "jest --coverage", "watch": "run-p watch:src watch:labextension", "watch:src": "tsc -w --sourceMap", "watch:labextension": "jupyter labextension watch ." }, "dependencies": { "@jupyterlab/application": "^4.0.0", "@jupyterlab/launcher": "^4.0.0" }, "devDependencies": { "@jupyterlab/builder": "^4.0.0", "@jupyterlab/testutils": "^4.0.0", "@types/jest": "^29.2.0", "@types/json-schema": "^7.0.11", "@types/react": "^18.0.26", "@typescript-eslint/eslint-plugin": "^5.55.0", "@typescript-eslint/parser": "^5.55.0", "css-loader": "^6.7.1", "eslint": "^8.36.0", "eslint-config-prettier": "^8.7.0", "eslint-plugin-prettier": "^4.2.1", "jest": "^29.2.0", "npm-run-all": "^4.1.5", "prettier": "^2.8.7", "rimraf": "^4.4.1", "source-map-loader": "^1.0.2", "style-loader": "^3.3.1", "stylelint": "^14.9.1", "stylelint-config-prettier": "^9.0.4", "stylelint-config-recommended": "^8.0.0", "stylelint-config-standard": "^26.0.0", "stylelint-prettier": "^2.0.0", "typescript": "^5.0", "yjs": "^13.5.0" }, "sideEffects": [ "style/*.css", "style/index.js" ], "styleModule": "style/index.js", "publishConfig": { "access": "public" }, "jupyterlab": { "extension": true, "outputDir": "build/labextension", "_build": { "load": "static/remoteEntry.cb75fcc929fa00484f36.js", "extension": "./extension", "style": "./style" } }, "eslintIgnore": [ "node_modules", "dist", "coverage", "**/*.d.ts", "tests", "**/__tests__", "ui-tests" ], "eslintConfig": { "extends": [ "eslint:recommended", "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended", "plugin:prettier/recommended" ], "parser": "@typescript-eslint/parser", "parserOptions": { "project": "tsconfig.json", "sourceType": "module" }, "plugins": [ "@typescript-eslint" ], "rules": { "@typescript-eslint/naming-convention": [ "error", { "selector": "interface", "format": [ "PascalCase" ], "custom": { "regex": "^I[A-Z]", "match": true } } ], "@typescript-eslint/no-unused-vars": [ "warn", { "args": "none" } ], "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-namespace": "off", "@typescript-eslint/no-use-before-define": "off", "@typescript-eslint/quotes": [ "error", "single", { "avoidEscape": true, "allowTemplateLiterals": false } ], "curly": [ "error", "all" ], "eqeqeq": "error", "prefer-arrow-callback": "error" } }, "prettier": { "singleQuote": true, "trailingComma": "none", "arrowParens": "avoid", "endOfLine": "auto" }, "stylelint": { "extends": [ "stylelint-config-recommended", "stylelint-config-standard", "stylelint-prettier/recommended" ], "rules": { "property-no-vendor-prefix": null, "selector-no-vendor-prefix": null, "value-no-vendor-prefix": null } } } ================================================ FILE: juvio/labextension/static/509.8b21d569ebbb8f039bdd.js ================================================ "use strict";(self.webpackChunkjuvio_frontend=self.webpackChunkjuvio_frontend||[]).push([[509],{509:(e,o,t)=>{t.r(o),t.d(o,{default:()=>n});const n={id:"juvio_frontend:plugin",description:"Juvio JupyterLab extension",autoStart:!0,requires:[t(625).ILauncher],activate:(e,o)=>{console.log("JupyterLab extension juvio_frontend is activated!"),e.docRegistry.addFileType({name:"juvio",displayName:"Juvio Notebook",extensions:[".juvio"],contentType:"notebook",fileFormat:"json"}),e.docRegistry.addFileType({name:"notebook",extensions:[".juvio"]});const{commands:t}=e;t.addCommand("create-juvio-file",{label:"Juvio Notebook",execute:()=>t.execute("docmanager:new-untitled",{type:"file",ext:".juvio",kernelName:"juvio"})}),o.add({category:"Notebook",rank:1,command:"create-juvio-file",kernelIconUrl:"https://iili.io/3WtCOjs.th.png"})}}}}]); ================================================ FILE: juvio/labextension/static/728.864a8d7732d3e14d284a.js ================================================ "use strict";(self.webpackChunkjuvio_frontend=self.webpackChunkjuvio_frontend||[]).push([[728],{56:(e,t,n)=>{e.exports=function(e){var t=n.nc;t&&e.setAttribute("nonce",t)}},72:e=>{var t=[];function n(e){for(var n=-1,r=0;r{e.exports=function(e,t){if(t.styleSheet)t.styleSheet.cssText=e;else{for(;t.firstChild;)t.removeChild(t.firstChild);t.appendChild(document.createTextNode(e))}}},314:e=>{e.exports=function(e){var t=[];return t.toString=function(){return this.map((function(t){var n="",r=void 0!==t[5];return t[4]&&(n+="@supports (".concat(t[4],") {")),t[2]&&(n+="@media ".concat(t[2]," {")),r&&(n+="@layer".concat(t[5].length>0?" ".concat(t[5]):""," {")),n+=e(t),r&&(n+="}"),t[2]&&(n+="}"),t[4]&&(n+="}"),n})).join("")},t.i=function(e,n,r,o,a){"string"==typeof e&&(e=[[null,e,void 0]]);var i={};if(r)for(var s=0;s0?" ".concat(l[5]):""," {").concat(l[1],"}")),l[5]=a),n&&(l[2]?(l[1]="@media ".concat(l[2]," {").concat(l[1],"}"),l[2]=n):l[2]=n),o&&(l[4]?(l[1]="@supports (".concat(l[4],") {").concat(l[1],"}"),l[4]=o):l[4]="".concat(o)),t.push(l))}},t}},475:(e,t,n)=>{n.d(t,{A:()=>s});var r=n(601),o=n.n(r),a=n(314),i=n.n(a)()(o());i.push([e.id,"/*\n See the JupyterLab Developer Guide for useful CSS Patterns:\n\n https://jupyterlab.readthedocs.io/en/stable/developer/css.html\n*/\n",""]);const s=i},540:e=>{e.exports=function(e){var t=document.createElement("style");return e.setAttributes(t,e.attributes),e.insert(t,e.options),t}},601:e=>{e.exports=function(e){return e[1]}},659:e=>{var t={};e.exports=function(e,n){var r=function(e){if(void 0===t[e]){var n=document.querySelector(e);if(window.HTMLIFrameElement&&n instanceof window.HTMLIFrameElement)try{n=n.contentDocument.head}catch(e){n=null}t[e]=n}return t[e]}(e);if(!r)throw new Error("Couldn't find a style target. This probably means that the value for the 'insert' parameter is invalid.");r.appendChild(n)}},728:(e,t,n)=>{var r=n(72),o=n.n(r),a=n(825),i=n.n(a),s=n(659),c=n.n(s),u=n(56),l=n.n(u),p=n(540),f=n.n(p),d=n(113),v=n.n(d),h=n(475),m={};m.styleTagTransform=v(),m.setAttributes=l(),m.insert=c().bind(null,"head"),m.domAPI=i(),m.insertStyleElement=f(),o()(h.A,m),h.A&&h.A.locals&&h.A.locals},825:e=>{e.exports=function(e){if("undefined"==typeof document)return{update:function(){},remove:function(){}};var t=e.insertStyleElement(e);return{update:function(n){!function(e,t,n){var r="";n.supports&&(r+="@supports (".concat(n.supports,") {")),n.media&&(r+="@media ".concat(n.media," {"));var o=void 0!==n.layer;o&&(r+="@layer".concat(n.layer.length>0?" ".concat(n.layer):""," {")),r+=n.css,o&&(r+="}"),n.media&&(r+="}"),n.supports&&(r+="}");var a=n.sourceMap;a&&"undefined"!=typeof btoa&&(r+="\n/*# sourceMappingURL=data:application/json;base64,".concat(btoa(unescape(encodeURIComponent(JSON.stringify(a))))," */")),t.styleTagTransform(r,e,t.options)}(t,e,n)},remove:function(){!function(e){if(null===e.parentNode)return!1;e.parentNode.removeChild(e)}(t)}}}}}]); ================================================ FILE: juvio/labextension/static/remoteEntry.cb75fcc929fa00484f36.js ================================================ var _JUPYTERLAB;(()=>{"use strict";var e,r,t,n,o,i,a,u,f,l,s,d,p,c,h,v,g,b,m,y={678:(e,r,t)=>{var n={"./index":()=>t.e(509).then((()=>()=>t(509))),"./extension":()=>t.e(509).then((()=>()=>t(509))),"./style":()=>t.e(728).then((()=>()=>t(728)))},o=(e,r)=>(t.R=r,r=t.o(n,e)?n[e]():Promise.resolve().then((()=>{throw new Error('Module "'+e+'" does not exist in container.')})),t.R=void 0,r),i=(e,r)=>{if(t.S){var n="default",o=t.S[n];if(o&&o!==e)throw new Error("Container initialization failed as it has already been initialized with a different share scope");return t.S[n]=e,t.I(n,r)}};t.d(r,{get:()=>o,init:()=>i})}},w={};function S(e){var r=w[e];if(void 0!==r)return r.exports;var t=w[e]={id:e,exports:{}};return y[e](t,t.exports,S),t.exports}S.m=y,S.c=w,S.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return S.d(r,{a:r}),r},S.d=(e,r)=>{for(var t in r)S.o(r,t)&&!S.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:r[t]})},S.f={},S.e=e=>Promise.all(Object.keys(S.f).reduce(((r,t)=>(S.f[t](e,r),r)),[])),S.u=e=>e+"."+{509:"8b21d569ebbb8f039bdd",728:"864a8d7732d3e14d284a"}[e]+".js?v="+{509:"8b21d569ebbb8f039bdd",728:"864a8d7732d3e14d284a"}[e],S.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),S.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),e={},r="juvio_frontend:",S.l=(t,n,o,i)=>{if(e[t])e[t].push(n);else{var a,u;if(void 0!==o)for(var f=document.getElementsByTagName("script"),l=0;l{a.onerror=a.onload=null,clearTimeout(p);var o=e[t];if(delete e[t],a.parentNode&&a.parentNode.removeChild(a),o&&o.forEach((e=>e(n))),r)return r(n)},p=setTimeout(d.bind(null,void 0,{type:"timeout",target:a}),12e4);a.onerror=d.bind(null,a.onerror),a.onload=d.bind(null,a.onload),u&&document.head.appendChild(a)}},S.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},(()=>{S.S={};var e={},r={};S.I=(t,n)=>{n||(n=[]);var o=r[t];if(o||(o=r[t]={}),!(n.indexOf(o)>=0)){if(n.push(o),e[t])return e[t];S.o(S.S,t)||(S.S[t]={});var i=S.S[t],a="juvio_frontend",u=[];return"default"===t&&((e,r,t,n)=>{var o=i[e]=i[e]||{},u=o[r];(!u||!u.loaded&&(1!=!u.eager?n:a>u.from))&&(o[r]={get:()=>S.e(509).then((()=>()=>S(509))),from:a,eager:!1})})("juvio_frontend","0.1.0"),e[t]=u.length?Promise.all(u).then((()=>e[t]=1)):1}}})(),(()=>{var e;S.g.importScripts&&(e=S.g.location+"");var r=S.g.document;if(!e&&r&&(r.currentScript&&"SCRIPT"===r.currentScript.tagName.toUpperCase()&&(e=r.currentScript.src),!e)){var t=r.getElementsByTagName("script");if(t.length)for(var n=t.length-1;n>-1&&(!e||!/^http(s?):/.test(e));)e=t[n--].src}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/^blob:/,"").replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),S.p=e})(),t=e=>{var r=e=>e.split(".").map((e=>+e==e?+e:e)),t=/^([^-+]+)?(?:-([^+]+))?(?:\+(.+))?$/.exec(e),n=t[1]?r(t[1]):[];return t[2]&&(n.length++,n.push.apply(n,r(t[2]))),t[3]&&(n.push([]),n.push.apply(n,r(t[3]))),n},n=(e,r)=>{e=t(e),r=t(r);for(var n=0;;){if(n>=e.length)return n=r.length)return"u"==i;var a=r[n],u=(typeof a)[0];if(i!=u)return"o"==i&&"n"==u||"s"==u||"u"==i;if("o"!=i&&"u"!=i&&o!=a)return o{var r=e[0],t="";if(1===e.length)return"*";if(r+.5){t+=0==r?">=":-1==r?"<":1==r?"^":2==r?"~":r>0?"=":"!=";for(var n=1,i=1;i0?".":"")+(n=2,u);return t}var a=[];for(i=1;i{if(0 in e){r=t(r);var n=e[0],o=n<0;o&&(n=-n-1);for(var a=0,u=1,f=!0;;u++,a++){var l,s,d=u=r.length||"o"==(s=(typeof(l=r[a]))[0]))return!f||("u"==d?u>n&&!o:""==d!=o);if("u"==s){if(!f||"u"!=d)return!1}else if(f)if(d==s)if(u<=n){if(l!=e[u])return!1}else{if(o?l>e[u]:le&&S.o(e,r),u=e=>(e.loaded=1,e.get()),f=e=>Object.keys(e).reduce(((r,t)=>(e[t].eager&&(r[t]=e[t]),r)),{}),l=(e,r,t)=>{var o=t?f(e[r]):e[r];return Object.keys(o).reduce(((e,r)=>!e||!o[e].loaded&&n(e,r)?r:e),0)},s=(e,r,t,n)=>"Unsatisfied version "+t+" from "+(t&&e[r][t].from)+" of shared singleton module "+r+" (required "+o(n)+")",d=e=>{throw new Error(e)},p=e=>{"undefined"!=typeof console&&console.warn&&console.warn(e)},c=(e,r,t)=>t?t():((e,r)=>d("Shared module "+r+" doesn't exist in shared scope "+e))(e,r),h=(e=>function(r,t,n,o,i){var a=S.I(r);return a&&a.then&&!n?a.then(e.bind(e,r,S.S[r],t,!1,o,i)):e(r,S.S[r],t,n,o,i)})(((e,r,t,n,o,f)=>{if(!a(r,t))return c(e,t,f);var d=l(r,t,n);return i(o,d)||p(s(r,t,d,o)),u(r[t][d])})),v={},g={625:()=>h("default","@jupyterlab/launcher",!1,[1,4,4,1])},b={509:[625]},m={},S.f.consumes=(e,r)=>{S.o(b,e)&&b[e].forEach((e=>{if(S.o(v,e))return r.push(v[e]);if(!m[e]){var t=r=>{v[e]=0,S.m[e]=t=>{delete S.c[e],t.exports=r()}};m[e]=!0;var n=r=>{delete v[e],S.m[e]=t=>{throw delete S.c[e],r}};try{var o=g[e]();o.then?r.push(v[e]=o.then(t).catch(n)):t(o)}catch(e){n(e)}}}))},(()=>{var e={561:0};S.f.j=(r,t)=>{var n=S.o(e,r)?e[r]:void 0;if(0!==n)if(n)t.push(n[2]);else{var o=new Promise(((t,o)=>n=e[r]=[t,o]));t.push(n[2]=o);var i=S.p+S.u(r),a=new Error;S.l(i,(t=>{if(S.o(e,r)&&(0!==(n=e[r])&&(e[r]=void 0),n)){var o=t&&("load"===t.type?"missing":t.type),i=t&&t.target&&t.target.src;a.message="Loading chunk "+r+" failed.\n("+o+": "+i+")",a.name="ChunkLoadError",a.type=o,a.request=i,n[1](a)}}),"chunk-"+r,r)}};var r=(r,t)=>{var n,o,[i,a,u]=t,f=0;if(i.some((r=>0!==e[r]))){for(n in a)S.o(a,n)&&(S.m[n]=a[n]);u&&u(S)}for(r&&r(t);f= self.timeout: raise TimeoutError( f"Timeout acquiring lock for {self.filepath}" ) time.sleep(self.delay) else: self.file = open(self.lockfile_path, "a+") while True: try: import fcntl fcntl.flock(self.file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) break except BlockingIOError: if (time.time() - start_time) >= self.timeout: self.file.close() raise TimeoutError( f"Timeout acquiring lock for {self.filepath}" ) time.sleep(self.delay) def release(self): if self.file: if platform.system() == "Windows": os.close(self.file) os.unlink(self.lockfile_path) else: import fcntl fcntl.flock(self.file.fileno(), fcntl.LOCK_UN) self.file.close() def __enter__(self): self.acquire() return self def __exit__(self, exc_type, exc_val, exc_tb): self.release() class AsyncFileLock: def __init__(self, filepath, timeout=10, delay=0.05): self.filepath = filepath self.timeout = timeout self.delay = delay if platform.system() == "Windows": abs_path = os.path.abspath(filepath) h = hashlib.sha256(abs_path.encode("utf-8")).hexdigest()[:16] lock_filename = f"{os.path.basename(filepath)}.{h}.lock" self.lockfile_path = os.path.join(tempfile.gettempdir(), lock_filename) else: self.lockfile_path = filepath self.file = None async def acquire(self): start_time = time.time() if platform.system() == "Windows": while True: try: self.file = os.open( self.lockfile_path, os.O_CREAT | os.O_EXCL | os.O_RDWR ) break except FileExistsError: if (time.time() - start_time) >= self.timeout: raise TimeoutError( f"Timeout acquiring lock for {self.filepath}" ) await asyncio.sleep(self.delay) else: self.file = open(self.lockfile_path, "a+") while True: try: fcntl.flock(self.file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) break except BlockingIOError: if (time.time() - start_time) >= self.timeout: self.file.close() raise TimeoutError( f"Timeout acquiring lock for {self.filepath}" ) await asyncio.sleep(self.delay) async def release(self): if self.file: if platform.system() == "Windows": os.close(self.file) os.unlink(self.lockfile_path) else: fcntl.flock(self.file.fileno(), fcntl.LOCK_UN) self.file.close() async def __aenter__(self): await self.acquire() return self async def __aexit__(self, exc_type, exc_value, traceback): await self.release() def load_ipython_extension(ip): def juvio(line): import subprocess, os, sys command = ["uv", "pip"] + line.split() try: subprocess.check_call(command) except subprocess.CalledProcessError as e: print(f"Error: {e}") original_no_color = os.environ.get("NO_COLOR", "1") os.environ["NO_COLOR"] = "1" try: result = subprocess.run( ["uv", "pip", "freeze", "--no-color"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf-8", text=True, check=True, ) deps = result.stdout.split("\n") except subprocess.CalledProcessError as e: print(f"Error: {e}") print("Stdout:", e.stdout) print("Stderr:", e.stderr) finally: os.environ["NO_COLOR"] = original_no_color notebook_path = os.environ.get("JPY_SESSION_NAME", None) if notebook_path is None: raise ValueError( "No notebook path found in environment variable JPY_SESSION_NAME." ) with FileLock(notebook_path): with open(notebook_path, "r+", encoding="utf-8") as f: content = f.read() blocks = content.split("# ///") cells = blocks[2:] pyver = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" lines = [ "# /// script", f'# requires-python = "=={pyver}"', "# dependencies = [", ] for d in deps: if len(d) > 0: lines.append(f'# "{d}",') lines.append("# ]\n") f.seek(0) metadata = "\n".join(lines) new = "# ///".join([metadata] + cells) f.write(new) f.truncate() ip.register_magic_function(juvio, "line") def start(): kernel_app = app_module.IPKernelApp.instance() kernel_app.initialize() load_ipython_extension(kernel_app.shell) kernel_app.start() if __name__ == "__main__": start() ================================================ FILE: juvio_frontend/.gitignore ================================================ *.bundle.* lib/ node_modules/ *.log .eslintcache .stylelintcache *.egg-info/ .ipynb_checkpoints *.tsbuildinfo juvio_frontend/labextension # Version file is handled by hatchling juvio_frontend/_version.py # Integration tests ui-tests/test-results/ ui-tests/playwright-report/ # Created by https://www.gitignore.io/api/python # Edit at https://www.gitignore.io/?templates=python ### Python ### # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage/ coverage.xml *.cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # Mr Developer .mr.developer.cfg .project .pydevproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # End of https://www.gitignore.io/api/python # OSX files .DS_Store # Yarn cache .yarn/ ================================================ FILE: juvio_frontend/.prettierignore ================================================ node_modules **/node_modules **/lib **/package.json !/package.json juvio_frontend ================================================ FILE: juvio_frontend/.yarnrc.yml ================================================ enableImmutableInstalls: false nodeLinker: node-modules ================================================ FILE: juvio_frontend/CHANGELOG.md ================================================ # Changelog ================================================ FILE: juvio_frontend/LICENSE ================================================ BSD 3-Clause License Copyright (c) 2025, Oleh Kostromin All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: juvio_frontend/README.md ================================================ # juvio_frontend [![Github Actions Status](-/workflows/Build/badge.svg)](-/actions/workflows/build.yml) Juvio jupyterlab extension. ## Requirements - JupyterLab >= 4.0.0 ## Install To install the extension, execute: ```bash pip install juvio_frontend ``` ## Uninstall To remove the extension, execute: ```bash pip uninstall juvio_frontend ``` ## Contributing ### Development install Note: You will need NodeJS to build the extension package. The `jlpm` command is JupyterLab's pinned version of [yarn](https://yarnpkg.com/) that is installed with JupyterLab. You may use `yarn` or `npm` in lieu of `jlpm` below. ```bash # Clone the repo to your local environment # Change directory to the juvio_frontend directory # Install package in development mode pip install -e "." # Link your development version of the extension with JupyterLab jupyter labextension develop . --overwrite # Rebuild extension Typescript source after making changes jlpm build ``` You can watch the source directory and run JupyterLab at the same time in different terminals to watch for changes in the extension's source and automatically rebuild the extension. ```bash # Watch the source directory in one terminal, automatically rebuilding when needed jlpm watch # Run JupyterLab in another terminal jupyter lab ``` With the watch command running, every saved change will immediately be built locally and available in your running JupyterLab. Refresh JupyterLab to load the change in your browser (you may need to wait several seconds for the extension to be rebuilt). By default, the `jlpm build` command generates the source maps for this extension to make it easier to debug using the browser dev tools. To also generate source maps for the JupyterLab core extensions, you can run the following command: ```bash jupyter lab build --minimize=False ``` ### Development uninstall ```bash pip uninstall juvio_frontend ``` In development mode, you will also need to remove the symlink created by `jupyter labextension develop` command. To find its location, you can run `jupyter labextension list` to figure out where the `labextensions` folder is located. Then you can remove the symlink named `juvio_frontend` within that folder. ### Testing the extension #### Frontend tests This extension is using [Jest](https://jestjs.io/) for JavaScript code testing. To execute them, execute: ```sh jlpm jlpm test ``` #### Integration tests This extension uses [Playwright](https://playwright.dev/docs/intro/) for the integration tests (aka user level tests). More precisely, the JupyterLab helper [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) is used to handle testing the extension in JupyterLab. More information are provided within the [ui-tests](./ui-tests/README.md) README. ### Packaging the extension See [RELEASE](RELEASE.md) ================================================ FILE: juvio_frontend/RELEASE.md ================================================ # Making a new release of juvio_frontend The extension can be published to `PyPI` and `npm` manually or using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). ## Manual release ### Python package This extension can be distributed as Python packages. All of the Python packaging instructions are in the `pyproject.toml` file to wrap your extension in a Python package. Before generating a package, you first need to install some tools: ```bash pip install build twine hatch ``` Bump the version using `hatch`. By default this will create a tag. See the docs on [hatch-nodejs-version](https://github.com/agoose77/hatch-nodejs-version#semver) for details. ```bash hatch version ``` Make sure to clean up all the development files before building the package: ```bash jlpm clean:all ``` You could also clean up the local git repository: ```bash git clean -dfX ``` To create a Python source package (`.tar.gz`) and the binary package (`.whl`) in the `dist/` directory, do: ```bash python -m build ``` > `python setup.py sdist bdist_wheel` is deprecated and will not work for this package. Then to upload the package to PyPI, do: ```bash twine upload dist/* ``` ### NPM package To publish the frontend part of the extension as a NPM package, do: ```bash npm login npm publish --access public ``` ## Automated releases with the Jupyter Releaser The extension repository should already be compatible with the Jupyter Releaser. Check out the [workflow documentation](https://jupyter-releaser.readthedocs.io/en/latest/get_started/making_release_from_repo.html) for more information. Here is a summary of the steps to cut a new release: - Add `ADMIN_GITHUB_TOKEN`, `PYPI_TOKEN` and `NPM_TOKEN` to the [Github Secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) in the repository - Go to the Actions panel - Run the "Step 1: Prep Release" workflow - Check the draft changelog - Run the "Step 2: Publish Release" workflow ## Publishing to `conda-forge` If the package is not on conda forge yet, check the documentation to learn how to add it: https://conda-forge.org/docs/maintainer/adding_pkgs.html Otherwise a bot should pick up the new version publish to PyPI, and open a new PR on the feedstock repository automatically. ================================================ FILE: juvio_frontend/babel.config.js ================================================ module.exports = require('@jupyterlab/testutils/lib/babel.config'); ================================================ FILE: juvio_frontend/install.json ================================================ { "packageManager": "python", "packageName": "juvio_frontend", "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package juvio_frontend" } ================================================ FILE: juvio_frontend/jest.config.js ================================================ const jestJupyterLab = require('@jupyterlab/testutils/lib/jest-config'); const esModules = [ '@codemirror', '@jupyter/ydoc', '@jupyterlab/', 'lib0', 'nanoid', 'vscode-ws-jsonrpc', 'y-protocols', 'y-websocket', 'yjs' ].join('|'); const baseConfig = jestJupyterLab(__dirname); module.exports = { ...baseConfig, automock: false, collectCoverageFrom: [ 'src/**/*.{ts,tsx}', '!src/**/*.d.ts', '!src/**/.ipynb_checkpoints/*' ], coverageReporters: ['lcov', 'text'], testRegex: 'src/.*/.*.spec.ts[x]?$', transformIgnorePatterns: [`/node_modules/(?!${esModules}).+`] }; ================================================ FILE: juvio_frontend/package.json ================================================ { "name": "juvio_frontend", "version": "0.1.0", "description": "Juvio jupyterlab extension.", "keywords": [ "jupyter", "jupyterlab", "jupyterlab-extension" ], "homepage": "-", "bugs": { "url": "-/issues" }, "license": "BSD-3-Clause", "author": { "name": "Oleh Kostromin", "email": "oleh@beastbyte.ai" }, "files": [ "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}" ], "main": "lib/index.js", "types": "lib/index.d.ts", "style": "style/index.css", "repository": { "type": "git", "url": "-.git" }, "workspaces": [ "ui-tests" ], "scripts": { "build": "jlpm build:lib && jlpm build:labextension:dev", "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension", "build:labextension": "jupyter labextension build .", "build:labextension:dev": "jupyter labextension build --development True .", "build:lib": "tsc --sourceMap", "build:lib:prod": "tsc", "clean": "jlpm clean:lib", "clean:lib": "rimraf lib tsconfig.tsbuildinfo", "clean:lintcache": "rimraf .eslintcache .stylelintcache", "clean:labextension": "rimraf juvio_frontend/labextension juvio_frontend/_version.py", "clean:all": "jlpm clean:lib && jlpm clean:labextension && jlpm clean:lintcache", "eslint": "jlpm eslint:check --fix", "eslint:check": "eslint . --cache --ext .ts,.tsx", "install:extension": "jlpm build", "lint": "jlpm stylelint && jlpm prettier && jlpm eslint", "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check", "prettier": "jlpm prettier:base --write --list-different", "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", "prettier:check": "jlpm prettier:base --check", "stylelint": "jlpm stylelint:check --fix", "stylelint:check": "stylelint --cache \"style/**/*.css\"", "test": "jest --coverage", "watch": "run-p watch:src watch:labextension", "watch:src": "tsc -w --sourceMap", "watch:labextension": "jupyter labextension watch ." }, "dependencies": { "@jupyterlab/application": "^4.0.0", "@jupyterlab/launcher": "^4.0.0" }, "devDependencies": { "@jupyterlab/builder": "^4.0.0", "@jupyterlab/testutils": "^4.0.0", "@types/jest": "^29.2.0", "@types/json-schema": "^7.0.11", "@types/react": "^18.0.26", "@typescript-eslint/eslint-plugin": "^5.55.0", "@typescript-eslint/parser": "^5.55.0", "css-loader": "^6.7.1", "eslint": "^8.36.0", "eslint-config-prettier": "^8.7.0", "eslint-plugin-prettier": "^4.2.1", "jest": "^29.2.0", "npm-run-all": "^4.1.5", "prettier": "^2.8.7", "rimraf": "^4.4.1", "source-map-loader": "^1.0.2", "style-loader": "^3.3.1", "stylelint": "^14.9.1", "stylelint-config-prettier": "^9.0.4", "stylelint-config-recommended": "^8.0.0", "stylelint-config-standard": "^26.0.0", "stylelint-prettier": "^2.0.0", "typescript": "^5.0", "yjs": "^13.5.0" }, "sideEffects": [ "style/*.css", "style/index.js" ], "styleModule": "style/index.js", "publishConfig": { "access": "public" }, "jupyterlab": { "extension": true, "outputDir": "build/labextension" }, "eslintIgnore": [ "node_modules", "dist", "coverage", "**/*.d.ts", "tests", "**/__tests__", "ui-tests" ], "eslintConfig": { "extends": [ "eslint:recommended", "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended", "plugin:prettier/recommended" ], "parser": "@typescript-eslint/parser", "parserOptions": { "project": "tsconfig.json", "sourceType": "module" }, "plugins": [ "@typescript-eslint" ], "rules": { "@typescript-eslint/naming-convention": [ "error", { "selector": "interface", "format": [ "PascalCase" ], "custom": { "regex": "^I[A-Z]", "match": true } } ], "@typescript-eslint/no-unused-vars": [ "warn", { "args": "none" } ], "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-namespace": "off", "@typescript-eslint/no-use-before-define": "off", "@typescript-eslint/quotes": [ "error", "single", { "avoidEscape": true, "allowTemplateLiterals": false } ], "curly": [ "error", "all" ], "eqeqeq": "error", "prefer-arrow-callback": "error" } }, "prettier": { "singleQuote": true, "trailingComma": "none", "arrowParens": "avoid", "endOfLine": "auto" }, "stylelint": { "extends": [ "stylelint-config-recommended", "stylelint-config-standard", "stylelint-prettier/recommended" ], "rules": { "property-no-vendor-prefix": null, "selector-no-vendor-prefix": null, "value-no-vendor-prefix": null } } } ================================================ FILE: juvio_frontend/pyproject.toml ================================================ [build-system] requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0,<5", "hatch-nodejs-version"] build-backend = "hatchling.build" [project] name = "juvio_frontend" readme = "README.md" license = { file = "LICENSE" } requires-python = ">=3.8" classifiers = [ "Framework :: Jupyter", "Framework :: Jupyter :: JupyterLab", "Framework :: Jupyter :: JupyterLab :: 4", "Framework :: Jupyter :: JupyterLab :: Extensions", "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", "License :: OSI Approved :: BSD License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", ] dependencies = [ ] dynamic = ["version", "description", "authors", "urls", "keywords"] [tool.hatch.version] source = "nodejs" [tool.hatch.metadata.hooks.nodejs] fields = ["description", "authors", "urls"] [tool.hatch.build.targets.sdist] artifacts = ["juvio_frontend/labextension"] exclude = [".github", "binder"] [tool.hatch.build.targets.wheel.shared-data] "juvio_frontend/labextension" = "share/jupyter/labextensions/juvio_frontend" "install.json" = "share/jupyter/labextensions/juvio_frontend/install.json" [tool.hatch.build.hooks.version] path = "juvio_frontend/_version.py" [tool.hatch.build.hooks.jupyter-builder] dependencies = ["hatch-jupyter-builder>=0.5"] build-function = "hatch_jupyter_builder.npm_builder" ensured-targets = [ "juvio_frontend/labextension/static/style.js", "juvio_frontend/labextension/package.json", ] skip-if-exists = ["juvio_frontend/labextension/static/style.js"] [tool.hatch.build.hooks.jupyter-builder.build-kwargs] build_cmd = "build:prod" npm = ["jlpm"] [tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs] build_cmd = "install:extension" npm = ["jlpm"] source_dir = "src" build_dir = "juvio_frontend/labextension" [tool.jupyter-releaser.options] version_cmd = "hatch version" [tool.jupyter-releaser.hooks] before-build-npm = [ "python -m pip install 'jupyterlab>=4.0.0,<5'", "jlpm", "jlpm build:prod" ] before-build-python = ["jlpm clean:all"] [tool.check-wheel-contents] ignore = ["W002"] ================================================ FILE: juvio_frontend/setup.py ================================================ __import__('setuptools').setup() ================================================ FILE: juvio_frontend/src/__tests__/juvio_frontend.spec.ts ================================================ /** * Example of [Jest](https://jestjs.io/docs/getting-started) unit tests */ describe('juvio_frontend', () => { it('should be tested', () => { expect(1 + 1).toEqual(2); }); }); ================================================ FILE: juvio_frontend/src/index.ts ================================================ import { JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/application'; import { ILauncher } from '@jupyterlab/launcher'; const plugin: JupyterFrontEndPlugin = { id: 'juvio_frontend:plugin', description: 'Juvio JupyterLab extension', autoStart: true, requires: [ILauncher], activate: ( app: JupyterFrontEnd, launcher: ILauncher ) => { console.log('JupyterLab extension juvio_frontend is activated!'); app.docRegistry.addFileType({ name: 'juvio', displayName: 'Juvio Notebook', extensions: ['.juvio'], contentType: 'notebook', fileFormat: 'json', }); app.docRegistry.addFileType({ name: 'notebook', extensions: ['.juvio'] }); const { commands } = app; commands.addCommand('create-juvio-file', { label: 'Juvio Notebook', execute: () => { return commands.execute('docmanager:new-untitled', { type: 'file', ext: '.juvio', kernelName: "juvio", }); } }); launcher.add({ category: 'Notebook', rank: 1, command: 'create-juvio-file', kernelIconUrl: 'https://gist.githubusercontent.com/OKUA1/d6e65e883546021ea774857878fd0537/raw/4de2ea217e25d9ff7b3d2a73899e85665ed7d94c/juvio_logo.svg', }); } }; export default plugin; ================================================ FILE: juvio_frontend/style/base.css ================================================ /* See the JupyterLab Developer Guide for useful CSS Patterns: https://jupyterlab.readthedocs.io/en/stable/developer/css.html */ ================================================ FILE: juvio_frontend/style/index.css ================================================ @import 'base.css'; ================================================ FILE: juvio_frontend/style/index.js ================================================ import './base.css'; ================================================ FILE: juvio_frontend/tsconfig.json ================================================ { "compilerOptions": { "allowSyntheticDefaultImports": true, "composite": true, "declaration": true, "esModuleInterop": true, "incremental": true, "jsx": "react", "module": "esnext", "moduleResolution": "node", "noEmitOnError": true, "noImplicitAny": true, "noUnusedLocals": true, "preserveWatchOutput": true, "resolveJsonModule": true, "outDir": "lib", "rootDir": "src", "strict": true, "strictNullChecks": true, "target": "ES2018", "types": ["jest"], "lib": ["ES2020", "DOM", "DOM.Iterable", "ESNext.Intl"], }, "include": ["src/*"] } ================================================ FILE: juvio_frontend/tsconfig.test.json ================================================ { "extends": "./tsconfig" } ================================================ FILE: juvio_frontend/ui-tests/README.md ================================================ # Integration Testing This folder contains the integration tests of the extension. They are defined using [Playwright](https://playwright.dev/docs/intro) test runner and [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) helper. The Playwright configuration is defined in [playwright.config.js](./playwright.config.js). The JupyterLab server configuration to use for the integration test is defined in [jupyter_server_test_config.py](./jupyter_server_test_config.py). The default configuration will produce video for failing tests and an HTML report. > There is a new experimental UI mode that you may fall in love with; see [that video](https://www.youtube.com/watch?v=jF0yA-JLQW0). ## Run the tests > All commands are assumed to be executed from the root directory To run the tests, you need to: 1. Compile the extension: ```sh jlpm install jlpm build:prod ``` > Check the extension is installed in JupyterLab. 2. Install test dependencies (needed only once): ```sh cd ./ui-tests jlpm install jlpm playwright install cd .. ``` 3. Execute the [Playwright](https://playwright.dev/docs/intro) tests: ```sh cd ./ui-tests jlpm playwright test ``` Test results will be shown in the terminal. In case of any test failures, the test report will be opened in your browser at the end of the tests execution; see [Playwright documentation](https://playwright.dev/docs/test-reporters#html-reporter) for configuring that behavior. ## Update the tests snapshots > All commands are assumed to be executed from the root directory If you are comparing snapshots to validate your tests, you may need to update the reference snapshots stored in the repository. To do that, you need to: 1. Compile the extension: ```sh jlpm install jlpm build:prod ``` > Check the extension is installed in JupyterLab. 2. Install test dependencies (needed only once): ```sh cd ./ui-tests jlpm install jlpm playwright install cd .. ``` 3. Execute the [Playwright](https://playwright.dev/docs/intro) command: ```sh cd ./ui-tests jlpm playwright test -u ``` > Some discrepancy may occurs between the snapshots generated on your computer and > the one generated on the CI. To ease updating the snapshots on a PR, you can > type `please update playwright snapshots` to trigger the update by a bot on the CI. > Once the bot has computed new snapshots, it will commit them to the PR branch. ## Create tests > All commands are assumed to be executed from the root directory To create tests, the easiest way is to use the code generator tool of playwright: 1. Compile the extension: ```sh jlpm install jlpm build:prod ``` > Check the extension is installed in JupyterLab. 2. Install test dependencies (needed only once): ```sh cd ./ui-tests jlpm install jlpm playwright install cd .. ``` 3. Start the server: ```sh cd ./ui-tests jlpm start ``` 4. Execute the [Playwright code generator](https://playwright.dev/docs/codegen) in **another terminal**: ```sh cd ./ui-tests jlpm playwright codegen localhost:8888 ``` ## Debug tests > All commands are assumed to be executed from the root directory To debug tests, a good way is to use the inspector tool of playwright: 1. Compile the extension: ```sh jlpm install jlpm build:prod ``` > Check the extension is installed in JupyterLab. 2. Install test dependencies (needed only once): ```sh cd ./ui-tests jlpm install jlpm playwright install cd .. ``` 3. Execute the Playwright tests in [debug mode](https://playwright.dev/docs/debug): ```sh cd ./ui-tests jlpm playwright test --debug ``` ## Upgrade Playwright and the browsers To update the web browser versions, you must update the package `@playwright/test`: ```sh cd ./ui-tests jlpm up "@playwright/test" jlpm playwright install ``` ================================================ FILE: juvio_frontend/ui-tests/jupyter_server_test_config.py ================================================ """Server configuration for integration tests. !! Never use this configuration in production because it opens the server to the world and provide access to JupyterLab JavaScript objects through the global window variable. """ from jupyterlab.galata import configure_jupyter_server configure_jupyter_server(c) # Uncomment to set server log level to debug level # c.ServerApp.log_level = "DEBUG" ================================================ FILE: juvio_frontend/ui-tests/package.json ================================================ { "name": "juvio_frontend-ui-tests", "version": "1.0.0", "description": "JupyterLab juvio_frontend Integration Tests", "private": true, "scripts": { "start": "jupyter lab --config jupyter_server_test_config.py", "test": "jlpm playwright test", "test:update": "jlpm playwright test --update-snapshots" }, "devDependencies": { "@jupyterlab/galata": "^5.0.0", "@playwright/test": "^1.32.0" } } ================================================ FILE: juvio_frontend/ui-tests/playwright.config.js ================================================ /** * Configuration for Playwright using default from @jupyterlab/galata */ const baseConfig = require('@jupyterlab/galata/lib/playwright-config'); module.exports = { ...baseConfig, webServer: { command: 'jlpm start', url: 'http://localhost:8888/lab', timeout: 120 * 1000, reuseExistingServer: !process.env.CI } }; ================================================ FILE: juvio_frontend/ui-tests/tests/juvio_frontend.spec.ts ================================================ import { expect, test } from '@jupyterlab/galata'; /** * Don't load JupyterLab webpage before running the tests. * This is required to ensure we capture all log messages. */ test.use({ autoGoto: false }); test('should emit an activation console message', async ({ page }) => { const logs: string[] = []; page.on('console', message => { logs.push(message.text()); }); await page.goto(); expect( logs.filter(s => s === 'JupyterLab extension juvio_frontend is activated!') ).toHaveLength(1); }); ================================================ FILE: pyproject.toml ================================================ [build-system] requires = ["setuptools>=64", "wheel"] build-backend = "setuptools.build_meta" [project] name = "juvio" version = "0.1.0b2" description = "Jupyter kernel using uv and per-notebook dependencies" authors = [{ name = "Oleh Kostromin", email = "oleh@beastbyte.ai" }] license = { text = "MIT" } requires-python = ">=3.11" dependencies = [ "notebook>=7.0,<8.0", "jupyterlab>=4.0,<5.0", "ipykernel>=6.25,<8.0", "aiofiles>=23.0.0" ] [project.scripts] juvio-kernel-launcher = "juvio.kernel_launcher:main" [project.entry-points."jupyter_server.extensions"] juvio = "juvio:_load_jupyter_server_extension" [tool.setuptools] include-package-data = true [tool.setuptools.packages.find] where = ["."] include = ["juvio"] [tool.setuptools.data-files] "share/jupyter/labextensions/juvio_frontend" = [ "juvio/labextension/package.json" ] "share/jupyter/labextensions/juvio_frontend/static" = [ "juvio/labextension/static/509.8b21d569ebbb8f039bdd.js", "juvio/labextension/static/728.864a8d7732d3e14d284a.js", "juvio/labextension/static/remoteEntry.cb75fcc929fa00484f36.js", "juvio/labextension/static/third-party-licenses.json" ] "share/jupyter/kernels/juvio" = [ "juvio/kernelspec/kernel.json", ]