Skip to content

Commit 901ace2

Browse files
committed
Fix file id not available for jupyter-server
Improve notebook cell server-side executor Add new API to get the room name from the collaborative drive
1 parent ecc0028 commit 901ace2

File tree

9 files changed

+545
-959
lines changed

9 files changed

+545
-959
lines changed

packages/docprovider-extension/package.json

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -55,22 +55,22 @@
5555
"dependencies": {
5656
"@jupyter/docprovider": "^3.0.0-alpha.1",
5757
"@jupyter/ydoc": "^1.1.0-a0",
58-
"@jupyterlab/application": "^4.2.0-beta.0",
59-
"@jupyterlab/apputils": "^4.2.0-beta.0",
60-
"@jupyterlab/docregistry": "^4.2.0-beta.0",
61-
"@jupyterlab/filebrowser": "^4.2.0-beta.0",
62-
"@jupyterlab/fileeditor": "^4.2.0-beta.0",
63-
"@jupyterlab/logconsole": "^4.2.0-beta.0",
64-
"@jupyterlab/notebook": "^4.2.0-beta.0",
65-
"@jupyterlab/settingregistry": "^4.2.0-beta.0",
66-
"@jupyterlab/translation": "^4.2.0-beta.0",
58+
"@jupyterlab/application": "^4.2.0",
59+
"@jupyterlab/apputils": "^4.2.0",
60+
"@jupyterlab/docregistry": "^4.2.0",
61+
"@jupyterlab/filebrowser": "^4.2.0",
62+
"@jupyterlab/fileeditor": "^4.2.0",
63+
"@jupyterlab/logconsole": "^4.2.0",
64+
"@jupyterlab/notebook": "^4.2.0",
65+
"@jupyterlab/settingregistry": "^4.2.0",
66+
"@jupyterlab/translation": "^4.2.0",
6767
"@lumino/commands": "^2.1.0",
6868
"y-protocols": "^1.0.5",
6969
"y-websocket": "^1.3.15",
7070
"yjs": "^13.5.40"
7171
},
7272
"devDependencies": {
73-
"@jupyterlab/builder": "^4.0.5",
73+
"@jupyterlab/builder": "^4.0.0",
7474
"@types/react": "~18.0.26",
7575
"npm-run-all": "^4.1.5",
7676
"rimraf": "^4.1.2",

packages/docprovider-extension/src/executor.ts

Lines changed: 15 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@
55
* @module docprovider-extension
66
*/
77

8+
import {
9+
ICollaborativeDrive,
10+
NotebookCellServerExecutor
11+
} from '@jupyter/docprovider';
812
import {
913
JupyterFrontEnd,
1014
JupyterFrontEndPlugin
1115
} from '@jupyterlab/application';
12-
import { PageConfig, URLExt } from '@jupyterlab/coreutils';
13-
import { ServerConnection } from '@jupyterlab/services';
14-
15-
import { type MarkdownCell } from '@jupyterlab/cells';
16+
import { PageConfig } from '@jupyterlab/coreutils';
1617
import { INotebookCellExecutor, runCell } from '@jupyterlab/notebook';
1718

1819
export const notebookCellExecutor: JupyterFrontEndPlugin<INotebookCellExecutor> =
@@ -22,55 +23,18 @@ export const notebookCellExecutor: JupyterFrontEndPlugin<INotebookCellExecutor>
2223
'Add notebook cell executor that uses REST API instead of kernel protocol over WebSocket.',
2324
autoStart: true,
2425
provides: INotebookCellExecutor,
25-
activate: (app: JupyterFrontEnd): INotebookCellExecutor => {
26+
requires: [ICollaborativeDrive],
27+
activate: (
28+
app: JupyterFrontEnd,
29+
collaborativeDrive: ICollaborativeDrive
30+
): INotebookCellExecutor => {
2631
if (PageConfig.getOption('serverSideExecution') === 'true') {
27-
return Object.freeze({ runCell: runCellServerSide });
32+
return new NotebookCellServerExecutor({
33+
contents: app.serviceManager.contents,
34+
drive: collaborativeDrive,
35+
serverSettings: app.serviceManager.serverSettings
36+
});
2837
}
2938
return Object.freeze({ runCell });
3039
}
3140
};
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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,12 @@
4242
},
4343
"dependencies": {
4444
"@jupyter/ydoc": "^1.1.0-a0",
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",
@@ -52,7 +56,7 @@
5256
"yjs": "^13.5.40"
5357
},
5458
"devDependencies": {
55-
"@jupyterlab/testing": "^4.0.5",
59+
"@jupyterlab/testing": "^4.0.0",
5660
"@types/jest": "^29.2.0",
5761
"jest": "^29.5.0",
5862
"rimraf": "^4.1.2",

packages/docprovider/src/index.ts

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

packages/docprovider/src/tokens.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,27 @@ export type SharedDocumentFactory = (
2323
options: Contents.ISharedFactoryOptions
2424
) => YDocument<DocumentChange>;
2525

26+
export interface IRoomNameOptions {
27+
/**
28+
* Document format; 'text', 'base64',...
29+
*/
30+
format: Contents.FileFormat;
31+
/**
32+
* Document type
33+
*/
34+
type: Contents.ContentType;
35+
/**
36+
* File path without the drive name
37+
*/
38+
localPath: string;
39+
}
40+
2641
/**
2742
* A Collaborative implementation for an `IDrive`, talking to the
2843
* server using the Jupyter REST API and a WebSocket connection.
2944
*/
3045
export interface ICollaborativeDrive extends Contents.IDrive {
46+
getRoomName(options: IRoomNameOptions): string | null;
3147
/**
3248
* SharedModel factory for the YDrive.
3349
*/

packages/docprovider/src/ydrive.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import { WebSocketProvider } from './yprovider';
1111
import {
1212
ICollaborativeDrive,
1313
ISharedModelFactory,
14-
SharedDocumentFactory
14+
SharedDocumentFactory,
15+
type IRoomNameOptions
1516
} from './tokens';
1617

1718
const DISABLE_RTC =
@@ -93,6 +94,12 @@ export class YDrive extends Drive implements ICollaborativeDrive {
9394
return super.get(localPath, options);
9495
}
9596

97+
getRoomName(options: IRoomNameOptions): string | null {
98+
const key = `${options.format}:${options.type}:${options.localPath}`;
99+
const provider = this._providers.get(key);
100+
return provider?.roomName ?? null;
101+
}
102+
96103
/**
97104
* Save a file.
98105
*

packages/docprovider/src/yprovider.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ export class WebSocketProvider implements IDocumentProvider {
7777
return this._ready.promise;
7878
}
7979

80+
get roomName(): string | null {
81+
return this._yWebsocketProvider?.roomname ?? null;
82+
}
83+
8084
/**
8185
* Dispose of the resources held by the object.
8286
*/

0 commit comments

Comments
 (0)