diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a72d7df5f..21fa4e3f3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,7 +25,7 @@ jobs: yarn=3 - name: Setup pip cache - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: ~/.cache/pip key: pip-3.9-${{ hashFiles('package.json') }} @@ -38,7 +38,7 @@ jobs: run: echo "::set-output name=dir::$(yarn cache dir)" - name: Setup yarn cache - uses: actions/cache@v2 + uses: actions/cache@v4 id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} @@ -47,7 +47,7 @@ jobs: yarn- - name: Setup OpenCascade build cache - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: | packages/opencascade/lib/jupytercad.opencascade.js @@ -84,6 +84,7 @@ jobs: python/jupytercad_core/dist/jupytercad* python/jupytercad_lab/dist/jupytercad* python/jupytercad_app/dist/jupytercad* + python/jupytercad/dist/jupytercad* if-no-files-found: error test_isolated: @@ -148,7 +149,7 @@ jobs: shell: bash -l {0} run: | set -eux - cp ./jupytercad_core/dist/jupytercad*.whl ./jupytercad_lab/dist/jupytercad*.whl ./jupytercad_app/dist/jupytercad*.whl . + cp ./jupytercad_core/dist/jupytercad*.whl ./jupytercad_lab/dist/jupytercad*.whl ./jupytercad_app/dist/jupytercad*.whl ./jupytercad/dist/jupytercad*.whl . python -m pip install jupytercad*.whl - name: Install dependencies @@ -159,7 +160,7 @@ jobs: run: jlpm install - name: Set up browser cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ${{ github.workspace }}/pw-browsers @@ -221,6 +222,7 @@ jobs: pip jupyterlite-core jupyterlite-xeus + jupyter_server - name: Download extension package uses: actions/download-artifact@v4 @@ -231,15 +233,16 @@ jobs: shell: bash -l {0} run: | set -eux - cp ./jupytercad_core/dist/jupytercad*.whl ./jupytercad_lab/dist/jupytercad*.whl ./jupytercad_app/dist/jupytercad*.whl . - python -m pip install jupytercad*.whl + cp ./jupytercad_core/dist/jupytercad*.whl jupytercad_core-0.0.1-py3-none-any.whl + cp ./jupytercad_lab/dist/jupytercad*.whl jupytercad_lab-0.0.1-py3-none-any.whl + cp ./jupytercad_app/dist/jupytercad*.whl jupytercad_app-0.0.1-py3-none-any.whl - name: Build the lite site shell: bash -l {0} working-directory: lite run: | set -eux - mkdir -p content && cp ../examples/*.jcad ../examples/*.STEP ../examples/*.stl ./content + mkdir -p content && cp ../examples/*.jcad ../examples/*.STEP ../examples/*.stl ../examples/*.ipynb ./content jupyter lite build --contents content --output-dir dist - name: Upload github-pages artifact diff --git a/lite/environment.yml b/lite/environment.yml index cf391c4ef..127d50f4b 100644 --- a/lite/environment.yml +++ b/lite/environment.yml @@ -1,6 +1,18 @@ name: xeus-python-kernel channels: - - https://repo.mamba.pm/emscripten-forge - - conda-forge + - https://prefix.dev/emscripten-forge-dev + - https://prefix.dev/conda-forge dependencies: + - python=3.13 + - pip - xeus-python + - requests + - jupyter_ydoc=2.1.5 + - ypywidgets>=0.9.6,<0.10.0 + - comm>=0.1.2,<0.2.0 + - pydantic>=2,<3 + - pip: + - yjs-widgets>=0.4,<0.5 + - my-jupyter-shared-drive + - ../jupytercad_core-0.0.1-py3-none-any.whl + - ../jupytercad_lab-0.0.1-py3-none-any.whl diff --git a/lite/jupyter-lite.json b/lite/jupyter-lite.json index e0c59798c..1907b9a8e 100644 --- a/lite/jupyter-lite.json +++ b/lite/jupyter-lite.json @@ -1,10 +1,4 @@ { "jupyter-lite-schema-version": 0, - "jupyter-config-data": { - "appName": "My JupyterCAD App", - "disabledExtensions": [ - "@jupyter/collaboration-extension", - "@jupyter/docprovider-extension" - ] - } + "jupyter-config-data": {} } diff --git a/packages/base/src/widget.ts b/packages/base/src/widget.ts index 34d3391a5..a57e68e37 100644 --- a/packages/base/src/widget.ts +++ b/packages/base/src/widget.ts @@ -268,12 +268,12 @@ export class JupyterCadPanel extends SplitPanel { (this._consoleTracker.widgetAdded as any).emit(consolePanel); await consolePanel.sessionContext.ready; - await consolePanel.console.inject( - `from jupytercad_lab import CadDocument\ndoc = CadDocument("${jcadPath}")` - ); this.addWidget(this._consoleView); this.setRelativeSizes([2, 1]); this._consoleOpened = true; + await consolePanel.console.inject( + `from jupytercad_lab import CadDocument\ndoc = CadDocument("${jcadPath}")` + ); consolePanel.console.sessionContext.kernelChanged.connect((_, arg) => { if (!arg.newValue) { this.removeConsole(); diff --git a/python/jupytercad/pyproject.toml b/python/jupytercad/pyproject.toml index acbb88f1a..0c2be0202 100644 --- a/python/jupytercad/pyproject.toml +++ b/python/jupytercad/pyproject.toml @@ -22,6 +22,8 @@ dependencies = [ "jupytercad_core==3.0.1", "jupytercad_lab==3.0.1", "jupytercad_app==3.0.1", + "jupyter-collaboration>=3,<4", + "jupyterlab>=4.3,<5", ] dynamic = ["version"] license = {file = "LICENSE"} diff --git a/python/jupytercad_app/package.json b/python/jupytercad_app/package.json index 3cb665a50..6d0ce4ab3 100644 --- a/python/jupytercad_app/package.json +++ b/python/jupytercad_app/package.json @@ -108,7 +108,7 @@ "@lumino/widgets": "^2.0.0", "react": "^18.0.1", "yjs": "^13.5.40", - "yjs-widgets": "^0.3.7" + "yjs-widgets": "^0.4" }, "devDependencies": { "@jupyterlab/builder": "^4.0.0", diff --git a/python/jupytercad_core/jupytercad_core/__init__.py b/python/jupytercad_core/jupytercad_core/__init__.py index 291737edb..ab2324111 100644 --- a/python/jupytercad_core/jupytercad_core/__init__.py +++ b/python/jupytercad_core/jupytercad_core/__init__.py @@ -9,8 +9,6 @@ warnings.warn("Importing 'jupytercad_core' outside a proper installation.") __version__ = "dev" -from .handlers import setup_handlers - def _jupyter_labextension_paths(): return [{"src": "labextension", "dest": "@jupytercad/jupytercad-core"}] @@ -24,5 +22,7 @@ def _load_jupyter_server_extension(server_app): server_app: jupyterlab.labapp.LabApp JupyterLab application instance """ + from .handlers import setup_handlers + setup_handlers(server_app.web_app) server_app.log.info("Registered jupytercad server extension") diff --git a/python/jupytercad_core/pyproject.toml b/python/jupytercad_core/pyproject.toml index 035b79716..9f5335b5a 100644 --- a/python/jupytercad_core/pyproject.toml +++ b/python/jupytercad_core/pyproject.toml @@ -19,9 +19,7 @@ classifiers = [ "Programming Language :: Python :: 3.12", ] dependencies = [ - "jupyter_server>=2.0.6,<3", "jupyter_ydoc>=2,<4", - "jupyter-collaboration>=3.1.0,<4", "pydantic>=2,<3", ] dynamic = ["version", "description", "authors", "urls", "keywords"] diff --git a/python/jupytercad_core/src/jcadplugin/modelfactory.ts b/python/jupytercad_core/src/jcadplugin/modelfactory.ts index 430b91fe2..002756476 100644 --- a/python/jupytercad_core/src/jcadplugin/modelfactory.ts +++ b/python/jupytercad_core/src/jcadplugin/modelfactory.ts @@ -19,8 +19,7 @@ export class JupyterCadJcadModelFactory /** * Whether the model is collaborative or not. */ - readonly collaborative = - document.querySelectorAll('[data-jupyter-lite-root]')[0] === undefined; + readonly collaborative = true; /** * The name of the model. diff --git a/python/jupytercad_core/src/stepplugin/modelfactory.ts b/python/jupytercad_core/src/stepplugin/modelfactory.ts index 057ec9229..9f5cea793 100644 --- a/python/jupytercad_core/src/stepplugin/modelfactory.ts +++ b/python/jupytercad_core/src/stepplugin/modelfactory.ts @@ -23,8 +23,7 @@ export class JupyterCadStepModelFactory /** * Whether the model is collaborative or not. */ - readonly collaborative = - document.querySelectorAll('[data-jupyter-lite-root]')[0] === undefined; + readonly collaborative = true; /** * The name of the model. diff --git a/python/jupytercad_core/src/stlplugin/modelfactory.ts b/python/jupytercad_core/src/stlplugin/modelfactory.ts index 2125d43bc..fc6b7c9ca 100644 --- a/python/jupytercad_core/src/stlplugin/modelfactory.ts +++ b/python/jupytercad_core/src/stlplugin/modelfactory.ts @@ -23,8 +23,7 @@ export class JupyterCadStlModelFactory /** * Whether the model is collaborative or not. */ - readonly collaborative = - document.querySelectorAll('[data-jupyter-lite-root]')[0] === undefined; + readonly collaborative = true; /** * The name of the model. diff --git a/python/jupytercad_lab/jupytercad_lab/notebook/cad_document.py b/python/jupytercad_lab/jupytercad_lab/notebook/cad_document.py index f0c3bb306..4300821fa 100644 --- a/python/jupytercad_lab/jupytercad_lab/notebook/cad_document.py +++ b/python/jupytercad_lab/jupytercad_lab/notebook/cad_document.py @@ -28,7 +28,6 @@ ShapeMetadata, IAny, ) -from .utils import normalize_path logger = logging.getLogger(__file__) @@ -72,7 +71,7 @@ def _path_to_comm(cls, filePath: Optional[str]) -> Dict: contentType = None if filePath is not None: - path = normalize_path(filePath) + path = filePath file_name = Path(path).name try: ext = file_name.split(".")[1].lower() diff --git a/python/jupytercad_lab/jupytercad_lab/notebook/utils.py b/python/jupytercad_lab/jupytercad_lab/notebook/utils.py index de2152866..a31196a55 100644 --- a/python/jupytercad_lab/jupytercad_lab/notebook/utils.py +++ b/python/jupytercad_lab/jupytercad_lab/notebook/utils.py @@ -1,4 +1,3 @@ -import os from enum import Enum from urllib.parse import urljoin @@ -15,10 +14,3 @@ def multi_urljoin(*parts) -> str: parts[0], "/".join(part for part in parts[1:]), ) - - -def normalize_path(path: str) -> str: - if os.path.isabs(path): - return path - else: - return os.path.abspath(os.path.join(os.getcwd(), path)) diff --git a/python/jupytercad_lab/package.json b/python/jupytercad_lab/package.json index 8c3a3b310..edc2ef865 100644 --- a/python/jupytercad_lab/package.json +++ b/python/jupytercad_lab/package.json @@ -68,7 +68,7 @@ "@lumino/messaging": "^2.0.0", "@lumino/widgets": "^2.0.0", "react": "^18.0.1", - "yjs-widgets": "^0.3.7" + "yjs-widgets": "^0.4" }, "devDependencies": { "@jupyterlab/builder": "^4.0.0", @@ -119,7 +119,7 @@ }, "@jupyter/docprovider": { "singleton": true, - "bundled": false + "bundled": true }, "yjs-widgets": { "singleton": true, diff --git a/python/jupytercad_lab/pyproject.toml b/python/jupytercad_lab/pyproject.toml index 247517a8b..53ffdd7bb 100644 --- a/python/jupytercad_lab/pyproject.toml +++ b/python/jupytercad_lab/pyproject.toml @@ -25,7 +25,7 @@ classifiers = [ dependencies = [ "pycrdt", "ypywidgets>=0.9.0,<0.10.0", - "yjs-widgets>=0.3.7,<0.4", + "yjs-widgets>=0.4,<0.5", "comm>=0.1.2,<0.2.0", "pydantic>=2,<3", "jupytercad_core>=3.0.0a0,<4", diff --git a/python/jupytercad_lab/src/notebookrenderer.ts b/python/jupytercad_lab/src/notebookrenderer.ts index 58c00d799..30d4692aa 100644 --- a/python/jupytercad_lab/src/notebookrenderer.ts +++ b/python/jupytercad_lab/src/notebookrenderer.ts @@ -15,6 +15,7 @@ import { JupyterCadModel } from '@jupytercad/schema'; +import { showErrorMessage } from '@jupyterlab/apputils'; import { JupyterFrontEnd, JupyterFrontEndPlugin @@ -22,11 +23,11 @@ import { import { Contents } from '@jupyterlab/services'; import { MessageLoop } from '@lumino/messaging'; import { Panel, Widget } from '@lumino/widgets'; -import * as Y from 'yjs'; import { IJupyterYWidget, IJupyterYWidgetManager, - JupyterYModel + JupyterYModel, + JupyterYDoc } from 'yjs-widgets'; import { Toolbar } from '@jupyterlab/ui-components'; import { ConsolePanel } from '@jupyterlab/console'; @@ -94,10 +95,6 @@ export class YJupyterCADLuminoWidget extends Panel { private _buildWidget = (options: IOptions) => { const { commands, workerRegistry, model, externalCommands, tracker } = options; - // Ensure the model filePath is relevant with the shared model path. - if (model.sharedModel.getState('path')) { - model.filePath = model.sharedModel.getState('path') as string; - } const content = new JupyterCadPanel({ model: model, workerRegistry: workerRegistry as IJCadWorkerRegistry @@ -153,17 +150,57 @@ export const notebookRenderePlugin: JupyterFrontEndPlugin = { console.error('Missing IJupyterYWidgetManager token!'); return; } - if (!drive) { - console.error('Missing ICollaborativeDrive token!'); - return; - } + class YJupyterCADModelFactory extends YJupyterCADModel { - ydocFactory(commMetadata: ICommMetadata): Y.Doc { + protected async initialize(commMetadata: { + [key: string]: any; + }): Promise { const { path, format, contentType } = commMetadata; const fileFormat = format as Contents.FileFormat; + if (!drive) { + showErrorMessage( + 'Error using the JupyterCAD Python API', + 'You cannot use the JupyterCAD Python API without a collaborative drive. You need to install a package providing collaboration features (e.g. jupyter-collaboration).' + ); + throw new Error( + 'Failed to create the YDoc without a collaborative drive' + ); + } + + // The path of the project is relative to the path of the notebook + let currentWidgetPath = ''; + const currentWidget = app.shell.currentWidget; + if ( + currentWidget instanceof NotebookPanel || + currentWidget instanceof ConsolePanel + ) { + currentWidgetPath = currentWidget.sessionContext.path; + } + + let localPath = ''; + if (path) { + localPath = PathExt.join(PathExt.dirname(currentWidgetPath), path); + + // If the file does not exist yet, create it + try { + await app.serviceManager.contents.get(localPath); + } catch (e) { + await app.serviceManager.contents.save(localPath, { + content: btoa('{}'), + format: 'base64' + }); + } + } else { + // If the user did not provide a path, do not create + localPath = PathExt.join( + PathExt.dirname(currentWidgetPath), + 'unsaved_project' + ); + } + const sharedModel = drive!.sharedModelFactory.createNew({ - path, + path: localPath, format: fileFormat, contentType, collaborative: true @@ -174,28 +211,10 @@ export const notebookRenderePlugin: JupyterFrontEndPlugin = { }); this.jupyterCADModel.contentsManager = app.serviceManager.contents; + this.jupyterCADModel.filePath = localPath; - if (!sharedModel) { - // The path of the project is set to the path of the notebook, to be able to - // add local geoJSON/shape file in a "file-less" project. - let currentWidgetPath: string | undefined = undefined; - const currentWidget = app.shell.currentWidget; - if ( - currentWidget instanceof NotebookPanel || - currentWidget instanceof ConsolePanel - ) { - currentWidgetPath = currentWidget.sessionContext.path; - } - - if (currentWidgetPath) { - this.jupyterCADModel.filePath = PathExt.join( - PathExt.dirname(currentWidgetPath), - 'unsaved_project' - ); - } - } - - return this.jupyterCADModel.sharedModel.ydoc; + this.ydoc = this.jupyterCADModel.sharedModel.ydoc; + this.sharedModel = new JupyterYDoc(commMetadata, this.ydoc); } } diff --git a/requirements-build.txt b/requirements-build.txt index c5894a8f9..9f5564ccc 100644 --- a/requirements-build.txt +++ b/requirements-build.txt @@ -1,4 +1,6 @@ # minimum needed to build jupytercad. datamodel-code-generator>=0.23.0 hatchling>=1.5.0,<2 +jupyter-collaboration>=3,<4 +jupyterlab>=4.3,<5 pydantic==2.9.2 diff --git a/scripts/build_packages.py b/scripts/build_packages.py index 96c3391fd..8868bd5b3 100644 --- a/scripts/build_packages.py +++ b/scripts/build_packages.py @@ -12,7 +12,12 @@ def build_packages(): install_build_deps = f"python -m pip install -r {requirements_build_path}" python_package_prefix = "python" - python_packages = ["jupytercad_core", "jupytercad_lab", "jupytercad_app"] + python_packages = [ + "jupytercad_core", + "jupytercad_lab", + "jupytercad_app", + "jupytercad", + ] execute(install_build_deps) diff --git a/yarn.lock b/yarn.lock index b2616d83d..062fe7b95 100644 --- a/yarn.lock +++ b/yarn.lock @@ -870,7 +870,7 @@ __metadata: typescript: ^5 webpack: ^5.76.3 yjs: ^13.5.40 - yjs-widgets: ^0.3.7 + yjs-widgets: ^0.4 languageName: unknown linkType: soft @@ -937,7 +937,7 @@ __metadata: rimraf: ^3.0.2 typescript: ^5 yjs: ^13.5.0 - yjs-widgets: ^0.3.7 + yjs-widgets: ^0.4 languageName: unknown linkType: soft @@ -13026,9 +13026,9 @@ __metadata: languageName: node linkType: hard -"yjs-widgets@npm:^0.3.7": - version: 0.3.8 - resolution: "yjs-widgets@npm:0.3.8" +"yjs-widgets@npm:^0.4": + version: 0.4.0 + resolution: "yjs-widgets@npm:0.4.0" dependencies: "@jupyter/ydoc": ^2.0.0 || ^3.0.0-a3 "@jupyterlab/application": ^4.0.0 @@ -13044,7 +13044,7 @@ __metadata: uuid: ^9.0.0 webpack: ^5.77.0 webpack-cli: ^5.0.1 - checksum: 1bd4d2f69a826fd8bf8b58c656afe7c7f246e070dab8676e12804afcf8e0c337ae8f569b4764e22a696db9ab10f3c65cc7778b653bcc98e767d1e9e5e0d0a865 + checksum: 147fc7ae1c3d13fd591d434af33d439bc20d23494ded601dd8da5986426c6c896d7ff2a6296e24bda8d5ee17921c5c390c3b095f14fd5195506c0f891747e2fd languageName: node linkType: hard