Skip to content

Commit b70eac0

Browse files
committed
Merge branch 'main' of github.com:mwakaba2/jupyterlab-notifications into main
2 parents 6075e75 + 6e680a4 commit b70eac0

File tree

5 files changed

+147
-14
lines changed

5 files changed

+147
-14
lines changed

README.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@
2020

2121
<img width="394" alt="Screen Shot 2021-07-31 at 12 49 49 PM" src="https://user-images.githubusercontent.com/3497137/127746862-79012afd-caa7-4319-930d-7acfc74fa2f4.png">
2222

23+
*Image when recieving a notification on a mobilephone using ntfy*
24+
25+
<img width="324" height="318" src="static/images/via_ntfy_sample.png">
26+
2327
## Quick demos and tutorials :notebook:
2428

2529
To test out this extension without any local set-up, please check out the [binder link](https://mybinder.org/v2/gh/mwakaba2/jupyterlab-notifications/main?urlpath=lab/tree/tutorial/py3_demo.ipynb). This will set-up the environment, install the extension, and take you to several demo notebooks for you to play around with to get familiar with the notifications extension.
@@ -80,6 +84,12 @@ Use the following settings to update cell execution time for a notification and
8084
// The minimum execution time to send out notification for a particular notebook cell (in seconds).
8185
"minimum_cell_execution_time": 60,
8286

87+
// Notification Methods
88+
// Option to send a notification with the specified method(s). The available options are 'browser' and 'ntfy'.
89+
"notification_methods": [
90+
"browser"
91+
],
92+
8393
// Report Notebook Cell Execution Time
8494
// Display notebook cell execution time in the notification.
8595
// If last_cell_only is set to true, the total duration of the selected cells will be displayed.
@@ -106,6 +116,47 @@ Use the following settings to update cell execution time for a notification and
106116

107117
The cell timing doesn't need to be enabled for Jupyterlab >= 3.1 and Jupyterlab notification version >= v0.3.0.
108118

119+
## (Optional) Notifications using `ntfy`
120+
121+
You can recieve notifications via `ntfy`.
122+
123+
**ntfy 2.7.0 documentation** https://ntfy.readthedocs.io/en/latest/
124+
125+
> ntfy brings notification to your shell. It can automatically provide desktop notifications when long running code executions finish or it can send push notifications to your phone when a specific execution finishes.
126+
127+
### How to enable notifications via `ntfy`
128+
129+
Install `ntfy`.
130+
131+
```console
132+
$ pip install ntfy
133+
```
134+
To configure ntfy, please check out [the ntfy official configuration docs](https://ntfy.readthedocs.io/en/latest/#configuring-ntfy)
135+
136+
For example, if your OS is Linux and you want to select `pushover` for the backend, set configuration as follows.
137+
138+
```console
139+
$ vim ~/.config/ntfy/ntfy.yml
140+
```
141+
142+
```yaml
143+
backends:
144+
- pushover
145+
pushover:
146+
user_key: YOUR_PUSHOVER_USER_KEY
147+
```
148+
149+
Change the notifications [settings](#settings). Append `"ntfy"` into `notification_methods` attribute.
150+
- NOTE: The value `browser` implies default conventional method, which uses Webbrowser's Notification API.
151+
152+
```json5
153+
{
154+
// ...
155+
"notification_methods": ["browser", "ntfy"]
156+
// ...
157+
}
158+
```
159+
109160
## Contributing
110161

111162
### Development install

schema/plugin.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,16 @@
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": ["browser", "ntfy"]
48+
},
49+
"title": "Notification Methods",
50+
"description": "Option to send a notification with the specified method(s). The available options are 'browser' and 'ntfy'.",
51+
"default": ["browser"]
4252
}
4353
}
4454
}

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,19 @@ 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+
}
5666
}
5767

