jupyter-server-ydoc 2.0.0rc0__py3-none-any.whl → 2.0.2__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__ = "2.0.0rc0"
1
+ __version__ = "2.0.2"
@@ -105,7 +105,7 @@ class YDocExtension(ExtensionApp):
105
105
  page_config.setdefault("serverSideExecution", self.server_side_execution)
106
106
 
107
107
  # Set configurable parameters to YStore class
108
- ystore_class = partial(self.ystore_class, config=self.config)
108
+ ystore_class: type[BaseYStore] = partial(self.ystore_class, config=self.config) # type:ignore[assignment]
109
109
 
110
110
  self.ywebsocket_server = JupyterWebsocketServer(
111
111
  rooms_ready=False,
@@ -205,7 +205,7 @@ class YDocExtension(ExtensionApp):
205
205
  if copy:
206
206
  update = room.ydoc.get_update()
207
207
 
208
- fork_ydoc = Doc()
208
+ fork_ydoc: Doc = Doc()
209
209
  fork_ydoc.apply_update(update)
210
210
 
211
211
  return YDOCS.get(room.file_type, YDOCS["file"])(fork_ydoc)
@@ -5,18 +5,17 @@ from __future__ import annotations
5
5
 
6
6
  import asyncio
7
7
  import json
8
- import time
9
8
  import uuid
10
9
  from logging import Logger
11
- from typing import Any, Literal
10
+ from typing import Any
12
11
  from uuid import uuid4
13
12
 
14
13
  from jupyter_server.auth import authorized
15
14
  from jupyter_server.base.handlers import APIHandler, JupyterHandler
16
15
  from jupyter_server.utils import ensure_async
17
16
  from jupyter_ydoc import ydocs as YDOCS
18
- from pycrdt import Doc, UndoManager, write_var_uint
19
- from pycrdt_websocket.websocket_server import YRoom
17
+ from pycrdt import Doc, UndoManager
18
+ from pycrdt_websocket.yroom import YRoom
20
19
  from pycrdt_websocket.ystore import BaseYStore
21
20
  from tornado import web
22
21
  from tornado.websocket import WebSocketHandler
@@ -28,7 +27,6 @@ from .utils import (
28
27
  JUPYTER_COLLABORATION_EVENTS_URI,
29
28
  JUPYTER_COLLABORATION_FORK_EVENTS_URI,
30
29
  LogLevel,
31
- MessageType,
32
30
  decode_file_path,
33
31
  encode_file_path,
34
32
  room_id_from_encoded_path,
@@ -117,7 +115,10 @@ class YDocWebSocketHandler(WebSocketHandler, JupyterHandler):
117
115
 
118
116
  file = self._file_loaders[file_id]
119
117
  updates_file_path = f".{file_type}:{file_id}.y"
120
- ystore = self._ystore_class(path=updates_file_path, log=self.log)
118
+ ystore = self._ystore_class(
119
+ path=updates_file_path,
120
+ log=self.log, # type:ignore[call-arg]
121
+ )
121
122
  self.room = DocumentRoom(
122
123
  self._room_id,
123
124
  file_format,
@@ -182,7 +183,7 @@ class YDocWebSocketHandler(WebSocketHandler, JupyterHandler):
182
183
  self._websocket_server = ywebsocket_server
183
184
  self._message_queue = asyncio.Queue()
184
185
  self._room_id = ""
185
- self.room = None
186
+ self.room = None # type:ignore
186
187
 
187
188
  @property
188
189
  def path(self):
@@ -219,7 +220,7 @@ class YDocWebSocketHandler(WebSocketHandler, JupyterHandler):
219
220
  raise web.HTTPError(403)
220
221
  return await super().get(*args, **kwargs)
221
222
 
222
- async def open(self, room_id):
223
+ async def open(self, room_id: str) -> None: # type:ignore[override]
223
224
  """
224
225
  On connection open.
225
226
  """
@@ -259,7 +260,7 @@ class YDocWebSocketHandler(WebSocketHandler, JupyterHandler):
259
260
  )
260
261
 
261
262
  # Clean up the room and delete the file loader
262
- if len(self.room.clients) == 0 or self.room.clients == [self]:
263
+ if len(self.room.clients) == 0 or self.room.clients == {self}:
263
264
  self._message_queue.put_nowait(b"")
264
265
  self._cleanup_delay = 0
265
266
  await self._clean_room()
@@ -290,28 +291,6 @@ class YDocWebSocketHandler(WebSocketHandler, JupyterHandler):
290
291
  """
291
292
  On message receive.
292
293
  """
293
- message_type = message[0]
294
-
295
- if message_type == MessageType.CHAT:
296
- msg = message[2:].decode("utf-8")
297
-
298
- user = self.current_user
299
- data = json.dumps(
300
- {
301
- "sender": user.username,
302
- "timestamp": time.time(),
303
- "content": json.loads(msg),
304
- }
305
- ).encode("utf8")
306
-
307
- for client in self.room.clients:
308
- if client != self:
309
- task = asyncio.create_task(
310
- client.send(bytes([MessageType.CHAT]) + write_var_uint(len(data)) + data)
311
- )
312
- self._websocket_server.background_tasks.add(task)
313
- task.add_done_callback(self._websocket_server.background_tasks.discard)
314
-
315
294
  self._message_queue.put_nowait(message)
316
295
  self._websocket_server.ypatch_nb += 1
317
296
 
@@ -321,7 +300,7 @@ class YDocWebSocketHandler(WebSocketHandler, JupyterHandler):
321
300
  """
322
301
  # stop serving this client
323
302
  self._message_queue.put_nowait(b"")
324
- if isinstance(self.room, DocumentRoom) and self.room.clients == [self]:
303
+ if isinstance(self.room, DocumentRoom) and self.room.clients == {self}:
325
304
  # no client in this room after we disconnect
326
305
  # keep the document for a while in case someone reconnects
327
306
  self.log.info("Cleaning room: %s", self._room_id)
@@ -386,9 +365,7 @@ class YDocWebSocketHandler(WebSocketHandler, JupyterHandler):
386
365
  self._emit(LogLevel.INFO, "clean", "Loader deleted.")
387
366
  del self._room_locks[self._room_id]
388
367
 
389
- def _on_global_awareness_event(
390
- self, topic: Literal["change", "update"], changes: tuple[dict[str, Any], Any]
391
- ) -> None:
368
+ def _on_global_awareness_event(self, topic: str, changes: tuple[dict[str, Any], Any]) -> None:
392
369
  """
393
370
  Update the users when the global awareness changes.
394
371
 
@@ -489,7 +466,7 @@ class TimelineHandler(APIHandler):
489
466
  try:
490
467
  room_id = room_id_from_encoded_path(encoded_path)
491
468
  room: YRoom = await self.ywebsocket_server.get_room(room_id)
492
- fork_ydoc = Doc()
469
+ fork_ydoc: Doc = Doc()
493
470
 
494
471
  ydoc_factory = YDOCS.get(content_type)
495
472
  if ydoc_factory is None:
@@ -505,7 +482,9 @@ class TimelineHandler(APIHandler):
505
482
  FORK_DOCUMENTS[idx] = ydoc_factory(fork_ydoc)
506
483
  undo_manager: UndoManager = FORK_DOCUMENTS[idx].undo_manager
507
484
 
508
- updates_and_timestamps = [(item[0], item[-1]) async for item in room.ystore.read()]
485
+ ystore = room.ystore
486
+ assert ystore
487
+ updates_and_timestamps = [(item[0], item[-1]) async for item in ystore.read()]
509
488
 
510
489
  result_timestamps = []
511
490
 
@@ -649,7 +628,7 @@ class DocForkHandler(APIHandler):
649
628
  return self.finish({"code": 404, "error": "Root room not found"})
650
629
 
651
630
  update = root_room.ydoc.get_update()
652
- fork_ydoc = Doc()
631
+ fork_ydoc: Doc = Doc()
653
632
  fork_ydoc.apply_update(update)
654
633
  model = self.get_json_body()
655
634
  synchronize = model.get("synchronize", False)
@@ -153,6 +153,9 @@ class FileLoader:
153
153
  path, format=model["format"], type=model["type"], content=False
154
154
  )
155
155
  )
