Skip to content

Commit fc85cdb

Browse files
committed
Implement get_document() API
1 parent 3ccda21 commit fc85cdb

File tree

5 files changed

+91
-3
lines changed

5 files changed

+91
-3
lines changed

docs/source/developer/python_api.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,25 @@
44
Python API
55
==========
66

7+
``jupyter_collaboration`` instantiates :any:`YDocExtension` and stores it under ``serverapp.settings`` dictionary, under the ``"jupyter_collaboration"`` key.
8+
This instance can be used in other extensions to access the public API methods.
9+
10+
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:
11+
12+
.. code-block::
13+
14+
collaboration = serverapp.settings["jupyter_collaboration"]
15+
document = collaboration.get_document(
16+
path='Untitled.ipynb',
17+
content_type="notebook",
18+
file_format="json"
19+
)
20+
content = document.get()
21+
22+
23+
API Reference
24+
-------------
25+
726
.. automodule:: jupyter_collaboration.app
827
:members:
928
:inherited-members:

jupyter_collaboration/app.py

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,26 @@
33
from __future__ import annotations
44

55
import asyncio
6+
from typing import Literal
67

78
from jupyter_server.extension.application import ExtensionApp
9+
from jupyter_ydoc import ydocs as YDOCS
10+
from jupyter_ydoc.ybasedoc import YBaseDoc
11+
from pycrdt import Doc
812
from pycrdt_websocket.ystore import BaseYStore
913
from traitlets import Bool, Float, Type
1014

1115
from .handlers import DocSessionHandler, YDocWebSocketHandler
1216
from .loaders import FileLoaderMapping
17+
from .rooms import DocumentRoom
1318
from .stores import SQLiteYStore
14-
from .utils import AWARENESS_EVENTS_SCHEMA_PATH, EVENTS_SCHEMA_PATH
15-
from .websocketserver import JupyterWebsocketServer
19+
from .utils import (
20+
AWARENESS_EVENTS_SCHEMA_PATH,
21+
EVENTS_SCHEMA_PATH,
22+
encode_file_path,
23+
room_id_from_encoded_path,
24+
)
25+
from .websocketserver import JupyterWebsocketServer, RoomNotFound
1626

1727

1828
class YDocExtension(ExtensionApp):
@@ -124,6 +134,39 @@ def initialize_handlers(self):
124134
]
125135
)
126136

137+
async def get_document(
138+
self: YDocExtension,
139+
*,
140+
path: str,
141+
content_type: Literal["notebook", "file"],
142+
file_format: Literal["json", "text"],
143+
) -> YBaseDoc | None:
144+
"""Get a read-only view of the shared model for the matching document.
145+
146+
The returned shared model is a fork, meaning that any changes made to it
147+
will not be propagated to the shared model used by the application.
148+
"""
149+
file_id_manager = self.serverapp.web_app.settings["file_id_manager"]
150+
file_id = file_id_manager.index(path)
151+
152+
encoded_path = encode_file_path(file_format, content_type, file_id)
153+
room_id = room_id_from_encoded_path(encoded_path)
154+
155+
try:
156+
room = await self.ywebsocket_server.get_room(room_id)
157+
except RoomNotFound:
158+
return None
159+
160+
if isinstance(room, DocumentRoom):
161+
update = room.ydoc.get_update()
162+
163+
fork_ydoc = Doc()
164+
fork_ydoc.apply_update(update)
165+
166+
return YDOCS.get(content_type, YDOCS["file"])(fork_ydoc)
167+
168+
return None
169+
127170
async def stop_extension(self):
128171
# Cancel tasks and clean up
129172
await asyncio.wait(

jupyter_collaboration/handlers.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
LogLevel,
2727
MessageType,
2828
decode_file_path,
29+
room_id_from_encoded_path,
2930
)
3031
from .websocketserver import JupyterWebsocketServer
3132

@@ -74,7 +75,7 @@ async def prepare(self):
7475
await self._websocket_server.started.wait()
7576

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

7980
async with self._room_lock(self._room_id):
8081
if self._websocket_server.room_exists(self._room_id):

jupyter_collaboration/utils.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,8 @@ def encode_file_path(format: str, file_type: str, file_id: str) -> str:
7070
path (str): File path.
7171
"""
7272
return f"{format}:{file_type}:{file_id}"
73+
74+
75+
def room_id_from_encoded_path(encoded_path: str) -> str:
76+
"""Transforms the encoded path into a stable room identifier."""
77+
return encoded_path.split("/")[-1]

tests/test_app.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,23 @@ def test_settings_should_change_ystore_class(jp_configurable_serverapp):
5959
settings = app.web_app.settings["jupyter_collaboration_config"]
6060

6161
assert settings["ystore_class"] == TempFileYStore
62+
63+
64+
async def test_get_document_file(rtc_create_file, jp_serverapp):
65+
path, content = await rtc_create_file("test.txt", "test", store=True)
66+
collaboration = jp_serverapp.web_app.settings["jupyter_collaboration"]
67+
document = await collaboration.get_document(path=path, content_type="file", file_format="text")
68+
assert document.get() == content == "test"
69+
await collaboration.stop_extension()
70+
71+
72+
async def test_get_document_file_is_a_fork(rtc_create_file, jp_serverapp, rtc_fetch_session):
73+
path, content = await rtc_create_file("test.txt", "test", store=True)
74+
collaboration = jp_serverapp.web_app.settings["jupyter_collaboration"]
75+
document = await collaboration.get_document(path=path, content_type="file", file_format="text")
76+
document.set("other")
77+
fresh_copy = await collaboration.get_document(
78+
path=path, content_type="file", file_format="text"
79+
)
80+
assert fresh_copy.get() == "test"
81+
await collaboration.stop_extension()

0 commit comments

Comments
 (0)