5868
/**
5969
* Trigger notification.
6070
*/
61-
function triggerNotification(
71+
async function triggerNotification(
6272
cell: Cell,
6373
notebook: Notebook,
6474
cellExecutionMetadataTable: LRU<string, ICellExecutionMetadata>,
@@ -69,7 +79,9 @@ function triggerNotification(
6979
cellNumberType: string,
7080
failedExecution: boolean,
7181
error: KernelError | null,
72-
lastCellOnly: boolean
82+
lastCellOnly: boolean,
83+
notificationMethods: string[],
84+
sessionContext: ISessionContext | null
7385
) {
7486
const cellEndTime = new Date();
7587
const codeCellModel = cell.model as ICodeCellModel;
@@ -102,15 +114,17 @@ function triggerNotification(
102114
? cellExecutionMetadata.index
103115
: codeCellModel.executionCount;
104116
const notebookName = notebook.title.label.replace(/\.[^/.]+$/, '');
105-
displayNotification(
117+
await displayNotification(
106118
cellDuration,
107119
cellNumber,
108120
notebookName,
109121
reportCellNumber,
110122
reportCellExecutionTime,
111123
failedExecution,
112124
error,
113-
lastCellOnly
125+
lastCellOnly,
126+
notificationMethods,
127+
sessionContext
114128
);
115129
}
116130
}
@@ -128,6 +142,8 @@ const extension: JupyterFrontEndPlugin<void> = {
128142
let reportCellNumber = true;
129143
let cellNumberType = 'cell_index';
130144
let lastCellOnly = false;
145+
let notificationMethods = ['browser'];
146+
131147
const cellExecutionMetadataTable: LRU<
132148
string,
133149
ICellExecutionMetadata
@@ -138,6 +154,13 @@ const extension: JupyterFrontEndPlugin<void> = {
138154
max: 500
139155
});
140156

157+
// SessionContext is used for running python codes
158+
const manager = app.serviceManager;
159+
const sessionContext = new SessionContext({
160+
sessionManager: manager.sessions as any,
161+
specsManager: manager.kernelspecs
162+
});
163+
141164
if (settingRegistry) {
142165
const setting = await settingRegistry.load(extension.id);
143166
const updateSettings = (): void => {
@@ -150,6 +173,8 @@ const extension: JupyterFrontEndPlugin<void> = {
150173
.composite as boolean;
151174
cellNumberType = setting.get('cell_number_type').composite as string;
152175
lastCellOnly = setting.get('last_cell_only').composite as boolean;
176+
notificationMethods = setting.get('notification_methods')
177+
.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: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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
9+
.initialize()
10+
.then(async value => {
11+
if (value) {
12+
const py3kernel = await KernelAPI.startNew({ name: 'python3' });
13+
await sessionContext.changeKernel(py3kernel);
14+
}
15+
})
16+
.catch(reason => {
17+
console.error(
18+
`Failed to initialize the session in jupyterlab-notifications.\n${reason}`
19+
);
20+
});
21+
}
22+
}
23+
24+
export async function issueNtfyNotification(
25+
title: string,
26+
notificationPayload: { body: string },
27+
sessionContext: ISessionContext
28+
): Promise<
29+
Kernel.IShellFuture<
30+
KernelMessage.IExecuteRequestMsg,
31+
KernelMessage.IExecuteReplyMsg
32+
>
33+
> {
34+
const { body } = notificationPayload;
35+
await ensureSessionContextKernelActivated(sessionContext);
36+
if (!sessionContext || !sessionContext.session?.kernel) {
37+
return;
38+
}
39+
const titleEscaped = title.replace(/"/g, '\\"');
40+
const bodyEscaped = body.replace(/"/g, '\\"');
41+
const code = `from ntfy import notify; notify("${bodyEscaped}", "${titleEscaped}")`;
42+
return sessionContext.session?.kernel?.requestExecute({ code });
43+
}

static/images/via_ntfy_sample.png

76.6 KB
Loading

0 commit comments

Comments
 (0)