Skip to content

Commit

Permalink
Implement get_document() API
Browse files Browse the repository at this point in the history
  • Loading branch information
krassowski committed Apr 1, 2024
1 parent 7898b25 commit 1048a24
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 3 deletions.
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
47 changes: 45 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 @@ -112,6 +122,39 @@ def initialize_handlers(self):
]
)

async def get_document(
self: YDocExtension,
*,
path: str,
content_type: Literal["notebook", "file"],
file_format: Literal["json", "text"],
) -> YBaseDoc | None:
"""Get a read-only view of the shared model for the matching document.
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):
update = room.ydoc.get_update()

fork_ydoc = Doc()
fork_ydoc.apply_update(update)

return YDOCS.get(content_type, YDOCS["file"])(fork_ydoc)

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]
22 changes: 22 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 json

from jupyter_collaboration.stores import SQLiteYStore, TempFileYStore


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

assert settings["ystore_class"] == TempFileYStore


async def test_get_document_file(rtc_create_file, jp_serverapp):
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")
assert document.get() == content == "test"
await collaboration.stop_extension()


async def test_get_document_file_is_a_fork(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")
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()

0 comments on commit 1048a24

Please sign in to comment.