Skip to content

Commit cb874d3

Browse files
committed
Implements the function to issue notification using ntfy
1 parent 4a3709a commit cb874d3

File tree

3 files changed

+91
-14
lines changed

3 files changed

+91
-14
lines changed

schema/plugin.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,19 @@
3939
"title": "Trigger only for the last selected notebook cell execution.",
4040
"description": "Trigger a notification only for the last selected executed notebook cell.",
4141
"default": false
42+
},
43+
"notification_methods": {
44+
"type": "array",
45+
"minItems": 1,
46+
"items": {
47+
"enum": [
48+
"browser",
49+
"ntfy"
50+
]
51+
},
52+
"title": "Notification Methods",
53+
"description": "Methods how to notificate messages. Select from 'browser' or 'ntfy'",
54+
"default": ["browser"]
4255
}
4356
}
4457
}

src/index.ts

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ import {
22
JupyterFrontEnd,
33
JupyterFrontEndPlugin
44
} from '@jupyterlab/application';
5+
import { ISessionContext, SessionContext } from '@jupyterlab/apputils';
56
import { KernelError, Notebook, NotebookActions } from '@jupyterlab/notebook';
67
import { Cell } from '@jupyterlab/cells';
78
import { ISettingRegistry } from '@jupyterlab/settingregistry';
89
import { ICodeCellModel } from '@jupyterlab/cells';
910
import { PageConfig } from '@jupyterlab/coreutils';
1011
import LRU from 'lru-cache';
1112
import moment from 'moment';
13+
import { issueNtfyNotification } from './ntfy';
1214
import { checkBrowserNotificationSettings } from './settings';
1315

