Skip to content

Commit ab80f15

Browse files
authored
Fix file id not available for jupyter-server (#307)
Improve notebook cell server-side executor Fix for testing drive Allow to request a document from its document_id/room_id Add documentation Rename state room_id to document_id Don't include custom logic for jupyter_server_nbmodel Co-authored-by: Frédéric Collonval <[email protected]>
1 parent 4f3fdfd commit ab80f15

File tree

13 files changed

+225
-204
lines changed

13 files changed

+225
-204
lines changed

.eslintignore

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
**/*.d.ts
88
**/test
99
**/ui-tests
10+
**/labextension
1011

1112
docs
1213
tests

.prettierignore

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
**/tsconfig.test.json
77
**/*.d.ts
88
**/test
9+
**/labextension
910

1011
docs
1112
tests

packages/collaboration-extension/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@
7171
},
7272
"devDependencies": {
7373
"@jupyterlab/builder": "^4.0.5",
74-
"@types/react": "~18.0.26",
74+
"@types/react": "~18.3.1",
7575
"npm-run-all": "^4.1.5",
7676
"rimraf": "^4.1.2",
7777
"typescript": "~5.0.4"

packages/collaboration/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
"yjs": "^13.5.40"
5757
},
5858
"devDependencies": {
59-
"@types/react": "^18.0.27",
59+
"@types/react": "~18.3.1",
6060
"rimraf": "^4.1.2",
6161
"typescript": "~5.0.4"
6262
},

packages/docprovider-extension/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,8 @@
7171
"yjs": "^13.5.40"
7272
},
7373
"devDependencies": {
74-
"@jupyterlab/builder": "^4.0.5",
75-
"@types/react": "~18.0.26",
74+
"@jupyterlab/builder": "^4.0.0",
75+
"@types/react": "~18.3.1",
7676
"npm-run-all": "^4.1.5",
7777
"rimraf": "^4.1.2",
7878
"typescript": "~5.0.4"

packages/docprovider-extension/src/executor.ts

+5-50
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,12 @@
55
* @module docprovider-extension
66
*/
77

8+
import { NotebookCellServerExecutor } from '@jupyter/docprovider';
89
import {
910
JupyterFrontEnd,
1011
JupyterFrontEndPlugin
1112
} from '@jupyterlab/application';
12-
import { PageConfig, URLExt } from '@jupyterlab/coreutils';
13-
import { ServerConnection } from '@jupyterlab/services';
14-
15-
import { type MarkdownCell } from '@jupyterlab/cells';
13+
import { PageConfig } from '@jupyterlab/coreutils';
1614
import { INotebookCellExecutor, runCell } from '@jupyterlab/notebook';
1715

1816
export const notebookCellExecutor: JupyterFrontEndPlugin<INotebookCellExecutor> =
@@ -24,53 +22,10 @@ export const notebookCellExecutor: JupyterFrontEndPlugin<INotebookCellExecutor>
2422
provides: INotebookCellExecutor,
2523
activate: (app: JupyterFrontEnd): INotebookCellExecutor => {
2624
if (PageConfig.getOption('serverSideExecution') === 'true') {
27-
return Object.freeze({ runCell: runCellServerSide });
25+
return new NotebookCellServerExecutor({
26+
serverSettings: app.serviceManager.serverSettings
27+
});
2828
}
2929
return Object.freeze({ runCell });
3030
}
3131
};
32-
33-
async function runCellServerSide({
34-
cell,
35-
notebook,
36-
notebookConfig,
37-
onCellExecuted,
38-
onCellExecutionScheduled,
39-
sessionContext,
40-
sessionDialogs,
41-
translator
42-
}: INotebookCellExecutor.IRunCellOptions): Promise<boolean> {
43-
switch (cell.model.type) {
44-
case 'markdown':
45-
(cell as MarkdownCell).rendered = true;
46-
cell.inputHidden = false;
47-
onCellExecuted({ cell, success: true });
48-
break;
49-
case 'code': {
50-
const kernelId = sessionContext?.session?.kernel?.id;
51-
const settings = ServerConnection.makeSettings();
52-
const apiURL = URLExt.join(
53-
settings.baseUrl,
54-
`api/kernels/${kernelId}/execute`
55-
);
56-
const cellId = cell.model.sharedModel.getId();
57-
const documentId = `json:notebook:${notebook.sharedModel.getState(
58-
'file_id'
59-
)}`;
60-
const body = `{"cell_id":"${cellId}","document_id":"${documentId}"}`;
61-
const init = {
62-
method: 'POST',
63-
body
64-
};
65-
try {
66-
await ServerConnection.makeRequest(apiURL, init, settings);
67-
} catch (error: any) {
68-
throw new ServerConnection.NetworkError(error);
69-
}
70-
break;
71-
}
72-
default:
73-
break;
74-
}
75-
return Promise.resolve(true);
76-
}

packages/docprovider/package.json

