jupyter-server-ydoc 1.0.1__py3-none-any.whl → 1.1.0a0__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.1"
1
+ __version__ = "1.1.0.a0"
@@ -14,6 +14,7 @@ from pycrdt_websocket.ystore import BaseYStore
14
14
  from traitlets import Bool, Float, Type
15
15
 
16
16
  from .handlers import (
17
+ DocForkHandler,
17
18
  DocSessionHandler,
18
19
  TimelineHandler,
19
20
  UndoRedoHandler,
@@ -25,6 +26,7 @@ from .stores import SQLiteYStore
25
26
  from .utils import (
26
27
  AWARENESS_EVENTS_SCHEMA_PATH,
27
28
  EVENTS_SCHEMA_PATH,
29
+ FORK_EVENTS_SCHEMA_PATH,
28
30
  encode_file_path,
29
31
  room_id_from_encoded_path,
30
32
  )
@@ -85,6 +87,7 @@ class YDocExtension(ExtensionApp):
85
87
  super().initialize()
86
88
  self.serverapp.event_logger.register_event_schema(EVENTS_SCHEMA_PATH)
87
89
  self.serverapp.event_logger.register_event_schema(AWARENESS_EVENTS_SCHEMA_PATH)
90
+ self.serverapp.event_logger.register_event_schema(FORK_EVENTS_SCHEMA_PATH)
88
91
 
89
92
  def initialize_settings(self):
90
93
  self.settings.update(
@@ -123,6 +126,13 @@ class YDocExtension(ExtensionApp):
123
126
 
124
127
  self.handlers.extend(
125
128
  [
129
+ (
130
+ r"/api/collaboration/fork/(.*)",
131
+ DocForkHandler,
132
+ {
133
+ "ywebsocket_server": self.ywebsocket_server,
134
+ },
135
+ ),
126
136
  (
127
137
  r"/api/collaboration/room/(.*)",
128
138
  YDocWebSocketHandler,
@@ -0,0 +1,56 @@
1
+ "$id": https://schema.jupyter.org/jupyter_collaboration/fork/v1
2
+ "$schema": "http://json-schema.org/draft-07/schema"
3
+ version: 1
4
+ title: Collaborative fork events
5
+ personal-data: true
6
+ description: |
7
+ Fork events emitted from server-side during a collaborative session.
8
+ type: object
9
+ required:
10
+ - fork_roomid
11
+ - fork_info
12
+ - username
13
+ - action
14
+ properties:
15
+ fork_roomid:
16
+ type: string
17
+ description: |
18
+ Fork root room ID.
19
+ fork_info:
20
+ type: object
21
+ description: |
22
+ Fork root room information.
23
+ required:
24
+ - root_roomid
25
+ - synchronize
26
+ - title
27
+ - description
28
+ properties:
29
+ root_roomid:
30
+ type: string
31
+ description: |
32
+ Root room ID. Usually composed by the file type, format and ID.
33
+ synchronize:
34
+ type: boolean
35
+ description: |
36
+ Whether the fork is kept in sync with the root.
37
+ title:
38
+ type: string
39
+ description: |
40
+ The title of the fork.
41
+ description:
42
+ type: string
43
+ description: |
44
+ The description of the fork.
45
+ username:
46
+ type: string
47
+ description: |
48
+ The name of the user who created or deleted the fork.
49
+ action:
50
+ enum:
51
+ - create
52
+ - delete
53
+ description: |
54
+ Possible values:
55
+ 1. create
56
+ 2. delete
@@ -26,6 +26,7 @@ from .rooms import DocumentRoom, TransientRoom
26
26
  from .utils import (
27
27
  JUPYTER_COLLABORATION_AWARENESS_EVENTS_URI,
28
28
  JUPYTER_COLLABORATION_EVENTS_URI,
29
+ JUPYTER_COLLABORATION_FORK_EVENTS_URI,
29
30
  LogLevel,
30
31
  MessageType,
31
32
  decode_file_path,
@@ -39,6 +40,7 @@ YFILE = YDOCS["file"]
39
40
 
40
41
  SERVER_SESSION = str(uuid.uuid4())
41
42
  FORK_DOCUMENTS = {}
43
+ FORK_ROOMS: dict[str, dict[str, str]] = {}
42
44
 
43
45
 
44
46
  class YDocWebSocketHandler(WebSocketHandler, JupyterHandler):
@@ -600,3 +602,101 @@ class UndoRedoHandler(APIHandler):
600
602
  if room_id in FORK_DOCUMENTS:
601
603
  del FORK_DOCUMENTS[room_id]
602
604
  self.log.info(f"Fork Document for {room_id} has been removed.")
605
+
606
+
607
+ class DocForkHandler(APIHandler):
608
+ """
609
+ Jupyter Server handler to:
610
+ - create a fork of a root document (optionally synchronizing with the root document),
611
+ - delete a fork of a root document (optionally merging back in the root document).
612
+ - get fork IDs of a root document.
613
+ """
614
+
615
+ auth_resource = "contents"
616
+
617
+ def initialize(
618
+ self,
619
+ ywebsocket_server: JupyterWebsocketServer,
620
+ ) -> None:
621
+ self._websocket_server = ywebsocket_server
622
+
623
+ @web.authenticated
624
+ @authorized
625
+ async def get(self, root_roomid):
626
+ """
627
+ Returns a dictionary of fork room ID to fork room information for the given root room ID.
628
+ """
629
+ self.write(
630
+ {
631
+ fork_roomid: fork_info
632
+ for fork_roomid, fork_info in FORK_ROOMS.items()
633
+ if fork_info["root_roomid"] == root_roomid
634
+ }
635
+ )
636
+
637
+ @web.authenticated
638
+ @authorized
639
+ async def put(self, root_roomid):
640
+ """
641
+ Creates a fork of a root document and returns its ID.
642
+ Optionally keeps the fork in sync with the root.
643
+ """
644
+ fork_roomid = uuid4().hex
645
+ root_room = await self._websocket_server.get_room(root_roomid)
646
+ update = root_room.ydoc.get_update()
647
+ fork_ydoc = Doc()
648
+ fork_ydoc.apply_update(update)
649
+ model = self.get_json_body()
650
+ synchronize = model.get("synchronize", False)
651
+ if synchronize:
652
+ root_room.ydoc.observe(lambda event: fork_ydoc.apply_update(event.update))
653
+ FORK_ROOMS[fork_roomid] = fork_info = {
654
+ "root_roomid": root_roomid,
655
+ "synchronize": synchronize,
656
+ "title": model.get("title", ""),
657
+ "description": model.get("description", ""),
658
+ }
659
+ fork_room = YRoom(ydoc=fork_ydoc)
660
+ self._websocket_server.rooms[fork_roomid] = fork_room
661
+ await self._websocket_server.start_room(fork_room)
662
+ self._emit_fork_event(self.current_user.username, fork_roomid, fork_info, "create")
663
+ data = json.dumps(
664
+ {
665
+ "sessionId": SERVER_SESSION,
666
+ "fork_roomid": fork_roomid,
667
+ "fork_info": fork_info,
668
+ }
669
+ )
670
+ self.set_status(201)
671
+ return self.finish(data)
672
+
673
+ @web.authenticated
674
+ @authorized
675
+ async def delete(self, fork_roomid):
676
+ """
677
+ Deletes a forked document, and optionally merges it back in the root document.
678
+ """
679
+ fork_info = FORK_ROOMS[fork_roomid]
680
+ root_roomid = fork_info["root_roomid"]
681
+ del FORK_ROOMS[fork_roomid]
682
+ if self.get_query_argument("merge") == "true":
683
+ root_room = await self._websocket_server.get_room(root_roomid)
684
+ root_ydoc = root_room.ydoc
685
+ fork_room = await self._websocket_server.get_room(fork_roomid)
686
+ fork_ydoc = fork_room.ydoc
687
+ fork_update = fork_ydoc.get_update()
688
+ root_ydoc.apply_update(fork_update)
689
+ await self._websocket_server.delete_room(name=fork_roomid)
690
+ self._emit_fork_event(self.current_user.username, fork_roomid, fork_info, "delete")
691
+ self.set_status(200)
692
+
693
+ def _emit_fork_event(
694
+ self, username: str, fork_roomid: str, fork_info: dict[str, str], action: str
695
+ ) -> None:
696
+ data = {
697
+ "username": username,
698
+ "fork_roomid": fork_roomid,
699
+ "fork_info": fork_info,
700
+ "action": action,
701
+ }
702
+ self.event_logger.emit(schema_id=JUPYTER_COLLABORATION_FORK_EVENTS_URI, data=data)
@@ -154,6 +154,65 @@ def rtc_connect_doc_client(jp_http_port, jp_base_url, rtc_fetch_session):
154
154
  return _inner
155
155
 
156
156
 
157
+ @pytest.fixture
158
+ def rtc_connect_fork_client(jp_http_port, jp_base_url, rtc_fetch_session):
159
+ async def _inner(room_id: str) -> Any:
160
+ return aconnect_ws(
161
+ f"http://127.0.0.1:{jp_http_port}{jp_base_url}api/collaboration/room/{room_id}"
162
+ )
163
+
164
+ return _inner
165
+
166
+
167
+ @pytest.fixture
168
+ def rtc_get_forks_client(jp_fetch):
169
+ async def _inner(root_roomid: str) -> Any:
170
+ return await jp_fetch(
171
+ "/api/collaboration/fork",
172
+ root_roomid,
173
+ method="GET",
174
+ )
175
+
176
+ return _inner
177
+
178
+
179
+ @pytest.fixture
180
+ def rtc_create_fork_client(jp_fetch):
181
+ async def _inner(
182
+ root_roomid: str,
183
+ synchronize: bool,
184
+ title: str | None = None,
185
+ description: str | None = None,
186
+ ) -> Any:
187
+ return await jp_fetch(
188
+ "/api/collaboration/fork",
189
+ root_roomid,
190
+ method="PUT",
191
+ body=json.dumps(
192
+ {
193
+ "synchronize": synchronize,
194
+ "title": title,
195
+ "description": description,
196
+ }
197
+ ),
198
+ )
199
+
200
+ return _inner
201
+
202
+
203
+ @pytest.fixture
204
+ def rtc_delete_fork_client(jp_fetch):
205
+ async def _inner(fork_roomid: str, merge: bool) -> Any:
206
+ return await jp_fetch(
207
+ "/api/collaboration/fork",
208
+ fork_roomid,
209
+ method="DELETE",
210
+ params={"merge": str(merge).lower()},
211
+ )
212
+
213
+ return _inner
214
+
215
+
157
216
  @pytest.fixture
158
217
  def rtc_add_doc_to_store(rtc_connect_doc_client):
159
218
  event = Event()
@@ -11,7 +11,9 @@ EVENTS_SCHEMA_PATH = EVENTS_FOLDER_PATH / "session.yaml"
11
11
  JUPYTER_COLLABORATION_AWARENESS_EVENTS_URI = (
12
12
  "https://schema.jupyter.org/jupyter_collaboration/awareness/v1"
13
13
  )
14
+ JUPYTER_COLLABORATION_FORK_EVENTS_URI = "https://schema.jupyter.org/jupyter_collaboration/fork/v1"
14
15
  AWARENESS_EVENTS_SCHEMA_PATH = EVENTS_FOLDER_PATH / "awareness.yaml"
16
+ FORK_EVENTS_SCHEMA_PATH = EVENTS_FOLDER_PATH / "fork.yaml"
15
17
 
16
18
 
17
19
  class MessageType(IntEnum):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: jupyter-server-ydoc
3
- Version: 1.0.1
3
+ Version: 1.1.0a0
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
@@ -84,6 +84,7 @@ Requires-Dist: pycrdt-websocket<0.16.0,>=0.15.0
84
84
  Provides-Extra: test
85
85
  Requires-Dist: anyio; extra == 'test'
86
86
  Requires-Dist: coverage; extra == 'test'
87
+ Requires-Dist: dirty-equals; extra == 'test'
87
88
  Requires-Dist: httpx-ws>=0.5.2; extra == 'test'
88
89
  Requires-Dist: importlib-metadata>=4.8.3; (python_version < '3.10') and extra == 'test'
89
90
  Requires-Dist: jupyter-server-fileid[test]; extra == 'test'
@@ -0,0 +1,19 @@
1
+ jupyter_server_ydoc/__init__.py,sha256=B8H7XLhzgrTCQD8304Lx91FYXslwabsnV9OuYu4M4Hw,346
2
+ jupyter_server_ydoc/_version.py,sha256=V3wzYVoLkHYNcCMBAod-R_QYjrYTZN4MxPCga1RAojQ,25
3
+ jupyter_server_ydoc/app.py,sha256=JqkpijoPdo_qum0CKqbG-I6W8fpC3-v2cFA3kFxK3mg,8111
4
+ jupyter_server_ydoc/handlers.py,sha256=5ywD2yuwXNQPnEFO_bkogxk_jPxuCZ-LhyzrkAFRmpE,27668
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=9isoK58uue7lqMnlHqyfQt29z16Otkh14oRe1k5vbKM,753
13
+ jupyter_server_ydoc/events/fork.yaml,sha256=v7WQX2tkD6Zoh9a9qiJpq6wsU4UVnhdZfZSxdnukk_4,1301
14
+ jupyter_server_ydoc/events/session.yaml,sha256=A7Wt7czyx38MXp5fpDbH7HLS0QNkeOqaEhHdP2x-0Mo,1594
15
+ jupyter_server_ydoc-1.1.0a0.data/data/etc/jupyter/jupyter_server_config.d/jupyter_collaboration.json,sha256=0thh2hJUxAKkZSmneJMG0U6QJRjdM6zGlwrTedEt-Jk,94
16
+ jupyter_server_ydoc-1.1.0a0.dist-info/METADATA,sha256=VSll8INg35cO8neUZC60zTp_dX21hRa5AAOlgmZCFVo,5078
17
+ jupyter_server_ydoc-1.1.0a0.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
18
+ jupyter_server_ydoc-1.1.0a0.dist-info/licenses/LICENSE,sha256=mhO0ZW9EiWOPg0dUgB-lNbJ0CGwRmTdbeAg_se1SOnY,2833
19
+ jupyter_server_ydoc-1.1.0a0.dist-info/RECORD,,
@@ -1,18 +0,0 @@
1
- jupyter_server_ydoc/__init__.py,sha256=B8H7XLhzgrTCQD8304Lx91FYXslwabsnV9OuYu4M4Hw,346
2
- jupyter_server_ydoc/_version.py,sha256=d4QHYmS_30j0hPN8NmNPnQ_Z0TphDRbu4MtQj9cT9e8,22
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=eWAleH6Plh7Aiqv_Res8b0aFmd6C0n-D1j4p1KuE0PA,6983
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=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.1.data/data/etc/jupyter/jupyter_server_config.d/jupyter_collaboration.json,sha256=0thh2hJUxAKkZSmneJMG0U6QJRjdM6zGlwrTedEt-Jk,94
15
- jupyter_server_ydoc-1.0.1.dist-info/METADATA,sha256=cKECBcDzWlzL8RGeLp405E61wVBiW4UfxWEb6zwp_i0,5031
16
- jupyter_server_ydoc-1.0.1.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
17
- jupyter_server_ydoc-1.0.1.dist-info/licenses/LICENSE,sha256=mhO0ZW9EiWOPg0dUgB-lNbJ0CGwRmTdbeAg_se1SOnY,2833
18
- jupyter_server_ydoc-1.0.1.dist-info/RECORD,,