jupyter-server-ydoc 1.0.0b3__tar.gz → 1.0.0b4__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.
Files changed (28) hide show
  1. {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b4}/PKG-INFO +2 -2
  2. jupyter_server_ydoc-1.0.0b4/jupyter_server_ydoc/_version.py +1 -0
  3. {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b4}/jupyter_server_ydoc/app.py +21 -1
  4. {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b4}/jupyter_server_ydoc/handlers.py +139 -2
  5. {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b4}/pyproject.toml +1 -1
  6. jupyter_server_ydoc-1.0.0b3/jupyter_server_ydoc/_version.py +0 -1
  7. {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b4}/.gitignore +0 -0
  8. {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b4}/LICENSE +0 -0
  9. {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b4}/README.md +0 -0
  10. {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b4}/jupyter-config/jupyter_server_ydoc.json +0 -0
  11. {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b4}/jupyter_server_ydoc/__init__.py +0 -0
  12. {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b4}/jupyter_server_ydoc/events/awareness.yaml +0 -0
  13. {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b4}/jupyter_server_ydoc/events/session.yaml +0 -0
  14. {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b4}/jupyter_server_ydoc/loaders.py +0 -0
  15. {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b4}/jupyter_server_ydoc/pytest_plugin.py +0 -0
  16. {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b4}/jupyter_server_ydoc/rooms.py +0 -0
  17. {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b4}/jupyter_server_ydoc/stores.py +0 -0
  18. {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b4}/jupyter_server_ydoc/test_utils.py +0 -0
  19. {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b4}/jupyter_server_ydoc/utils.py +0 -0
  20. {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b4}/jupyter_server_ydoc/websocketserver.py +0 -0
  21. {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b4}/setup.py +0 -0
  22. {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b4}/tests/__init__.py +0 -0
  23. {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b4}/tests/conftest.py +0 -0
  24. {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b4}/tests/test_app.py +0 -0
  25. {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b4}/tests/test_documents.py +0 -0
  26. {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b4}/tests/test_handlers.py +0 -0
  27. {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b4}/tests/test_loaders.py +0 -0
  28. {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b4}/tests/test_rooms.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: jupyter-server-ydoc
3
- Version: 1.0.0b3
3
+ Version: 1.0.0b4
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
@@ -81,7 +81,7 @@ Requires-Dist: jupyter-server-fileid<1,>=0.7.0
81
81
  Requires-Dist: jupyter-server<3.0.0,>=2.11.1
82
82
  Requires-Dist: jupyter-ydoc<4.0.0,>=2.0.0
83
83
  Requires-Dist: pycrdt
84
- Requires-Dist: pycrdt-websocket<0.15.0,>=0.14.0
84
+ Requires-Dist: pycrdt-websocket<0.15.0,>=0.14.2
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.0b4"
@@ -13,7 +13,12 @@ from pycrdt import Doc
13
13
  from pycrdt_websocket.ystore import BaseYStore
14
14
  from traitlets import Bool, Float, Type
15
15
 
16
- from .handlers import DocSessionHandler, YDocWebSocketHandler
16
+ from .handlers import (
17
+ DocSessionHandler,
18
+ TimelineHandler,
19
+ UndoRedoHandler,
20
+ YDocWebSocketHandler,
21
+ )
17
22
  from .loaders import FileLoaderMapping
18
23
  from .rooms import DocumentRoom
19
24
  from .stores import SQLiteYStore
@@ -130,6 +135,21 @@ class YDocExtension(ExtensionApp):
130
135
  },
131
136
  ),
132
137
  (r"/api/collaboration/session/(.*)", DocSessionHandler),
138
+ (
139
+ r"/api/collaboration/timeline/(.*)",
140
+ TimelineHandler,
141
+ {
142
+ "ystore_class": self.ystore_class,
143
+ "ywebsocket_server": self.ywebsocket_server,
144
+ },
145
+ ),
146
+ (
147
+ r"/api/collaboration/undo_redo/(.*)",
148
+ UndoRedoHandler,
149
+ {
150
+ "ywebsocket_server": self.ywebsocket_server,
151
+ },
152
+ ),
133
153
  ]
