├── .yarnrc.yml ├── style ├── index.js ├── index.css └── base.css ├── setup.py ├── .prettyignore ├── CHANGELOG.md ├── .prettierignore ├── static └── images │ ├── notification.gif │ ├── notification.png │ ├── error_notification.png │ ├── pushover_android_example.png │ ├── pushover_iphone_example.jpeg │ └── last_cell_only_notification.png ├── binder ├── environment.yml └── postBuild ├── install.json ├── .github └── workflows │ ├── binder-on-pr.yml │ ├── enforce-label.yml │ ├── check-release.yml │ ├── prep-release.yml │ ├── publish-release.yml │ └── build.yml ├── tsconfig.json ├── jupyterlab_notifications └── __init__.py ├── src ├── settings.ts ├── ntfy.ts └── index.ts ├── LICENSE ├── tutorial ├── julia_demo.ipynb ├── py3_demo_3.ipynb ├── py3_demo_2.ipynb └── py3_demo.ipynb ├── schema └── plugin.json ├── .gitignore ├── pyproject.toml ├── RELEASE.md ├── package.json └── README.md /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /style/index.js: -------------------------------------------------------------------------------- 1 | import './base.css'; 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | __import__("setuptools").setup() 2 | -------------------------------------------------------------------------------- /style/index.css: -------------------------------------------------------------------------------- 1 | @import url('base.css'); 2 | -------------------------------------------------------------------------------- /.prettyignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | **/node_modules 3 | **/lib 4 | **/package.json 5 | jupyterlab-notifications -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | **/node_modules 3 | **/lib 4 | **/package.json 5 | !/package.json 6 | jupyterlab_notifications 7 | -------------------------------------------------------------------------------- /static/images/notification.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwakaba2/jupyterlab-notifications/HEAD/static/images/notification.gif -------------------------------------------------------------------------------- /static/images/notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwakaba2/jupyterlab-notifications/HEAD/static/images/notification.png -------------------------------------------------------------------------------- /static/images/error_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwakaba2/jupyterlab-notifications/HEAD/static/images/error_notification.png -------------------------------------------------------------------------------- /static/images/pushover_android_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwakaba2/jupyterlab-notifications/HEAD/static/images/pushover_android_example.png -------------------------------------------------------------------------------- /static/images/pushover_iphone_example.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwakaba2/jupyterlab-notifications/HEAD/static/images/pushover_iphone_example.jpeg -------------------------------------------------------------------------------- /static/images/last_cell_only_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwakaba2/jupyterlab-notifications/HEAD/static/images/last_cell_only_notification.png -------------------------------------------------------------------------------- /style/base.css: -------------------------------------------------------------------------------- 1 | /* 2 | See the JupyterLab Developer Guide for useful CSS Patterns: 3 | 4 | https://jupyterlab.readthedocs.io/en/stable/developer/css.html 5 | */ 6 | -------------------------------------------------------------------------------- /binder/environment.yml: -------------------------------------------------------------------------------- 1 | name: jupyterlab-notifications 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - jupyterlab>=3.1 6 | - pip: 7 | - jupyterlab-notifications==0.4.1 8 | -------------------------------------------------------------------------------- /install.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageManager": "python", 3 | "packageName": "jupyterlab-notifications", 4 | "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package jupyterlab-notifications" 5 | } 6 | -------------------------------------------------------------------------------- /.github/workflows/binder-on-pr.yml: -------------------------------------------------------------------------------- 1 | name: Binder Badge 2 | on: 3 | pull_request_target: 4 | types: [opened] 5 | 6 | jobs: 7 | binder: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | pull-requests: write 11 | steps: 12 | - uses: jupyterlab/maintainer-tools/.github/actions/binder-link@v1 13 | with: 14 | github_token: ${{ secrets.GITHUB_TOKEN }} 15 | -------------------------------------------------------------------------------- /.github/workflows/enforce-label.yml: -------------------------------------------------------------------------------- 1 | name: Enforce PR label 2 | 3 | on: 4 | pull_request: 5 | types: [labeled, unlabeled, opened, edited, synchronize] 6 | jobs: 7 | enforce-label: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | pull-requests: write 11 | steps: 12 | - name: enforce-triage-label 13 | uses: jupyterlab/maintainer-tools/.github/actions/enforce-label@v1 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "composite": true, 5 | "declaration": true, 6 | "esModuleInterop": true, 7 | "incremental": true, 8 | "jsx": "react", 9 | "module": "esnext", 10 | "moduleResolution": "node", 11 | "noEmitOnError": true, 12 | "noImplicitAny": true, 13 | "noUnusedLocals": true, 14 | "preserveWatchOutput": true, 15 | "resolveJsonModule": true, 16 | "outDir": "lib", 17 | "rootDir": "src", 18 | "strict": true, 19 | "strictNullChecks": false, 20 | "target": "ES2018" 21 | }, 22 | "include": ["src/*"] 23 | } 24 | -------------------------------------------------------------------------------- /jupyterlab_notifications/__init__.py: -------------------------------------------------------------------------------- 1 | try: 2 | from ._version import __version__ 3 | except ImportError: 4 | # Fallback when using the package in dev mode without installing 5 | # in editable mode with pip. It is highly recommended to install 6 | # the package from a stable release or in editable mode: https://pip.pypa.io/en/stable/topics/local-project-installs/#editable-installs 7 | import warnings 8 | warnings.warn("Importing 'jupyterlab_notifications' outside a proper installation.") 9 | __version__ = "dev" 10 | 11 | 12 | def _jupyter_labextension_paths(): 13 | return [{ 14 | "src": "labextension", 15 | "dest": "jupyterlab-notifications" 16 | }] 17 | -------------------------------------------------------------------------------- /.github/workflows/check-release.yml: -------------------------------------------------------------------------------- 1 | name: Check Release 2 | on: 3 | push: 4 | branches: ['main'] 5 | pull_request: 6 | branches: ['*'] 7 | 8 | jobs: 9 | check_release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | - name: Base Setup 15 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 16 | - name: Check Release 17 | uses: jupyter-server/jupyter_releaser/.github/actions/check-release@v2 18 | with: 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | 21 | - name: Upload Distributions 22 | uses: actions/upload-artifact@v3 23 | with: 24 | name: jupyterlab_notifications-releaser-dist-${{ github.run_number }} 25 | path: .jupyter_releaser_checkout/dist 26 | -------------------------------------------------------------------------------- /src/settings.ts: -------------------------------------------------------------------------------- 1 | function checkNotificationPromise(): boolean { 2 | try { 3 | Notification.requestPermission().then(); 4 | } catch (e) { 5 | return false; 6 | } 7 | return true; 8 | } 9 | 10 | function handlePermission(permission: string): void { 11 | if (permission !== 'granted') { 12 | alert( 13 | 'Browser Notifications are not allowed. Please update your browser settings to allow notifications.' 14 | ); 15 | } 16 | } 17 | 18 | export function checkBrowserNotificationSettings(): void { 19 | if (!('Notification' in window)) { 20 | alert('This browser does not support notifications.'); 21 | } else if (Notification.permission !== 'granted') { 22 | if (checkNotificationPromise()) { 23 | Notification.requestPermission().then(permission => { 24 | handlePermission(permission); 25 | }); 26 | } else { 27 | Notification.requestPermission(permission => { 28 | handlePermission(permission); 29 | }); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/ntfy.ts: -------------------------------------------------------------------------------- 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 { 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 | } 44 | -------------------------------------------------------------------------------- /binder/postBuild: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ perform a development install of jupyterlab_notifications 3 | 4 | On Binder, this will run _after_ the environment has been fully created from 5 | the environment.yml in this directory. 6 | 7 | This script should also run locally on Linux/MacOS/Windows: 8 | 9 | python3 binder/postBuild 10 | """ 11 | import subprocess 12 | import sys 13 | from pathlib import Path 14 | 15 | 16 | ROOT = Path.cwd() 17 | 18 | def _(*args, **kwargs): 19 | """ Run a command, echoing the args 20 | 21 | fails hard if something goes wrong 22 | """ 23 | print("\n\t", " ".join(args), "\n") 24 | return_code = subprocess.call(args, **kwargs) 25 | if return_code != 0: 26 | print("\nERROR", return_code, " ".join(args)) 27 | sys.exit(return_code) 28 | 29 | # verify the environment is self-consistent before even starting 30 | _(sys.executable, "-m", "pip", "check") 31 | 32 | # install the labextension 33 | _(sys.executable, "-m", "pip", "install", "-e", ".") 34 | _(sys.executable, "-m", "jupyter", "labextension", "develop", "--overwrite", ".") 35 | 36 | # verify the environment the extension didn't break anything 37 | _(sys.executable, "-m", "pip", "check") 38 | 39 | # list the extensions 40 | _("jupyter", "server", "extension", "list") 41 | 42 | # initially list installed extensions to determine if there are any surprises 43 | _("jupyter", "labextension", "list") 44 | 45 | 46 | print("JupyterLab with jupyterlab_notifications is ready to run with:\n") 47 | print("\tjupyter lab\n") 48 | -------------------------------------------------------------------------------- /.github/workflows/prep-release.yml: -------------------------------------------------------------------------------- 1 | name: 'Step 1: Prep Release' 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | version_spec: 6 | description: 'New Version Specifier' 7 | default: 'next' 8 | required: false 9 | branch: 10 | description: 'The branch to target' 11 | required: false 12 | post_version_spec: 13 | description: 'Post Version Specifier' 14 | required: false 15 | since: 16 | description: 'Use PRs with activity since this date or git reference' 17 | required: false 18 | since_last_stable: 19 | description: 'Use PRs with activity since the last stable git tag' 20 | required: false 21 | type: boolean 22 | jobs: 23 | prep_release: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 27 | 28 | - name: Prep Release 29 | id: prep-release 30 | uses: jupyter-server/jupyter_releaser/.github/actions/prep-release@v2 31 | with: 32 | token: ${{ secrets.ADMIN_GITHUB_TOKEN }} 33 | version_spec: ${{ github.event.inputs.version_spec }} 34 | post_version_spec: ${{ github.event.inputs.post_version_spec }} 35 | branch: ${{ github.event.inputs.branch }} 36 | since: ${{ github.event.inputs.since }} 37 | since_last_stable: ${{ github.event.inputs.since_last_stable }} 38 | 39 | - name: '** Next Step **' 40 | run: | 41 | echo "Optional): Review Draft Release: ${{ steps.prep-release.outputs.release_url }}" 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, Mariko Wakabayashi All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | * Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /tutorial/julia_demo.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "source": [ 6 | "## Jupyterlab Notification Julia Demo\n", 7 | "For this demo, make sure to update `minimum_cell_execution_time` to 1 second in Settings > Advanced Settings Editor > Notifications.\n", 8 | "\n", 9 | "For instructions on how to install and set-up a Julia Notebook Kernel, please check out [here](https://julialang.github.io/IJulia.jl/dev/manual/running/)." 10 | ], 11 | "metadata": {} 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": 7, 16 | "source": [ 17 | "sleep(4)\n", 18 | "print(\"hello world!\")" 19 | ], 20 | "outputs": [ 21 | { 22 | "output_type": "stream", 23 | "name": "stdout", 24 | "text": [ 25 | "hello world!" 26 | ] 27 | } 28 | ], 29 | "metadata": { 30 | "execution": { 31 | "iopub.execute_input": "2021-04-19T20:53:28.895000-06:00", 32 | "iopub.status.busy": "2021-04-19T20:53:28.895000-06:00", 33 | "iopub.status.idle": "2021-04-19T20:53:32.912000-06:00", 34 | "shell.execute_reply": "2021-04-19T20:53:32.911000-06:00" 35 | }, 36 | "tags": [] 37 | } 38 | }, 39 | { 40 | "cell_type": "code", 41 | "execution_count": null, 42 | "source": [], 43 | "outputs": [], 44 | "metadata": {} 45 | } 46 | ], 47 | "metadata": { 48 | "kernelspec": { 49 | "display_name": "Julia 1.6.0", 50 | "language": "julia", 51 | "name": "julia-1.6" 52 | }, 53 | "language_info": { 54 | "file_extension": ".jl", 55 | "mimetype": "application/julia", 56 | "name": "julia", 57 | "version": "1.6.0" 58 | } 59 | }, 60 | "nbformat": 4, 61 | "nbformat_minor": 5 62 | } -------------------------------------------------------------------------------- /.github/workflows/publish-release.yml: -------------------------------------------------------------------------------- 1 | name: 'Step 2: Publish Release' 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | branch: 6 | description: 'The target branch' 7 | required: false 8 | release_url: 9 | description: 'The URL of the draft GitHub release' 10 | required: false 11 | steps_to_skip: 12 | description: 'Comma separated list of steps to skip' 13 | required: false 14 | 15 | jobs: 16 | publish_release: 17 | runs-on: ubuntu-latest 18 | permissions: 19 | # This is useful if you want to use PyPI trusted publisher 20 | # and NPM provenance 21 | id-token: write 22 | steps: 23 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 24 | 25 | - name: Populate Release 26 | id: populate-release 27 | uses: jupyter-server/jupyter_releaser/.github/actions/populate-release@v2 28 | with: 29 | token: ${{ secrets.ADMIN_GITHUB_TOKEN }} 30 | branch: ${{ github.event.inputs.branch }} 31 | release_url: ${{ github.event.inputs.release_url }} 32 | steps_to_skip: ${{ github.event.inputs.steps_to_skip }} 33 | 34 | - name: Finalize Release 35 | id: finalize-release 36 | env: 37 | # The following are needed if you use legacy PyPI set up 38 | # PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} 39 | # PYPI_TOKEN_MAP: ${{ secrets.PYPI_TOKEN_MAP }} 40 | # TWINE_USERNAME: __token__ 41 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 42 | uses: jupyter-server/jupyter_releaser/.github/actions/finalize-release@v2 43 | with: 44 | token: ${{ secrets.ADMIN_GITHUB_TOKEN }} 45 | release_url: ${{ steps.populate-release.outputs.release_url }} 46 | 47 | - name: '** Next Step **' 48 | if: ${{ success() }} 49 | run: | 50 | echo "Verify the final release" 51 | echo ${{ steps.finalize-release.outputs.release_url }} 52 | 53 | - name: '** Failure Message **' 54 | if: ${{ failure() }} 55 | run: | 56 | echo "Failed to Publish the Draft Release Url:" 57 | echo ${{ steps.populate-release.outputs.release_url }} 58 | -------------------------------------------------------------------------------- /schema/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Notifications", 3 | "description": "Settings for the Notifications extension", 4 | "type": "object", 5 | "properties": { 6 | "enabled": { 7 | "type": "boolean", 8 | "title": "Enabled Status", 9 | "description": "Enable the extension or not.", 10 | "default": true 11 | }, 12 | "minimum_cell_execution_time": { 13 | "type": "number", 14 | "title": "Minimum Notebook Cell Execution Time", 15 | "description": "The minimum execution time to send out notification for a particular notebook cell (in seconds).", 16 | "default": 60 17 | }, 18 | "report_cell_execution_time": { 19 | "type": "boolean", 20 | "title": "Report Notebook Cell Execution Time", 21 | "description": "Display notebook cell execution time in the notification. If last_cell_only is set to true, the total duration of the selected cells will be displayed.", 22 | "default": true 23 | }, 24 | "report_cell_number": { 25 | "type": "boolean", 26 | "title": "Report Notebook Cell Number", 27 | "description": "Display notebook cell number in the notification.", 28 | "default": true 29 | }, 30 | "cell_number_type": { 31 | "type": "string", 32 | "title": "Cell Number Type", 33 | "description": "Type of cell number to display when the report_cell_number is true. Select from 'cell_index' or ‘cell_execution_count'.", 34 | "enum": ["cell_index", "cell_execution_count"], 35 | "default": "cell_index" 36 | }, 37 | "last_cell_only": { 38 | "type": "boolean", 39 | "title": "Trigger only for the last selected notebook cell execution.", 40 | "description": "Trigger a notification only for the last selected executed notebook cell.", 41 | "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"] 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tutorial/py3_demo_3.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "source": [ 6 | "## Jupyterlab Notification Python 3 Demo\n", 7 | "For this demo, make sure to update `minimum_cell_execution_time` to 1 second in Settings > Advanced Settings Editor > Notifications.\n", 8 | "\n", 9 | "This notebook in conjunction with `py3_demo_2.ipynb` was created to test simultaneous runs with `py3_demo.ipynb`.\n", 10 | "\n", 11 | "Try running all the cells in notebooks with the prefix `py3_demo_` to see that notebook name and cell execution duration is correct in the notification. " 12 | ], 13 | "metadata": {} 14 | }, 15 | { 16 | "cell_type": "code", 17 | "execution_count": 7, 18 | "source": [ 19 | "!sleep 3\n", 20 | "print(\"hello from notebook 3\")" 21 | ], 22 | "outputs": [ 23 | { 24 | "output_type": "stream", 25 | "name": "stdout", 26 | "text": [ 27 | "hello from notebook 3\n" 28 | ] 29 | } 30 | ], 31 | "metadata": { 32 | "execution": { 33 | "iopub.execute_input": "2021-07-26T02:25:20.354141Z", 34 | "iopub.status.busy": "2021-07-26T02:25:20.353849Z", 35 | "iopub.status.idle": "2021-07-26T02:25:23.476677Z", 36 | "shell.execute_reply": "2021-07-26T02:25:23.475669Z", 37 | "shell.execute_reply.started": "2021-07-26T02:25:20.354100Z" 38 | }, 39 | "tags": [] 40 | } 41 | }, 42 | { 43 | "cell_type": "code", 44 | "execution_count": null, 45 | "source": [], 46 | "outputs": [], 47 | "metadata": {} 48 | }, 49 | { 50 | "cell_type": "code", 51 | "execution_count": null, 52 | "source": [], 53 | "outputs": [], 54 | "metadata": {} 55 | } 56 | ], 57 | "metadata": { 58 | "kernelspec": { 59 | "display_name": "Python 3", 60 | "language": "python", 61 | "name": "python3" 62 | }, 63 | "language_info": { 64 | "codemirror_mode": { 65 | "name": "ipython", 66 | "version": 3 67 | }, 68 | "file_extension": ".py", 69 | "mimetype": "text/x-python", 70 | "name": "python", 71 | "nbconvert_exporter": "python", 72 | "pygments_lexer": "ipython3", 73 | "version": "3.7.10" 74 | } 75 | }, 76 | "nbformat": 4, 77 | "nbformat_minor": 5 78 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.bundle.* 2 | lib/ 3 | node_modules/ 4 | *.log 5 | .eslintcache 6 | .stylelintcache 7 | *.egg-info/ 8 | .ipynb_checkpoints 9 | *.tsbuildinfo 10 | jupyterlab_notifications/labextension 11 | # Version file is handled by hatchling 12 | jupyterlab_notifications/_version.py 13 | 14 | # Created by https://www.gitignore.io/api/python 15 | # Edit at https://www.gitignore.io/?templates=python 16 | 17 | ### Python ### 18 | # Byte-compiled / optimized / DLL files 19 | __pycache__/ 20 | *.py[cod] 21 | *$py.class 22 | 23 | # C extensions 24 | *.so 25 | 26 | # Distribution / packaging 27 | .Python 28 | build/ 29 | develop-eggs/ 30 | dist/ 31 | downloads/ 32 | eggs/ 33 | .eggs/ 34 | lib/ 35 | lib64/ 36 | parts/ 37 | sdist/ 38 | var/ 39 | wheels/ 40 | pip-wheel-metadata/ 41 | share/python-wheels/ 42 | .installed.cfg 43 | *.egg 44 | MANIFEST 45 | 46 | # PyInstaller 47 | # Usually these files are written by a python script from a template 48 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 49 | *.manifest 50 | *.spec 51 | 52 | # Installer logs 53 | pip-log.txt 54 | pip-delete-this-directory.txt 55 | 56 | # Unit test / coverage reports 57 | htmlcov/ 58 | .tox/ 59 | .nox/ 60 | .coverage 61 | .coverage.* 62 | .cache 63 | nosetests.xml 64 | coverage/ 65 | coverage.xml 66 | *.cover 67 | .hypothesis/ 68 | .pytest_cache/ 69 | 70 | # Translations 71 | *.mo 72 | *.pot 73 | 74 | # Scrapy stuff: 75 | .scrapy 76 | 77 | # Sphinx documentation 78 | docs/_build/ 79 | 80 | # PyBuilder 81 | target/ 82 | 83 | # pyenv 84 | .python-version 85 | 86 | # celery beat schedule file 87 | celerybeat-schedule 88 | 89 | # SageMath parsed files 90 | *.sage.py 91 | 92 | # Spyder project settings 93 | .spyderproject 94 | .spyproject 95 | 96 | # Rope project settings 97 | .ropeproject 98 | 99 | # Mr Developer 100 | .mr.developer.cfg 101 | .project 102 | .pydevproject 103 | 104 | # mkdocs documentation 105 | /site 106 | 107 | # mypy 108 | .mypy_cache/ 109 | .dmypy.json 110 | dmypy.json 111 | 112 | # Pyre type checker 113 | .pyre/ 114 | 115 | # End of https://www.gitignore.io/api/python 116 | 117 | # OSX files 118 | .DS_Store 119 | 120 | # Yarn cache 121 | .yarn/ 122 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: main 6 | pull_request: 7 | branches: '*' 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | 17 | - name: Base Setup 18 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 19 | 20 | - name: Install dependencies 21 | run: python -m pip install -U "jupyterlab>=4.0.0,<5" 22 | 23 | - name: Lint the extension 24 | run: | 25 | set -eux 26 | jlpm 27 | jlpm run lint:check 28 | 29 | - name: Build the extension 30 | run: | 31 | set -eux 32 | python -m pip install .[test] 33 | 34 | jupyter labextension list 35 | jupyter labextension list 2>&1 | grep -ie "jupyterlab-notifications.*OK" 36 | python -m jupyterlab.browser_check 37 | 38 | - name: Package the extension 39 | run: | 40 | set -eux 41 | 42 | pip install build 43 | python -m build 44 | pip uninstall -y "jupyterlab_notifications" jupyterlab 45 | 46 | - name: Upload extension packages 47 | uses: actions/upload-artifact@v3 48 | with: 49 | name: extension-artifacts 50 | path: dist/jupyterlab_notifications* 51 | if-no-files-found: error 52 | 53 | test_isolated: 54 | needs: build 55 | runs-on: ubuntu-latest 56 | 57 | steps: 58 | - name: Install Python 59 | uses: actions/setup-python@v4 60 | with: 61 | python-version: '3.9' 62 | architecture: 'x64' 63 | - uses: actions/download-artifact@v3 64 | with: 65 | name: extension-artifacts 66 | - name: Install and Test 67 | run: | 68 | set -eux 69 | # Remove NodeJS, twice to take care of system and locally installed node versions. 70 | sudo rm -rf $(which node) 71 | sudo rm -rf $(which node) 72 | 73 | pip install "jupyterlab>=4.0.0,<5" jupyterlab_notifications*.whl 74 | 75 | 76 | jupyter labextension list 77 | jupyter labextension list 2>&1 | grep -ie "jupyterlab-notifications.*OK" 78 | python -m jupyterlab.browser_check --no-browser-test 79 | 80 | check_links: 81 | name: Check Links 82 | runs-on: ubuntu-latest 83 | timeout-minutes: 15 84 | steps: 85 | - uses: actions/checkout@v3 86 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 87 | - uses: jupyterlab/maintainer-tools/.github/actions/check-links@v1 88 | -------------------------------------------------------------------------------- /tutorial/py3_demo_2.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "source": [ 6 | "## Jupyterlab Notification Python 2 Demo\n", 7 | "For this demo, make sure to update `minimum_cell_execution_time` to 1 second in Settings > Advanced Settings Editor > Notifications.\n", 8 | "\n", 9 | "This notebook in conjunction with `py3_demo_3.ipynb` was created to test simultaneous runs with `py3_demo.ipynb`." 10 | ], 11 | "metadata": {} 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": 17, 16 | "source": [ 17 | "!sleep 2\n", 18 | "print(\"hello from notebook 2\")" 19 | ], 20 | "outputs": [ 21 | { 22 | "output_type": "stream", 23 | "name": "stdout", 24 | "text": [ 25 | "hello from notebook 2\n" 26 | ] 27 | } 28 | ], 29 | "metadata": { 30 | "execution": { 31 | "iopub.execute_input": "2021-07-26T02:25:17.832538Z", 32 | "iopub.status.busy": "2021-07-26T02:25:17.832281Z", 33 | "iopub.status.idle": "2021-07-26T02:25:19.961884Z", 34 | "shell.execute_reply": "2021-07-26T02:25:19.961025Z", 35 | "shell.execute_reply.started": "2021-07-26T02:25:17.832508Z" 36 | }, 37 | "tags": [] 38 | } 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": 18, 43 | "source": [ 44 | "!sleep 2\n", 45 | "print(\"hello from notebook 2: cell 1\")" 46 | ], 47 | "outputs": [ 48 | { 49 | "output_type": "stream", 50 | "name": "stdout", 51 | "text": [ 52 | "hello from notebook 2: cell 1\n" 53 | ] 54 | } 55 | ], 56 | "metadata": { 57 | "execution": { 58 | "iopub.execute_input": "2021-07-26T02:25:23.469719Z", 59 | "iopub.status.busy": "2021-07-26T02:25:23.469509Z", 60 | "iopub.status.idle": "2021-07-26T02:25:25.595772Z", 61 | "shell.execute_reply": "2021-07-26T02:25:25.594884Z", 62 | "shell.execute_reply.started": "2021-07-26T02:25:23.469693Z" 63 | }, 64 | "tags": [] 65 | } 66 | }, 67 | { 68 | "cell_type": "code", 69 | "execution_count": null, 70 | "source": [], 71 | "outputs": [], 72 | "metadata": {} 73 | } 74 | ], 75 | "metadata": { 76 | "kernelspec": { 77 | "display_name": "Python 3", 78 | "language": "python", 79 | "name": "python3" 80 | }, 81 | "language_info": { 82 | "codemirror_mode": { 83 | "name": "ipython", 84 | "version": 3 85 | }, 86 | "file_extension": ".py", 87 | "mimetype": "text/x-python", 88 | "name": "python", 89 | "nbconvert_exporter": "python", 90 | "pygments_lexer": "ipython3", 91 | "version": "3.7.10" 92 | } 93 | }, 94 | "nbformat": 4, 95 | "nbformat_minor": 5 96 | } -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "hatchling>=1.5.0", 4 | "jupyterlab>=4.0.0,<5", 5 | "hatch-nodejs-version>=0.3.2", 6 | ] 7 | build-backend = "hatchling.build" 8 | 9 | [project] 10 | name = "jupyterlab-notifications" 11 | readme = "README.md" 12 | requires-python = ">=3.8" 13 | classifiers = [ 14 | "Framework :: Jupyter", 15 | "Framework :: Jupyter :: JupyterLab", 16 | "Framework :: Jupyter :: JupyterLab :: 4", 17 | "Framework :: Jupyter :: JupyterLab :: Extensions", 18 | "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", 19 | "License :: OSI Approved :: BSD License", 20 | "Programming Language :: Python", 21 | "Programming Language :: Python :: 3", 22 | "Programming Language :: Python :: 3.8", 23 | "Programming Language :: Python :: 3.9", 24 | "Programming Language :: Python :: 3.10", 25 | "Programming Language :: Python :: 3.11", 26 | ] 27 | dependencies = [] 28 | dynamic = [ 29 | "version", 30 | "description", 31 | "authors", 32 | "urls", 33 | "keywords", 34 | ] 35 | 36 | [project.license] 37 | file = "LICENSE" 38 | 39 | [tool.hatch.version] 40 | source = "nodejs" 41 | 42 | [tool.hatch.metadata.hooks.nodejs] 43 | fields = [ 44 | "description", 45 | "authors", 46 | "urls", 47 | ] 48 | 49 | [tool.hatch.build.targets.sdist] 50 | artifacts = [ 51 | "jupyterlab_notifications/labextension", 52 | ] 53 | exclude = [ 54 | ".github", 55 | "binder", 56 | ] 57 | 58 | [tool.hatch.build.targets.wheel.shared-data] 59 | "jupyterlab_notifications/labextension" = "share/jupyter/labextensions/jupyterlab-notifications" 60 | "install.json" = "share/jupyter/labextensions/jupyterlab-notifications/install.json" 61 | 62 | [tool.hatch.build.hooks.version] 63 | path = "jupyterlab_notifications/_version.py" 64 | 65 | [tool.hatch.build.hooks.jupyter-builder] 66 | dependencies = [ 67 | "hatch-jupyter-builder>=0.5", 68 | ] 69 | build-function = "hatch_jupyter_builder.npm_builder" 70 | ensured-targets = [ 71 | "jupyterlab_notifications/labextension/static/style.js", 72 | "jupyterlab_notifications/labextension/package.json", 73 | ] 74 | skip-if-exists = [ 75 | "jupyterlab_notifications/labextension/static/style.js", 76 | ] 77 | 78 | [tool.hatch.build.hooks.jupyter-builder.build-kwargs] 79 | build_cmd = "build:prod" 80 | npm = [ 81 | "jlpm", 82 | ] 83 | 84 | [tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs] 85 | build_cmd = "install:extension" 86 | npm = [ 87 | "jlpm", 88 | ] 89 | source_dir = "src" 90 | build_dir = "jupyterlab_notifications/labextension" 91 | 92 | [tool.jupyter-releaser.options] 93 | version_cmd = "hatch version" 94 | 95 | [tool.jupyter-releaser.hooks] 96 | before-build-npm = [ 97 | "python -m pip install 'jupyterlab>=4.0.0,<5'", 98 | "jlpm", 99 | "jlpm build:prod", 100 | ] 101 | before-build-python = [ 102 | "jlpm clean:all", 103 | ] 104 | 105 | [tool.check-wheel-contents] 106 | ignore = [ 107 | "W002", 108 | ] 109 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Making a new release of jupyterlab_notifications 2 | 3 | The extension can be published to `PyPI` and `npm` manually or using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). 4 | 5 | ## Manual release 6 | 7 | ### Python package 8 | 9 | This extension can be distributed as Python packages. All of the Python 10 | packaging instructions are in the `pyproject.toml` file to wrap your extension in a 11 | Python package. Before generating a package, you first need to install some tools: 12 | 13 | ```bash 14 | pip install build twine hatch 15 | ``` 16 | 17 | Bump the version using `hatch`. By default this will create a tag. 18 | See the docs on [hatch-nodejs-version](https://github.com/agoose77/hatch-nodejs-version#semver) for details. 19 | 20 | ```bash 21 | hatch version 22 | ``` 23 | 24 | Make sure to clean up all the development files before building the package: 25 | 26 | ```bash 27 | jlpm clean:all 28 | ``` 29 | 30 | You could also clean up the local git repository: 31 | 32 | ```bash 33 | git clean -dfX 34 | ``` 35 | 36 | To create a Python source package (`.tar.gz`) and the binary package (`.whl`) in the `dist/` directory, do: 37 | 38 | ```bash 39 | python -m build 40 | ``` 41 | 42 | > `python setup.py sdist bdist_wheel` is deprecated and will not work for this package. 43 | 44 | Then to upload the package to PyPI, do: 45 | 46 | ```bash 47 | twine upload dist/* 48 | ``` 49 | 50 | ### NPM package 51 | 52 | To publish the frontend part of the extension as a NPM package, do: 53 | 54 | ```bash 55 | npm login 56 | npm publish --access public 57 | ``` 58 | 59 | ## Automated releases with the Jupyter Releaser 60 | 61 | The extension repository should already be compatible with the Jupyter Releaser. 62 | 63 | Check out the [workflow documentation](https://jupyter-releaser.readthedocs.io/en/latest/get_started/making_release_from_repo.html) for more information. 64 | 65 | Here is a summary of the steps to cut a new release: 66 | 67 | - Add tokens to the [Github Secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) in the repository: 68 | - `ADMIN_GITHUB_TOKEN` (with "public_repo" and "repo:status" permissions); see the [documentation](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) 69 | - `NPM_TOKEN` (with "automation" permission); see the [documentation](https://docs.npmjs.com/creating-and-viewing-access-tokens) 70 | - Set up PyPI 71 | 72 |
Using PyPI trusted publisher (modern way) 73 | 74 | - Set up your PyPI project by [adding a trusted publisher](https://docs.pypi.org/trusted-publishers/adding-a-publisher/) 75 | - The _workflow name_ is `publish-release.yml` and the _environment_ should be left blank. 76 | - Ensure the publish release job as `permissions`: `id-token : write` (see the [documentation](https://docs.pypi.org/trusted-publishers/using-a-publisher/)) 77 | 78 |
79 | 80 |
Using PyPI token (legacy way) 81 | 82 | - If the repo generates PyPI release(s), create a scoped PyPI [token](https://packaging.python.org/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/#saving-credentials-on-github). We recommend using a scoped token for security reasons. 83 | 84 | - You can store the token as `PYPI_TOKEN` in your fork's `Secrets`. 85 | 86 | - Advanced usage: if you are releasing multiple repos, you can create a secret named `PYPI_TOKEN_MAP` instead of `PYPI_TOKEN` that is formatted as follows: 87 | 88 | ```text 89 | owner1/repo1,token1 90 | owner2/repo2,token2 91 | ``` 92 | 93 | If you have multiple Python packages in the same repository, you can point to them as follows: 94 | 95 | ```text 96 | owner1/repo1/path/to/package1,token1 97 | owner1/repo1/path/to/package2,token2 98 | ``` 99 | 100 |
101 | 102 | - Go to the Actions panel 103 | - Run the "Step 1: Prep Release" workflow 104 | - Check the draft changelog 105 | - Run the "Step 2: Publish Release" workflow 106 | 107 | ## Publishing to `conda-forge` 108 | 109 | If the package is not on conda forge yet, check the documentation to learn how to add it: https://conda-forge.org/docs/maintainer/adding_pkgs.html 110 | 111 | Otherwise a bot should pick up the new version publish to PyPI, and open a new PR on the feedstock repository automatically. 112 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jupyterlab-notifications", 3 | "version": "0.5.0", 4 | "description": "Jupyterlab extension to show notebook cell completion browser notifications", 5 | "keywords": [ 6 | "jupyter", 7 | "jupyterlab", 8 | "jupyterlab-extension" 9 | ], 10 | "homepage": "https://github.com/mwakaba2/jupyterlab-notifications", 11 | "bugs": { 12 | "url": "https://github.com/mwakaba2/jupyterlab-notifications/issues" 13 | }, 14 | "license": "BSD-3-Clause", 15 | "author": { 16 | "name": "Mariko Wakabayashi", 17 | "email": "" 18 | }, 19 | "files": [ 20 | "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", 21 | "schema/**/*.json", 22 | "style/index.js" 23 | ], 24 | "main": "lib/index.js", 25 | "types": "lib/index.d.ts", 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/mwakaba2/jupyterlab-notifications.git" 29 | }, 30 | "scripts": { 31 | "build": "jlpm build:lib && jlpm build:labextension:dev", 32 | "build:labextension": "jupyter labextension build .", 33 | "build:labextension:dev": "jupyter labextension build --development True .", 34 | "build:lib": "tsc --sourceMap", 35 | "build:lib:prod": "tsc", 36 | "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension", 37 | "clean": "jlpm clean:lib", 38 | "clean:all": "jlpm clean:lib && jlpm clean:labextension && jlpm clean:lintcache", 39 | "clean:labextension": "rimraf jupyterlab_notifications/labextension jupyterlab_notifications/_version.py", 40 | "clean:lib": "rimraf lib tsconfig.tsbuildinfo", 41 | "clean:lintcache": "rimraf .eslintcache .stylelintcache", 42 | "eslint": "jlpm eslint:check --fix", 43 | "eslint:check": "eslint . --cache --ext .ts,.tsx", 44 | "install:extension": "jlpm build", 45 | "lint": "jlpm stylelint && jlpm prettier && jlpm eslint", 46 | "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check", 47 | "prettier": "jlpm prettier:base --write --list-different", 48 | "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", 49 | "prettier:check": "jlpm prettier:base --check", 50 | "stylelint": "jlpm stylelint:check --fix", 51 | "stylelint:check": "stylelint --cache \"style/**/*.css\"", 52 | "watch": "run-p watch:src watch:labextension", 53 | "watch:labextension": "jupyter labextension watch .", 54 | "watch:src": "tsc -w --sourceMap" 55 | }, 56 | "dependencies": { 57 | "@jupyterlab/application": "^4.0.7", 58 | "@jupyterlab/notebook": "^4.0.7", 59 | "@jupyterlab/settingregistry": "^4.0.7", 60 | "@types/lru-cache": "^5.1.1", 61 | "lru-cache": "^6.0.0", 62 | "moment": "^2.29.1" 63 | }, 64 | "devDependencies": { 65 | "@jupyterlab/builder": "^4.0.0", 66 | "@types/json-schema": "^7.0.11", 67 | "@types/react": "^18.0.26", 68 | "@types/react-addons-linked-state-mixin": "^0.14.22", 69 | "@typescript-eslint/eslint-plugin": "^6.1.0", 70 | "@typescript-eslint/parser": "^6.1.0", 71 | "css-loader": "^6.7.1", 72 | "eslint": "^8.36.0", 73 | "eslint-config-prettier": "^8.8.0", 74 | "eslint-plugin-prettier": "^5.0.0", 75 | "npm-run-all": "^4.1.5", 76 | "prettier": "^3.0.0", 77 | "rimraf": "^5.0.1", 78 | "source-map-loader": "^1.0.2", 79 | "style-loader": "^3.3.1", 80 | "stylelint": "^15.10.1", 81 | "stylelint-config-recommended": "^13.0.0", 82 | "stylelint-config-standard": "^34.0.0", 83 | "stylelint-csstree-validator": "^3.0.0", 84 | "stylelint-prettier": "^4.0.0", 85 | "typescript": "~5.0.2", 86 | "yjs": "^13.5.40" 87 | }, 88 | "jupyterlab": { 89 | "extension": true, 90 | "schemaDir": "schema", 91 | "outputDir": "jupyterlab_notifications/labextension" 92 | }, 93 | "eslintConfig": { 94 | "extends": [ 95 | "eslint:recommended", 96 | "plugin:@typescript-eslint/eslint-recommended", 97 | "plugin:@typescript-eslint/recommended", 98 | "plugin:prettier/recommended" 99 | ], 100 | "parser": "@typescript-eslint/parser", 101 | "parserOptions": { 102 | "project": "tsconfig.json", 103 | "sourceType": "module" 104 | }, 105 | "plugins": [ 106 | "@typescript-eslint" 107 | ], 108 | "rules": { 109 | "@typescript-eslint/naming-convention": [ 110 | "error", 111 | { 112 | "selector": "interface", 113 | "format": [ 114 | "PascalCase" 115 | ], 116 | "custom": { 117 | "regex": "^I[A-Z]", 118 | "match": true 119 | } 120 | } 121 | ], 122 | "@typescript-eslint/no-unused-vars": [ 123 | "warn", 124 | { 125 | "args": "none" 126 | } 127 | ], 128 | "@typescript-eslint/no-explicit-any": "off", 129 | "@typescript-eslint/no-namespace": "off", 130 | "@typescript-eslint/no-use-before-define": "off", 131 | "@typescript-eslint/quotes": [ 132 | "error", 133 | "single", 134 | { 135 | "avoidEscape": true, 136 | "allowTemplateLiterals": false 137 | } 138 | ], 139 | "curly": [ 140 | "error", 141 | "all" 142 | ], 143 | "eqeqeq": "error", 144 | "prefer-arrow-callback": "error" 145 | } 146 | }, 147 | "eslintIgnore": [ 148 | "node_modules", 149 | "dist", 150 | "coverage", 151 | "**/*.d.ts" 152 | ], 153 | "prettier": { 154 | "singleQuote": true, 155 | "trailingComma": "none", 156 | "arrowParens": "avoid", 157 | "endOfLine": "auto", 158 | "overrides": [ 159 | { 160 | "files": "package.json", 161 | "options": { 162 | "tabWidth": 4 163 | } 164 | } 165 | ] 166 | }, 167 | "stylelint": { 168 | "extends": [ 169 | "stylelint-config-recommended", 170 | "stylelint-config-standard", 171 | "stylelint-prettier/recommended" 172 | ], 173 | "plugins": [ 174 | "stylelint-csstree-validator" 175 | ], 176 | "rules": { 177 | "csstree/validator": true, 178 | "property-no-vendor-prefix": null, 179 | "selector-no-vendor-prefix": null, 180 | "value-no-vendor-prefix": null 181 | } 182 | }, 183 | "styleModule": "style/index.js" 184 | } 185 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | JupyterFrontEnd, 3 | JupyterFrontEndPlugin 4 | } from '@jupyterlab/application'; 5 | import { ISessionContext, SessionContext } from '@jupyterlab/apputils'; 6 | import { KernelError, Notebook, NotebookActions } from '@jupyterlab/notebook'; 7 | import { Cell } from '@jupyterlab/cells'; 8 | import { ISettingRegistry } from '@jupyterlab/settingregistry'; 9 | import { ICodeCellModel } from '@jupyterlab/cells'; 10 | import { PageConfig } from '@jupyterlab/coreutils'; 11 | import LRU from 'lru-cache'; 12 | import moment from 'moment'; 13 | import { issueNtfyNotification } from './ntfy'; 14 | import { checkBrowserNotificationSettings } from './settings'; 15 | 16 | interface ICellExecutionMetadata { 17 | index: number; 18 | scheduledTime: Date; 19 | } 20 | 21 | /** 22 | * Constructs notification message and displays it. 23 | */ 24 | async function displayNotification( 25 | cellDuration: string, 26 | cellNumber: number, 27 | notebookName: string, 28 | reportCellNumber: boolean, 29 | reportCellExecutionTime: boolean, 30 | failedExecution: boolean, 31 | error: KernelError | null, 32 | lastCellOnly: boolean, 33 | notificationMethods: string[], 34 | sessionContext: ISessionContext | null 35 | ): Promise { 36 | const base = PageConfig.getBaseUrl(); 37 | const notificationPayload = { 38 | icon: base + 'static/favicon.ico', 39 | body: '' 40 | }; 41 | const title = failedExecution 42 | ? `${notebookName} Failed!` 43 | : `${notebookName} Completed!`; 44 | let message = ''; 45 | 46 | if (failedExecution) { 47 | message = error ? `${error.errorName} ${error.errorValue}` : ''; 48 | } else if (lastCellOnly) { 49 | message = `Total Duration: ${cellDuration}`; 50 | } else if (reportCellNumber && reportCellExecutionTime) { 51 | message = `Cell[${cellNumber}] Duration: ${cellDuration}`; 52 | } else if (reportCellNumber) { 53 | message = `Cell Number: ${cellNumber}`; 54 | } else if (reportCellExecutionTime) { 55 | message = `Cell Duration: ${cellDuration}`; 56 | } 57 | 58 | notificationPayload.body = message; 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 | } 67 | 68 | /** 69 | * Trigger notification. 70 | */ 71 | async function triggerNotification( 72 | cell: Cell, 73 | notebook: Notebook, 74 | cellExecutionMetadataTable: LRU, 75 | recentNotebookExecutionTimes: LRU, 76 | minimumCellExecutionTime: number, 77 | reportCellNumber: boolean, 78 | reportCellExecutionTime: boolean, 79 | cellNumberType: string, 80 | failedExecution: boolean, 81 | error: KernelError | null, 82 | lastCellOnly: boolean, 83 | notificationMethods: string[], 84 | sessionContext: ISessionContext | null 85 | ) { 86 | const cellEndTime = new Date(); 87 | const codeCellModel = cell.model as ICodeCellModel; 88 | const codeCell = codeCellModel.type === 'code'; 89 | const nonEmptyCell = codeCellModel.sharedModel.getSource().length > 0; 90 | if (codeCell && nonEmptyCell) { 91 | const cellId = codeCellModel.id; 92 | const notebookId = notebook.id; 93 | const cellExecutionMetadata = cellExecutionMetadataTable.get(cellId); 94 | const scheduledTime = cellExecutionMetadata.scheduledTime; 95 | // Get the cell's execution scheduled time if the recent notebook execution state doesn't exist. 96 | // This happens commonly for first time notebook executions or notebooks that haven't been executed for a while. 97 | const recentExecutedCellTime = 98 | recentNotebookExecutionTimes.get(notebookId) || scheduledTime; 99 | 100 | // Multiple cells can be scheduled at the same time, and the schedule time doesn't necessarily equate to the actual start time. 101 | // If another cell has been executed more recently than the current cell's scheduled time, treat the recent execution as the cell's start time. 102 | const cellStartTime = 103 | scheduledTime >= recentExecutedCellTime 104 | ? scheduledTime 105 | : recentExecutedCellTime; 106 | recentNotebookExecutionTimes.set(notebookId, cellEndTime); 107 | const cellDuration = moment 108 | .utc(moment(cellEndTime).diff(cellStartTime)) 109 | .format('HH:mm:ss'); 110 | const diffSeconds = moment.duration(cellDuration).asSeconds(); 111 | if (diffSeconds >= minimumCellExecutionTime) { 112 | const cellNumber = 113 | cellNumberType === 'cell_index' 114 | ? cellExecutionMetadata.index 115 | : codeCellModel.executionCount; 116 | const notebookName = notebook.title.label.replace(/\.[^/.]+$/, ''); 117 | await displayNotification( 118 | cellDuration, 119 | cellNumber, 120 | notebookName, 121 | reportCellNumber, 122 | reportCellExecutionTime, 123 | failedExecution, 124 | error, 125 | lastCellOnly, 126 | notificationMethods, 127 | sessionContext 128 | ); 129 | } 130 | } 131 | } 132 | 133 | const extension: JupyterFrontEndPlugin = { 134 | id: 'jupyterlab-notifications:plugin', 135 | autoStart: true, 136 | requires: [ISettingRegistry], 137 | activate: async (app: JupyterFrontEnd, settingRegistry: ISettingRegistry) => { 138 | checkBrowserNotificationSettings(); 139 | let enabled = true; 140 | let minimumCellExecutionTime = 60; 141 | let reportCellExecutionTime = true; 142 | let reportCellNumber = true; 143 | let cellNumberType = 'cell_index'; 144 | let lastCellOnly = false; 145 | let notificationMethods = ['browser']; 146 | 147 | const cellExecutionMetadataTable: LRU = 148 | new LRU({ 149 | max: 500 * 5 // to save 500 notebooks x 5 cells 150 | }); 151 | const recentNotebookExecutionTimes: LRU = new LRU({ 152 | max: 500 153 | }); 154 | 155 | // SessionContext is used for running python codes 156 | const manager = app.serviceManager; 157 | const sessionContext = new SessionContext({ 158 | sessionManager: manager.sessions as any, 159 | specsManager: manager.kernelspecs 160 | }); 161 | 162 | if (settingRegistry) { 163 | const setting = await settingRegistry.load(extension.id); 164 | const updateSettings = (): void => { 165 | enabled = setting.get('enabled').composite as boolean; 166 | minimumCellExecutionTime = setting.get('minimum_cell_execution_time') 167 | .composite as number; 168 | reportCellExecutionTime = setting.get('report_cell_execution_time') 169 | .composite as boolean; 170 | reportCellNumber = setting.get('report_cell_number') 171 | .composite as boolean; 172 | cellNumberType = setting.get('cell_number_type').composite as string; 173 | lastCellOnly = setting.get('last_cell_only').composite as boolean; 174 | notificationMethods = setting.get('notification_methods') 175 | .composite as string[]; 176 | }; 177 | updateSettings(); 178 | setting.changed.connect(updateSettings); 179 | } 180 | 181 | NotebookActions.executionScheduled.connect((_, args) => { 182 | const { cell, notebook } = args; 183 | if (enabled) { 184 | cellExecutionMetadataTable.set(cell.model.id, { 185 | index: notebook.activeCellIndex, 186 | scheduledTime: new Date() 187 | }); 188 | } 189 | }); 190 | 191 | NotebookActions.executed.connect(async (_, args) => { 192 | if (enabled && !lastCellOnly) { 193 | const { cell, notebook, success, error } = args; 194 | await triggerNotification( 195 | cell, 196 | notebook, 197 | cellExecutionMetadataTable, 198 | recentNotebookExecutionTimes, 199 | minimumCellExecutionTime, 200 | reportCellNumber, 201 | reportCellExecutionTime, 202 | cellNumberType, 203 | !success, 204 | error, 205 | lastCellOnly, 206 | notificationMethods, 207 | sessionContext 208 | ); 209 | } 210 | }); 211 | 212 | NotebookActions.selectionExecuted.connect(async (_, args) => { 213 | if (enabled && lastCellOnly) { 214 | const { lastCell, notebook } = args; 215 | const failedExecution = false; 216 | await triggerNotification( 217 | lastCell, 218 | notebook, 219 | cellExecutionMetadataTable, 220 | recentNotebookExecutionTimes, 221 | minimumCellExecutionTime, 222 | reportCellNumber, 223 | reportCellExecutionTime, 224 | cellNumberType, 225 | failedExecution, 226 | null, 227 | lastCellOnly, 228 | notificationMethods, 229 | sessionContext 230 | ); 231 | } 232 | }); 233 | } 234 | }; 235 | 236 | export default extension; 237 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jupyterlab-notifications 2 | 3 | ![Github Actions Status](https://github.com/mwakaba2/jupyterlab-notifications/workflows/Build/badge.svg) 4 | [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/mwakaba2/jupyterlab-notifications/main?urlpath=lab/tree/tutorial/py3_demo.ipynb) 5 | [![PyPI](https://img.shields.io/pypi/v/jupyterlab-notifications.svg)](https://pypi.org/project/jupyterlab-notifications) 6 | [![npm](https://img.shields.io/npm/v/jupyterlab-notifications.svg)](https://www.npmjs.com/package/jupyterlab-notifications) 7 | [![Conda Version](https://img.shields.io/conda/vn/conda-forge/jupyterlab-notifications.svg)](https://anaconda.org/conda-forge/jupyterlab-notifications) 8 | 9 | ### Notebook Cell Completion Notifications for JupyterLab. 10 | 11 | Option to get notebook cell completion notifications via browser, slack, mobile, telegram, and many more! 12 | 13 | _Image of successful notebook cell execution browser notification_ 14 | 15 | notification 16 | 17 | _Image of failed notebook cell execution browser notification_ (Available only in >= v0.3.0) 18 | 19 | error_notification 20 | 21 | _Image of last selected notebook cell execution browser notification_ (Available only in >= v0.3.0) 22 | 23 | last_selected_notebook_cell_notification 24 | 25 | _Image of mobile phone notifications using `ntfy` + Pushover_ 26 | 27 |

28 | pushover_iphone_example 29 | pushover_android_example 30 |

31 | 32 | _Image of slack notification via `ntfy + slack_webhook`_ 33 | 34 | Screen Shot 2021-10-09 at 1 57 51 PM 35 | 36 | ## Quick demos and tutorials :notebook: 37 | 38 | 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. 39 | 40 | In the `tutorial` directory, there are several example notebooks you can use to test out the notifications extension. 41 | 42 | - Notebooks with `py3_demo_` prefix - Minimal Python3 Notebooks to test out the extension. 43 | - `julia_demo.ipynb` - Minimal Julia Notebook to test out the extension. :warning: Note: The `tutorial/julia_demo.ipynb` will not work in the binder environment and will require additional set-up to test the Julia Notebook Kernel locally. 44 | 45 | ## Compatible Versions and Requirements 🧰 46 | 47 | | `jupyterlab-notifications` | `jupyterlab` | `Notebook Cell Timing Enabled` | `Browser Requirements` | 48 | | -------------------------- | ------------ | ------------------------------ | ----------------------------------- | 49 | | `>=0.3.0` | `>=3.1.0` | Not required | Supports the Notification Web API\* | 50 | | `<0.3.0` | `>3.0.0` | Required | Supports the Notification Web API\* | 51 | 52 | \*For Notification Web API support, please check out [Browser Compatibility Chart](https://developer.mozilla.org/en-US/docs/Web/API/notification#browser_compatibility)) 53 | 54 | ## Install 55 | 56 | For JupyterLab 3.x, the extension can be installed with `pip`: 57 | 58 | ```bash 59 | pip install jupyterlab-notifications 60 | ``` 61 | 62 | or `conda`: 63 | 64 | ```bash 65 | conda install -c conda-forge jupyterlab-notifications 66 | ``` 67 | 68 | ## Settings 69 | 70 | Use the following settings to update cell execution time for a notification and information to display in the notification. (in `Settings > Advanced Settings Editor`): 71 | 72 | ```json5 73 | { 74 | // Notifications 75 | // jupyterlab-notifications:plugin 76 | // Settings for the Notifications extension 77 | // **************************************** 78 | 79 | // Cell Number Type 80 | // Type of cell number to display when the report_cell_number is true. Select from 'cell_index' or ‘cell_execution_count'. 81 | cell_number_type: 'cell_index', 82 | 83 | // Enabled Status 84 | // Enable the extension or not. 85 | enabled: true, 86 | 87 | // Trigger only for the last selected notebook cell execution. 88 | // Trigger a notification only for the last selected executed notebook cell. 89 | // NOTE: Only Available in version >= v0.3.0 90 | last_cell_only: false, 91 | 92 | // Minimum Notebook Cell Execution Time 93 | // The minimum execution time to send out notification for a particular notebook cell (in seconds). 94 | minimum_cell_execution_time: 60, 95 | 96 | // Notification Methods 97 | // Option to send a notification with the specified method(s). The available options are 'browser' and 'ntfy'. 98 | notification_methods: ['browser'], 99 | 100 | // Report Notebook Cell Execution Time 101 | // Display notebook cell execution time in the notification. 102 | // If last_cell_only is set to true, the total duration of the selected cells will be displayed. 103 | report_cell_execution_time: true, 104 | 105 | // Report Notebook Cell Number 106 | // Display notebook cell number in the notification. 107 | report_cell_number: true 108 | } 109 | ``` 110 | 111 | ![notification](https://user-images.githubusercontent.com/3497137/111881088-01db5200-897d-11eb-8faa-4701cabfcde4.gif) 112 | 113 | ### How to enable Notebook Cell Timing 114 | 115 | :warning: For versions < `0.3.0`, Notebook Cell Timing needs to be enabled for Jupyterlab Notifications to work. Please go to Settings -> Advanced Settings Editor -> Notebook and update setting to: 116 | 117 | ```json5 118 | { 119 | // Recording timing 120 | // Should timing data be recorded in cell metadata 121 | recordTiming: true 122 | } 123 | ``` 124 | 125 | The cell timing doesn't need to be enabled for Jupyterlab >= 3.1 and Jupyterlab notification version >= v0.3.0. 126 | 127 | ## (Optional) Notifications using `ntfy` 128 | 129 | You can recieve notifications via `ntfy`. 130 | 131 | **ntfy 2.7.0 documentation** https://ntfy.readthedocs.io/en/latest/ 132 | 133 | > 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. 134 | 135 | ### How to enable notifications via `ntfy` 136 | 137 | Install `ntfy`. 138 | 139 | ```console 140 | $ pip install ntfy 141 | ``` 142 | 143 | You can find configuration instructions for different operating systems in [the ntfy official configuration docs](https://ntfy.readthedocs.io/en/latest/#configuring-ntfy) 144 | 145 | For example, if you want to get notifications via the [Pushover mobile app](https://pushover.net/), make sure to create the configuration file in the right location and select `pushover` for the backend. 146 | 147 | ```console 148 | $ vim ~/.config/ntfy/ntfy.yml # Linux 149 | $ vim ~/Library/Application Support/ntfy/ntfy.yml # macOS 150 | ``` 151 | 152 | **Note:** You'll need to first install the Pushover mobile app and create an account to generate your user key. 153 | 154 | ```yaml 155 | backends: 156 | - pushover 157 | pushover: 158 | user_key: YOUR_PUSHOVER_USER_KEY 159 | ``` 160 | 161 | Change the notifications [settings](#settings). Append `"ntfy"` into `notification_methods` attribute. 162 | 163 | - NOTE: The value `browser` implies default conventional method, which uses Webbrowser's Notification API. 164 | 165 | ```json5 166 | { 167 | // ... 168 | notification_methods: ['browser', 'ntfy'] 169 | // ... 170 | } 171 | ``` 172 | 173 | ## Contributing 174 | 175 | ### Development install 176 | 177 | Note: You will need NodeJS to build the extension package. 178 | 179 | The `jlpm` command is JupyterLab's pinned version of 180 | [yarn](https://yarnpkg.com/) that is installed with JupyterLab. You may use 181 | `yarn` or `npm` in lieu of `jlpm` below. 182 | 183 | ```bash 184 | # Clone the repo to your local environment 185 | # Change directory to the jupyterlab-notifications directory 186 | # Install package in development mode 187 | pip install -e . 188 | # Link your development version of the extension with JupyterLab 189 | jlpm run install:extension 190 | # Rebuild extension Typescript source after making changes 191 | jlpm run build 192 | ``` 193 | 194 | You can watch the source directory and run JupyterLab at the same time in different terminals to watch for changes in the extension's source and automatically rebuild the extension. 195 | 196 | ```bash 197 | # Watch the source directory in one terminal, automatically rebuilding when needed 198 | jlpm run watch 199 | # Run JupyterLab in another terminal 200 | jupyter lab 201 | ``` 202 | 203 | With the watch command running, every saved change will immediately be built locally and available in your running JupyterLab. Refresh JupyterLab to load the change in your browser (you may need to wait several seconds for the extension to be rebuilt). 204 | 205 | ### Uninstall 206 | 207 | ```bash 208 | pip uninstall jupyterlab-notifications 209 | ``` 210 | -------------------------------------------------------------------------------- /tutorial/py3_demo.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "source": [ 6 | "## Jupyterlab Notification Python Demo\n", 7 | "For this demo, make sure to update `minimum_cell_execution_time` to 1 second in Settings > Advanced Settings Editor > Notifications.\n", 8 | "\n", 9 | "Try running all the cells in notebooks with the prefix `py3_demo_` to see that notebook name and cell execution duration is correct in the notification. " 10 | ], 11 | "metadata": {} 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": 143, 16 | "source": [ 17 | "!sleep 11\n", 18 | "print(\"hello world!\")" 19 | ], 20 | "outputs": [ 21 | { 22 | "output_type": "stream", 23 | "name": "stdout", 24 | "text": [ 25 | "hello world!\n" 26 | ] 27 | } 28 | ], 29 | "metadata": { 30 | "execution": { 31 | "iopub.execute_input": "2021-07-26T02:25:15.792618Z", 32 | "iopub.status.busy": "2021-07-26T02:25:15.792309Z", 33 | "iopub.status.idle": "2021-07-26T02:25:26.923099Z", 34 | "shell.execute_reply": "2021-07-26T02:25:26.922160Z", 35 | "shell.execute_reply.started": "2021-07-26T02:25:15.792579Z" 36 | }, 37 | "status": "ok", 38 | "tags": [] 39 | } 40 | }, 41 | { 42 | "cell_type": "code", 43 | "execution_count": 142, 44 | "source": [ 45 | "!sleep 1\n", 46 | "print(\"hello world!\")" 47 | ], 48 | "outputs": [ 49 | { 50 | "output_type": "stream", 51 | "name": "stdout", 52 | "text": [ 53 | "hello world!\n" 54 | ] 55 | } 56 | ], 57 | "metadata": { 58 | "execution": { 59 | "iopub.execute_input": "2021-07-26T02:23:54.629936Z", 60 | "iopub.status.busy": "2021-07-26T02:23:54.629690Z", 61 | "iopub.status.idle": "2021-07-26T02:25:00.767228Z", 62 | "shell.execute_reply": "2021-07-26T02:25:00.765983Z", 63 | "shell.execute_reply.started": "2021-07-26T02:23:54.629905Z" 64 | }, 65 | "status": "ok", 66 | "tags": [] 67 | } 68 | }, 69 | { 70 | "cell_type": "code", 71 | "execution_count": 62, 72 | "source": [ 73 | "!sleep 2\n", 74 | "print(\"hello world again!\")" 75 | ], 76 | "outputs": [ 77 | { 78 | "output_type": "stream", 79 | "name": "stdout", 80 | "text": [ 81 | "hello world again!\n" 82 | ] 83 | } 84 | ], 85 | "metadata": { 86 | "execution": { 87 | "iopub.execute_input": "2021-07-18T19:49:34.088207Z", 88 | "iopub.status.busy": "2021-07-18T19:49:34.087802Z", 89 | "iopub.status.idle": "2021-07-18T19:49:36.219369Z", 90 | "shell.execute_reply": "2021-07-18T19:49:36.218766Z", 91 | "shell.execute_reply.started": "2021-07-18T19:49:34.088134Z" 92 | }, 93 | "tags": [] 94 | } 95 | }, 96 | { 97 | "cell_type": "code", 98 | "execution_count": 63, 99 | "source": [ 100 | "!sleep 5\n", 101 | "print(\"bye world!\")" 102 | ], 103 | "outputs": [ 104 | { 105 | "output_type": "stream", 106 | "name": "stdout", 107 | "text": [ 108 | "bye world!\n" 109 | ] 110 | } 111 | ], 112 | "metadata": { 113 | "execution": { 114 | "iopub.execute_input": "2021-07-18T19:49:36.221581Z", 115 | "iopub.status.busy": "2021-07-18T19:49:36.221322Z", 116 | "iopub.status.idle": "2021-07-18T19:49:41.353081Z", 117 | "shell.execute_reply": "2021-07-18T19:49:41.352358Z", 118 | "shell.execute_reply.started": "2021-07-18T19:49:36.221557Z" 119 | }, 120 | "tags": [] 121 | } 122 | }, 123 | { 124 | "cell_type": "markdown", 125 | "source": [ 126 | "## Notification for Failed Notebook Cell Execution\n", 127 | "\n", 128 | "If a cell execution fails, the extension will notify you with the failure and share the failure message in the notification as well. " 129 | ], 130 | "metadata": {} 131 | }, 132 | { 133 | "cell_type": "code", 134 | "execution_count": 40, 135 | "source": [ 136 | "!sleep 3\n", 137 | "raise Exception('hello world!')" 138 | ], 139 | "outputs": [ 140 | { 141 | "output_type": "error", 142 | "ename": "Exception", 143 | "evalue": "hello world!", 144 | "traceback": [ 145 | "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", 146 | "\u001b[0;31mException\u001b[0m Traceback (most recent call last)", 147 | "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0mget_ipython\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msystem\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'sleep 3'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 2\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mException\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'hello world!'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", 148 | "\u001b[0;31mException\u001b[0m: hello world!" 149 | ] 150 | } 151 | ], 152 | "metadata": { 153 | "execution": { 154 | "iopub.execute_input": "2021-07-18T19:41:58.007998Z", 155 | "iopub.status.busy": "2021-07-18T19:41:58.007728Z", 156 | "iopub.status.idle": "2021-07-18T19:42:01.145486Z", 157 | "shell.execute_reply": "2021-07-18T19:42:01.144436Z", 158 | "shell.execute_reply.started": "2021-07-18T19:41:58.007971Z" 159 | }, 160 | "tags": [] 161 | } 162 | }, 163 | { 164 | "cell_type": "markdown", 165 | "source": [ 166 | "## Last Cell Execution Notification Only\n", 167 | "For this demo, make sure to update `last_cell_only` to true in Settings > Advanced Settings Editor > Notifications.\n", 168 | "Please select the following three cells and run them at the same time.\n", 169 | "The extension will notify you after the last cell execution completion and display the total duration of the selected cells. \n", 170 | "\n", 171 | "How to select Multiple Cells Shift + J or Shift + Down selects the next sell in a downwards direction." 172 | ], 173 | "metadata": {} 174 | }, 175 | { 176 | "cell_type": "code", 177 | "execution_count": 138, 178 | "source": [ 179 | "!sleep 4\n", 180 | "print(\"hola!\")" 181 | ], 182 | "outputs": [ 183 | { 184 | "output_type": "stream", 185 | "name": "stdout", 186 | "text": [ 187 | "hola!\n" 188 | ] 189 | } 190 | ], 191 | "metadata": { 192 | "execution": { 193 | "iopub.execute_input": "2021-07-26T02:15:24.129175Z", 194 | "iopub.status.busy": "2021-07-26T02:15:24.128902Z", 195 | "iopub.status.idle": "2021-07-26T02:15:28.256314Z", 196 | "shell.execute_reply": "2021-07-26T02:15:28.255193Z", 197 | "shell.execute_reply.started": "2021-07-26T02:15:24.129139Z" 198 | }, 199 | "tags": [] 200 | } 201 | }, 202 | { 203 | "cell_type": "code", 204 | "execution_count": 139, 205 | "source": [ 206 | "!sleep 2\n", 207 | "print(\"bonjour!\")" 208 | ], 209 | "outputs": [ 210 | { 211 | "output_type": "stream", 212 | "name": "stdout", 213 | "text": [ 214 | "bonjour!\n" 215 | ] 216 | } 217 | ], 218 | "metadata": { 219 | "execution": { 220 | "iopub.execute_input": "2021-07-26T02:15:28.258778Z", 221 | "iopub.status.busy": "2021-07-26T02:15:28.258381Z", 222 | "iopub.status.idle": "2021-07-26T02:15:30.389469Z", 223 | "shell.execute_reply": "2021-07-26T02:15:30.388524Z", 224 | "shell.execute_reply.started": "2021-07-26T02:15:28.258744Z" 225 | }, 226 | "tags": [] 227 | } 228 | }, 229 | { 230 | "cell_type": "code", 231 | "execution_count": 140, 232 | "source": [ 233 | "!sleep 3\n", 234 | "print(\"yo!\")" 235 | ], 236 | "outputs": [ 237 | { 238 | "output_type": "stream", 239 | "name": "stdout", 240 | "text": [ 241 | "yo!\n" 242 | ] 243 | } 244 | ], 245 | "metadata": { 246 | "execution": { 247 | "iopub.execute_input": "2021-07-26T02:15:30.391586Z", 248 | "iopub.status.busy": "2021-07-26T02:15:30.391285Z", 249 | "iopub.status.idle": "2021-07-26T02:15:33.519909Z", 250 | "shell.execute_reply": "2021-07-26T02:15:33.518958Z", 251 | "shell.execute_reply.started": "2021-07-26T02:15:30.391542Z" 252 | }, 253 | "tags": [] 254 | } 255 | }, 256 | { 257 | "cell_type": "markdown", 258 | "source": [ 259 | "## Notifications are for code cells only.\n", 260 | "The extensions will only notify you for non-empty code cell executions, so the following `raw | markdown` cells will not produce any notifications. " 261 | ], 262 | "metadata": {} 263 | }, 264 | { 265 | "cell_type": "markdown", 266 | "source": [ 267 | "## Hello World" 268 | ], 269 | "metadata": { 270 | "tags": [] 271 | } 272 | }, 273 | { 274 | "cell_type": "raw", 275 | "source": [ 276 | "Hello World" 277 | ], 278 | "metadata": { 279 | "execution": { 280 | "iopub.execute_input": "2021-03-27T02:29:48.597351Z", 281 | "iopub.status.busy": "2021-03-27T02:29:48.597122Z", 282 | "iopub.status.idle": "2021-03-27T02:29:48.602494Z", 283 | "shell.execute_reply": "2021-03-27T02:29:48.600860Z", 284 | "shell.execute_reply.started": "2021-03-27T02:29:48.597324Z" 285 | } 286 | } 287 | }, 288 | { 289 | "cell_type": "code", 290 | "execution_count": null, 291 | "source": [], 292 | "outputs": [], 293 | "metadata": {} 294 | } 295 | ], 296 | "metadata": { 297 | "kernelspec": { 298 | "display_name": "Python 3", 299 | "language": "python", 300 | "name": "python3" 301 | }, 302 | "language_info": { 303 | "codemirror_mode": { 304 | "name": "ipython", 305 | "version": 3 306 | }, 307 | "file_extension": ".py", 308 | "mimetype": "text/x-python", 309 | "name": "python", 310 | "nbconvert_exporter": "python", 311 | "pygments_lexer": "ipython3", 312 | "version": "3.7.10" 313 | } 314 | }, 315 | "nbformat": 4, 316 | "nbformat_minor": 4 317 | } --------------------------------------------------------------------------------