156
+ # Skip saving if file is not writable
157
+ if not m["writable"]:
158
+ return None
156
159
 
157
160
  if self.last_modified == m["last_modified"]:
158
161
  self._log.info("Saving file: %s", path)
@@ -200,13 +203,28 @@ class FileLoader:
200
203
  if self._poll_interval is None:
201
204
  return
202
205
 
206
+ consecutive_error_logs = 0
207
+ max_consecutive_logs = 3
208
+ suppression_logged = False
209
+
203
210
  while True:
204
211
  try:
205
212
  await asyncio.sleep(self._poll_interval)
206
213
  try:
207
214
  await self.maybe_notify()
215
+ consecutive_error_logs = 0
216
+ suppression_logged = False
208
217
  except Exception as e:
209
- self._log.error(f"Error watching file: {self.path}\n{e!r}", exc_info=e)
218
+ if consecutive_error_logs < max_consecutive_logs:
219
+ self._log.error(f"Error watching file: {self.path}\n{e!r}", exc_info=e)
220
+ consecutive_error_logs += 1
221
+ elif not suppression_logged:
222
+ self._log.warning(
223
+ "Too many errors while watching %s — suppressing further logs.",
224
+ self.path,
225
+ )
226
+ suppression_logged = True
227
+
210
228
  except asyncio.CancelledError:
