jupyter-server-ydoc 1.0.0b2__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.
- {jupyter_server_ydoc-1.0.0b2 → jupyter_server_ydoc-1.0.0b4}/PKG-INFO +3 -3
- jupyter_server_ydoc-1.0.0b4/jupyter_server_ydoc/_version.py +1 -0
- {jupyter_server_ydoc-1.0.0b2 → jupyter_server_ydoc-1.0.0b4}/jupyter_server_ydoc/app.py +21 -1
- {jupyter_server_ydoc-1.0.0b2 → jupyter_server_ydoc-1.0.0b4}/jupyter_server_ydoc/handlers.py +139 -2
- {jupyter_server_ydoc-1.0.0b2 → jupyter_server_ydoc-1.0.0b4}/jupyter_server_ydoc/loaders.py +17 -3
- {jupyter_server_ydoc-1.0.0b2 → jupyter_server_ydoc-1.0.0b4}/jupyter_server_ydoc/rooms.py +3 -1
- {jupyter_server_ydoc-1.0.0b2 → jupyter_server_ydoc-1.0.0b4}/pyproject.toml +2 -2
- jupyter_server_ydoc-1.0.0b2/jupyter_server_ydoc/_version.py +0 -1
- {jupyter_server_ydoc-1.0.0b2 → jupyter_server_ydoc-1.0.0b4}/.gitignore +0 -0
- {jupyter_server_ydoc-1.0.0b2 → jupyter_server_ydoc-1.0.0b4}/LICENSE +0 -0
- {jupyter_server_ydoc-1.0.0b2 → jupyter_server_ydoc-1.0.0b4}/README.md +0 -0
- {jupyter_server_ydoc-1.0.0b2 → jupyter_server_ydoc-1.0.0b4}/jupyter-config/jupyter_server_ydoc.json +0 -0
- {jupyter_server_ydoc-1.0.0b2 → jupyter_server_ydoc-1.0.0b4}/jupyter_server_ydoc/__init__.py +0 -0
- {jupyter_server_ydoc-1.0.0b2 → jupyter_server_ydoc-1.0.0b4}/jupyter_server_ydoc/events/awareness.yaml +0 -0
- {jupyter_server_ydoc-1.0.0b2 → jupyter_server_ydoc-1.0.0b4}/jupyter_server_ydoc/events/session.yaml +0 -0
- {jupyter_server_ydoc-1.0.0b2 → jupyter_server_ydoc-1.0.0b4}/jupyter_server_ydoc/pytest_plugin.py +0 -0
- {jupyter_server_ydoc-1.0.0b2 → jupyter_server_ydoc-1.0.0b4}/jupyter_server_ydoc/stores.py +0 -0
- {jupyter_server_ydoc-1.0.0b2 → jupyter_server_ydoc-1.0.0b4}/jupyter_server_ydoc/test_utils.py +0 -0
- {jupyter_server_ydoc-1.0.0b2 → jupyter_server_ydoc-1.0.0b4}/jupyter_server_ydoc/utils.py +0 -0
- {jupyter_server_ydoc-1.0.0b2 → jupyter_server_ydoc-1.0.0b4}/jupyter_server_ydoc/websocketserver.py +0 -0
- {jupyter_server_ydoc-1.0.0b2 → jupyter_server_ydoc-1.0.0b4}/setup.py +0 -0
- {jupyter_server_ydoc-1.0.0b2 → jupyter_server_ydoc-1.0.0b4}/tests/__init__.py +0 -0
- {jupyter_server_ydoc-1.0.0b2 → jupyter_server_ydoc-1.0.0b4}/tests/conftest.py +0 -0
- {jupyter_server_ydoc-1.0.0b2 → jupyter_server_ydoc-1.0.0b4}/tests/test_app.py +0 -0
- {jupyter_server_ydoc-1.0.0b2 → jupyter_server_ydoc-1.0.0b4}/tests/test_documents.py +0 -0
- {jupyter_server_ydoc-1.0.0b2 → jupyter_server_ydoc-1.0.0b4}/tests/test_handlers.py +0 -0
- {jupyter_server_ydoc-1.0.0b2 → jupyter_server_ydoc-1.0.0b4}/tests/test_loaders.py +0 -0
- {jupyter_server_ydoc-1.0.0b2 → 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.
|
|
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
|
|
@@ -78,10 +78,10 @@ Requires-Python: >=3.8
|
|
|
78
78
|
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
|
-
Requires-Dist: jupyter-server<3.0.0,>=2.
|
|
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.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
|
|
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.")
|
|
@@ -117,7 +117,7 @@ class FileLoader:
|
|
|
117
117
|
self.last_modified = model["last_modified"]
|
|
118
118
|
return model
|
|
119
119
|
|
|
120
|
-
async def maybe_save_content(self, model: dict[str, Any]) -> None:
|
|
120
|
+
async def maybe_save_content(self, model: dict[str, Any]) -> dict[str, Any] | None:
|
|
121
121
|
"""
|
|
122
122
|
Save the content of the file.
|
|
123
123
|
|
|
@@ -149,20 +149,34 @@ class FileLoader:
|
|
|
149
149
|
# otherwise it could corrupt the file
|
|
150
150
|
done_saving = asyncio.Event()
|
|
151
151
|
task = asyncio.create_task(self._save_content(model, done_saving))
|
|
152
|
+
saved_model = None
|
|
152
153
|
try:
|
|
153
|
-
await asyncio.shield(task)
|
|
154
|
+
saved_model = await asyncio.shield(task)
|
|
154
155
|
except asyncio.CancelledError:
|
|
155
156
|
pass
|
|
156
157
|
await done_saving.wait()
|
|
158
|
+
return saved_model
|
|
157
159
|
else:
|
|
158
160
|
# file changed on disk, raise an error
|
|
159
161
|
self.last_modified = m["last_modified"]
|
|
160
162
|
raise OutOfBandChanges
|
|
161
163
|
|
|
162
|
-
async def _save_content(
|
|
164
|
+
async def _save_content(
|
|
165
|
+
self, model: dict[str, Any], done_saving: asyncio.Event
|
|
166
|
+
) -> dict[str, Any]:
|
|
163
167
|
try:
|
|
164
168
|
m = await ensure_async(self._contents_manager.save(model, self.path))
|
|
165
169
|
self.last_modified = m["last_modified"]
|
|
170
|
+
# TODO, get rid of the extra `get` here once upstream issue:
|
|
171
|
+
# https://github.com/jupyter-server/jupyter_server/issues/1453 is resolved
|
|
172
|
+
model_with_hash = await ensure_async(
|
|
173
|
+
self._contents_manager.get(
|
|
174
|
+
self.path,
|
|
175
|
+
content=False,
|
|
176
|
+
require_hash=True,
|
|
177
|
+
)
|
|
178
|
+
)
|
|
179
|
+
return {**m, "hash": model_with_hash["hash"]}
|
|
166
180
|
finally:
|
|
167
181
|
done_saving.set()
|
|
168
182
|
|
|
@@ -271,7 +271,7 @@ class DocumentRoom(YRoom):
|
|
|
271
271
|
await asyncio.sleep(self._save_delay)
|
|
272
272
|
|
|
273
273
|
self.log.info("Saving the content from room %s", self._room_id)
|
|
274
|
-
await self._file.maybe_save_content(
|
|
274
|
+
saved_model = await self._file.maybe_save_content(
|
|
275
275
|
{
|
|
276
276
|
"format": self._file_format,
|
|
277
277
|
"type": self._file_type,
|
|
@@ -280,6 +280,8 @@ class DocumentRoom(YRoom):
|
|
|
280
280
|
)
|
|
281
281
|
async with self._update_lock:
|
|
282
282
|
self._document.dirty = False
|
|
283
|
+
if saved_model:
|
|
284
|
+
self._document.hash = saved_model["hash"]
|
|
283
285
|
|
|
284
286
|
self._emit(LogLevel.INFO, "save", "Content saved.")
|
|
285
287
|
|
|
@@ -28,10 +28,10 @@ authors = [
|
|
|
28
28
|
{ name = "Jupyter Development Team", email = "jupyter@googlegroups.com" },
|
|
29
29
|
]
|
|
30
30
|
dependencies = [
|
|
31
|
-
"jupyter_server>=2.
|
|
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.0b2"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{jupyter_server_ydoc-1.0.0b2 → jupyter_server_ydoc-1.0.0b4}/jupyter-config/jupyter_server_ydoc.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{jupyter_server_ydoc-1.0.0b2 → jupyter_server_ydoc-1.0.0b4}/jupyter_server_ydoc/events/session.yaml
RENAMED
|
File without changes
|
{jupyter_server_ydoc-1.0.0b2 → jupyter_server_ydoc-1.0.0b4}/jupyter_server_ydoc/pytest_plugin.py
RENAMED
|
File without changes
|
|
File without changes
|
{jupyter_server_ydoc-1.0.0b2 → jupyter_server_ydoc-1.0.0b4}/jupyter_server_ydoc/test_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
{jupyter_server_ydoc-1.0.0b2 → jupyter_server_ydoc-1.0.0b4}/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
|