Skip to content

Commit c6e5115

Browse files
committed
Notify for failed notebook cell executions
1 parent c724dc6 commit c6e5115

File tree

7 files changed

+1139
-504
lines changed

7 files changed

+1139
-504
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,15 @@ Notebook Cell Completion Browser Notifications for JupyterLab.
1414

1515
* Web Browser that supports the Notification Web API (See [Browser Compatibility Chart](https://developer.mozilla.org/en-US/docs/Web/API/notification#browser_compatibility))
1616
* JupyterLab >= 3.0
17-
* Notebook Cell Timing needs to be enabled for Jupyterlab Notifications to work. Please go to Settings -> Advanced Settings Editor -> Notebook and update setting to:
17+
* Notebook Cell Timing needs to be enabled for Jupyterlab Notifications with version < `0.3.0` to work. Please go to Settings -> Advanced Settings Editor -> Notebook and update setting to:
1818
```json5
1919
{
2020
// Recording timing
2121
// Should timing data be recorded in cell metadata
2222
"recordTiming": true
2323
}
2424
```
25+
* [BETA] the cell timing doesn't need to be enabled for Jupyterlab >= 3.1 and Jupyterlab notification version 0.3.0.
2526

2627
## Install
2728

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
},
4747
"dependencies": {
4848
"@jupyterlab/application": "^3.0.6",
49-
"@jupyterlab/notebook": "^3.0.6",
49+
"@jupyterlab/notebook": "^3.1.0-beta.1",
5050
"@jupyterlab/settingregistry": "^3.0.3"
5151
},
5252
"devDependencies": {

src/index.ts

Lines changed: 79 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,13 @@ import {
22
JupyterFrontEnd,
33
JupyterFrontEndPlugin
44
} from '@jupyterlab/application';
5-
import { NotebookActions } from '@jupyterlab/notebook';
6-
import { IObservableJSON } from '@jupyterlab/observables';
5+
import { KernelError, Notebook, NotebookActions } from '@jupyterlab/notebook';
6+
import { Cell } from '@jupyterlab/cells';
77
import { ISettingRegistry } from '@jupyterlab/settingregistry';
88
import { ICodeCellModel } from '@jupyterlab/cells';
99

1010
import { checkBrowserNotificationSettings } from './settings';
1111

12-
/**
13-
* Extracts Code Cell Start and End Time
14-
*/
15-
function extractExecutionMetadata(metadata: IObservableJSON): [Date, Date] {
16-
const executionMetadata = Object.assign({}, metadata.get('execution') as any);
17-
const cellStartTime = new Date(
18-
executionMetadata['shell.execute_reply.started'] ||
19-
executionMetadata['iopub.execute_input']
20-
);
21-
const cellEndTime = new Date(executionMetadata['shell.execute_reply']);
22-
return [cellStartTime, cellEndTime];
23-
}
2412

2513
/**
2614
* Constructs notification message and displays it.
@@ -30,14 +18,20 @@ function displayNotification(
3018
cellNumber: number,
3119
notebookName: string,
3220
reportCellNumber: boolean,
33-
reportCellExecutionTime: boolean
21+
reportCellExecutionTime: boolean,
22+
failedExecution: boolean,
23+
error: KernelError | null
3424
): void {
3525
const notificationPayload = {
3626
icon: '/static/favicon.ico',
3727
body: ''
3828
};
29+
const title = failedExecution ? `${notebookName} Failed!` : `${notebookName} Completed!`;
3930
let message = '';
40-
if (reportCellNumber && reportCellExecutionTime) {
31+
32+
if (failedExecution) {
33+
message = error ? `${error.errorName} ${error.errorValue}` : '';
34+
} else if (reportCellNumber && reportCellExecutionTime) {
4135
message = `Cell[${cellNumber}] Duration: ${cellDuration}`;
4236
} else if (reportCellNumber) {
4337
message = `Cell Number: ${cellNumber}`;
@@ -46,20 +40,65 @@ function displayNotification(
4640
}
4741

4842
notificationPayload.body = message;
49-
new Notification(`${notebookName} Cell Completed!`, notificationPayload);
43+
new Notification(title, notificationPayload);
44+
}
45+
46+
/**
47+
* Trigger notification.
48+
*/
49+
function triggerNotification(
50+
cell: Cell,
51+
notebook: Notebook,
52+
cellStartTime: Date,
53+
cellEndTime: Date,
54+
minimumCellExecutionTime: number,
55+
reportCellNumber: boolean,
56+
reportCellExecutionTime: boolean,
57+
cellNumberType: string,
58+
failedExecution: boolean,
59+
error: KernelError | null
60+
) {
61+
const codeCell = cell.model.type === 'code';
62+
const nonEmptyCell = cell.model.value.text.length > 0;
63+
if (codeCell && nonEmptyCell) {
64+
const codeCellModel = cell.model as ICodeCellModel;
65+
const diff = new Date(<any>cellEndTime - <any>cellStartTime);
66+
const diffSeconds = Math.floor(diff.getTime() / 1000);
67+
if (diffSeconds >= minimumCellExecutionTime) {
68+
const cellDuration = diff.toISOString().substr(11, 8);
69+
const cellNumber =
70+
cellNumberType === 'cell_index'
71+
? notebook.activeCellIndex
72+
: codeCellModel.executionCount;
73+
const notebookName = notebook.title.label.replace(
74+
/\.[^/.]+$/,
75+
''
76+
);
77+
displayNotification(
78+
cellDuration,
79+
cellNumber,
80+
notebookName,
81+
reportCellNumber,
82+
reportCellExecutionTime,
83+
failedExecution,
84+
error
85+
);
86+
}
87+
}
5088
}
5189

5290
const extension: JupyterFrontEndPlugin<void> = {
5391
id: 'jupyterlab-notifications:plugin',
5492
autoStart: true,
55-
optional: [ISettingRegistry],
93+
requires: [ISettingRegistry],
5694
activate: async (app: JupyterFrontEnd, settingRegistry: ISettingRegistry) => {
5795
checkBrowserNotificationSettings();
5896
let enabled = true;
5997
let minimumCellExecutionTime = 60;
6098
let reportCellExecutionTime = true;
6199
let reportCellNumber = true;
62100
let cellNumberType = 'cell_index';
101+
63102
if (settingRegistry) {
64103
const setting = await settingRegistry.load(extension.id);
65104
const updateSettings = (): void => {
@@ -70,51 +109,35 @@ const extension: JupyterFrontEndPlugin<void> = {
70109
.composite as boolean;
71110
reportCellNumber = setting.get('report_cell_number')
72111
.composite as boolean;
73-
cellNumberType = setting.get('cell_number_type').composite as string;
112+
cellNumberType = setting.get('cell_number_type')
113+
.composite as string;
74114
};
75115
updateSettings();
76116
setting.changed.connect(updateSettings);
77117
}
78118

119+
let cellStartTime = new Date();
120+
121+
NotebookActions.executionScheduled.connect((_, args) => {
122+
if (enabled) {
123+
cellStartTime = new Date();
124+
}
125+
});
126+
79127
NotebookActions.executed.connect((_, args) => {
80128
if (enabled) {
81-
const { cell, notebook } = args;
82-
const codeCell = cell.model.type === 'code';
83-
const nonEmptyCell = cell.model.value.text.length > 0;
84-
const metadata = cell.model.metadata;
85-
if (codeCell && nonEmptyCell) {
86-
const codeCellModel = cell.model as ICodeCellModel;
87-
if (metadata.has('execution')) {
88-
const [cellStartTime, cellEndTime] = extractExecutionMetadata(
89-
metadata
90-
);
91-
const diff = new Date(<any>cellEndTime - <any>cellStartTime);
92-
const diffSeconds = Math.floor(diff.getTime() / 1000);
93-
if (diffSeconds >= minimumCellExecutionTime) {
94-
const cellDuration = diff.toISOString().substr(11, 8);
95-
const cellNumber =
96-
cellNumberType === 'cell_index'
97-
? notebook.activeCellIndex
98-
: codeCellModel.executionCount;
99-
const notebookName = notebook.title.label.replace(
100-
/\.[^/.]+$/,
101-
''
102-
);
103-
displayNotification(
104-
cellDuration,
105-
cellNumber,
106-
notebookName,
107-
reportCellNumber,
108-
reportCellExecutionTime
109-
);
110-
}
111-
} else {
112-
alert(
113-
'Notebook Cell Timing needs to be enabled for Jupyterlab Notifications to work. ' +
114-
'Please go to Settings -> Advanced Settings Editor -> Notebook and update setting to {"recordTiming": true}'
115-
);
116-
}
117-
}
129+
const { cell, notebook, success, error } = args;
130+
const cellEndTime = new Date();
131+
triggerNotification(cell,
132+
notebook,
133+
cellStartTime,
134+
cellEndTime,
135+
minimumCellExecutionTime,
136+
reportCellNumber,
137+
reportCellExecutionTime,
138+
cellNumberType,
139+
!success,
140+
error);
118141
}
119142
});
120143
}
51.1 KB
Loading

tutorial/notebook_notification_py3_demo.ipynb

Lines changed: 0 additions & 172 deletions
This file was deleted.

0 commit comments

Comments
 (0)