211
229
  break
212
230
 
@@ -240,7 +240,12 @@ def rtc_add_doc_to_store(rtc_connect_doc_client):
240
240
 
241
241
  def rtc_create_SQLite_store_factory(jp_serverapp):
242
242
  async def _inner(type: str, path: str, content: str) -> DocumentRoom:
243
- db = SQLiteYStore(path=f"{type}:{path}", config=jp_serverapp.config)
243
+ db = SQLiteYStore(
244
+ path=f"{type}:{path}",
245
+ # `SQLiteYStore` here is a subclass of booth `LoggingConfigurable`
246
+ # and `pycrdt_websocket.ystore.SQLiteYStore`, but mypy gets lost:
247
+ config=jp_serverapp.config, # type:ignore[call-arg]
248
+ )
244
249
  _ = create_task(db.start())
245
250
  await db.started.wait()
246
251
 
@@ -271,13 +276,16 @@ def rtc_create_mock_document_room():
271
276
  last_modified: datetime | None = None,
272
277
  save_delay: float | None = None,
273
278
  store: SQLiteYStore | None = None,
279
+ writable: bool = False,
274
280
  ) -> tuple[FakeContentsManager, FileLoader, DocumentRoom]:
275
281
  paths = {id: path}
276
282
 
277
283
  if last_modified is None:
278
- cm = FakeContentsManager({"content": content})
284
+ cm = FakeContentsManager({"content": content, "writable": writable})
279
285
  else:
280
- cm = FakeContentsManager({"last_modified": datetime.now(), "content": content})
286
+ cm = FakeContentsManager(
287
+ {"last_modified": datetime.now(), "content": content, "writable": writable}
288
+ )
281
289
 
