Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add forking API #382

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ jobs:
run: |
python -m pip install "jupyterlab>=4.0.0,<5"
python -m pip install -e ".[test]" codecov
python -m pip install -e projects/jupyter-collaboration-ui -e projects/jupyter-docprovider -e projects/jupyter-server-ydoc
python -m pip install -e projects/jupyter-collaboration-ui -e projects/jupyter-docprovider -e "projects/jupyter-server-ydoc[test]"

- name: List installed packages
run: |
Expand Down Expand Up @@ -163,7 +163,7 @@ jobs:
- name: Install the Python dependencies
run: |
pip install -e ".[test]"
pip install -e projects/jupyter-collaboration-ui -e projects/jupyter-docprovider -e projects/jupyter-server-ydoc
pip install -e projects/jupyter-collaboration-ui -e projects/jupyter-docprovider -e "projects/jupyter-server-ydoc[test]"

- name: Run the unit tests
run: |
Expand All @@ -184,7 +184,7 @@ jobs:
- name: Install the Python dependencies
run: |
pip install -e ".[test]"
pip install -e projects/jupyter-collaboration-ui -e projects/jupyter-docprovider -e projects/jupyter-server-ydoc
pip install -e projects/jupyter-collaboration-ui -e projects/jupyter-docprovider -e "projects/jupyter-server-ydoc[test]"

- name: List installed packages
run: |
Expand Down
10 changes: 10 additions & 0 deletions projects/jupyter-server-ydoc/jupyter_server_ydoc/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from traitlets import Bool, Float, Type