134
154
  )
135
155
 
@@ -9,12 +9,13 @@ import time
9
9
  import uuid
10
10
  from logging import Logger
11
11
  from typing import Any
12
+ from uuid import uuid4
12
13
 
13
14
  from jupyter_server.auth import authorized
14
15
  from jupyter_server.base.handlers import APIHandler, JupyterHandler
15
16
  from jupyter_server.utils import ensure_async
16
17
  from jupyter_ydoc import ydocs as YDOCS
17
- from pycrdt import YMessageType, write_var_uint
18
+ from pycrdt import Doc, UndoManager, YMessageType, write_var_uint
18
19
  from pycrdt_websocket.websocket_server import YRoom
19
20
  from pycrdt_websocket.ystore import BaseYStore
20
21
  from tornado import web
@@ -28,13 +29,16 @@ from .utils import (
28
29
  LogLevel,
29
30
  MessageType,
30
31
  decode_file_path,
32
+ encode_file_path,
31
33
  room_id_from_encoded_path,
32
34
  )
33
- from .websocketserver import JupyterWebsocketServer
35
+ from .websocketserver import JupyterWebsocketServer, RoomNotFound
34
36
 
35
37
  YFILE = YDOCS["file"]
36
38
 
39
+
37
40
  SERVER_SESSION = str(uuid.uuid4())
41
+ FORK_DOCUMENTS = {}
38
42
 
39
43
 
40
44
  class YDocWebSocketHandler(WebSocketHandler, JupyterHandler):
@@ -459,3 +463,136 @@ class DocSessionHandler(APIHandler):
459
463
  )
460
464
  self.set_status(201)
461
465
  return self.finish(data)
