jupyter-server-ydoc 1.0.0b6__py3-none-any.whl → 1.0.0rc0__py3-none-any.whl

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.
@@ -1 +1 @@
1
- __version__ = "1.0.0b6"
1
+ __version__ = "1.0.0rc0"
@@ -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, YMessageType, write_var_uint
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(self, id: str, callback: Callable[[], Coroutine[Any, Any, None]]) -> None:
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(self.path, content=False))
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.
@@ -16,6 +16,9 @@ class FakeFileIDManager:
16
16
  def get_path(self, id: str) -> str:
17
17
  return self.mapping[id]
18
18
 
19
+ def move(self, id: str, new_path: str) -> None:
20
+ self.mapping[id] = new_path
21
+
19
22
 
20
23
  class FakeContentsManager:
21
24
  def __init__(self, model: dict):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: jupyter-server-ydoc
3
- Version: 1.0.0b6
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.0.0
82
+ Requires-Dist: jupyter-ydoc<4.0.0,>=2.1.2
83
83
  Requires-Dist: pycrdt
84
- Requires-Dist: pycrdt-websocket<0.15.0,>=0.14.2
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,18 @@
1
+ jupyter_server_ydoc/__init__.py,sha256=B8H7XLhzgrTCQD8304Lx91FYXslwabsnV9OuYu4M4Hw,346
2
+ jupyter_server_ydoc/_version.py,sha256=ADMFoxfJGF9NOqRkj97eB9fanmTmrwk7WkC0txwvWIg,25
3
+ jupyter_server_ydoc/app.py,sha256=8vOKWLYa4OKu8Pw24TLWtbgs2SfH0_R64dPrErdWyQM,7739
4
+ jupyter_server_ydoc/handlers.py,sha256=xfcTzrOV08wJVIwxHhp9DdVH3ogEY5E_ktLpzBXV5qE,24016
5
+ jupyter_server_ydoc/loaders.py,sha256=TijilImdgYk9K91cXEIP_DzkOr6phSddwQFpLI5l_RA,10564
6
+ jupyter_server_ydoc/pytest_plugin.py,sha256=pSw5KGHid4qRgu1PqQ-GiNOH7IBSWudXQX21J43cB3o,6805
7
+ jupyter_server_ydoc/rooms.py,sha256=szOAfMldhQIrmVpqoF75O0_KXY54X_TrzJz6vpjR6kE,12254
8
+ jupyter_server_ydoc/stores.py,sha256=_5J6eNs3R5Tv88PCc-GGuszxQstfvNoBCYABqzBzJXA,1004
9
+ jupyter_server_ydoc/test_utils.py,sha256=IFXUPf1efHd4DgC1GT7ZMJMhKryKlB0Lx4vU2-mhz4Q,1540
10
+ jupyter_server_ydoc/utils.py,sha256=yQC-uRdLyFDYbt2Zms_hA1HyjlwznMK4yQ3_FUwTlnQ,2013
11
+ jupyter_server_ydoc/websocketserver.py,sha256=7fLPJcWczD-4R_-LXtfvNxM_pUXFasZWDmT4RIrOQHE,5150
12
+ jupyter_server_ydoc/events/awareness.yaml,sha256=9isoK58uue7lqMnlHqyfQt29z16Otkh14oRe1k5vbKM,753
13
+ jupyter_server_ydoc/events/session.yaml,sha256=A7Wt7czyx38MXp5fpDbH7HLS0QNkeOqaEhHdP2x-0Mo,1594
14
+ jupyter_server_ydoc-1.0.0rc0.data/data/etc/jupyter/jupyter_server_config.d/jupyter_collaboration.json,sha256=0thh2hJUxAKkZSmneJMG0U6QJRjdM6zGlwrTedEt-Jk,94
15
+ jupyter_server_ydoc-1.0.0rc0.dist-info/METADATA,sha256=bqsJxI6LGBSIs8SBfmPyTJXv-HAlBScjBh7Fcjb_kmc,5013
16
+ jupyter_server_ydoc-1.0.0rc0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
17
+ jupyter_server_ydoc-1.0.0rc0.dist-info/licenses/LICENSE,sha256=mhO0ZW9EiWOPg0dUgB-lNbJ0CGwRmTdbeAg_se1SOnY,2833
18
+ jupyter_server_ydoc-1.0.0rc0.dist-info/RECORD,,
@@ -1,18 +0,0 @@
1
- jupyter_server_ydoc/__init__.py,sha256=B8H7XLhzgrTCQD8304Lx91FYXslwabsnV9OuYu4M4Hw,346
2
- jupyter_server_ydoc/_version.py,sha256=ajvVBO3s82jFaWgmuxKgMBKuik170Y7v3eDsSfXF7-o,24
3
- jupyter_server_ydoc/app.py,sha256=8vOKWLYa4OKu8Pw24TLWtbgs2SfH0_R64dPrErdWyQM,7739
4
- jupyter_server_ydoc/handlers.py,sha256=delGUrN9pslVmkxgDEolbemLvOGHW4Sb8m41IOuQBZA,23816
5
- jupyter_server_ydoc/loaders.py,sha256=3k2xiXNyzhC9CXAW0gkgac-8YTFwEuSa1QdmI_xHoYc,9733
6
- jupyter_server_ydoc/pytest_plugin.py,sha256=pSw5KGHid4qRgu1PqQ-GiNOH7IBSWudXQX21J43cB3o,6805
7
- jupyter_server_ydoc/rooms.py,sha256=aqiF-6wGMsgwvT5toos6ax2b__h1B0WY-uVf6O_FXas,12009
8
- jupyter_server_ydoc/stores.py,sha256=_5J6eNs3R5Tv88PCc-GGuszxQstfvNoBCYABqzBzJXA,1004
9
- jupyter_server_ydoc/test_utils.py,sha256=QEtV4d_ZNe9g-EOTsoMoqT1qCbGEu2qMlPmPxliog80,1451
10
- jupyter_server_ydoc/utils.py,sha256=yQC-uRdLyFDYbt2Zms_hA1HyjlwznMK4yQ3_FUwTlnQ,2013
11
- jupyter_server_ydoc/websocketserver.py,sha256=7fLPJcWczD-4R_-LXtfvNxM_pUXFasZWDmT4RIrOQHE,5150
12
- jupyter_server_ydoc/events/awareness.yaml,sha256=9isoK58uue7lqMnlHqyfQt29z16Otkh14oRe1k5vbKM,753
13
- jupyter_server_ydoc/events/session.yaml,sha256=A7Wt7czyx38MXp5fpDbH7HLS0QNkeOqaEhHdP2x-0Mo,1594
14
- jupyter_server_ydoc-1.0.0b6.data/data/etc/jupyter/jupyter_server_config.d/jupyter_collaboration.json,sha256=0thh2hJUxAKkZSmneJMG0U6QJRjdM6zGlwrTedEt-Jk,94
15
- jupyter_server_ydoc-1.0.0b6.dist-info/METADATA,sha256=8-iKo3w7Y0tCmVzBOllbS9Wys_eSzlCTIHt-hmgh0Mc,5012
16
- jupyter_server_ydoc-1.0.0b6.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
17
- jupyter_server_ydoc-1.0.0b6.dist-info/licenses/LICENSE,sha256=mhO0ZW9EiWOPg0dUgB-lNbJ0CGwRmTdbeAg_se1SOnY,2833
18
- jupyter_server_ydoc-1.0.0b6.dist-info/RECORD,,