from .handlers import (
DocForkHandler,
DocSessionHandler,
TimelineHandler,
UndoRedoHandler,
Expand All @@ -25,6 +26,7 @@
from .utils import (
AWARENESS_EVENTS_SCHEMA_PATH,
EVENTS_SCHEMA_PATH,
FORK_EVENTS_SCHEMA_PATH,
encode_file_path,
room_id_from_encoded_path,
)
Expand Down Expand Up @@ -85,6 +87,7 @@ def initialize(self):
super().initialize()
self.serverapp.event_logger.register_event_schema(EVENTS_SCHEMA_PATH)
self.serverapp.event_logger.register_event_schema(AWARENESS_EVENTS_SCHEMA_PATH)
self.serverapp.event_logger.register_event_schema(FORK_EVENTS_SCHEMA_PATH)

def initialize_settings(self):
self.settings.update(
Expand Down Expand Up @@ -123,6 +126,13 @@ def initialize_handlers(self):

self.handlers.extend(
[
(
r"/api/collaboration/fork/(.*)",
DocForkHandler,
{
"ywebsocket_server": self.ywebsocket_server,
},
),
(
r"/api/collaboration/room/(.*)",
YDocWebSocketHandler,
Expand Down
34 changes: 34 additions & 0 deletions projects/jupyter-server-ydoc/jupyter_server_ydoc/events/fork.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"$id": https://schema.jupyter.org/jupyter_collaboration/fork/v1
"$schema": "http://json-schema.org/draft-07/schema"
version: 1
title: Collaborative fork events
personal-data: true
description: |
Fork events emitted from server-side during a collaborative session.
type: object
required:
- root_roomid
- fork_roomid
- username
- action
properties:
root_roomid:
type: string
description: |
Root room ID. Usually composed by the file type, format and ID.
fork_roomid:
type: string
description: |
Fork root room ID.
username:
type: string
description: |
The name of the user who created or deleted the fork.
action:
enum:
- create
- delete
description: |
Possible values:
1. create
2. delete
94 changes: 94 additions & 0 deletions projects/jupyter-server-ydoc/jupyter_server_ydoc/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from .utils import (
JUPYTER_COLLABORATION_AWARENESS_EVENTS_URI,
JUPYTER_COLLABORATION_EVENTS_URI,
JUPYTER_COLLABORATION_FORK_EVENTS_URI,
LogLevel,
MessageType,
decode_file_path,
Expand All @@ -39,6 +40,7 @@

SERVER_SESSION = str(uuid.uuid4())
FORK_DOCUMENTS = {}
FORK_ROOMS: dict[str, str] = {}


class YDocWebSocketHandler(WebSocketHandler, JupyterHandler):
Expand Down Expand Up @@ -600,3 +602,95 @@ async def _cleanup_undo_manager(self, room_id: str) -> None:
if room_id in FORK_DOCUMENTS:
del FORK_DOCUMENTS[room_id]
self.log.info(f"Fork Document for {room_id} has been removed.")


class DocForkHandler(APIHandler):
"""
Jupyter Server handler to:
- create a fork of a root document (optionally synchronizing with the root document),
- delete a fork of a root document (optionally merging back in the root document).
- get fork IDs of a root document.
"""

auth_resource = "contents"

def initialize(
self,
ywebsocket_server: JupyterWebsocketServer,
) -> None:
self._websocket_server = ywebsocket_server

@web.authenticated
@authorized
async def get(self, root_roomid):
"""
Returns a dictionary of given root room ID to the root's fork room IDs.
"""
self.write(
{
root_roomid: [
fork_roomid
for fork_roomid, _root_roomid in FORK_ROOMS.items()
if _root_roomid == root_roomid
]
}
)

@web.authenticated
@authorized
async def put(self, root_roomid):
"""
Creates a fork of a root document and returns its ID.
Optionally keeps the fork in sync with the root.
"""
fork_roomid = uuid4().hex
FORK_ROOMS[fork_roomid] = root_roomid
root_room = await self._websocket_server.get_room(root_roomid)
update = root_room.ydoc.get_update()
fork_ydoc = Doc()
fork_ydoc.apply_update(update)
model = self.get_json_body()
if model.get("sync"):
root_room.ydoc.observe(lambda event: fork_ydoc.apply_update(event.update))
fork_room = YRoom(ydoc=fork_ydoc)
self._websocket_server.rooms[fork_roomid] = fork_room
await self._websocket_server.start_room(fork_room)
self._emit_fork_event(self.current_user.username, root_roomid, fork_roomid, "create")
data = json.dumps(
{
"sessionId": SERVER_SESSION,
"roomId": fork_roomid,
}
)
self.set_status(201)
return self.finish(data)

@web.authenticated
@authorized
async def delete(self, fork_roomid):
"""
Deletes a forked document, and optionally merges it back in the root document.
"""
root_roomid = FORK_ROOMS[fork_roomid]
del FORK_ROOMS[fork_roomid]
if int(self.get_query_argument("merge")):
root_room = await self._websocket_server.get_room(root_roomid)
root_ydoc = root_room.ydoc
fork_room = await self._websocket_server.get_room(fork_roomid)
fork_ydoc = fork_room.ydoc
fork_update = fork_ydoc.get_update()
root_ydoc.apply_update(fork_update)
await self._websocket_server.delete_room(name=fork_roomid)
self._emit_fork_event(self.current_user.username, root_roomid, fork_roomid, "delete")
self.set_status(200)

def _emit_fork_event(
self, username: str, root_roomid: str, fork_roomid: str, action: str
) -> None:
data = {
"username": username,
"root_roomid": root_roomid,
"fork_roomid": fork_roomid,
"action": action,
}
self.event_logger.emit(schema_id=JUPYTER_COLLABORATION_FORK_EVENTS_URI, data=data)
48 changes: 48 additions & 0 deletions projects/jupyter-server-ydoc/jupyter_server_ydoc/pytest_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,54 @@ async def _inner(format: str, type: str, path: str) -> Any:
return _inner


@pytest.fixture
def rtc_connect_fork_client(jp_http_port, jp_base_url, rtc_fetch_session):
async def _inner(room_id: str) -> Any:
return connect(
f"ws://127.0.0.1:{jp_http_port}{jp_base_url}api/collaboration/room/{room_id}"
)

return _inner


@pytest.fixture
def rtc_get_forks_client(jp_fetch):
async def _inner(root_roomid: str) -> Any:
return await jp_fetch(
"/api/collaboration/fork",
root_roomid,
method="GET",
)

return _inner


@pytest.fixture
def rtc_create_fork_client(jp_fetch):
async def _inner(root_roomid: str, sync: bool) -> Any:
return await jp_fetch(
"/api/collaboration/fork",
root_roomid,
method="PUT",
body=json.dumps({"sync": sync}),
)

return _inner


@pytest.fixture
def rtc_delete_fork_client(jp_fetch):
async def _inner(fork_roomid: str, merge: int) -> Any:
return await jp_fetch(
"/api/collaboration/fork",
fork_roomid,
method="DELETE",
params={"merge": merge},
)

return _inner


@pytest.fixture
def rtc_add_doc_to_store(rtc_connect_doc_client):
event = Event()
Expand Down
2 changes: 2 additions & 0 deletions projects/jupyter-server-ydoc/jupyter_server_ydoc/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
JUPYTER_COLLABORATION_AWARENESS_EVENTS_URI = (
"https://schema.jupyter.org/jupyter_collaboration/awareness/v1"
)
JUPYTER_COLLABORATION_FORK_EVENTS_URI = "https://schema.jupyter.org/jupyter_collaboration/fork/v1"
AWARENESS_EVENTS_SCHEMA_PATH = EVENTS_FOLDER_PATH / "awareness.yaml"
FORK_EVENTS_SCHEMA_PATH = EVENTS_FOLDER_PATH / "fork.yaml"


class MessageType(IntEnum):
Expand Down
1 change: 1 addition & 0 deletions projects/jupyter-server-ydoc/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ dynamic = ["version"]
[project.optional-dependencies]
test = [
"coverage",
"dirty-equals",
"jupyter_server[test]>=2.4.0",
"jupyter_server_fileid[test]",
"pytest>=7.0",
Expand Down
Loading
Loading