diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..cbd920f --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..8612018 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,103 @@ +name: Build + +on: + push: + branches: ["main"] + pull_request: +# schedule: +# - cron: "0 0 * * *" + +defaults: + run: + shell: bash -eux {0} + +jobs: + build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + python-version: [ '3.8', '3.9', '3.10', "3.11" ] + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Base Setup + uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 + + - name: Install dependencies + run: python -m pip install -U jupyter_server + + - name: Build the extension + run: | + python -m pip install . + jupyter server extension list 2>&1 | grep -ie "message_replay.*OK" + + pip install build + python -m build --sdist + cp dist/*.tar.gz my_server_extension.tar.gz + pip uninstall -y "message_replay" jupyter_server + rm -rf "message_replay" + + - uses: actions/upload-artifact@v2 + with: + name: my_server_extension-sdist + path: my_server_extension.tar.gz + + check_links: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 + - uses: jupyterlab/maintainer-tools/.github/actions/check-links@v1 + + test_lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 + - name: Run Linters + run: | + bash ./.github/workflows/lint.sh + + check_release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Base Setup + uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 + - name: Install Dependencies + run: | + pip install -e . + - name: Check Release + uses: jupyter-server/jupyter_releaser/.github/actions/check-release@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + - name: Upload Distributions + uses: actions/upload-artifact@v2 + with: + name: message_replay-releaser-dist-${{ github.run_number }} + path: .jupyter_releaser_checkout/dist + + test_sdist: + needs: build + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Install Python + uses: actions/setup-python@v2 + with: + python-version: '3.8' + architecture: 'x64' + - uses: actions/download-artifact@v2 + with: + name: my_server_extension-sdist + - name: Install and Test + run: | + pip install my_server_extension.tar.gz + pip install jupyter_server + jupyter server extension list 2>&1 | grep -ie "message_replay.*OK" diff --git a/.github/workflows/lint.sh b/.github/workflows/lint.sh new file mode 100644 index 0000000..26ff0d4 --- /dev/null +++ b/.github/workflows/lint.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +pip install -e ".[test,lint]" +mypy --install-types --non-interactive . +ruff . +black --check --diff . +mdformat --check *.md +pipx run 'validate-pyproject[all]' pyproject.toml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ecb2472 --- /dev/null +++ b/.gitignore @@ -0,0 +1,102 @@ +*.egg-info/ +.ipynb_checkpoints + +# Created by https://www.gitignore.io/api/python +# Edit at https://www.gitignore.io/?templates=python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +dist/ +downloads/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +.installed.cfg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# End of https://www.gitignore.io/api/python + +# OSX files +.DS_Store diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..bdaad89 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,42 @@ +ci: + autoupdate_schedule: monthly + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: end-of-file-fixer + - id: check-case-conflict + - id: check-executables-have-shebangs + - id: requirements-txt-fixer + - id: check-added-large-files + - id: check-case-conflict + - id: check-toml + - id: check-yaml + - id: debug-statements + - id: forbid-new-submodules + - id: check-builtin-literals + - id: trailing-whitespace + + - repo: https://github.com/python-jsonschema/check-jsonschema + rev: 0.19.2 + hooks: + - id: check-github-workflows + + - repo: https://github.com/executablebooks/mdformat + rev: 0.7.16 + hooks: + - id: mdformat + additional_dependencies: + [mdformat-gfm, mdformat-frontmatter, mdformat-footnote] + + - repo: https://github.com/psf/black + rev: 22.10.0 + hooks: + - id: black + + - repo: https://github.com/charliermarsh/ruff-pre-commit + rev: v0.0.165 + hooks: + - id: ruff + args: ["--fix"] diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2d352af --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + + + + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..133f008 --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2023, Matt Wiese +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index 09ce69f..1ad4d96 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,92 @@ # message_replay +[![Github Actions Status](https://github.com/jupyter-server/message_replay/workflows/Build/badge.svg)](https://github.com/jupyter-server/message_replay/actions/workflows/build.yml) + +Restore Notebook execution progress when a browser page is reloaded + See https://github.com/jupyter-server/team-compass/issues/52 to learn about how this repository came into existence. + +## Requirements + +- Jupyter Server + +## Install + +To install the extension, execute: + +```bash +pip install message_replay +``` + +## Uninstall + +To remove the extension, execute: + +```bash +pip uninstall message_replay +``` + +## Troubleshoot + +If you are seeing the frontend extension, but it is not working, check +that the server extension is enabled: + +```bash +jupyter server extension list +``` + +## Contributing + +### Development install + +```bash +# Clone the repo to your local environment +# Change directory to the message_replay directory +# Install package in development mode - will automatically enable +# The server extension. +pip install -e . +``` + +You can watch the source directory and run your Jupyter Server-based application at the same time in different terminals to watch for changes in the extension's source and automatically rebuild the extension. For example, +when running JupyterLab: + +```bash +jupyter lab --autoreload +``` + +If your extension does not depend a particular frontend, you can run the +server directly: + +```bash +jupyter server --autoreload +``` + +### Running Tests + +Install dependencies: + +```bash +pip install -e ".[test]" +``` + +To run the python tests, use: + +```bash +pytest + +# To test a specific file +pytest message_replay/tests/test_handlers.py + +# To run a specific test +pytest message_replay/tests/test_handlers.py -k "test_get" +``` + +### Development uninstall + +```bash +pip uninstall message_replay +``` + +### Packaging the extension + +See [RELEASE](RELEASE.md) diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..b167c91 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,61 @@ +# Making a new release of message_replay + +The extension can be published to `PyPI` manually or using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). + +## Manual release + +### Python package + +This extension can be distributed as Python +packages. All of the Python +packaging instructions in the `pyproject.toml` file to wrap your extension in a +Python package. Before generating a package, we first need to install `build`. + +```bash +pip install build twine +``` + +To create a Python source package (`.tar.gz`) and the binary package (`.whl`) in the `dist/` directory, do: + +```bash +python -m build +``` + +> `python setup.py sdist bdist_wheel` is deprecated and will not work for this package. + +Then to upload the package to PyPI, do: + +```bash +twine upload dist/* +``` + +### NPM package + +To publish the frontend part of the extension as a NPM package, do: + +```bash +npm login +npm publish --access public +``` + +## Automated releases with the Jupyter Releaser + +The extension repository should already be compatible with the Jupyter Releaser. + +Check out the [workflow documentation](https://github.com/jupyter-server/jupyter_releaser#typical-workflow) for more information. + +Here is a summary of the steps to cut a new release: + +- Fork the [`jupyter-releaser` repo](https://github.com/jupyter-server/jupyter_releaser) +- Add `ADMIN_GITHUB_TOKEN`, `PYPI_TOKEN` and `NPM_TOKEN` to the Github Secrets in the fork +- Go to the Actions panel +- Run the "Draft Changelog" workflow +- Merge the Changelog PR +- Run the "Draft Release" workflow +- Run the "Publish Release" workflow + +## Publishing to `conda-forge` + +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 + +Otherwise a bot should pick up the new version publish to PyPI, and open a new PR on the feedstock repository automatically. diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000..e82692a --- /dev/null +++ b/conftest.py @@ -0,0 +1,8 @@ +import pytest + +pytest_plugins = ["jupyter_server.pytest_plugin"] + + +@pytest.fixture +def jp_server_config(jp_server_config): + return {"ServerApp": {"jpserver_extensions": {"message_replay": True}}} diff --git a/jupyter-config/jupyter_server_config.d/message_replay.json b/jupyter-config/jupyter_server_config.d/message_replay.json new file mode 100644 index 0000000..7fdfc01 --- /dev/null +++ b/jupyter-config/jupyter_server_config.d/message_replay.json @@ -0,0 +1,7 @@ +{ + "ServerApp": { + "jpserver_extensions": { + "message_replay": true + } + } +} diff --git a/message_replay/__init__.py b/message_replay/__init__.py new file mode 100644 index 0000000..afc00cd --- /dev/null +++ b/message_replay/__init__.py @@ -0,0 +1,8 @@ +"""Restore Notebook execution progress when a browser page is reloaded""" +from .extension import Extension + +__version__ = "0.1.0" + + +def _jupyter_server_extension_points(): + return [{"module": "message_replay", "app": Extension}] diff --git a/message_replay/extension.py b/message_replay/extension.py new file mode 100644 index 0000000..0d41e5a --- /dev/null +++ b/message_replay/extension.py @@ -0,0 +1,17 @@ +from jupyter_server.extension.application import ExtensionApp +from traitlets import Unicode + +from .handlers import PingHandler + + +class Extension(ExtensionApp): + + name = "message_replay" + handlers = [("message-replay/ping", PingHandler)] + + # Example of a configurable trait. This is meant to be replaced + # with configurable traits for this extension. + ping_response = Unicode(default_value="pong").tag(config=True) + + def initialize_settings(self): + self.settings.update({"ping_response": self.ping_response}) diff --git a/message_replay/handlers.py b/message_replay/handlers.py new file mode 100644 index 0000000..86532b7 --- /dev/null +++ b/message_replay/handlers.py @@ -0,0 +1,18 @@ +import json + +import tornado +from jupyter_server.base.handlers import APIHandler +from jupyter_server.extension.handler import ExtensionHandlerMixin + + +class PingHandler(ExtensionHandlerMixin, APIHandler): + # The following decorator should be present on all verb methods (head, get, post, + # patch, put, delete, options) to ensure only authorized user can request the + # Jupyter server + @property + def ping_response(self): + return self.settings["ping_response"] + + @tornado.web.authenticated + def get(self): + self.finish(json.dumps({"ping_response": self.ping_response})) diff --git a/message_replay/tests/__init__.py b/message_replay/tests/__init__.py new file mode 100644 index 0000000..2611116 --- /dev/null +++ b/message_replay/tests/__init__.py @@ -0,0 +1 @@ +"""Python unit tests for message_replay.""" diff --git a/message_replay/tests/test_handlers.py b/message_replay/tests/test_handlers.py new file mode 100644 index 0000000..b2cd420 --- /dev/null +++ b/message_replay/tests/test_handlers.py @@ -0,0 +1,9 @@ +import json + + +async def test_get(jp_fetch): + response = await jp_fetch("message-replay/ping") + + assert response.code == 200 + payload = json.loads(response.body) + assert payload == {"ping_response": "pong"} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..90fafa9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,93 @@ +[build-system] +requires = ["hatchling>=1.5"] +build-backend = "hatchling.build" + +[project] +name = "message_replay" +authors = [{name = "Matt Wiese", email = "matthew.wiese@cornell.edu"}] +dynamic = ["version"] +readme = "README.md" +requires-python = ">=3.8" +keywords = ["Jupyter", "Extension"] +classifiers = [ + "License :: OSI Approved :: BSD License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Framework :: Jupyter", +] +dependencies = ["jupyter_server>=1.6,<3"] + +[project.optional-dependencies] +test = [ + "pytest>=7.0", + "pytest-jupyter[server]>=0.6" +] +lint = [ + "black>=22.6.0", + "mdformat>0.7", + "mdformat-gfm>=0.3.5", + "ruff>=0.0.156" +] +typing = ["mypy>=0.990"] + +[project.license] +file="LICENSE" + +[project.urls] +Home = "https://github.com/jupyter-server/message_replay" + +[tool.hatch.version] +path = "message_replay/__init__.py" + +[tool.hatch.build.targets.wheel.shared-data] +"jupyter-config" = "etc/jupyter" + +[tool.pytest.ini_options] +filterwarnings = [ + "error", + "ignore:There is no current event loop:DeprecationWarning", + "module:make_current is deprecated:DeprecationWarning", + "module:clear_current is deprecated:DeprecationWarning", + "module:Jupyter is migrating its paths to use standard platformdirs:DeprecationWarning", +] + +[tool.mypy] +check_untyped_defs = true +disallow_incomplete_defs = true +no_implicit_optional = true +pretty = true +show_error_context = true +show_error_codes = true +strict_equality = true +warn_unused_configs = true +warn_unused_ignores = true +warn_redundant_casts = true + +[tool.black] +line-length = 100 +target-version = ["py38"] +skip-string-normalization = true + +[tool.ruff] +target-version = "py38" +line-length = 100 +select = [ + "A", "B", "C", "E", "F", "FBT", "I", "N", "Q", "RUF", "S", "T", + "UP", "W", "YTT", +] +ignore = [ +# Q000 Single quotes found but double quotes preferred +"Q000", +# FBT001 Boolean positional arg in function definition +"FBT001", "FBT002", "FBT003", +# C901 `foo` is too complex (12) +"C901", +] + +[tool.ruff.per-file-ignores] +# S101 Use of `assert` detected +"message_replay/tests/*" = ["S101"]