+8-3
Original file line numberDiff line numberDiff line change
@@ -42,17 +42,22 @@
4242
},
4343
"dependencies": {
4444
"@jupyter/ydoc": "^2.0.0",
45-
"@jupyterlab/coreutils": "^6.0.5",
46-
"@jupyterlab/services": "^7.0.5",
45+
"@jupyterlab/apputils": "^4.2.0",
46+
"@jupyterlab/cells": "^4.2.0",
47+
"@jupyterlab/coreutils": "^6.2.0",
48+
"@jupyterlab/notebook": "^4.2.0",
49+
"@jupyterlab/services": "^7.2.0",
50+
"@jupyterlab/translation": "^4.2.0",
4751
"@lumino/coreutils": "^2.1.0",
4852
"@lumino/disposable": "^2.1.0",
4953
"@lumino/signaling": "^2.1.0",
54+
"@lumino/widgets": "^2.2.0",
5055
"y-protocols": "^1.0.5",
5156
"y-websocket": "^1.3.15",
5257
"yjs": "^13.5.40"
5358
},
5459
"devDependencies": {
55-
"@jupyterlab/testing": "^4.0.5",
60+
"@jupyterlab/testing": "^4.0.0",
5661
"@types/jest": "^29.2.0",
5762
"jest": "^29.5.0",
5863
"rimraf": "^4.1.2",

packages/docprovider/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
*/
99

1010
export * from './awareness';
11+
export * from './notebookCellExecutor';
12+
export * from './requests';
1113
export * from './ydrive';
1214
export * from './yprovider';
1315
export * from './tokens';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/* -----------------------------------------------------------------------------
2+
| Copyright (c) Jupyter Development Team.
3+
| Distributed under the terms of the Modified BSD License.
4+
|----------------------------------------------------------------------------*/
5+
6+
import { Dialog, showDialog } from '@jupyterlab/apputils';
7+
import { type ICodeCellModel, type MarkdownCell } from '@jupyterlab/cells';
8+
import { URLExt } from '@jupyterlab/coreutils';
9+
import { INotebookCellExecutor } from '@jupyterlab/notebook';
10+
import { ServerConnection } from '@jupyterlab/services';
11+
import { nullTranslator } from '@jupyterlab/translation';
12+
13+
/**
14+
* Notebook cell executor posting a request to the server for execution.
15+
*/
16+
export class NotebookCellServerExecutor implements INotebookCellExecutor {
17+
private _serverSettings: ServerConnection.ISettings;
18+
19+
/**
20+
* Constructor
21+
*
22+
* @param options Constructor options; the contents manager, the collaborative drive and optionally the server settings.
23+
*/
24+
constructor(options: { serverSettings?: ServerConnection.ISettings }) {
25+
this._serverSettings =
26+
options.serverSettings ?? ServerConnection.makeSettings();
27+
}
28+
29+
/**
30+
* Execute a given cell of the notebook.
31+
*
32+
* @param options Execution options
33+
* @returns Execution success status
34+
*/
35+
async runCell({
36+
cell,
37+
notebook,
38+
notebookConfig,
39+
onCellExecuted,
40+
onCellExecutionScheduled,
41+
sessionContext,
42+
sessionDialogs,
43+
translator
44+
}: INotebookCellExecutor.IRunCellOptions): Promise<boolean> {
45+
translator = translator ?? nullTranslator;
46+
const trans = translator.load('jupyterlab');
47+
48+
switch (cell.model.type) {
49+
case 'markdown':
50+
(cell as MarkdownCell).rendered = true;
51+
cell.inputHidden = false;
52+
onCellExecuted({ cell, success: true });
53+
break;
54+
case 'code':
55+
if (sessionContext) {
56+
if (sessionContext.isTerminating) {
57+
await showDialog({
58+
title: trans.__('Kernel Terminating'),
59+
body: trans.__(
60+
'The kernel for %1 appears to be terminating. You can not run any cell for now.',
61+
sessionContext.session?.path
62+
),
63+
buttons: [Dialog.okButton()]
64+
});
65+
break;
66+
}
67+
if (sessionContext.pendingInput) {
68+
await showDialog({
69+
title: trans.__('Cell not executed due to pending input'),
70+
body: trans.__(
71+
'The cell has not been executed to avoid kernel deadlock as there is another pending input! Submit your pending input and try again.'
72+
),
73+
buttons: [Dialog.okButton()]
74+
});
75+
return false;
76+
}
77+
if (sessionContext.hasNoKernel) {
78+
const shouldSelect = await sessionContext.startKernel();
79+
if (shouldSelect && sessionDialogs) {
80+
await sessionDialogs.selectKernel(sessionContext);
81+
}
82+
}
83+
84+
if (sessionContext.hasNoKernel) {
85+
cell.model.sharedModel.transact(() => {
86+
(cell.model as ICodeCellModel).clearExecution();
87+
});
88+
return true;
89+
}
90+
91+
const kernelId = sessionContext?.session?.kernel?.id;
92+
const apiURL = URLExt.join(
93+
this._serverSettings.baseUrl,
94+
`api/kernels/${kernelId}/execute`
95+
);
96+
const cellId = cell.model.sharedModel.getId();
97+
const documentId = notebook.sharedModel.getState('document_id');
98+
99+
const init = {
100+
method: 'POST',
101+
body: JSON.stringify({ cell_id: cellId, document_id: documentId })
102+
};
103+
onCellExecutionScheduled({ cell });
104+
let success = false;
105+
try {
106+
// FIXME quid of deletedCells and timing record
107+
const response = await ServerConnection.makeRequest(
108+
apiURL,
109+
init,
110+
this._serverSettings
111+
);
112+
success = response.ok;
113+
} catch (error: unknown) {
114+
onCellExecuted({
115+
cell,
116+
success: false
117+
});
118+
if (cell.isDisposed) {
119+
return false;
120+
} else {
121+
throw error;
122+
}
123+
}
124+
125+
onCellExecuted({ cell, success });
126+
127+
return true;
128+
}
129+
cell.model.sharedModel.transact(() => {
130+
(cell.model as ICodeCellModel).clearExecution();
131+
}, false);
132+
break;
133+
default:
134+
break;
135+
}
136+
return Promise.resolve(true);
137+
}
138+
}

packages/docprovider/src/yprovider.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,13 @@ export class WebSocketProvider implements IDocumentProvider {
133133

134134
private _onSync = (isSynced: boolean) => {
135135
if (isSynced) {
136+
if (this._yWebsocketProvider) {
137+
this._yWebsocketProvider.off('sync', this._onSync);
138+
139+
const state = this._sharedModel.ydoc.getMap('state');
140+
state.set('document_id', this._yWebsocketProvider.roomname);
141+
}
136142
this._ready.resolve();
137-
this._yWebsocketProvider?.off('sync', this._onSync);
138143
}
139144
};
140145

projects/jupyter-server-ydoc/jupyter_server_ydoc/app.py

+22-8
Original file line numberDiff line numberDiff line change
@@ -136,21 +136,35 @@ def initialize_handlers(self):
136136
async def get_document(
137137
self: YDocExtension,
138138
*,
139-
path: str,
140-
content_type: Literal["notebook", "file"],
141-
file_format: Literal["json", "text"],
139+
path: str | None = None,
140+
content_type: str | None = None,
141+
file_format: Literal["json", "text"] | None = None,
142+
room_id: str | None = None,
142143
copy: bool = True,
143144
) -> YBaseDoc | None:
144145
"""Get a view of the shared model for the matching document.
145146
147+
You need to provide either a ``room_id`` or the ``path``,
148+
the ``content_type`` and the ``file_format``.
149+
146150
If `copy=True`, the returned shared model is a fork, meaning that any changes
147151
made to it will not be propagated to the shared model used by the application.
148152
"""
149-
file_id_manager = self.serverapp.web_app.settings["file_id_manager"]
150-
file_id = file_id_manager.index(path)
153+
error_msg = "You need to provide either a ``room_id`` or the ``path``, the ``content_type`` and the ``file_format``."
154+
if room_id is None:
155+
if path is None or content_type is None or file_format is None:
156+
raise ValueError(error_msg)
157+
158+
file_id_manager = self.serverapp.web_app.settings["file_id_manager"]
159+
file_id = file_id_manager.index(path)
160+
161+
encoded_path = encode_file_path(file_format, content_type, file_id)
162+
room_id = room_id_from_encoded_path(encoded_path)
151163

152-
encoded_path = encode_file_path(file_format, content_type, file_id)
153-
room_id = room_id_from_encoded_path(encoded_path)
164+
elif path is not None or content_type is not None or file_format is not None:
165+
raise ValueError(error_msg)
166+
else:
167+
room_id = room_id
154168

155169
try:
156170
room = await self.ywebsocket_server.get_room(room_id)
@@ -164,7 +178,7 @@ async def get_document(
164178
fork_ydoc = Doc()
165179
fork_ydoc.apply_update(update)
166180

167-
return YDOCS.get(content_type, YDOCS["file"])(fork_ydoc)
181+
return YDOCS.get(room.file_type, YDOCS["file"])(fork_ydoc)
168182
else:
169183
return room._document
170184

projects/jupyter-server-ydoc/jupyter_server_ydoc/rooms.py

+10
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,16 @@ def __init__(
5656
self._document.observe(self._on_document_change)
5757
self._file.observe(self.room_id, self._on_outofband_change)
5858

59+
@property
60+
def file_format(self) -> str:
61+
"""Document file format."""
62+
return self._file_format
63+
64+
@property
65+
def file_type(self) -> str:
66+
"""Document file type."""
67+
return self._file_type
68+
5969
@property
6070
def room_id(self) -> str:
6171
"""

0 commit comments

Comments
 (0)