466
+
467
+
468
+ class TimelineHandler(APIHandler):
469
+ def initialize(
470
+ self, ystore_class: type[BaseYStore], ywebsocket_server: JupyterWebsocketServer
471
+ ) -> None:
472
+ self.ystore_class = ystore_class
473
+ self.ywebsocket_server = ywebsocket_server
474
+
475
+ async def get(self, path: str) -> None:
476
+ idx = uuid4().hex
477
+ file_id_manager = self.settings["file_id_manager"]
478
+ file_id = file_id_manager.get_id(path)
479
+
480
+ format = str(self.request.query_arguments.get("format")[0].decode("utf-8"))
481
+ content_type = str(self.request.query_arguments.get("type")[0].decode("utf-8"))
482
+ encoded_path = encode_file_path(format, content_type, file_id)
483
+ try:
484
+ room_id = room_id_from_encoded_path(encoded_path)
485
+ room: YRoom = await self.ywebsocket_server.get_room(room_id)
486
+ fork_ydoc = Doc()
487
+
488
+ ydoc_factory = YDOCS.get(content_type)
489
+ if ydoc_factory is None:
490
+ self.set_status(404)
491
+ self.finish(
492
+ {
493
+ "code": 404,
494
+ "error": f"No document factory found for content type: {content_type}",
495
+ }
496
+ )
497
+ return
498
+
499
+ FORK_DOCUMENTS[idx] = ydoc_factory(fork_ydoc)
500
+ undo_manager: UndoManager = FORK_DOCUMENTS[idx].undo_manager
501
+
502
+ updates_and_timestamps = [(item[0], item[-1]) async for item in room.ystore.read()]
503
+
504
+ result_timestamps = []
505
+
506
+ for update, timestamp in updates_and_timestamps:
507
+ undo_stack_len = len(undo_manager.undo_stack)
508
+ fork_ydoc.apply_update(update)
509
+ if len(undo_manager.undo_stack) > undo_stack_len:
510
+ result_timestamps.append(timestamp)
511
+
512
+ fork_room = YRoom(ydoc=fork_ydoc)
513
+ self.ywebsocket_server.add_room(idx, fork_room)
514
+
515
+ data = {
516
+ "roomId": room_id,
517
+ "timestamps": result_timestamps,
518
+ "forkRoom": idx,
519
+ "sessionId": SERVER_SESSION,
520
+ }
521
+ self.set_status(200)
522
+ self.finish(json.dumps(data))
523
+
524
+ except RoomNotFound:
525
+ self.set_status(404)
526
+ self.finish({"code": 404, "error": "Room not found"})
527
+
528
+
529
+ class UndoRedoHandler(APIHandler):
530
+ def initialize(self, ywebsocket_server: JupyterWebsocketServer) -> None:
531
+ self._websocket_server = ywebsocket_server
532
+
533
+ async def put(self, room_id):
534
+ try:
535
+ action = str(self.request.query_arguments.get("action")[0].decode("utf-8"))
536
+ steps = int(self.request.query_arguments.get("steps")[0].decode("utf-8"))
537
+ fork_room_id = str(self.request.query_arguments.get("forkRoom")[0].decode("utf-8"))
538
+
539
+ fork_document = FORK_DOCUMENTS.get(fork_room_id)
540
+ if not fork_document:
541
+ self.set_status(404)
542
+ self.log.warning(f"Fork document not found for room ID {fork_room_id}")
543
+ return self.finish({"code": 404, "error": "Fork document not found"})
544
+
545
+ undo_manager = fork_document.undo_manager
546
+
547
+ if action == "undo":
548
+ if undo_manager.can_undo():
549
+ await self._perform_undo_or_redo(undo_manager, "undo", steps)
550
+ self.set_status(200)
551
+ self.log.info(f"Undo operation performed in room {room_id} for {steps} steps")
552
+ return self.finish({"status": "undone"})
553
+ else:
554
+ self.log.info(f"No more undo operations available in room {room_id}")
555
+ return self.finish({"error": "No more undo operations available"})
556
+ elif action == "redo":
557
+ if undo_manager.can_redo():
558
+ await self._perform_undo_or_redo(undo_manager, "redo", steps)
559
+ self.set_status(200)
560
+ self.log.info(f"Redo operation performed in room {room_id} for {steps} steps")
561
+ return self.finish({"status": "redone"})
562
+ else:
563
+ self.log.info(f"No more redo operations available in room {room_id}")
564
+ return self.finish({"error": "No more redo operations available"})
565
+ elif action == "restore":
566
+ try:
567
+ # Cleanup undo manager after restoration
568
+ await self._cleanup_undo_manager(fork_room_id)
569
+
570
+ self.set_status(200)
571
+ self.log.info(f"Document in room {room_id} restored successfully")
572
+ return self.finish({"code": 200, "status": "Document restored successfully"})
573
+ except Exception as e:
574
+ self.log.error(f"Error during document restore in room {room_id}: {str(e)}")
575
+ self.set_status(500)
576
+ return self.finish(
577
+ {"code": 500, "error": "Internal server error", "message": str(e)}
578
+ )
579
+
580
+ except Exception as e:
581
+ self.log.error(f"Error during undo/redo/restore operation in room {room_id}: {str(e)}")
582
+
583
+ async def _perform_undo_or_redo(
584
+ self, undo_manager: UndoManager, action: str, steps: int
585
+ ) -> None:
586
+ for _ in range(steps):
587
+ if action == "undo" and len(undo_manager.undo_stack) > 1:
588
+ undo_manager.undo()
589
+
590
+ elif action == "redo" and undo_manager.can_redo():
591
+ undo_manager.redo()
592
+ else:
593
+ break
594
+
595
+ async def _cleanup_undo_manager(self, room_id: str) -> None:
596
+ if room_id in FORK_DOCUMENTS:
597
+ del FORK_DOCUMENTS[room_id]
598
+ self.log.info(f"Fork Document for {room_id} has been removed.")
@@ -31,7 +31,7 @@ dependencies = [
31
31
  "jupyter_server>=2.11.1,<3.0.0",
32
32
  "jupyter_ydoc>=2.0.0,<4.0.0",
33
33
  "pycrdt",
34
- "pycrdt-websocket>=0.14.0,<0.15.0",
34
+ "pycrdt-websocket>=0.14.2,<0.15.0",
35
35
  "jupyter_events>=0.10.0",
36
36
  "jupyter_server_fileid>=0.7.0,<1",
37
37
  "jsonschema>=4.18.0"
@@ -1 +0,0 @@
1
- __version__ = "1.0.0b3"