diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a88eca8eb..47d40b45e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -250,74 +250,3 @@ jobs: - uses: jupyterlab/maintainer-tools/.github/actions/check-links@v1 with: ignore_links: 'lite/' - - build-docs-and-lite: - name: Build docs with JupyterLite deployment - needs: build - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install Conda environment with Micromamba - uses: mamba-org/setup-micromamba@v2 - with: - micromamba-version: '1.5.5-0' - environment-name: build-env - create-args: >- - python=3.10 - pip - jupyterlite-core>=0.4.0,<0.6.0 - jupyterlite-xeus>=2,<3 - jupyterlite-sphinx - sphinx - sphinx-tabs - pydata-sphinx-theme - sphinx-autodoc-typehints - sphinx-exercise - sphinx-togglebutton - myst-parser<4.0.0 - - - name: Download extension package - uses: actions/download-artifact@v4 - with: - name: extension-artifacts - - - name: Install the extension - shell: bash -l {0} - run: | - set -eux - cp ./jupytergis_core/dist/jupytergis*.whl ./jupytergis_lab/dist/jupytergis*.whl ./jupytergis_qgis/dist/jupytergis*.whl . - python -m pip install jupytergis*.whl - - - name: Build the docs site - shell: bash -l {0} - working-directory: docs - run: | - set -eux - sphinx-build . dist - - - name: Upload artifact - id: upload-docs-artifact - uses: actions/upload-pages-artifact@v3 - with: - path: ./docs/dist - retention-days: 30 - - deploy: - needs: build-docs-and-lite - if: github.ref == 'refs/heads/main' - permissions: - pages: write - id-token: write - - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - - runs-on: ubuntu-latest - steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 1b6be8391..84b970360 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -20,11 +20,13 @@ build: # Override the create_environment step (undocumented feature) to create # multiple environments. create_environment: + # Pin micromamba + - /bin/bash --login -c "micromamba self-update --version 2.0.5" # Create the env for building the docs - /bin/bash --login -c "micromamba env create -n jupytergis-docs -f docs/environment-docs.yml" # Create the isolated env for building JupyterGIS - - /bin/bash --login -c "micromamba create -n jupytergis-build -c conda-forge yarn=3 hatch pip python=3.12" - - /bin/bash --login -c "micromamba run -n jupytergis-build pip install -r requirements-build.txt" + - /bin/bash --login -c "micromamba create -n jupytergis-build -c conda-forge nodejs hatch pip python=3.13" + - /bin/bash --login -c "micromamba run -n jupytergis-build pip install 'jupyterlab==4.3' 'datamodel-code-generator>=0.23.0'" # Override the install step to do nothing - we already created the envs install: @@ -33,18 +35,8 @@ build: # Before building the docs, build JupyterGIS in its isolated environment, # then install the wheels into the docs environment. pre_build: - - /bin/bash --login -c "micromamba run -n jupytergis-build yarn install" - - /bin/bash --login -c "micromamba run -n jupytergis-build yarn dev" - - /bin/bash --login -c "micromamba run -n jupytergis-build jupyter labextension list 2>&1 | grep -ie 'jupytergis-core.*OK'" - - /bin/bash --login -c "micromamba run -n jupytergis-build jupyter labextension list 2>&1 | grep -ie 'jupytergis-lab.*OK'" - - /bin/bash --login -c "micromamba run -n jupytergis-build yarn build:packages" - - - |- - /bin/bash --login -c "micromamba run -n jupytergis-docs \ - python -m pip install \ - $(ls ./python/jupytergis_core/dist/jupytergis*.whl) \ - $(ls ./python/jupytergis_lab/dist/jupytergis*.whl) \ - $(ls ./python/jupytergis_qgis/dist/jupytergis*.whl)" + - /bin/bash --login -c "micromamba run -n jupytergis-build jlpm install" + - /bin/bash --login -c "micromamba run -n jupytergis-build jlpm build" build: html: diff --git a/README.md b/README.md index 856b50116..8ed6ea086 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ [![lite-badge]][lite] [![docs-badge]][docs] [lite-badge]: https://jupyterlite.rtfd.io/en/latest/_static/badge.svg -[lite]: https://geojupyter.github.io/jupytergis/lite/lab/index.html?path=france_hiking.jGIS/ +[lite]: https://jupytergis.readthedocs.io/en/latest/lite/lab/index.html?path=france_hiking.jGIS/ [docs-badge]: https://readthedocs.org/projects/jupytergis/badge/?version=latest -[docs]: https://geojupyter.github.io/jupytergis +[docs]: https://jupytergis.readthedocs.io ⚠️ This extension is work in progress. Features and APIs are subject to change quickly. ⚠️ @@ -18,7 +18,7 @@ - **QGIS File Support**: Load, visualize, and manipulate QGIS project files (`.qgs`, `.qgz`), and other GIS data formats. - **Interactive Maps**: Render interactive maps and geospatial visualizations within Jupyter notebooks using the JupyterGIS Python API. -## [🪄 Try JupyterGIS now! ✨](https://geojupyter.github.io/jupytergis/lite/lab/index.html?path=france_hiking.jGIS) +## [🪄 Try JupyterGIS now! ✨](https://jupytergis.readthedocs.io/en/latest/lite/lab/index.html?path=france_hiking.jGIS) This demo runs a JupyterLab instance entirely in your browser with WebAssembly! 🤯 diff --git a/docs/conf.py b/docs/conf.py index 54459086c..344cb8eab 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -40,6 +40,7 @@ "../examples/*.gif", "../examples/*.geojson", "../examples/*.tif", + "../examples/*.ipynb", ] jupyterlite_dir = "." jupyterlite_config = "jupyter_lite_config.json" diff --git a/docs/environment-docs.yml b/docs/environment-docs.yml index 3f1d8319d..28feb5133 100644 --- a/docs/environment-docs.yml +++ b/docs/environment-docs.yml @@ -7,8 +7,8 @@ dependencies: - pip # Needed to install jupytergis wheels in RTD build # Build docs & JupyterLite - - jupyterlite-core>=0.4.0,<0.6.0 - - jupyterlite-xeus>=2,<3 + - jupyterlite-core + - jupyterlite-xeus>=3.1.3,<4 - jupyterlite-sphinx - sphinx - sphinx-tabs @@ -16,9 +16,5 @@ dependencies: - sphinx-autodoc-typehints - sphinx-exercise - sphinx-togglebutton - + - myst-parser - xeus-python # TODO: Do we need this? - - # Install myst-parser via pip - - pip: - - myst-parser<4.0.0 diff --git a/docs/environment.yml b/docs/environment.yml index cf391c4ef..46243e9b9 100644 --- a/docs/environment.yml +++ b/docs/environment.yml @@ -1,6 +1,18 @@ name: xeus-python-kernel channels: - - https://repo.mamba.pm/emscripten-forge + - https://prefix.dev/emscripten-forge-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 + - ../python/jupytergis_lab + - ../python/jupytergis_core diff --git a/docs/jupyter-lite.json b/docs/jupyter-lite.json index b865b7086..1907b9a8e 100644 --- a/docs/jupyter-lite.json +++ b/docs/jupyter-lite.json @@ -1,10 +1,4 @@ { "jupyter-lite-schema-version": 0, - "jupyter-config-data": { - "appName": "JupyterGIS 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 cb657f98c..b21e34b2b 100644 --- a/packages/base/src/widget.ts +++ b/packages/base/src/widget.ts @@ -197,12 +197,12 @@ export class JupyterGISPanel extends SplitPanel { (this._consoleTracker.widgetAdded as any).emit(consolePanel); await consolePanel.sessionContext.ready; - await consolePanel.console.inject( - `from jupytergis_lab import GISDocument\ndoc = GISDocument("${jgisPath}")` - ); this.addWidget(this._consoleView); this.setRelativeSizes([2, 1]); this._consoleOpened = true; + await consolePanel.console.inject( + `from jupytergis_lab import GISDocument\ndoc = GISDocument("${jgisPath}")` + ); consolePanel.console.sessionContext.kernelChanged.connect((_, arg) => { if (!arg.newValue) { this.removeConsole(); diff --git a/python/jupytergis_core/src/jgisplugin/modelfactory.ts b/python/jupytergis_core/src/jgisplugin/modelfactory.ts index f4eeaad3c..38c1967c4 100644 --- a/python/jupytergis_core/src/jgisplugin/modelfactory.ts +++ b/python/jupytergis_core/src/jgisplugin/modelfactory.ts @@ -18,8 +18,7 @@ export class JupyterGISModelFactory /** * 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/jupytergis_lab/jupytergis_lab/__init__.py b/python/jupytergis_lab/jupytergis_lab/__init__.py index 47c21a2fc..0d4f3c58f 100644 --- a/python/jupytergis_lab/jupytergis_lab/__init__.py +++ b/python/jupytergis_lab/jupytergis_lab/__init__.py @@ -8,13 +8,6 @@ __version__ = "dev" -import sys - -if sys.platform == "emscripten": - raise ImportError( - "Cannot use the JupyterGIS Python API in a JupyterLite kernel yet" - ) - from .notebook import GISDocument # noqa diff --git a/python/jupytergis_lab/jupytergis_lab/notebook/gis_document.py b/python/jupytergis_lab/jupytergis_lab/notebook/gis_document.py index 3d0bf9b27..a19b2f22e 100644 --- a/python/jupytergis_lab/jupytergis_lab/notebook/gis_document.py +++ b/python/jupytergis_lab/jupytergis_lab/notebook/gis_document.py @@ -29,7 +29,6 @@ LayerType, SourceType, ) -from .utils import normalize_path logger = logging.getLogger(__file__) @@ -64,11 +63,6 @@ def __init__( comm_metadata = GISDocument._path_to_comm(path) - # Create an empty project file if it does not exist - if comm_metadata["path"] and not os.path.isfile(comm_metadata["path"]): - with open(comm_metadata["path"], "w") as fd: - fd.write("{}") - ydoc = Doc() super().__init__( @@ -708,7 +702,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/jupytergis_lab/jupytergis_lab/notebook/tests/test_api.py b/python/jupytergis_lab/jupytergis_lab/notebook/tests/test_api.py index 45a1ee15c..8f723100a 100644 --- a/python/jupytergis_lab/jupytergis_lab/notebook/tests/test_api.py +++ b/python/jupytergis_lab/jupytergis_lab/notebook/tests/test_api.py @@ -4,23 +4,6 @@ from jupytergis_lab import GISDocument -class ProjectCreation(unittest.TestCase): - filename = "test.jgis" - - def setUp(self): - if os.path.isfile(self.filename): - os.remove(self.filename) - - def tearDown(self): - if os.path.isfile(self.filename): - os.remove(self.filename) - - def test_creation(self): - self.doc = GISDocument(self.filename) - - assert os.path.isfile(self.filename) - - class VectorTileTests(unittest.TestCase): def setUp(self): self.doc = GISDocument() diff --git a/python/jupytergis_lab/jupytergis_lab/notebook/utils.py b/python/jupytergis_lab/jupytergis_lab/notebook/utils.py index 36ca3a583..064d09c97 100644 --- a/python/jupytergis_lab/jupytergis_lab/notebook/utils.py +++ b/python/jupytergis_lab/jupytergis_lab/notebook/utils.py @@ -1,4 +1,3 @@ -import os from enum import Enum from urllib.parse import urljoin import requests @@ -16,10 +15,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/jupytergis_lab/package.json b/python/jupytergis_lab/package.json index 8745166bc..40b345908 100644 --- a/python/jupytergis_lab/package.json +++ b/python/jupytergis_lab/package.json @@ -69,7 +69,7 @@ "@lumino/messaging": "^2.0.0", "@lumino/widgets": "^2.0.0", "react": "^18.0.1", - "yjs-widgets": "^0.3.9" + "yjs-widgets": "^0.4" }, "devDependencies": { "@jupyterlab/builder": "^4.3.0", diff --git a/python/jupytergis_lab/pyproject.toml b/python/jupytergis_lab/pyproject.toml index 974618b1d..713d5cf6f 100644 --- a/python/jupytergis_lab/pyproject.toml +++ b/python/jupytergis_lab/pyproject.toml @@ -26,7 +26,7 @@ dependencies = [ "requests", "jupyter-ydoc>=2,<4", "ypywidgets>=0.9.0,<0.10.0", - "yjs-widgets>=0.3.9,<0.4", + "yjs-widgets>=0.4,<0.5", "comm>=0.1.2,<0.2.0", "pydantic>=2,<3", "jupytergis_core>=0.1.0,<1", diff --git a/python/jupytergis_lab/src/notebookrenderer.ts b/python/jupytergis_lab/src/notebookrenderer.ts index af15ae3b6..c262c5d3c 100644 --- a/python/jupytergis_lab/src/notebookrenderer.ts +++ b/python/jupytergis_lab/src/notebookrenderer.ts @@ -16,6 +16,7 @@ import { JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/application'; +import { showErrorMessage } from '@jupyterlab/apputils'; import { ConsolePanel } from '@jupyterlab/console'; import { PathExt } from '@jupyterlab/coreutils'; import { NotebookPanel } from '@jupyterlab/notebook'; @@ -24,10 +25,11 @@ import { Toolbar } from '@jupyterlab/ui-components'; import { CommandRegistry } from '@lumino/commands'; import { MessageLoop } from '@lumino/messaging'; import { Panel, Widget } from '@lumino/widgets'; -import * as Y from 'yjs'; import { IJupyterYWidget, IJupyterYWidgetManager, + JupyterYDoc, + // JupyterYDoc, JupyterYModel } from 'yjs-widgets'; @@ -80,10 +82,6 @@ export class YJupyterGISLuminoWidget extends Panel { */ private _buildWidget = (options: IOptions) => { const { commands, 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 JupyterGISPanel({ model }); let toolbar: Toolbar | undefined = undefined; if (model.filePath) { @@ -132,18 +130,57 @@ export const notebookRendererPlugin: JupyterFrontEndPlugin = { console.error('Missing IJupyterYWidgetManager token!'); return; } - if (!drive) { - console.error( - 'Cannot setup JupyterGIS Python API without a collaborative drive' - ); - return; - } + class YJupyterGISModelFactory extends YJupyterGISModel { - 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 JupyterGIS Python API', + 'You cannot use the JupyterGIS 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 @@ -153,27 +190,10 @@ export const notebookRendererPlugin: JupyterFrontEndPlugin = { }); this.jupyterGISModel.contentsManager = app.serviceManager.contents; + this.jupyterGISModel.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.jupyterGISModel.filePath = PathExt.join( - PathExt.dirname(currentWidgetPath), - 'unsaved_project' - ); - } - } - return this.jupyterGISModel.sharedModel.ydoc; + this.ydoc = this.jupyterGISModel.sharedModel.ydoc; + this.sharedModel = new JupyterYDoc(commMetadata, this.ydoc); } } diff --git a/yarn.lock b/yarn.lock index 02bf94bf7..3740c67e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -849,7 +849,7 @@ __metadata: rimraf: ^3.0.2 typescript: ^5 yjs: ^13.5.0 - yjs-widgets: ^0.3.9 + yjs-widgets: ^0.4 languageName: unknown linkType: soft @@ -11471,9 +11471,9 @@ __metadata: languageName: node linkType: hard -"yjs-widgets@npm:^0.3.9": - version: 0.3.9 - resolution: "yjs-widgets@npm:0.3.9" +"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 @@ -11489,7 +11489,7 @@ __metadata: uuid: ^9.0.0 webpack: ^5.77.0 webpack-cli: ^5.0.1 - checksum: 463d0d2e18cfbe9c8eb769fe25390a03a3ee3c50741763de0eb3c15806d55ab5c0b6a7ace8eef8367275fd15100a4b4f4cab21a0dd9f49a18538d2c952ecdd04 + checksum: 147fc7ae1c3d13fd591d434af33d439bc20d23494ded601dd8da5986426c6c896d7ff2a6296e24bda8d5ee17921c5c390c3b095f14fd5195506c0f891747e2fd languageName: node linkType: hard