jupyter-server-ydoc 1.0.0b6__tar.gz → 1.0.0rc0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {jupyter_server_ydoc-1.0.0b6 → jupyter_server_ydoc-1.0.0rc0}/PKG-INFO +3 -3
- jupyter_server_ydoc-1.0.0rc0/jupyter_server_ydoc/_version.py +1 -0
- {jupyter_server_ydoc-1.0.0b6 → jupyter_server_ydoc-1.0.0rc0}/jupyter_server_ydoc/handlers.py +31 -27
- {jupyter_server_ydoc-1.0.0b6 → jupyter_server_ydoc-1.0.0rc0}/jupyter_server_ydoc/loaders.py +24 -2
- {jupyter_server_ydoc-1.0.0b6 → jupyter_server_ydoc-1.0.0rc0}/jupyter_server_ydoc/rooms.py +9 -2
- {jupyter_server_ydoc-1.0.0b6 → jupyter_server_ydoc-1.0.0rc0}/jupyter_server_ydoc/test_utils.py +3 -0
- {jupyter_server_ydoc-1.0.0b6 → jupyter_server_ydoc-1.0.0rc0}/pyproject.toml +2 -2
- {jupyter_server_ydoc-1.0.0b6 → jupyter_server_ydoc-1.0.0rc0}/tests/test_rooms.py +21 -0
- jupyter_server_ydoc-1.0.0b6/jupyter_server_ydoc/_version.py +0 -1
- {jupyter_server_ydoc-1.0.0b6 → jupyter_server_ydoc-1.0.0rc0}/.gitignore +0 -0
- {jupyter_server_ydoc-1.0.0b6 → jupyter_server_ydoc-1.0.0rc0}/LICENSE +0 -0
- {jupyter_server_ydoc-1.0.0b6 → jupyter_server_ydoc-1.0.0rc0}/README.md +0 -0
- {jupyter_server_ydoc-1.0.0b6 → jupyter_server_ydoc-1.0.0rc0}/jupyter-config/jupyter_server_ydoc.json +0 -0
- {jupyter_server_ydoc-1.0.0b6 → jupyter_server_ydoc-1.0.0rc0}/jupyter_server_ydoc/__init__.py +0 -0
- {jupyter_server_ydoc-1.0.0b6 → jupyter_server_ydoc-1.0.0rc0}/jupyter_server_ydoc/app.py +0 -0
- {jupyter_server_ydoc-1.0.0b6 → jupyter_server_ydoc-1.0.0rc0}/jupyter_server_ydoc/events/awareness.yaml +0 -0
- {jupyter_server_ydoc-1.0.0b6 → jupyter_server_ydoc-1.0.0rc0}/jupyter_server_ydoc/events/session.yaml +0 -0
- {jupyter_server_ydoc-1.0.0b6 → jupyter_server_ydoc-1.0.0rc0}/jupyter_server_ydoc/pytest_plugin.py +0 -0
- {jupyter_server_ydoc-1.0.0b6 → jupyter_server_ydoc-1.0.0rc0}/jupyter_server_ydoc/stores.py +0 -0
- {jupyter_server_ydoc-1.0.0b6 → jupyter_server_ydoc-1.0.0rc0}/jupyter_server_ydoc/utils.py +0 -0
- {jupyter_server_ydoc-1.0.0b6 → jupyter_server_ydoc-1.0.0rc0}/jupyter_server_ydoc/websocketserver.py +0 -0
- {jupyter_server_ydoc-1.0.0b6 → jupyter_server_ydoc-1.0.0rc0}/setup.py +0 -0
- {jupyter_server_ydoc-1.0.0b6 → jupyter_server_ydoc-1.0.0rc0}/tests/__init__.py +0 -0
- {jupyter_server_ydoc-1.0.0b6 → jupyter_server_ydoc-1.0.0rc0}/tests/conftest.py +0 -0
- {jupyter_server_ydoc-1.0.0b6 → jupyter_server_ydoc-1.0.0rc0}/tests/test_app.py +0 -0
- {jupyter_server_ydoc-1.0.0b6 → jupyter_server_ydoc-1.0.0rc0}/tests/test_documents.py +0 -0
- {jupyter_server_ydoc-1.0.0b6 → jupyter_server_ydoc-1.0.0rc0}/tests/test_handlers.py +0 -0
- {jupyter_server_ydoc-1.0.0b6 → jupyter_server_ydoc-1.0.0rc0}/tests/test_loaders.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: jupyter-server-ydoc
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.0rc0
|
|
4
4
|
Summary: jupyter-server extension integrating collaborative shared models.
|
|
5
5
|
Author-email: Jupyter Development Team <jupyter@googlegroups.com>
|
|
6
6
|
License: # Licensing terms
|
|
@@ -79,9 +79,9 @@ Requires-Dist: jsonschema>=4.18.0
|
|
|
79
79
|
Requires-Dist: jupyter-events>=0.10.0
|
|
80
80
|
Requires-Dist: jupyter-server-fileid<1,>=0.7.0
|
|
81
81
|
Requires-Dist: jupyter-server<3.0.0,>=2.11.1
|
|
82
|
-
Requires-Dist: jupyter-ydoc<4.0.0,>=2.
|
|
82
|
+
Requires-Dist: jupyter-ydoc<4.0.0,>=2.1.2
|
|
83
83
|
Requires-Dist: pycrdt
|
|
84
|
-
Requires-Dist: pycrdt-websocket<0.
|
|
84
|
+
Requires-Dist: pycrdt-websocket<0.16.0,>=0.15.0
|
|
85
85
|
Provides-Extra: test
|
|
86
86
|
Requires-Dist: coverage; extra == 'test'
|
|
87
87
|
Requires-Dist: importlib-metadata>=4.8.3; (python_version < '3.10') and extra == 'test'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.0rc0"
|
{jupyter_server_ydoc-1.0.0b6 → jupyter_server_ydoc-1.0.0rc0}/jupyter_server_ydoc/handlers.py
RENAMED
|
@@ -8,14 +8,14 @@ import json
|
|
|
8
8
|
import time
|
|
9
9
|
import uuid
|
|
10
10
|
from logging import Logger
|
|
11
|
-
from typing import Any
|
|
11
|
+
from typing import Any, Literal
|
|
12
12
|
from uuid import uuid4
|
|
13
13
|
|
|
14
14
|
from jupyter_server.auth import authorized
|
|
15
15
|
from jupyter_server.base.handlers import APIHandler, JupyterHandler
|
|
16
16
|
from jupyter_server.utils import ensure_async
|
|
17
17
|
from jupyter_ydoc import ydocs as YDOCS
|
|
18
|
-
from pycrdt import Doc, UndoManager,
|
|
18
|
+
from pycrdt import Doc, UndoManager, write_var_uint
|
|
19
19
|
from pycrdt_websocket.websocket_server import YRoom
|
|
20
20
|
from pycrdt_websocket.ystore import BaseYStore
|
|
21
21
|
from tornado import web
|
|
@@ -137,6 +137,10 @@ class YDocWebSocketHandler(WebSocketHandler, JupyterHandler):
|
|
|
137
137
|
exception_handler=exception_logger,
|
|
138
138
|
)
|
|
139
139
|
|
|
140
|
+
if self._room_id == "JupyterLab:globalAwareness":
|
|
141
|
+
# Listen for the changes in GlobalAwareness to update users
|
|
142
|
+
self.room.awareness.observe(self._on_global_awareness_event)
|
|
143
|
+
|
|
140
144
|
try:
|
|
141
145
|
await self._websocket_server.start_room(self.room)
|
|
142
146
|
except Exception as e:
|
|
@@ -286,31 +290,6 @@ class YDocWebSocketHandler(WebSocketHandler, JupyterHandler):
|
|
|
286
290
|
"""
|
|
287
291
|
message_type = message[0]
|
|
288
292
|
|
|
289
|
-
if message_type == YMessageType.AWARENESS:
|
|
290
|
-
# awareness
|
|
291
|
-
skip = False
|
|
292
|
-
changes = self.room.awareness.get_changes(message[1:])
|
|
293
|
-
added_users = changes["added"]
|
|
294
|
-
removed_users = changes["removed"]
|
|
295
|
-
for i, user in enumerate(added_users):
|
|
296
|
-
u = changes["states"][i]
|
|
297
|
-
if "user" in u:
|
|
298
|
-
name = u["user"]["name"]
|
|
299
|
-
self._websocket_server.connected_users[user] = name
|
|
300
|
-
self.log.debug("Y user joined: %s", name)
|
|
301
|
-
for user in removed_users:
|
|
302
|
-
if user in self._websocket_server.connected_users:
|
|
303
|
-
name = self._websocket_server.connected_users[user]
|
|
304
|
-
del self._websocket_server.connected_users[user]
|
|
305
|
-
self.log.debug("Y user left: %s", name)
|
|
306
|
-
# filter out message depending on changes
|
|
307
|
-
if skip:
|
|
308
|
-
self.log.debug(
|
|
309
|
-
"Filtered out Y message of type: %s",
|
|
310
|
-
YMessageType(message_type).name,
|
|
311
|
-
)
|
|
312
|
-
return skip
|
|
313
|
-
|
|
314
293
|
if message_type == MessageType.CHAT:
|
|
315
294
|
msg = message[2:].decode("utf-8")
|
|
316
295
|
|
|
@@ -405,6 +384,31 @@ class YDocWebSocketHandler(WebSocketHandler, JupyterHandler):
|
|
|
405
384
|
self._emit(LogLevel.INFO, "clean", "Loader deleted.")
|
|
406
385
|
del self._room_locks[self._room_id]
|
|
407
386
|
|
|
387
|
+
def _on_global_awareness_event(
|
|
388
|
+
self, topic: Literal["change", "update"], changes: tuple[dict[str, Any], Any]
|
|
389
|
+
) -> None:
|
|
390
|
+
"""
|
|
391
|
+
Update the users when the global awareness changes.
|
|
392
|
+
|
|
393
|
+
Parameters:
|
|
394
|
+
topic (str): `"update"` or `"change"` (`"change"` is triggered only if the states are modified).
|
|
395
|
+
changes (tuple[dict[str, Any], Any]): The changes and the origin of the changes.
|
|
396
|
+
"""
|
|
397
|
+
if topic != "change":
|
|
398
|
+
return
|
|
399
|
+
added_users = changes[0]["added"]
|
|
400
|
+
removed_users = changes[0]["removed"]
|
|
401
|
+
for user in added_users:
|
|
402
|
+
u = self.room.awareness.states[user]
|
|
403
|
+
if "user" in u:
|
|
404
|
+
name = u["user"]["name"]
|
|
405
|
+
self._websocket_server.connected_users[user] = name
|
|
406
|
+
self.log.debug("Y user joined: %s", name)
|
|
407
|
+
for user in removed_users:
|
|
408
|
+
if user in self._websocket_server.connected_users:
|
|
409
|
+
name = self._websocket_server.connected_users.pop(user)
|
|
410
|
+
self.log.debug("Y user left: %s", name)
|
|
411
|
+
|
|
408
412
|
def check_origin(self, origin):
|
|
409
413
|
"""
|
|
410
414
|
Check origin
|
|
@@ -39,9 +39,11 @@ class FileLoader:
|
|
|
39
39
|
|
|
40
40
|
self._log = log or getLogger(__name__)
|
|
41
41
|
self._subscriptions: dict[str, Callable[[], Coroutine[Any, Any, None]]] = {}
|
|
42
|
+
self._filepath_subscriptions: dict[str, Callable[[], Coroutine[Any, Any, None] | None]] = {}
|
|
42
43
|
|
|
43
44
|
self._watcher = asyncio.create_task(self._watch_file()) if self._poll_interval else None
|
|
44
45
|
self.last_modified = None
|
|
46
|
+
self._current_path = self.path
|
|
45
47
|
|
|
46
48
|
@property
|
|
47
49
|
def file_id(self) -> str:
|
|
@@ -79,7 +81,12 @@ class FileLoader:
|
|
|
79
81
|
except asyncio.CancelledError:
|
|
80
82
|
self._log.info(f"file watcher for '{self.file_id}' is cancelled now")
|
|
81
83
|
|
|
82
|
-
def observe(
|
|
84
|
+
def observe(
|
|
85
|
+
self,
|
|
86
|
+
id: str,
|
|
87
|
+
callback: Callable[[], Coroutine[Any, Any, None]],
|
|
88
|
+
filepath_callback: Callable[[], Coroutine[Any, Any, None] | None] | None = None,
|
|
89
|
+
) -> None:
|
|
83
90
|
"""
|
|
84
91
|
Subscribe to the file to get notified about out-of-band file changes.
|
|
85
92
|
|
|
@@ -88,6 +95,8 @@ class FileLoader:
|
|
|
88
95
|
callback (Callable): Callback for notifying the room.
|
|
89
96
|
"""
|
|
90
97
|
self._subscriptions[id] = callback
|
|
98
|
+
if filepath_callback is not None:
|
|
99
|
+
self._filepath_subscriptions[id] = filepath_callback
|
|
91
100
|
|
|
92
101
|
def unobserve(self, id: str) -> None:
|
|
93
102
|
"""
|
|
@@ -97,6 +106,8 @@ class FileLoader:
|
|
|
97
106
|
id (str): Room ID
|
|
98
107
|
"""
|
|
99
108
|
del self._subscriptions[id]
|
|
109
|
+
if id in self._filepath_subscriptions.keys():
|
|
110
|
+
del self._filepath_subscriptions[id]
|
|
100
111
|
|
|
101
112
|
async def load_content(self, format: str, file_type: str) -> dict[str, Any]:
|
|
102
113
|
"""
|
|
@@ -204,15 +215,26 @@ class FileLoader:
|
|
|
204
215
|
Notifies subscribed rooms about out-of-band file changes.
|
|
205
216
|
"""
|
|
206
217
|
do_notify = False
|
|
218
|
+
filepath_change = False
|
|
207
219
|
async with self._lock:
|
|
220
|
+
path = self.path
|
|
221
|
+
if self._current_path != path:
|
|
222
|
+
self._current_path = path
|
|
223
|
+
filepath_change = True
|
|
224
|
+
|
|
208
225
|
# Get model metadata; format and type are not need
|
|
209
|
-
model = await ensure_async(self._contents_manager.get(
|
|
226
|
+
model = await ensure_async(self._contents_manager.get(path, content=False))
|
|
210
227
|
|
|
211
228
|
if self.last_modified is not None and self.last_modified < model["last_modified"]:
|
|
212
229
|
do_notify = True
|
|
213
230
|
|
|
214
231
|
self.last_modified = model["last_modified"]
|
|
215
232
|
|
|
233
|
+
if filepath_change:
|
|
234
|
+
# Notify filepath change
|
|
235
|
+
for callback in self._filepath_subscriptions.values():
|
|
236
|
+
await ensure_async(callback())
|
|
237
|
+
|
|
216
238
|
if do_notify:
|
|
217
239
|
# Notify out-of-band change
|
|
218
240
|
# callbacks will load the file content, thus release the lock before calling them
|
|
@@ -41,7 +41,8 @@ class DocumentRoom(YRoom):
|
|
|
41
41
|
self._file_format: str = file_format
|
|
42
42
|
self._file_type: str = file_type
|
|
43
43
|
self._file: FileLoader = file
|
|
44
|
-
self._document = YDOCS.get(self._file_type, YFILE)(self.ydoc)
|
|
44
|
+
self._document = YDOCS.get(self._file_type, YFILE)(self.ydoc, self.awareness)
|
|
45
|
+
self._document.path = self._file.path
|
|
45
46
|
|
|
46
47
|
self._logger = logger
|
|
47
48
|
self._save_delay = save_delay
|
|
@@ -54,7 +55,7 @@ class DocumentRoom(YRoom):
|
|
|
54
55
|
|
|
55
56
|
# Listen for document changes
|
|
56
57
|
self._document.observe(self._on_document_change)
|
|
57
|
-
self._file.observe(self.room_id, self._on_outofband_change)
|
|
58
|
+
self._file.observe(self.room_id, self._on_outofband_change, self._on_filepath_change)
|
|
58
59
|
|
|
59
60
|
@property
|
|
60
61
|
def file_format(self) -> str:
|
|
@@ -225,6 +226,12 @@ class DocumentRoom(YRoom):
|
|
|
225
226
|
self._document.source = model["content"]
|
|
226
227
|
self._document.dirty = False
|
|
227
228
|
|
|
229
|
+
def _on_filepath_change(self) -> None:
|
|
230
|
+
"""
|
|
231
|
+
Update the document path property.
|
|
232
|
+
"""
|
|
233
|
+
self._document.path = self._file.path
|
|
234
|
+
|
|
228
235
|
def _on_document_change(self, target: str, event: Any) -> None:
|
|
229
236
|
"""
|
|
230
237
|
Called when the shared document changes.
|
|
@@ -29,9 +29,9 @@ authors = [
|
|
|
29
29
|
]
|
|
30
30
|
dependencies = [
|
|
31
31
|
"jupyter_server>=2.11.1,<3.0.0",
|
|
32
|
-
"jupyter_ydoc>=2.
|
|
32
|
+
"jupyter_ydoc>=2.1.2,<4.0.0",
|
|
33
33
|
"pycrdt",
|
|
34
|
-
"pycrdt-websocket>=0.
|
|
34
|
+
"pycrdt-websocket>=0.15.0,<0.16.0",
|
|
35
35
|
"jupyter_events>=0.10.0",
|
|
36
36
|
"jupyter_server_fileid>=0.7.0,<1",
|
|
37
37
|
"jsonschema>=4.18.0"
|
|
@@ -75,3 +75,24 @@ async def test_undefined_save_delay_should_not_save_content_after_document_chang
|
|
|
75
75
|
await asyncio.sleep(0.15)
|
|
76
76
|
|
|
77
77
|
assert "save" not in cm.actions
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# The following test should be restored when package versions are fixed.
|
|
81
|
+
|
|
82
|
+
# async def test_document_path(rtc_create_mock_document_room):
|
|
83
|
+
# id = "test-id"
|
|
84
|
+
# path = "test.txt"
|
|
85
|
+
# new_path = "test2.txt"
|
|
86
|
+
|
|
87
|
+
# _, loader, room = rtc_create_mock_document_room(id, path, "")
|
|
88
|
+
|
|
89
|
+
# await room.initialize()
|
|
90
|
+
# assert room._document.path == path
|
|
91
|
+
|
|
92
|
+
# # Update the path
|
|
93
|
+
# loader._file_id_manager.move(id, new_path)
|
|
94
|
+
|
|
95
|
+
# # Wait for a bit more than the poll_interval
|
|
96
|
+
# await asyncio.sleep(0.15)
|
|
97
|
+
|
|
98
|
+
# assert room._document.path == new_path
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.0.0b6"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{jupyter_server_ydoc-1.0.0b6 → jupyter_server_ydoc-1.0.0rc0}/jupyter-config/jupyter_server_ydoc.json
RENAMED
|
File without changes
|
{jupyter_server_ydoc-1.0.0b6 → jupyter_server_ydoc-1.0.0rc0}/jupyter_server_ydoc/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{jupyter_server_ydoc-1.0.0b6 → jupyter_server_ydoc-1.0.0rc0}/jupyter_server_ydoc/events/session.yaml
RENAMED
|
File without changes
|
{jupyter_server_ydoc-1.0.0b6 → jupyter_server_ydoc-1.0.0rc0}/jupyter_server_ydoc/pytest_plugin.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{jupyter_server_ydoc-1.0.0b6 → jupyter_server_ydoc-1.0.0rc0}/jupyter_server_ydoc/websocketserver.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|