1416
interface ICellExecutionMetadata {
@@ -19,16 +21,18 @@ interface ICellExecutionMetadata {
1921
/**
2022
* Constructs notification message and displays it.
2123
*/
22-
function displayNotification(
24+
async function displayNotification(
2325
cellDuration: string,
2426
cellNumber: number,
2527
notebookName: string,
2628
reportCellNumber: boolean,
2729
reportCellExecutionTime: boolean,
2830
failedExecution: boolean,
2931
error: KernelError | null,
30-
lastCellOnly: boolean
31-
): void {
32+
lastCellOnly: boolean,
33+
notificationMethods: string[],
34+
sessionContext: ISessionContext | null,
35+
): Promise<void> {
3236
const base = PageConfig.getBaseUrl();
3337
const notificationPayload = {
3438
icon: base + 'static/favicon.ico',
@@ -52,13 +56,20 @@ function displayNotification(
5256
}
5357

5458
notificationPayload.body = message;
55-
new Notification(title, notificationPayload);
59+
60+
if (notificationMethods.includes('browser')) {
61+
new Notification(title, notificationPayload);
62+
}
63+
if ((notificationMethods.includes('ntfy')) && (sessionContext)) {
64+
await issueNtfyNotification(title, notificationPayload, sessionContext);
65+
}
66+
5667
}
5768

5869
/**
5970
* Trigger notification.
6071
*/
61-
function triggerNotification(
72+
async function triggerNotification(
6273
cell: Cell,
6374
notebook: Notebook,
6475
cellExecutionMetadataTable: LRU<string, ICellExecutionMetadata>,
@@ -69,7 +80,9 @@ function triggerNotification(
6980
cellNumberType: string,
7081
failedExecution: boolean,
7182
error: KernelError | null,
72-
lastCellOnly: boolean
83+
lastCellOnly: boolean,
84+
notificationMethods: string[],
85+
sessionContext: ISessionContext | null,
7386
) {
7487
const cellEndTime = new Date();
7588
const codeCellModel = cell.model as ICodeCellModel;
@@ -102,15 +115,17 @@ function triggerNotification(
102115
? cellExecutionMetadata.index
103116
: codeCellModel.executionCount;
104117
const notebookName = notebook.title.label.replace(/\.[^/.]+$/, '');
105-
displayNotification(
118+
await displayNotification(
106119
cellDuration,
107120
cellNumber,
108121
notebookName,
109122
reportCellNumber,
110123
reportCellExecutionTime,
111124
failedExecution,
112125
error,
113-
lastCellOnly
126+
lastCellOnly,
127+
notificationMethods,
128+
sessionContext,
114129
);
115130
}
116131
}
@@ -128,6 +143,8 @@ const extension: JupyterFrontEndPlugin<void> = {
128143
let reportCellNumber = true;
129144
let cellNumberType = 'cell_index';
130145
let lastCellOnly = false;
146+
let notificationMethods = ['browser'];
147+
131148
const cellExecutionMetadataTable: LRU<
132149
string,
133150
ICellExecutionMetadata
@@ -138,6 +155,13 @@ const extension: JupyterFrontEndPlugin<void> = {
138155
max: 500
139156
});
140157

158+
// SessionContext is used for running python codes
159+
const manager = app.serviceManager;
160+
const sessionContext = new SessionContext({
161+
sessionManager: manager.sessions as any,
162+
specsManager: manager.kernelspecs,
163+
});
164+
141165
if (settingRegistry) {
142166
const setting = await settingRegistry.load(extension.id);
143167
const updateSettings = (): void => {
@@ -150,6 +174,7 @@ const extension: JupyterFrontEndPlugin<void> = {
150174
.composite as boolean;
151175
cellNumberType = setting.get('cell_number_type').composite as string;
152176
lastCellOnly = setting.get('last_cell_only').composite as boolean;
177+
notificationMethods = setting.get('notification_methods').composite as string[];
153178
};
154179
updateSettings();
155180
setting.changed.connect(updateSettings);
@@ -165,10 +190,10 @@ const extension: JupyterFrontEndPlugin<void> = {
165190
}
166191
});
167192

168-
NotebookActions.executed.connect((_, args) => {
193+
NotebookActions.executed.connect(async (_, args) => {
169194
if (enabled && !lastCellOnly) {
170195
const { cell, notebook, success, error } = args;
171-
triggerNotification(
196+
await triggerNotification(
172197
cell,
173198
notebook,
174199
cellExecutionMetadataTable,
@@ -179,16 +204,18 @@ const extension: JupyterFrontEndPlugin<void> = {
179204
cellNumberType,
180205
!success,
181206
error,
182-
lastCellOnly
207+
lastCellOnly,
208+
notificationMethods,
209+
sessionContext,
183210
);
184211
}
185212
});
186213

187-
NotebookActions.selectionExecuted.connect((_, args) => {
214+
NotebookActions.selectionExecuted.connect(async (_, args) => {
188215
if (enabled && lastCellOnly) {
189216
const { lastCell, notebook } = args;
190217
const failedExecution = false;
191-
triggerNotification(
218+
await triggerNotification(
192219
lastCell,
193220
notebook,
194221
cellExecutionMetadataTable,
@@ -199,7 +226,9 @@ const extension: JupyterFrontEndPlugin<void> = {
199226
cellNumberType,
200227
failedExecution,
201228
null,
202-
lastCellOnly
229+
lastCellOnly,
230+
notificationMethods,
231+
sessionContext,
203232
);
204233
}
205234
});

src/ntfy.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { ISessionContext } from '@jupyterlab/apputils';
2+
import { Kernel, KernelAPI, KernelMessage } from '@jupyterlab/services';
3+
4+
export async function ensureSessionContextKernelActivated(
5+
sessionContext: ISessionContext
6+
): Promise<void> {
7+
if (sessionContext.hasNoKernel) {
8+
await sessionContext.initialize()
9+
.then(async (value) => {
10+
if (value) {
11+
const py3kernel = await KernelAPI.startNew({ name: 'python3' });
12+
await sessionContext.changeKernel(py3kernel);
13+
}
14+
})
15+
.catch((reason) => {
16+
console.error(`Failed to initialize the session in jupyterlab-notifications.\n${reason}`);
17+
});;
18+
}
19+
}
20+
21+
export async function issueNtfyNotification(
22+
title: string,
23+
notificationPayload: { body: string },
24+
sessionContext: ISessionContext,
25+
): Promise<Kernel.IShellFuture<KernelMessage.IExecuteRequestMsg, KernelMessage.IExecuteReplyMsg>> {
26+
const { body } = notificationPayload;
27+
await ensureSessionContextKernelActivated(sessionContext);
28+
if (!sessionContext || !sessionContext.session?.kernel) {
29+
return;
30+
}
31+
const titleEscaped = title.replace(/"/g, '\\"');
32+
const bodyEscaped = body.replace(/"/g, '\\"');
33+
const code = `from ntfy import notify; notify("${bodyEscaped}", "${titleEscaped}")`;
34+
return sessionContext.session?.kernel?.requestExecute({ code });
35+
}

0 commit comments

Comments
 (0)