From 49f3ef110e77c39f8d48892adcf3280104dc4904 Mon Sep 17 00:00:00 2001 From: David Brochart Date: Thu, 24 Oct 2024 18:39:32 +0200 Subject: [PATCH] Add fork Jupyter events --- .github/workflows/test.yml | 6 +- .../jupyter_server_ydoc/app.py | 2 + .../jupyter_server_ydoc/events/fork.yaml | 34 ++++++++++ .../jupyter_server_ydoc/handlers.py | 14 ++++ .../jupyter_server_ydoc/utils.py | 2 + projects/jupyter-server-ydoc/pyproject.toml | 1 + tests/test_handlers.py | 67 ++++++++++++++++--- 7 files changed, 112 insertions(+), 14 deletions(-) create mode 100644 projects/jupyter-server-ydoc/jupyter_server_ydoc/events/fork.yaml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4f5130b1..6e4b7146 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -108,7 +108,7 @@ jobs: run: | python -m pip install "jupyterlab>=4.0.0,<5" python -m pip install -e ".[test]" codecov - python -m pip install -e projects/jupyter-collaboration-ui -e projects/jupyter-docprovider -e projects/jupyter-server-ydoc + python -m pip install -e projects/jupyter-collaboration-ui -e projects/jupyter-docprovider -e "projects/jupyter-server-ydoc[test]" - name: List installed packages run: | @@ -163,7 +163,7 @@ jobs: - name: Install the Python dependencies run: | pip install -e ".[test]" - pip install -e projects/jupyter-collaboration-ui -e projects/jupyter-docprovider -e projects/jupyter-server-ydoc + pip install -e projects/jupyter-collaboration-ui -e projects/jupyter-docprovider -e "projects/jupyter-server-ydoc[test]" - name: Run the unit tests run: | @@ -184,7 +184,7 @@ jobs: - name: Install the Python dependencies run: | pip install -e ".[test]" - pip install -e projects/jupyter-collaboration-ui -e projects/jupyter-docprovider -e projects/jupyter-server-ydoc + pip install -e projects/jupyter-collaboration-ui -e projects/jupyter-docprovider -e "projects/jupyter-server-ydoc[test]" - name: List installed packages run: | diff --git a/projects/jupyter-server-ydoc/jupyter_server_ydoc/app.py b/projects/jupyter-server-ydoc/jupyter_server_ydoc/app.py index 3efc6e31..b49ee7af 100644 --- a/projects/jupyter-server-ydoc/jupyter_server_ydoc/app.py +++ b/projects/jupyter-server-ydoc/jupyter_server_ydoc/app.py @@ -26,6 +26,7 @@ from .utils import ( AWARENESS_EVENTS_SCHEMA_PATH, EVENTS_SCHEMA_PATH, + FORK_EVENTS_SCHEMA_PATH, encode_file_path, room_id_from_encoded_path, ) @@ -86,6 +87,7 @@ def initialize(self): super().initialize() self.serverapp.event_logger.register_event_schema(EVENTS_SCHEMA_PATH) self.serverapp.event_logger.register_event_schema(AWARENESS_EVENTS_SCHEMA_PATH) + self.serverapp.event_logger.register_event_schema(FORK_EVENTS_SCHEMA_PATH) def initialize_settings(self): self.settings.update( diff --git a/projects/jupyter-server-ydoc/jupyter_server_ydoc/events/fork.yaml b/projects/jupyter-server-ydoc/jupyter_server_ydoc/events/fork.yaml new file mode 100644 index 00000000..832f1eb7 --- /dev/null +++ b/projects/jupyter-server-ydoc/jupyter_server_ydoc/events/fork.yaml @@ -0,0 +1,34 @@ +"$id": https://schema.jupyter.org/jupyter_collaboration/fork/v1 +"$schema": "http://json-schema.org/draft-07/schema" +version: 1 +title: Collaborative fork events +personal-data: true +description: | + Fork events emitted from server-side during a collaborative session. +type: object +required: + - root_roomid + - fork_roomid + - username + - action +properties: + root_roomid: + type: string + description: | + Root room ID. Usually composed by the file type, format and ID. + fork_roomid: + type: string + description: | + Fork root room ID. + username: + type: string + description: | + The name of the user who created or deleted the fork. + action: + enum: + - create + - delete + description: | + Possible values: + 1. create + 2. delete diff --git a/projects/jupyter-server-ydoc/jupyter_server_ydoc/handlers.py b/projects/jupyter-server-ydoc/jupyter_server_ydoc/handlers.py index 95a604c1..b8bd64b4 100644 --- a/projects/jupyter-server-ydoc/jupyter_server_ydoc/handlers.py +++ b/projects/jupyter-server-ydoc/jupyter_server_ydoc/handlers.py @@ -26,6 +26,7 @@ from .utils import ( JUPYTER_COLLABORATION_AWARENESS_EVENTS_URI, JUPYTER_COLLABORATION_EVENTS_URI, + JUPYTER_COLLABORATION_FORK_EVENTS_URI, LogLevel, MessageType, decode_file_path, @@ -654,6 +655,7 @@ async def put(self, root_roomid): fork_room = YRoom(ydoc=fork_ydoc) self._websocket_server.rooms[fork_roomid] = fork_room await self._websocket_server.start_room(fork_room) + self._emit_fork_event(self.current_user.username, root_roomid, fork_roomid, "create") data = json.dumps( { "sessionId": SERVER_SESSION, @@ -679,4 +681,16 @@ async def delete(self, fork_roomid): fork_update = fork_ydoc.get_update() root_ydoc.apply_update(fork_update) await self._websocket_server.delete_room(name=fork_roomid) + self._emit_fork_event(self.current_user.username, root_roomid, fork_roomid, "delete") self.set_status(200) + + def _emit_fork_event( + self, username: str, root_roomid: str, fork_roomid: str, action: str + ) -> None: + data = { + "username": username, + "root_roomid": root_roomid, + "fork_roomid": fork_roomid, + "action": action, + } + self.event_logger.emit(schema_id=JUPYTER_COLLABORATION_FORK_EVENTS_URI, data=data) diff --git a/projects/jupyter-server-ydoc/jupyter_server_ydoc/utils.py b/projects/jupyter-server-ydoc/jupyter_server_ydoc/utils.py index d1c74ce4..22c51d87 100644 --- a/projects/jupyter-server-ydoc/jupyter_server_ydoc/utils.py +++ b/projects/jupyter-server-ydoc/jupyter_server_ydoc/utils.py @@ -11,7 +11,9 @@ JUPYTER_COLLABORATION_AWARENESS_EVENTS_URI = ( "https://schema.jupyter.org/jupyter_collaboration/awareness/v1" ) +JUPYTER_COLLABORATION_FORK_EVENTS_URI = "https://schema.jupyter.org/jupyter_collaboration/fork/v1" AWARENESS_EVENTS_SCHEMA_PATH = EVENTS_FOLDER_PATH / "awareness.yaml" +FORK_EVENTS_SCHEMA_PATH = EVENTS_FOLDER_PATH / "fork.yaml" class MessageType(IntEnum): diff --git a/projects/jupyter-server-ydoc/pyproject.toml b/projects/jupyter-server-ydoc/pyproject.toml index 7007980e..2e5227c8 100644 --- a/projects/jupyter-server-ydoc/pyproject.toml +++ b/projects/jupyter-server-ydoc/pyproject.toml @@ -41,6 +41,7 @@ dynamic = ["version"] [project.optional-dependencies] test = [ "coverage", + "dirty-equals", "jupyter_server[test]>=2.4.0", "jupyter_server_fileid[test]", "pytest>=7.0", diff --git a/tests/test_handlers.py b/tests/test_handlers.py index dedf0621..bf688b9e 100644 --- a/tests/test_handlers.py +++ b/tests/test_handlers.py @@ -7,6 +7,7 @@ from asyncio import Event, sleep from typing import Any +from dirty_equals import IsStr from jupyter_events.logger import EventLogger from jupyter_ydoc import YUnicode from pycrdt import Text @@ -219,6 +220,7 @@ async def my_listener(logger: EventLogger, schema_id: str, data: dict) -> None: async def test_fork_handler( + jp_serverapp, rtc_create_file, rtc_connect_doc_client, rtc_connect_fork_client, @@ -227,6 +229,17 @@ async def test_fork_handler( rtc_delete_fork_client, rtc_fetch_session, ): + collected_data = [] + + async def my_listener(logger: EventLogger, schema_id: str, data: dict) -> None: + collected_data.append(data) + + event_logger = jp_serverapp.event_logger + event_logger.add_listener( + schema_id="https://schema.jupyter.org/jupyter_collaboration/fork/v1", + listener=my_listener, + ) + path, _ = await rtc_create_file("test.txt", "Hello") root_connect_event = Event() @@ -241,27 +254,45 @@ def _on_root_change(topic: str, event: Any) -> None: resp = await rtc_fetch_session("text", "file", path) data = json.loads(resp.body.decode("utf-8")) file_id = data["fileId"] + root_roomid = f"text:file:{file_id}" async with await rtc_connect_doc_client("text", "file", path) as ws, WebsocketProvider( root_ydoc.ydoc, ws ): await root_connect_event.wait() - resp = await rtc_create_fork_client(f"text:file:{file_id}", False) + resp = await rtc_create_fork_client(root_roomid, False) data = json.loads(resp.body.decode("utf-8")) fork_roomid0 = data["roomId"] - resp = await rtc_get_forks_client(f"text:file:{file_id}") + resp = await rtc_get_forks_client(root_roomid) data = json.loads(resp.body.decode("utf-8")) - assert data == {f"text:file:{file_id}": [fork_roomid0]} - - resp = await rtc_create_fork_client(f"text:file:{file_id}", True) + assert data == {root_roomid: [fork_roomid0]} + + assert collected_data == [ + { + "username": IsStr(), + "root_roomid": root_roomid, + "fork_roomid": fork_roomid0, + "action": "create", + } + ] + + resp = await rtc_create_fork_client(root_roomid, True) data = json.loads(resp.body.decode("utf-8")) fork_roomid1 = data["roomId"] - resp = await rtc_get_forks_client(f"text:file:{file_id}") + resp = await rtc_get_forks_client(root_roomid) data = json.loads(resp.body.decode("utf-8")) - assert data == {f"text:file:{file_id}": [fork_roomid0, fork_roomid1]} + assert data == {root_roomid: [fork_roomid0, fork_roomid1]} + + assert len(collected_data) == 2 + assert collected_data[1] == { + "username": IsStr(), + "root_roomid": root_roomid, + "fork_roomid": fork_roomid1, + "action": "create", + } fork_ydoc = YUnicode() fork_connect_event = Event() @@ -289,13 +320,27 @@ def _on_fork_change(topic: str, event: Any) -> None: await rtc_delete_fork_client(fork_roomid0, 1) await sleep(0.1) assert str(root_text) == "Hello, World!" - resp = await rtc_get_forks_client(f"text:file:{file_id}") + resp = await rtc_get_forks_client(root_roomid) data = json.loads(resp.body.decode("utf-8")) - assert data == {f"text:file:{file_id}": [fork_roomid1]} + assert data == {root_roomid: [fork_roomid1]} + assert len(collected_data) == 3 + assert collected_data[2] == { + "username": IsStr(), + "root_roomid": root_roomid, + "fork_roomid": fork_roomid0, + "action": "delete", + } await rtc_delete_fork_client(fork_roomid1, 1) await sleep(0.1) assert str(root_text) == "Hello, World! Hi!" - resp = await rtc_get_forks_client(f"text:file:{file_id}") + resp = await rtc_get_forks_client(root_roomid) data = json.loads(resp.body.decode("utf-8")) - assert data == {f"text:file:{file_id}": []} + assert data == {root_roomid: []} + assert len(collected_data) == 4 + assert collected_data[3] == { + "username": IsStr(), + "root_roomid": root_roomid, + "fork_roomid": fork_roomid1, + "action": "delete", + }