jupyter-server-ydoc 1.0.0b3__tar.gz → 1.0.0b5__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.0b3 → jupyter_server_ydoc-1.0.0b5}/PKG-INFO +2 -2
- jupyter_server_ydoc-1.0.0b5/jupyter_server_ydoc/_version.py +1 -0
- {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b5}/jupyter_server_ydoc/app.py +21 -1
- {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b5}/jupyter_server_ydoc/handlers.py +139 -2
- {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b5}/pyproject.toml +1 -1
- jupyter_server_ydoc-1.0.0b3/jupyter_server_ydoc/_version.py +0 -1
- {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b5}/.gitignore +0 -0
- {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b5}/LICENSE +0 -0
- {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b5}/README.md +0 -0
- {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b5}/jupyter-config/jupyter_server_ydoc.json +0 -0
- {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b5}/jupyter_server_ydoc/__init__.py +0 -0
- {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b5}/jupyter_server_ydoc/events/awareness.yaml +0 -0
- {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b5}/jupyter_server_ydoc/events/session.yaml +0 -0
- {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b5}/jupyter_server_ydoc/loaders.py +0 -0
- {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b5}/jupyter_server_ydoc/pytest_plugin.py +0 -0
- {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b5}/jupyter_server_ydoc/rooms.py +0 -0
- {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b5}/jupyter_server_ydoc/stores.py +0 -0
- {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b5}/jupyter_server_ydoc/test_utils.py +0 -0
- {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b5}/jupyter_server_ydoc/utils.py +0 -0
- {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b5}/jupyter_server_ydoc/websocketserver.py +0 -0
- {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b5}/setup.py +0 -0
- {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b5}/tests/__init__.py +0 -0
- {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b5}/tests/conftest.py +0 -0
- {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b5}/tests/test_app.py +0 -0
- {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b5}/tests/test_documents.py +0 -0
- {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b5}/tests/test_handlers.py +0 -0
- {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b5}/tests/test_loaders.py +0 -0
- {jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b5}/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.
|
|
3
|
+
Version: 1.0.0b5
|
|
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.
|
|
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.0b5"
|
|
@@ -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
|
|
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.
|
|
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"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b5}/jupyter-config/jupyter_server_ydoc.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b5}/jupyter_server_ydoc/events/session.yaml
RENAMED
|
File without changes
|
|
File without changes
|
{jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b5}/jupyter_server_ydoc/pytest_plugin.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b5}/jupyter_server_ydoc/test_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
{jupyter_server_ydoc-1.0.0b3 → jupyter_server_ydoc-1.0.0b5}/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
|
|
File without changes
|