282
290
  loader = FileLoader(
283
291
  id,
@@ -9,7 +9,7 @@ from typing import Any, Callable
9
9
 
10
10
  from jupyter_events import EventLogger
11
11
  from jupyter_ydoc import ydocs as YDOCS
12
- from pycrdt_websocket.websocket_server import YRoom
12
+ from pycrdt_websocket.yroom import YRoom
13
13
  from pycrdt_websocket.ystore import BaseYStore, YDocNotFound
14
14
 
15
15
  from .loaders import FileLoader
@@ -103,7 +103,7 @@ class DocumentRoom(YRoom):
103
103
  It is important to set the ready property in the parent class (`self.ready = True`),
104
104
  this setter will subscribe for updates on the shared document.
105
105
  """
106
- if self.ready: # type: ignore[has-type]
106
+ if self.ready:
107
107
  return
108
108
 
109
109
  self.log.info("Initializing room %s", self._room_id)
@@ -285,9 +285,9 @@ class DocumentRoom(YRoom):
285
285
  "content": self._document.source,
286
286
  }
287
287
  )
288
- async with self._update_lock:
289
- self._document.dirty = False
290
- if saved_model:
288
+ if saved_model:
289
+ async with self._update_lock:
290
+ self._document.dirty = False
291
291
  self._document.hash = saved_model["hash"]
292
292
 
293
293
  self._emit(LogLevel.INFO, "save", "Content saved.")
@@ -7,9 +7,10 @@ import asyncio
7
7
  from logging import Logger
8
8
  from typing import Any, Callable
9
9
 
10
- from pycrdt_websocket.websocket_server import WebsocketServer, YRoom
10
+ from pycrdt_websocket.websocket import Websocket
11
+ from pycrdt_websocket.websocket_server import WebsocketServer
12
+ from pycrdt_websocket.yroom import YRoom
11
13
  from pycrdt_websocket.ystore import BaseYStore
12
- from tornado.websocket import WebSocketHandler
13
14
 
14
15
 
15
16
  class RoomNotFound(LookupError):
@@ -38,7 +39,7 @@ class JupyterWebsocketServer(WebsocketServer):
38
39
 
39
40
  def __init__(
40
41
  self,
41
- ystore_class: BaseYStore,
42
+ ystore_class: type[BaseYStore],
42
43
  rooms_ready: bool = True,
43
44
  auto_clean_rooms: bool = True,
44
45
  exception_handler: Callable[[Exception, Logger], bool] | None = None,
@@ -132,7 +133,7 @@ class JupyterWebsocketServer(WebsocketServer):
132
133
  await self.start_room(room)
133
134
  return room
134
135
 
135
- async def serve(self, websocket: WebSocketHandler) -> None:
136
+ async def serve(self, websocket: Websocket) -> None:
136
137
  # start monitoring here as the event loop is not yet available when initializing the object
137
138
  if self.monitor_task is None:
138
139
  self.monitor_task = asyncio.create_task(self._monitor())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jupyter-server-ydoc
3
- Version: 2.0.0rc0
3
+ Version: 2.0.2
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
@@ -0,0 +1,19 @@
1
+ jupyter_server_ydoc/__init__.py,sha256=B8H7XLhzgrTCQD8304Lx91FYXslwabsnV9OuYu4M4Hw,346
2
+ jupyter_server_ydoc/_version.py,sha256=tATvJM5shAzfspHYjdVwpV2w3-gDA119NlEYi5X2lFY,22
3
+ jupyter_server_ydoc/app.py,sha256=6Zca0acaEFCKyUbXdmcPmVk1Dgu91Y-DFjYR16WKFlg,8161
4
+ jupyter_server_ydoc/handlers.py,sha256=Prsi_IlJa9wl9VszRr-2qKZ8z224u-o7lRYI5SNJBaE,27276
5
+ jupyter_server_ydoc/loaders.py,sha256=XUQqg2EbfQUYlQVjHY183gYKeVZ6x92VHy4EsOQz4fA,11303
6
+ jupyter_server_ydoc/pytest_plugin.py,sha256=MfFqL5xJLGeIuLG8GF1kr2TumRjIIQfxoeNLGjvQjY8,8742
7
+ jupyter_server_ydoc/rooms.py,sha256=u4umGLsudteR6oJvw4pcszzrZTDgzOZkVXZEOfBPk8M,12221
8
+ jupyter_server_ydoc/stores.py,sha256=_5J6eNs3R5Tv88PCc-GGuszxQstfvNoBCYABqzBzJXA,1004
9
+ jupyter_server_ydoc/test_utils.py,sha256=utUwB5FThc_SCQshhUbLNih9GUa5qBcmMgU6-jx0ZnA,2275
10
+ jupyter_server_ydoc/utils.py,sha256=EgKC15js8VOS8-5jGMs4pfHQfV9drnNT2Gew5UlyXZc,2171
11
+ jupyter_server_ydoc/websocketserver.py,sha256=h1yTgJcsCK17_97Ne5x-lbgIFsxylwnltxagcuAlTJY,5185
12
+ jupyter_server_ydoc/events/awareness.yaml,sha256=2FrCci5rZIaU4rn8pIPZJkd132YAZdzKjSNSwjOY7Dk,755
13
+ jupyter_server_ydoc/events/fork.yaml,sha256=3OrhQjhVyLjlBJWMiffbnZodL3GzFafLwEmSBFrK33o,1303
14
+ jupyter_server_ydoc/events/session.yaml,sha256=PS0MxowpRwY5QFYm-LJvHUxKHnsictV8_6VEwfhYxcQ,1596
15
+ jupyter_server_ydoc-2.0.2.data/data/etc/jupyter/jupyter_server_config.d/jupyter_collaboration.json,sha256=0thh2hJUxAKkZSmneJMG0U6QJRjdM6zGlwrTedEt-Jk,94
16
+ jupyter_server_ydoc-2.0.2.dist-info/METADATA,sha256=Wcc5iSM2ZdOp74R__1pRnFDoPYvNK0lm-y9WkQJjBX0,5115
17
+ jupyter_server_ydoc-2.0.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
18
+ jupyter_server_ydoc-2.0.2.dist-info/licenses/LICENSE,sha256=mhO0ZW9EiWOPg0dUgB-lNbJ0CGwRmTdbeAg_se1SOnY,2833
19
+ jupyter_server_ydoc-2.0.2.dist-info/RECORD,,
@@ -1,19 +0,0 @@
1
- jupyter_server_ydoc/__init__.py,sha256=B8H7XLhzgrTCQD8304Lx91FYXslwabsnV9OuYu4M4Hw,346
2
- jupyter_server_ydoc/_version.py,sha256=Cs4kVkiMsFMyfJdp74Cseb_7_XMravPFYhL_jneOp-w,25
3
- jupyter_server_ydoc/app.py,sha256=JqkpijoPdo_qum0CKqbG-I6W8fpC3-v2cFA3kFxK3mg,8111
4
- jupyter_server_ydoc/handlers.py,sha256=Q0A638Ke_TR-hSdAgyz8u1gIOCk3FIqWRhV-QH2PLMM,27977
5
- jupyter_server_ydoc/loaders.py,sha256=TijilImdgYk9K91cXEIP_DzkOr6phSddwQFpLI5l_RA,10564
6
- jupyter_server_ydoc/pytest_plugin.py,sha256=1Y-iNZnEyhajx4HU-40aZ9iRVWcC5ikC5Y8JJHCH0So,8419
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=utUwB5FThc_SCQshhUbLNih9GUa5qBcmMgU6-jx0ZnA,2275
10
- jupyter_server_ydoc/utils.py,sha256=EgKC15js8VOS8-5jGMs4pfHQfV9drnNT2Gew5UlyXZc,2171
11
- jupyter_server_ydoc/websocketserver.py,sha256=7fLPJcWczD-4R_-LXtfvNxM_pUXFasZWDmT4RIrOQHE,5150
12
- jupyter_server_ydoc/events/awareness.yaml,sha256=2FrCci5rZIaU4rn8pIPZJkd132YAZdzKjSNSwjOY7Dk,755
13
- jupyter_server_ydoc/events/fork.yaml,sha256=3OrhQjhVyLjlBJWMiffbnZodL3GzFafLwEmSBFrK33o,1303
14
- jupyter_server_ydoc/events/session.yaml,sha256=PS0MxowpRwY5QFYm-LJvHUxKHnsictV8_6VEwfhYxcQ,1596
15
- jupyter_server_ydoc-2.0.0rc0.data/data/etc/jupyter/jupyter_server_config.d/jupyter_collaboration.json,sha256=0thh2hJUxAKkZSmneJMG0U6QJRjdM6zGlwrTedEt-Jk,94
16
- jupyter_server_ydoc-2.0.0rc0.dist-info/METADATA,sha256=HUXA92OIcOMQmhX9TRjJaYL65EWpUlHJbCS2GUK-ufE,5118
17
- jupyter_server_ydoc-2.0.0rc0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
18
- jupyter_server_ydoc-2.0.0rc0.dist-info/licenses/LICENSE,sha256=mhO0ZW9EiWOPg0dUgB-lNbJ0CGwRmTdbeAg_se1SOnY,2833
19
- jupyter_server_ydoc-2.0.0rc0.dist-info/RECORD,,