Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a public API for getting a read-only view of the shared model #275

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/source/developer/python_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,25 @@
Python API
==========

``jupyter_collaboration`` instantiates :any:`YDocExtension` and stores it under ``serverapp.settings`` dictionary, under the ``"jupyter_collaboration"`` key.
This instance can be used in other extensions to access the public API methods.

For example, to access a read-only view of the shared notebook model in your jupyter-server extension, you can use the :any:`get_document` method:

.. code-block::

collaboration = serverapp.settings["jupyter_collaboration"]
document = collaboration.get_document(
path='Untitled.ipynb',
content_type="notebook",
file_format="json"
)
content = document.get()


API Reference
-------------

.. automodule:: jupyter_collaboration.app
:members:
:inherited-members:
Expand Down
51 changes: 49 additions & 2 deletions jupyter_collaboration/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,26 @@
from __future__ import annotations

import asyncio
from typing import Literal

from jupyter_server.extension.application import ExtensionApp
from jupyter_ydoc import ydocs as YDOCS
from jupyter_ydoc.ybasedoc import YBaseDoc
from pycrdt import Doc
from pycrdt_websocket.ystore import BaseYStore
from traitlets import Bool, Float, Type

from .handlers import DocSessionHandler, YDocWebSocketHandler
from .loaders import FileLoaderMapping
from .rooms import DocumentRoom
from .stores import SQLiteYStore
from .utils import AWARENESS_EVENTS_SCHEMA_PATH, EVENTS_SCHEMA_PATH
from .websocketserver import JupyterWebsocketServer
from .utils import (
AWARENESS_EVENTS_SCHEMA_PATH,
EVENTS_SCHEMA_PATH,
encode_file_path,
room_id_from_encoded_path,
)
from .websocketserver import JupyterWebsocketServer, RoomNotFound


class YDocExtension(ExtensionApp):
Expand Down Expand Up @@ -124,6 +134,43 @@ def initialize_handlers(self):
]
)

async def get_document(
self: YDocExtension,
*,
path: str,
content_type: Literal["notebook", "file"],
file_format: Literal["json", "text"],
copy: bool = True,
) -> YBaseDoc | None:
"""Get a view of the shared model for the matching document.

If `copy=True`, the returned shared model is a fork, meaning that any changes
made to it will not be propagated to the shared model used by the application.
"""
file_id_manager = self.serverapp.web_app.settings["file_id_manager"]
file_id = file_id_manager.index(path)

encoded_path = encode_file_path(file_format, content_type, file_id)
room_id = room_id_from_encoded_path(encoded_path)

try:
room = await self.ywebsocket_server.get_room(room_id)
except RoomNotFound:
return None

if isinstance(room, DocumentRoom):
if copy:
update = room.ydoc.get_update()

fork_ydoc = Doc()
fork_ydoc.apply_update(update)

return YDOCS.get(content_type, YDOCS["file"])(fork_ydoc)
krassowski marked this conversation as resolved.
Show resolved Hide resolved
else:
return room._document

return None

async def stop_extension(self):
# Cancel tasks and clean up
await asyncio.wait(
Expand Down
3 changes: 2 additions & 1 deletion jupyter_collaboration/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
LogLevel,
MessageType,
decode_file_path,
room_id_from_encoded_path,
)
from .websocketserver import JupyterWebsocketServer

Expand Down Expand Up @@ -74,7 +75,7 @@ async def prepare(self):
await self._websocket_server.started.wait()

# Get room
self._room_id: str = self.request.path.split("/")[-1]
self._room_id: str = room_id_from_encoded_path(self.request.path)

async with self._room_lock(self._room_id):
if self._websocket_server.room_exists(self._room_id):
Expand Down
5 changes: 5 additions & 0 deletions jupyter_collaboration/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,8 @@ def encode_file_path(format: str, file_type: str, file_id: str) -> str:
path (str): File path.
"""
return f"{format}:{file_type}:{file_id}"


def room_id_from_encoded_path(encoded_path: str) -> str:
"""Transforms the encoded path into a stable room identifier."""
return encoded_path.split("/")[-1]
29 changes: 29 additions & 0 deletions tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

from __future__ import annotations

import pytest

from jupyter_collaboration.stores import SQLiteYStore, TempFileYStore


Expand Down Expand Up @@ -59,3 +61,30 @@ def test_settings_should_change_ystore_class(jp_configurable_serverapp):
settings = app.web_app.settings["jupyter_collaboration_config"]

assert settings["ystore_class"] == TempFileYStore


@pytest.mark.parametrize("copy", [True, False])
async def test_get_document_file(rtc_create_file, jp_serverapp, copy):
path, content = await rtc_create_file("test.txt", "test", store=True)
collaboration = jp_serverapp.web_app.settings["jupyter_collaboration"]
document = await collaboration.get_document(
path=path, content_type="file", file_format="text", copy=copy
)
assert document.get() == content == "test"
await collaboration.stop_extension()


async def test_get_document_file_copy_is_independent(
rtc_create_file, jp_serverapp, rtc_fetch_session
):
path, content = await rtc_create_file("test.txt", "test", store=True)
collaboration = jp_serverapp.web_app.settings["jupyter_collaboration"]
document = await collaboration.get_document(
path=path, content_type="file", file_format="text", copy=True
)
document.set("other")
fresh_copy = await collaboration.get_document(
path=path, content_type="file", file_format="text"
)
assert fresh_copy.get() == "test"
await collaboration.stop_extension()
Loading