├── tests ├── __init__.py ├── conftest.py ├── test_disable_app.py ├── test_auth.py └── test_terminal.py ├── jupyter_server_terminals ├── py.typed ├── _version.py ├── base.py ├── __init__.py ├── api_handlers.py ├── handlers.py ├── rest-api.yml ├── app.py └── terminalmanager.py ├── .gitconfig ├── docs ├── source │ ├── contributors │ │ └── contributing.rst │ ├── api.rst │ ├── index.rst │ ├── _static │ │ └── jupyter_server_logo.svg │ └── conf.py ├── README.md ├── Makefile └── make.bat ├── jupyter-config └── jupyter_server_terminals.json ├── .readthedocs.yaml ├── .github ├── workflows │ ├── enforce-label.yml │ ├── publish-changelog.yml │ ├── prep-release.yml │ ├── publish-release.yml │ └── test.yml └── dependabot.yml ├── .gitignore ├── RELEASE.md ├── LICENSE ├── README.md ├── .pre-commit-config.yaml ├── CONTRIBUTING.rst ├── pyproject.toml └── CHANGELOG.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /jupyter_server_terminals/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitconfig: -------------------------------------------------------------------------------- 1 | [blame] 2 | ignoreRevsFile = .git-blame-ignore-revs 3 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | pytest_plugins = ["pytest_jupyter.jupyter_server"] 2 | -------------------------------------------------------------------------------- /docs/source/contributors/contributing.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: sh 2 | 3 | .. include:: ../../../CONTRIBUTING.rst 4 | -------------------------------------------------------------------------------- /jupyter_server_terminals/_version.py: -------------------------------------------------------------------------------- 1 | """Version info for jupyter_server_terminals.""" 2 | __version__ = "0.5.3" 3 | -------------------------------------------------------------------------------- /jupyter-config/jupyter_server_terminals.json: -------------------------------------------------------------------------------- 1 | { 2 | "ServerApp": { 3 | "jpserver_extensions": { 4 | "jupyter_server_terminals": true 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Jupyter Server Terminals Docs Sources 2 | 3 | Read [this page](https://jupyter-server-terminals.readthedocs.io/en/latest/contributors/contributing.html#building-the-docs) to build the docs. 4 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | sphinx: 3 | configuration: docs/source/conf.py 4 | python: 5 | install: 6 | # install itself with pip install . 7 | - method: pip 8 | path: . 9 | extra_requirements: 10 | - docs 11 | build: 12 | os: ubuntu-22.04 13 | tools: 14 | python: "3.11" 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 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | groups: 8 | actions: 9 | patterns: 10 | - "*" 11 | - package-ecosystem: "pip" 12 | directory: "/" 13 | schedule: 14 | interval: "weekly" 15 | groups: 16 | actions: 17 | patterns: 18 | - "*" 19 | -------------------------------------------------------------------------------- /tests/test_disable_app.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from traitlets.config.loader import Config 3 | 4 | 5 | @pytest.fixture() 6 | def jp_server_config(): 7 | return Config({"ServerApp": {"terminals_enabled": False}}) 8 | 9 | 10 | async def test_not_enabled(jp_configurable_serverapp): 11 | assert jp_configurable_serverapp().terminals_enabled is False 12 | assert jp_configurable_serverapp().web_app.settings["terminals_available"] is False 13 | assert "terminal_manager" not in jp_configurable_serverapp().web_app.settings 14 | -------------------------------------------------------------------------------- /jupyter_server_terminals/base.py: -------------------------------------------------------------------------------- 1 | """Base classes.""" 2 | from __future__ import annotations 3 | 4 | from typing import TYPE_CHECKING 5 | 6 | from jupyter_server.extension.handler import ExtensionHandlerMixin 7 | 8 | if TYPE_CHECKING: 9 | from jupyter_server_terminals.terminalmanager import TerminalManager 10 | 11 | 12 | class TerminalsMixin(ExtensionHandlerMixin): 13 | """An extension mixin for terminals.""" 14 | 15 | @property 16 | def terminal_manager(self) -> TerminalManager: 17 | return self.settings["terminal_manager"] # type:ignore[no-any-return] 18 | -------------------------------------------------------------------------------- /docs/source/api.rst: -------------------------------------------------------------------------------- 1 | -------- 2 | REST API 3 | -------- 4 | 5 | The same Jupyter Server Terminals API spec, as found here, is available in an interactive form 6 | `here (on swagger's petstore) `__. 7 | The `OpenAPI Initiative`_ (fka Swagger™) is a project used to describe 8 | and document RESTful APIs. 9 | 10 | .. openapi:: ../../jupyter_server_terminals/rest-api.yml 11 | :examples: 12 | 13 | 14 | .. _OpenAPI Initiative: https://www.openapis.org/ 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | MANIFEST 2 | build 3 | dist 4 | _build 5 | docs/man/*.gz 6 | docs/source/api/generated 7 | docs/source/config.rst 8 | docs/gh-pages 9 | node_modules 10 | *.py[co] 11 | __pycache__ 12 | *.egg-info 13 | *~ 14 | *.bak 15 | .ipynb_checkpoints 16 | .tox 17 | .DS_Store 18 | \#*# 19 | .#* 20 | .coverage* 21 | .pytest_cache 22 | src 23 | 24 | *.swp 25 | *.map 26 | Read the Docs 27 | config.rst 28 | docs/source/changelog.md 29 | 30 | /.project 31 | /.pydevproject 32 | 33 | # jetbrains ide stuff 34 | *.iml 35 | .idea/ 36 | 37 | # vscode ide stuff 38 | *.code-workspace 39 | .history 40 | .vscode/* 41 | !.vscode/*.template 42 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. jupyter_server_terminals documentation master file, created by 2 | sphinx-quickstart. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root ``toctree``` directive. 5 | 6 | Welcome to Jupyter Server Terminals documentation! 7 | ================================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 1 11 | :caption: Contents: 12 | 13 | changelog 14 | api 15 | contributors/contributing 16 | 17 | 18 | Indices and tables 19 | ================== 20 | 21 | * :ref:`genindex` 22 | * :ref:`modindex` 23 | * :ref:`search` 24 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /jupyter_server_terminals/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, List 2 | 3 | from ._version import __version__ # noqa:F401 4 | 5 | try: 6 | from jupyter_server._version import version_info 7 | except ModuleNotFoundError: 8 | msg = "Jupyter Server must be installed to use this extension." 9 | raise ModuleNotFoundError(msg) from None 10 | 11 | if int(version_info[0]) < 2: # type:ignore[call-overload] 12 | msg = "Jupyter Server Terminals requires Jupyter Server 2.0+" 13 | raise RuntimeError(msg) 14 | 15 | from .app import TerminalsExtensionApp 16 | 17 | 18 | def _jupyter_server_extension_points() -> List[Dict[str, Any]]: # pragma: no cover 19 | return [ 20 | { 21 | "module": "jupyter_server_terminals.app", 22 | "app": TerminalsExtensionApp, 23 | }, 24 | ] 25 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.https://www.sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /.github/workflows/publish-changelog.yml: -------------------------------------------------------------------------------- 1 | name: "Publish Changelog" 2 | on: 3 | release: 4 | types: [published] 5 | 6 | workflow_dispatch: 7 | inputs: 8 | branch: 9 | description: "The branch to target" 10 | required: false 11 | 12 | jobs: 13 | publish_changelog: 14 | runs-on: ubuntu-latest 15 | environment: release 16 | steps: 17 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 18 | 19 | - uses: actions/create-github-app-token@v1 20 | id: app-token 21 | with: 22 | app-id: ${{ vars.APP_ID }} 23 | private-key: ${{ secrets.APP_PRIVATE_KEY }} 24 | 25 | - name: Publish changelog 26 | id: publish-changelog 27 | uses: jupyter-server/jupyter_releaser/.github/actions/publish-changelog@v2 28 | with: 29 | token: ${{ steps.app-token.outputs.token }} 30 | branch: ${{ github.event.inputs.branch }} 31 | 32 | - name: "** Next Step **" 33 | run: | 34 | echo "Merge the changelog update PR: ${{ steps.publish-changelog.outputs.pr_url }}" 35 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Making a Jupyter Server Release 2 | 3 | ## Using `jupyter_releaser` 4 | 5 | The recommended way to make a release is to use [`jupyter_releaser`](https://jupyter-releaser.readthedocs.io/en/latest/get_started/making_release_from_repo.html). 6 | 7 | ## Manual Release 8 | 9 | To create a manual release, perform the following steps: 10 | 11 | ### Set up 12 | 13 | ```bash 14 | pip install pipx 15 | git pull origin $(git branch --show-current) 16 | git clean -dffx 17 | ``` 18 | 19 | ### Update the version and apply the tag 20 | 21 | ```bash 22 | echo "Enter new version" 23 | read script_version 24 | pipx run hatch version ${script_version} 25 | git tag -a ${script_version} -m "${script_version}" 26 | ``` 27 | 28 | ### Build the artifacts 29 | 30 | ```bash 31 | rm -rf dist 32 | pipx run build . 33 | ``` 34 | 35 | ### Update the version back to dev 36 | 37 | ```bash 38 | echo "Enter dev version" 39 | read dev_version 40 | pipx run hatch version ${dev_version} 41 | git push origin $(git branch --show-current) 42 | ``` 43 | 44 | ### Publish the artifacts to pypi 45 | 46 | ```bash 47 | pipx run twine check dist/* 48 | pipx run twine upload dist/* 49 | ``` 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | - Copyright (c) 2021-, Jupyter Development Team 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 | All rights reserved. 9 | 10 | 1. Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | 2. Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | 3. Neither the name of the copyright holder nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /.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 | silent: 16 | description: "Set a placeholder in the changelog and don't publish the release." 17 | required: false 18 | type: boolean 19 | since: 20 | description: "Use PRs with activity since this date or git reference" 21 | required: false 22 | since_last_stable: 23 | description: "Use PRs with activity since the last stable git tag" 24 | required: false 25 | type: boolean 26 | jobs: 27 | prep_release: 28 | runs-on: ubuntu-latest 29 | permissions: 30 | contents: write 31 | steps: 32 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 33 | 34 | - name: Prep Release 35 | id: prep-release 36 | uses: jupyter-server/jupyter_releaser/.github/actions/prep-release@v2 37 | with: 38 | token: ${{ secrets.GITHUB_TOKEN }} 39 | version_spec: ${{ github.event.inputs.version_spec }} 40 | silent: ${{ github.event.inputs.silent }} 41 | post_version_spec: ${{ github.event.inputs.post_version_spec }} 42 | target: ${{ github.event.inputs.target }} 43 | branch: ${{ github.event.inputs.branch }} 44 | since: ${{ github.event.inputs.since }} 45 | since_last_stable: ${{ github.event.inputs.since_last_stable }} 46 | 47 | - name: "** Next Step **" 48 | run: | 49 | echo "Optional): Review Draft Release: ${{ steps.prep-release.outputs.release_url }}" 50 | -------------------------------------------------------------------------------- /.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 | environment: release 19 | permissions: 20 | id-token: write 21 | steps: 22 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 23 | 24 | - uses: actions/create-github-app-token@v1 25 | id: app-token 26 | with: 27 | app-id: ${{ vars.APP_ID }} 28 | private-key: ${{ secrets.APP_PRIVATE_KEY }} 29 | 30 | - name: Populate Release 31 | id: populate-release 32 | uses: jupyter-server/jupyter_releaser/.github/actions/populate-release@v2 33 | with: 34 | token: ${{ steps.app-token.outputs.token }} 35 | branch: ${{ github.event.inputs.branch }} 36 | release_url: ${{ github.event.inputs.release_url }} 37 | steps_to_skip: ${{ github.event.inputs.steps_to_skip }} 38 | 39 | - name: Finalize Release 40 | id: finalize-release 41 | uses: jupyter-server/jupyter_releaser/.github/actions/finalize-release@v2 42 | with: 43 | token: ${{ steps.app-token.outputs.token }} 44 | release_url: ${{ steps.populate-release.outputs.release_url }} 45 | 46 | - name: "** Next Step **" 47 | if: ${{ success() }} 48 | run: | 49 | echo "Verify the final release" 50 | echo ${{ steps.finalize-release.outputs.release_url }} 51 | 52 | - name: "** Failure Message **" 53 | if: ${{ failure() }} 54 | run: | 55 | echo "Failed to Publish the Draft Release Url:" 56 | echo ${{ steps.populate-release.outputs.release_url }} 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jupyter Server Terminals 2 | 3 | [![Build Status](https://github.com/jupyter-server/jupyter_server_terminals/actions/workflows/test.yml/badge.svg?query=branch%3Amain++)](https://github.com/jupyter-server/jupyter_server_terminals/actions?query=branch%3Amain++) 4 | [![Documentation Status](https://readthedocs.org/projects/jupyter-server-terminals/badge/?version=latest)](http://jupyter-server-terminals.readthedocs.io/en/latest/?badge=latest) 5 | 6 | Jupyter Server Terminals is a Jupyter Server Extension providing support for terminals. 7 | 8 | ## Installation and Basic usage 9 | 10 | To install the latest release locally, make sure you have 11 | [pip installed](https://pip.readthedocs.io/en/stable/installing/) and run: 12 | 13 | ``` 14 | pip install jupyter_server_terminals 15 | ``` 16 | 17 | Jupyter Server Terminals currently supports Python>=3.6 on Linux, OSX and Windows. 18 | 19 | ### Testing 20 | 21 | See [CONTRIBUTING](./CONTRIBUTING.rst#running-tests). 22 | 23 | ## Contributing 24 | 25 | If you are interested in contributing to the project, see [CONTRIBUTING](./CONTRIBUTING.rst). 26 | 27 | ## About the Jupyter Development Team 28 | 29 | The Jupyter Development Team is the set of all contributors to the Jupyter project. 30 | This includes all of the Jupyter subprojects. 31 | 32 | The core team that coordinates development on GitHub can be found here: 33 | https://github.com/jupyter/. 34 | 35 | ## Our Copyright Policy 36 | 37 | Jupyter uses a shared copyright model. Each contributor maintains copyright 38 | over their contributions to Jupyter. But, it is important to note that these 39 | contributions are typically only changes to the repositories. Thus, the Jupyter 40 | source code, in its entirety is not the copyright of any single person or 41 | institution. Instead, it is the collective copyright of the entire Jupyter 42 | Development Team. If individual contributors want to maintain a record of what 43 | changes/contributions they have specific copyright on, they should indicate 44 | their copyright in the commit message of the change, when they commit the 45 | change to one of the Jupyter repositories. 46 | 47 | With this in mind, the following banner should be used in any source code file 48 | to indicate the copyright and license terms: 49 | 50 | ``` 51 | # Copyright (c) Jupyter Development Team. 52 | # Distributed under the terms of the Modified BSD License. 53 | ``` 54 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | ci: 2 | autoupdate_schedule: monthly 3 | autoupdate_commit_msg: "chore: update pre-commit hooks" 4 | 5 | repos: 6 | - repo: https://github.com/pre-commit/pre-commit-hooks 7 | rev: v4.5.0 8 | hooks: 9 | - id: check-case-conflict 10 | - id: check-ast 11 | - id: check-docstring-first 12 | - id: check-executables-have-shebangs 13 | - id: check-added-large-files 14 | - id: check-case-conflict 15 | - id: check-merge-conflict 16 | - id: check-json 17 | - id: check-toml 18 | - id: check-yaml 19 | - id: debug-statements 20 | - id: end-of-file-fixer 21 | - id: trailing-whitespace 22 | 23 | - repo: https://github.com/python-jsonschema/check-jsonschema 24 | rev: 0.27.4 25 | hooks: 26 | - id: check-github-workflows 27 | 28 | - repo: https://github.com/executablebooks/mdformat 29 | rev: 0.7.17 30 | hooks: 31 | - id: mdformat 32 | 33 | - repo: https://github.com/pre-commit/mirrors-prettier 34 | rev: "v4.0.0-alpha.8" 35 | hooks: 36 | - id: prettier 37 | types_or: [yaml, html, json] 38 | 39 | - repo: https://github.com/adamchainz/blacken-docs 40 | rev: "1.16.0" 41 | hooks: 42 | - id: blacken-docs 43 | additional_dependencies: [black==23.7.0] 44 | 45 | - repo: https://github.com/codespell-project/codespell 46 | rev: "v2.2.6" 47 | hooks: 48 | - id: codespell 49 | args: ["-L", "sur,nd"] 50 | 51 | - repo: https://github.com/pre-commit/pygrep-hooks 52 | rev: "v1.10.0" 53 | hooks: 54 | - id: rst-backticks 55 | - id: rst-directive-colons 56 | - id: rst-inline-touching-normal 57 | 58 | - repo: https://github.com/pre-commit/mirrors-mypy 59 | rev: "v1.8.0" 60 | hooks: 61 | - id: mypy 62 | files: "^jupyter_server_terminals" 63 | stages: [manual] 64 | args: ["--install-types", "--non-interactive"] 65 | additional_dependencies: 66 | ["traitlets>=5.13", "jupyter_server>=2.10.1", "terminado>=0.18"] 67 | 68 | - repo: https://github.com/astral-sh/ruff-pre-commit 69 | rev: v0.2.0 70 | hooks: 71 | - id: ruff 72 | types_or: [python, jupyter] 73 | args: ["--fix", "--show-fixes"] 74 | - id: ruff-format 75 | types_or: [python, jupyter] 76 | 77 | - repo: https://github.com/scientific-python/cookie 78 | rev: "2024.01.24" 79 | hooks: 80 | - id: sp-repo-review 81 | additional_dependencies: ["repo-review[cli]"] 82 | -------------------------------------------------------------------------------- /jupyter_server_terminals/api_handlers.py: -------------------------------------------------------------------------------- 1 | """API handlers for terminals.""" 2 | from __future__ import annotations 3 | 4 | import json 5 | from pathlib import Path 6 | from typing import Any 7 | 8 | from jupyter_server.auth.decorator import authorized 9 | from jupyter_server.base.handlers import APIHandler 10 | from tornado import web 11 | 12 | from .base import TerminalsMixin 13 | 14 | AUTH_RESOURCE = "terminals" 15 | 16 | 17 | class TerminalAPIHandler(APIHandler): 18 | """The base terminal handler.""" 19 | 20 | auth_resource = AUTH_RESOURCE 21 | 22 | 23 | class TerminalRootHandler(TerminalsMixin, TerminalAPIHandler): 24 | """The root termanal API handler.""" 25 | 26 | @web.authenticated 27 | @authorized 28 | def get(self) -> None: 29 | """Get the list of terminals.""" 30 | models = self.terminal_manager.list() 31 | self.finish(json.dumps(models)) 32 | 33 | @web.authenticated 34 | @authorized 35 | def post(self) -> None: 36 | """POST /terminals creates a new terminal and redirects to it""" 37 | data = self.get_json_body() or {} 38 | 39 | # if cwd is a relative path, it should be relative to the root_dir, 40 | # but if we pass it as relative, it will we be considered as relative to 41 | # the path jupyter_server was started in 42 | if "cwd" in data: 43 | cwd: Path | None = Path(data["cwd"]) 44 | assert cwd is not None 45 | if not cwd.resolve().exists(): 46 | cwd = Path(self.settings["server_root_dir"]).expanduser() / cwd 47 | if not cwd.resolve().exists(): 48 | cwd = None 49 | 50 | if cwd is None: 51 | server_root_dir = self.settings["server_root_dir"] 52 | self.log.debug( 53 | "Failed to find requested terminal cwd: %s\n" 54 | " It was not found within the server root neither: %s.", 55 | data.get("cwd"), 56 | server_root_dir, 57 | ) 58 | del data["cwd"] 59 | else: 60 | self.log.debug("Opening terminal in: %s", cwd.resolve()) 61 | data["cwd"] = str(cwd.resolve()) 62 | 63 | model = self.terminal_manager.create(**data) 64 | self.finish(json.dumps(model)) 65 | 66 | 67 | class TerminalHandler(TerminalsMixin, TerminalAPIHandler): 68 | """A handler for a specific terminal.""" 69 | 70 | SUPPORTED_METHODS = ("GET", "DELETE", "OPTIONS") # type:ignore[assignment] 71 | 72 | @web.authenticated 73 | @authorized 74 | def get(self, name: str) -> None: 75 | """Get a terminal by name.""" 76 | model = self.terminal_manager.get(name) 77 | self.finish(json.dumps(model)) 78 | 79 | @web.authenticated 80 | @authorized 81 | async def delete(self, name: str) -> None: 82 | """Remove a terminal by name.""" 83 | await self.terminal_manager.terminate(name, force=True) 84 | self.set_status(204) 85 | self.finish() 86 | 87 | 88 | default_handlers: list[tuple[str, type[Any]]] = [ 89 | (r"/api/terminals", TerminalRootHandler), 90 | (r"/api/terminals/(\w+)", TerminalHandler), 91 | ] 92 | -------------------------------------------------------------------------------- /jupyter_server_terminals/handlers.py: -------------------------------------------------------------------------------- 1 | """Tornado handlers for the terminal emulator.""" 2 | # Copyright (c) Jupyter Development Team. 3 | # Distributed under the terms of the Modified BSD License. 4 | from __future__ import annotations 5 | 6 | import typing as t 7 | 8 | from jupyter_core.utils import ensure_async 9 | from jupyter_server._tz import utcnow 10 | from jupyter_server.auth.utils import warn_disabled_authorization 11 | from jupyter_server.base.handlers import JupyterHandler 12 | from jupyter_server.base.websocket import WebSocketMixin 13 | from terminado.management import NamedTermManager 14 | from terminado.websocket import TermSocket as BaseTermSocket 15 | from tornado import web 16 | 17 | from .base import TerminalsMixin 18 | 19 | AUTH_RESOURCE = "terminals" 20 | 21 | 22 | class TermSocket(TerminalsMixin, WebSocketMixin, JupyterHandler, BaseTermSocket): 23 | """A terminal websocket.""" 24 | 25 | auth_resource = AUTH_RESOURCE 26 | 27 | def initialize( # type:ignore[override] 28 | self, name: str, term_manager: NamedTermManager, **kwargs: t.Any 29 | ) -> None: 30 | """Initialize the socket.""" 31 | BaseTermSocket.initialize(self, term_manager, **kwargs) 32 | TerminalsMixin.initialize(self, name) 33 | 34 | def origin_check(self, origin: t.Any = None) -> bool: 35 | """Terminado adds redundant origin_check 36 | Tornado already calls check_origin, so don't do anything here. 37 | """ 38 | return True 39 | 40 | async def get(self, *args: t.Any, **kwargs: t.Any) -> None: 41 | """Get the terminal socket.""" 42 | user = self.current_user 43 | 44 | if not user: 45 | raise web.HTTPError(403) 46 | 47 | # authorize the user. 48 | if self.authorizer is None: 49 | # Warn if an authorizer is unavailable. 50 | warn_disabled_authorization() # type:ignore[unreachable] 51 | elif not await ensure_async( 52 | self.authorizer.is_authorized(self, user, "execute", self.auth_resource) 53 | ): 54 | raise web.HTTPError(403) 55 | 56 | if args[0] not in self.term_manager.terminals: # type:ignore[attr-defined] 57 | raise web.HTTPError(404) 58 | resp = super().get(*args, **kwargs) 59 | if resp is not None: 60 | await ensure_async(resp) # type:ignore[arg-type] 61 | 62 | async def on_message(self, message: t.Any) -> None: # type:ignore[override] 63 | """Handle a socket message.""" 64 | await ensure_async(super().on_message(message)) # type:ignore[arg-type] 65 | self._update_activity() 66 | 67 | def write_message(self, message: t.Any, binary: bool = False) -> None: # type:ignore[override] 68 | """Write a message to the socket.""" 69 | super().write_message(message, binary=binary) 70 | self._update_activity() 71 | 72 | def _update_activity(self) -> None: 73 | self.application.settings["terminal_last_activity"] = utcnow() 74 | # terminal may not be around on deletion/cull 75 | if self.term_name in self.terminal_manager.terminals: 76 | self.terminal_manager.terminals[self.term_name].last_activity = utcnow() # type:ignore[attr-defined] 77 | -------------------------------------------------------------------------------- /jupyter_server_terminals/rest-api.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.1 2 | info: 3 | title: Jupyter Server Terminals API 4 | description: Terminals API 5 | contact: 6 | name: Jupyter Project 7 | url: https://jupyter.org 8 | version: "1" 9 | servers: 10 | - url: / 11 | paths: 12 | /api/terminals: 13 | get: 14 | tags: 15 | - terminals 16 | summary: Get available terminals 17 | responses: 18 | 200: 19 | description: A list of all available terminal ids. 20 | content: 21 | application/json: 22 | schema: 23 | type: array 24 | items: 25 | $ref: "#/components/schemas/Terminal" 26 | 403: 27 | description: Forbidden to access 28 | content: {} 29 | 404: 30 | description: Not found 31 | content: {} 32 | post: 33 | tags: 34 | - terminals 35 | summary: Create a new terminal 36 | responses: 37 | 200: 38 | description: Successfully created a new terminal 39 | content: 40 | application/json: 41 | schema: 42 | $ref: "#/components/schemas/Terminal" 43 | 403: 44 | description: Forbidden to access 45 | content: {} 46 | 404: 47 | description: Not found 48 | content: {} 49 | /api/terminals/{terminal_id}: 50 | get: 51 | tags: 52 | - terminals 53 | summary: Get a terminal session corresponding to an id. 54 | parameters: 55 | - name: terminal_id 56 | in: path 57 | description: ID of terminal session 58 | required: true 59 | schema: 60 | type: string 61 | responses: 62 | 200: 63 | description: Terminal session with given id 64 | content: 65 | application/json: 66 | schema: 67 | $ref: "#/components/schemas/Terminal" 68 | 403: 69 | description: Forbidden to access 70 | content: {} 71 | 404: 72 | description: Not found 73 | content: {} 74 | delete: 75 | tags: 76 | - terminals 77 | summary: Delete a terminal session corresponding to an id. 78 | parameters: 79 | - name: terminal_id 80 | in: path 81 | description: ID of terminal session 82 | required: true 83 | schema: 84 | type: string 85 | responses: 86 | 204: 87 | description: Successfully deleted terminal session 88 | content: {} 89 | 403: 90 | description: Forbidden to access 91 | content: {} 92 | 404: 93 | description: Not found 94 | content: {} 95 | components: 96 | schemas: 97 | Terminal: 98 | required: 99 | - name 100 | type: object 101 | properties: 102 | name: 103 | type: string 104 | description: name of terminal 105 | last_activity: 106 | type: string 107 | description: | 108 | ISO 8601 timestamp for the last-seen activity on this terminal. Use 109 | this to identify which terminals have been inactive since a given time. 110 | Timestamps will be UTC, indicated 'Z' suffix. 111 | description: A Terminal object 112 | parameters: 113 | terminal_id: 114 | name: terminal_id 115 | in: path 116 | description: ID of terminal session 117 | required: true 118 | schema: 119 | type: string 120 | -------------------------------------------------------------------------------- /tests/test_auth.py: -------------------------------------------------------------------------------- 1 | """Tests for authorization""" 2 | import asyncio 3 | from typing import Dict 4 | 5 | import pytest 6 | from jupyter_server.auth.authorizer import Authorizer 7 | from jupyter_server.auth.utils import HTTP_METHOD_TO_AUTH_ACTION, match_url_to_resource 8 | from tornado.httpclient import HTTPClientError 9 | from tornado.websocket import WebSocketHandler 10 | from traitlets.config.loader import Config 11 | 12 | 13 | class AuthorizerforTesting(Authorizer): 14 | # Set these class attributes from within a test 15 | # to verify that they match the arguments passed 16 | # by the REST API. 17 | permissions: Dict[str, str] = {} # noqa: RUF012 18 | 19 | def normalize_url(self, path): 20 | """Drop the base URL and make sure path leads with a /""" 21 | base_url = self.parent.base_url 22 | # Remove base_url 23 | if path.startswith(base_url): 24 | path = path[len(base_url) :] 25 | # Make sure path starts with / 26 | if not path.startswith("/"): 27 | path = "/" + path 28 | return path 29 | 30 | def is_authorized(self, handler, user, action, resource): 31 | # Parse Request 32 | method = "WEBSOCKET" if isinstance(handler, WebSocketHandler) else handler.request.method 33 | url = self.normalize_url(handler.request.path) 34 | 35 | # Map request parts to expected action and resource. 36 | expected_action = HTTP_METHOD_TO_AUTH_ACTION[method] 37 | expected_resource = match_url_to_resource(url) 38 | 39 | # Assert that authorization layer returns the 40 | # correct action + resource. 41 | assert action == expected_action 42 | assert resource == expected_resource 43 | 44 | # Now, actually apply the authorization layer. 45 | return all( 46 | [ 47 | action in self.permissions.get("actions", []), 48 | resource in self.permissions.get("resources", []), 49 | ] 50 | ) 51 | 52 | 53 | @pytest.fixture() 54 | def jp_server_config(): 55 | return Config( 56 | { 57 | "ServerApp": { 58 | "jpserver_extensions": {"jupyter_server_terminals": True}, 59 | "authorizer_class": AuthorizerforTesting, 60 | } 61 | } 62 | ) 63 | 64 | 65 | @pytest.fixture() 66 | def send_request(jp_fetch, jp_ws_fetch): 67 | """Send to Jupyter Server and return response code.""" 68 | 69 | async def _(url, **fetch_kwargs): 70 | fetch = jp_ws_fetch if url.endswith("channels") or "/websocket/" in url else jp_fetch 71 | 72 | try: 73 | r = await fetch(url, **fetch_kwargs, allow_nonstandard_methods=True) 74 | code = r.code 75 | except HTTPClientError as err: 76 | code = err.code 77 | else: 78 | if fetch is jp_ws_fetch: 79 | r.close() 80 | 81 | print(code, url, fetch_kwargs) 82 | return code 83 | 84 | return _ 85 | 86 | 87 | HTTP_REQUESTS = [ 88 | { 89 | "method": "POST", 90 | "url": "/api/terminals", 91 | "body": "", 92 | }, 93 | { 94 | "method": "GET", 95 | "url": "/api/terminals", 96 | }, 97 | { 98 | "method": "GET", 99 | "url": "/terminals/websocket/{term_name}", 100 | }, 101 | { 102 | "method": "DELETE", 103 | "url": "/api/terminals/{term_name}", 104 | }, 105 | ] 106 | 107 | HTTP_REQUESTS_PARAMETRIZED = [(req["method"], req["url"], req.get("body")) for req in HTTP_REQUESTS] 108 | 109 | # -------- Test scenarios ----------- 110 | 111 | 112 | @pytest.mark.parametrize("method, url, body", HTTP_REQUESTS_PARAMETRIZED) # noqa: PT006 113 | @pytest.mark.parametrize("allowed", (True, False)) # noqa: PT007 114 | async def test_authorized_requests( 115 | request, 116 | io_loop, 117 | send_request, 118 | jp_serverapp, 119 | method, 120 | url, 121 | body, 122 | allowed, 123 | ): 124 | term_manager = jp_serverapp.web_app.settings["terminal_manager"] 125 | request.addfinalizer(lambda: io_loop.run_sync(term_manager.terminate_all)) 126 | term_model = term_manager.create() 127 | term_name = term_model["name"] 128 | 129 | url = url.format(term_name=term_name) 130 | if allowed: 131 | # Create a server with full permissions 132 | permissions = { 133 | "actions": ["read", "write", "execute"], 134 | "resources": [ 135 | "terminals", 136 | ], 137 | } 138 | expected_codes = {200, 201, 204, None} # Websockets don't return a code 139 | else: 140 | permissions = {"actions": [], "resources": []} 141 | expected_codes = {403} 142 | jp_serverapp.authorizer.permissions = permissions 143 | 144 | while True: 145 | code = await send_request(url, body=body, method=method) 146 | if code == 404: 147 | await asyncio.sleep(1) 148 | continue 149 | assert code in expected_codes 150 | break 151 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | push: 4 | branches: ["main"] 5 | pull_request: 6 | 7 | schedule: 8 | - cron: "0 8 * * *" 9 | 10 | defaults: 11 | run: 12 | shell: bash -eux {0} 13 | 14 | jobs: 15 | test_lint: 16 | name: Test Lint 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 21 | - name: Run Linters 22 | run: | 23 | hatch run typing:test 24 | hatch run lint:build 25 | pipx run interrogate . 26 | pipx run doc8 --max-line-length=200 27 | 28 | test: 29 | runs-on: ${{ matrix.os }} 30 | timeout-minutes: 10 31 | strategy: 32 | fail-fast: false 33 | matrix: 34 | os: [ubuntu-latest, windows-latest, macos-latest] 35 | python-version: ["3.8", "3.12"] 36 | include: 37 | - os: windows-latest 38 | python-version: "3.9" 39 | - os: ubuntu-latest 40 | python-version: "pypy-3.9" 41 | - os: macos-latest 42 | python-version: "3.10" 43 | - os: ubuntu-latest 44 | python-version: "3.11" 45 | steps: 46 | - name: Checkout 47 | uses: actions/checkout@v4 48 | - name: Base Setup 49 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 50 | - name: Run the tests on pypy 51 | if: ${{ startsWith(matrix.python-version, 'pypy') }} 52 | run: | 53 | hatch run test:nowarn || hatch run test:nowarn --lf 54 | - name: Run the tests 55 | if: ${{ !startsWith(matrix.python-version, 'pypy') }} 56 | run: | 57 | hatch run cov:test --cov-fail-under 75 || hatch run test:test --lf 58 | - uses: jupyterlab/maintainer-tools/.github/actions/upload-coverage@v1 59 | 60 | coverage: 61 | runs-on: ubuntu-latest 62 | needs: 63 | - test 64 | steps: 65 | - uses: actions/checkout@v4 66 | - uses: jupyterlab/maintainer-tools/.github/actions/report-coverage@v1 67 | 68 | test_docs: 69 | name: Test Docs 70 | runs-on: windows-latest 71 | steps: 72 | - uses: actions/checkout@v4 73 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 74 | - run: hatch run docs:build 75 | 76 | test_minimum_versions: 77 | name: Test Minimum Versions 78 | timeout-minutes: 20 79 | runs-on: ubuntu-latest 80 | steps: 81 | - uses: actions/checkout@v4 82 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 83 | with: 84 | dependency_type: minimum 85 | - name: Run the unit tests 86 | run: | 87 | hatch -vv run test:nowarn || hatch run test:nowarn --lf 88 | 89 | test_prereleases: 90 | name: Test Prereleases 91 | runs-on: ubuntu-latest 92 | timeout-minutes: 20 93 | steps: 94 | - uses: actions/checkout@v4 95 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 96 | with: 97 | dependency_type: pre 98 | - name: Run the tests 99 | run: | 100 | hatch run test:nowarn || hatch run test:nowarn --lf 101 | 102 | make_sdist: 103 | name: Make SDist 104 | runs-on: ubuntu-latest 105 | timeout-minutes: 10 106 | steps: 107 | - uses: actions/checkout@v4 108 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 109 | - uses: jupyterlab/maintainer-tools/.github/actions/make-sdist@v1 110 | 111 | test_sdist: 112 | runs-on: ubuntu-latest 113 | needs: [make_sdist] 114 | name: Install from SDist and Test 115 | timeout-minutes: 15 116 | steps: 117 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 118 | - uses: jupyterlab/maintainer-tools/.github/actions/test-sdist@v1 119 | with: 120 | extra_test: 'jupyter server extension list 2>&1 | grep -ie "jupyter_server_terminals.*OK"' 121 | 122 | check_release: 123 | runs-on: ubuntu-latest 124 | steps: 125 | - name: Checkout 126 | uses: actions/checkout@v4 127 | - name: Base Setup 128 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 129 | - name: Install Dependencies 130 | run: | 131 | pip install -e . 132 | - name: Check Release 133 | uses: jupyter-server/jupyter_releaser/.github/actions/check-release@v2 134 | with: 135 | token: ${{ secrets.GITHUB_TOKEN }} 136 | 137 | check_links: 138 | runs-on: ubuntu-latest 139 | timeout-minutes: 10 140 | steps: 141 | - uses: actions/checkout@v4 142 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 143 | - uses: jupyterlab/maintainer-tools/.github/actions/check-links@v1 144 | 145 | tests_check: # This job does nothing and is only used for the branch protection 146 | if: always() 147 | needs: 148 | - coverage 149 | - test_lint 150 | - test_docs 151 | - test_minimum_versions 152 | - test_prereleases 153 | - check_links 154 | - check_release 155 | - test_sdist 156 | runs-on: ubuntu-latest 157 | steps: 158 | - name: Decide whether the needed jobs succeeded or failed 159 | uses: re-actors/alls-green@release/v1 160 | with: 161 | jobs: ${{ toJSON(needs) }} 162 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | General Jupyter contributor guidelines 2 | ====================================== 3 | 4 | If you're reading this section, you're probably interested in contributing to 5 | Jupyter. Welcome and thanks for your interest in contributing! 6 | 7 | Please take a look at the Contributor documentation, familiarize yourself with 8 | using the Jupyter Server, and introduce yourself on the mailing list and 9 | share what area of the project you are interested in working on. 10 | 11 | For general documentation about contributing to Jupyter projects, see the 12 | `Project Jupyter Contributor Documentation`__. 13 | 14 | __ https://jupyter.readthedocs.io/en/latest/contributing/content-contributor.html 15 | 16 | Setting Up a Development Environment 17 | ==================================== 18 | 19 | Installing Jupyter Server Terminals 20 | ----------------------------------- 21 | 22 | The development version of the server requires `node `_ and `pip `_. 23 | 24 | Once you have installed the dependencies mentioned above, use the following 25 | steps:: 26 | 27 | pip install --upgrade pip 28 | git clone https://github.com/jupyter/jupyter_server_terminals 29 | cd jupyter_server_terminals 30 | pip install -e . 31 | 32 | If you are using a system-wide Python installation and you only want to install the server for you, 33 | you can add ``--user`` to the install commands. 34 | 35 | Once you have done this, you can launch the main branch of Jupyter server 36 | from any directory in your system with:: 37 | 38 | jupyter server 39 | 40 | 41 | Code Styling 42 | ----------------------------- 43 | ``jupyter_server_terminals`` has adopted automatic code formatting so you shouldn't 44 | need to worry too much about your code style. 45 | As long as your code is valid, 46 | the pre-commit hook should take care of how it should look. 47 | To install ``pre-commit``, run the following:: 48 | 49 | pip install pre-commit 50 | pre-commit install 51 | 52 | 53 | You can invoke the pre-commit hook by hand at any time with:: 54 | 55 | pre-commit run 56 | 57 | which should run any autoformatting on your code 58 | and tell you about any errors it couldn't fix automatically. 59 | You may also install `black integration `_ 60 | into your text editor to format code automatically. 61 | 62 | If you have already committed files before setting up the pre-commit 63 | hook with ``pre-commit install``, you can fix everything up using 64 | ``pre-commit run --all-files``. You need to make the fixing commit 65 | yourself after that. 66 | 67 | Some of the hooks only run on CI by default, but you can invoke them by 68 | running with the ``--hook-stage manual`` argument. 69 | 70 | Troubleshooting the Installation 71 | -------------------------------- 72 | 73 | If you do not see that your Jupyter Server is not running on dev mode, it's possible that you are 74 | running other instances of Jupyter Server. You can try the following steps: 75 | 76 | 1. Uninstall all instances of the jupyter_server_terminals package. These include any installations you made using 77 | pip or conda 78 | 2. Run ``python3 -m pip install -e .`` in the jupyter_server_terminals repository to install jupyter_server_terminals from there 79 | 3. Launch with ``python3 -m jupyter_server --port 8989``, and check that the browser is pointing to ``localhost:8989`` 80 | (rather than the default 8888). You don't necessarily have to launch with port 8989, as long as you use 81 | a port that is neither the default nor in use, then it should be fine. 82 | 4. Verify the installation with the steps in the previous section. 83 | 84 | Running Tests 85 | ============= 86 | 87 | Install dependencies:: 88 | 89 | pip install -e .[test] 90 | pip install -e examples/simple # to test the examples 91 | 92 | To run the Python tests, use:: 93 | 94 | pytest jupyter_server_terminals 95 | 96 | 97 | Building the Docs 98 | ================= 99 | 100 | To build the documentation you'll need `Sphinx `_, 101 | `pandoc `_ and a few other packages. 102 | 103 | To install (and activate) a `conda environment`_ named ``jupyter_server_terminals_docs`` 104 | containing all the necessary packages (except pandoc), use:: 105 | 106 | conda env create -f docs/environment.yml 107 | conda activate jupyter_server_terminals_docs 108 | 109 | .. _conda environment: 110 | https://conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#creating-an-environment-from-an-environment-yml-file 111 | 112 | If you want to install the necessary packages with ``pip`` instead:: 113 | 114 | pip install -r docs/doc-requirements.txt 115 | 116 | Once you have installed the required packages, you can build the docs with:: 117 | 118 | cd docs 119 | make html 120 | 121 | After that, the generated HTML files will be available at 122 | ``build/html/index.html``. You may view the docs in your browser. 123 | 124 | You can automatically check if all hyperlinks are still valid:: 125 | 126 | make linkcheck 127 | 128 | Windows users can find ``make.bat`` in the ``docs`` folder. 129 | 130 | You should also have a look at the `Project Jupyter Documentation Guide`__. 131 | 132 | __ https://jupyter.readthedocs.io/en/latest/contributing/content-contributor.html 133 | -------------------------------------------------------------------------------- /jupyter_server_terminals/app.py: -------------------------------------------------------------------------------- 1 | """A terminals extension app.""" 2 | from __future__ import annotations 3 | 4 | import os 5 | import shlex 6 | import sys 7 | import typing as t 8 | from shutil import which 9 | 10 | from jupyter_core.utils import ensure_async 11 | from jupyter_server.extension.application import ExtensionApp 12 | from jupyter_server.transutils import trans 13 | from traitlets import Type 14 | 15 | from . import api_handlers, handlers 16 | from .terminalmanager import TerminalManager 17 | 18 | 19 | class TerminalsExtensionApp(ExtensionApp): 20 | """A terminals extension app.""" 21 | 22 | name = "jupyter_server_terminals" 23 | 24 | terminal_manager_class: type[TerminalManager] = Type( # type:ignore[assignment] 25 | default_value=TerminalManager, help="The terminal manager class to use." 26 | ).tag(config=True) 27 | 28 | # Since use of terminals is also a function of whether the terminado package is 29 | # available, this variable holds the "final indication" of whether terminal functionality 30 | # should be considered (particularly during shutdown/cleanup). It is enabled only 31 | # once both the terminals "service" can be initialized and terminals_enabled is True. 32 | # Note: this variable is slightly different from 'terminals_available' in the web settings 33 | # in that this variable *could* remain false if terminado is available, yet the terminal 34 | # service's initialization still fails. As a result, this variable holds the truth. 35 | terminals_available = False 36 | 37 | def initialize_settings(self) -> None: 38 | """Initialize settings.""" 39 | if not self.serverapp or not self.serverapp.terminals_enabled: 40 | self.settings.update({"terminals_available": False}) 41 | return 42 | self.initialize_configurables() 43 | self.settings.update( 44 | {"terminals_available": True, "terminal_manager": self.terminal_manager} 45 | ) 46 | 47 | def initialize_configurables(self) -> None: 48 | """Initialize configurables.""" 49 | default_shell = "powershell.exe" if os.name == "nt" else which("sh") 50 | assert self.serverapp is not None 51 | shell_override = self.serverapp.terminado_settings.get("shell_command") 52 | if isinstance(shell_override, str): 53 | shell_override = shlex.split(shell_override) 54 | shell = ( 55 | [os.environ.get("SHELL") or default_shell] if shell_override is None else shell_override 56 | ) 57 | # When the notebook server is not running in a terminal (e.g. when 58 | # it's launched by a JupyterHub spawner), it's likely that the user 59 | # environment hasn't been fully set up. In that case, run a login 60 | # shell to automatically source /etc/profile and the like, unless 61 | # the user has specifically set a preferred shell command. 62 | if os.name != "nt" and shell_override is None and not sys.stdout.isatty(): 63 | shell.append("-l") 64 | 65 | self.terminal_manager = self.terminal_manager_class( 66 | shell_command=shell, 67 | extra_env={ 68 | "JUPYTER_SERVER_ROOT": self.serverapp.root_dir, 69 | "JUPYTER_SERVER_URL": self.serverapp.connection_url, 70 | }, 71 | parent=self.serverapp, 72 | ) 73 | self.terminal_manager.log = self.serverapp.log 74 | 75 | def initialize_handlers(self) -> None: 76 | """Initialize handlers.""" 77 | if not self.serverapp: 78 | # Already set `terminals_available` as `False` in `initialize_settings` 79 | return 80 | 81 | if not self.serverapp.terminals_enabled: 82 | # webapp settings for backwards compat (used by nbclassic), #12 83 | self.serverapp.web_app.settings["terminals_available"] = self.settings[ 84 | "terminals_available" 85 | ] 86 | return 87 | self.handlers.append( 88 | ( 89 | r"/terminals/websocket/(\w+)", 90 | handlers.TermSocket, 91 | {"term_manager": self.terminal_manager}, 92 | ) 93 | ) 94 | self.handlers.extend(api_handlers.default_handlers) 95 | assert self.serverapp is not None 96 | self.serverapp.web_app.settings["terminal_manager"] = self.terminal_manager 97 | self.serverapp.web_app.settings["terminals_available"] = self.settings[ 98 | "terminals_available" 99 | ] 100 | 101 | def current_activity(self) -> dict[str, t.Any] | None: 102 | """Get current activity info.""" 103 | if self.terminals_available: 104 | terminals = self.terminal_manager.terminals 105 | if terminals: 106 | return terminals 107 | return None 108 | 109 | async def cleanup_terminals(self) -> None: 110 | """Shutdown all terminals. 111 | 112 | The terminals will shutdown themselves when this process no longer exists, 113 | but explicit shutdown allows the TerminalManager to cleanup. 114 | """ 115 | if not self.terminals_available: 116 | return 117 | 118 | terminal_manager = self.terminal_manager 119 | n_terminals = len(terminal_manager.list()) 120 | terminal_msg = trans.ngettext( 121 | "Shutting down %d terminal", "Shutting down %d terminals", n_terminals 122 | ) 123 | self.log.info("%s %% %s", terminal_msg, n_terminals) 124 | await ensure_async(terminal_manager.terminate_all()) # type:ignore[arg-type] 125 | 126 | async def stop_extension(self) -> None: 127 | """Stop the extension.""" 128 | await self.cleanup_terminals() 129 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling>=1.5"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "jupyter_server_terminals" 7 | readme = "README.md" 8 | dynamic = ["version"] 9 | license = { file = "LICENSE" } 10 | description = "A Jupyter Server Extension Providing Terminals." 11 | keywords = ["ipython", "jupyter"] 12 | classifiers = [ 13 | "Intended Audience :: Developers", 14 | "Intended Audience :: System Administrators", 15 | "Intended Audience :: Science/Research", 16 | "License :: OSI Approved :: BSD License", 17 | "Programming Language :: Python", 18 | "Programming Language :: Python :: 3.8", 19 | "Programming Language :: Python :: 3.9", 20 | "Programming Language :: Python :: 3.10", 21 | "Programming Language :: Python :: 3.11" 22 | ] 23 | requires-python = ">=3.8" 24 | dependencies = [ 25 | "pywinpty>=2.0.3;os_name=='nt'", 26 | "terminado>=0.8.3", 27 | ] 28 | 29 | [[project.authors]] 30 | name = "Jupyter Development Team" 31 | email = "jupyter@googlegroups.com" 32 | 33 | [project.urls] 34 | Homepage = "https://jupyter.org" 35 | Documentation = "https://jupyter-server-terminals.readthedocs.io" 36 | Funding = "https://jupyter.org/about#donate" 37 | Source = "https://github.com/jupyter-server/jupyter_server_terminals" 38 | Tracker = "https://github.com/jupyter-server/jupyter_server_terminals/issues" 39 | 40 | [project.optional-dependencies] 41 | test = [ 42 | "jupyter_server>=2.0.0", 43 | "pytest-jupyter[server]>=0.5.3", 44 | "pytest>=7.0", 45 | "pytest-timeout", 46 | ] 47 | docs = [ 48 | "jinja2", 49 | "jupyter_server", 50 | "mistune<4.0", # https://github.com/jupyter/nbconvert/issues/1685" 51 | "myst-parser", 52 | "nbformat", 53 | "packaging", 54 | "tornado", 55 | "pydata_sphinx_theme", 56 | "sphinxcontrib-openapi", 57 | "sphinxcontrib_github_alt", 58 | "sphinxemoji", 59 | "sphinxcontrib-spelling", 60 | ] 61 | 62 | [tool.hatch.version] 63 | path = "jupyter_server_terminals/_version.py" 64 | 65 | [tool.hatch.build.targets.wheel.shared-data] 66 | "jupyter-config" = "etc/jupyter/jupyter_server_config.d" 67 | 68 | [tool.hatch.envs.docs] 69 | features = ["docs"] 70 | [tool.hatch.envs.docs.scripts] 71 | build = "make -C docs html SPHINXOPTS='-W'" 72 | 73 | [tool.hatch.envs.test] 74 | features = ["test"] 75 | [tool.hatch.envs.test.scripts] 76 | test = "python -m pytest -vv {args}" 77 | nowarn = "test -W default {args}" 78 | 79 | [tool.hatch.envs.cov] 80 | features = ["test"] 81 | dependencies = ["coverage[toml]", "pytest-cov"] 82 | [tool.hatch.envs.cov.scripts] 83 | test = "python -m pytest -vv --cov jupyter_server_terminals --cov-branch --cov-report term-missing:skip-covered {args}" 84 | nowarn = "test -W default {args}" 85 | 86 | [tool.hatch.envs.lint] 87 | detached = true 88 | dependencies = ["pre-commit"] 89 | [tool.hatch.envs.lint.scripts] 90 | build = [ 91 | "pre-commit run --all-files ruff", 92 | "pre-commit run --all-files ruff-format" 93 | ] 94 | 95 | [tool.hatch.envs.typing] 96 | dependencies = [ "pre-commit"] 97 | detached = true 98 | [tool.hatch.envs.typing.scripts] 99 | test = "pre-commit run --all-files --hook-stage manual mypy" 100 | 101 | [tool.pytest.ini_options] 102 | minversion = "6.0" 103 | xfail_strict = true 104 | log_cli_level = "info" 105 | addopts = [ 106 | "-ra", "--durations=10", "--color=yes", "--doctest-modules", 107 | "--showlocals", "--strict-markers", "--strict-config", 108 | ] 109 | testpaths = [ 110 | "tests/" 111 | ] 112 | timeout = 300 113 | # Restore this setting to debug failures 114 | # timeout_method = "thread" 115 | filterwarnings = [ 116 | "error", 117 | # from tornado 118 | "ignore:unclosed 100 characters) 165 | "SIM105", # Use `contextlib.suppress(...)` 166 | "T201", # `print` found 167 | "S101", # Use of `assert` detected 168 | ] 169 | unfixable = [ 170 | # Don't touch print statements 171 | "T201", 172 | # Don't touch noqa lines 173 | "RUF100", 174 | ] 175 | 176 | [tool.ruff.lint.per-file-ignores] 177 | # B011: Do not call assert False since python -O removes these calls 178 | # F841 local variable 'foo' is assigned to but never used 179 | # S101 Use of `assert` detected 180 | "tests/*" = ["B011", "F841"] 181 | "docs/*" = ["PTH"] 182 | 183 | [tool.interrogate] 184 | ignore-init-module=true 185 | ignore-private=true 186 | ignore-semiprivate=true 187 | ignore-property-decorators=true 188 | ignore-nested-functions=true 189 | ignore-nested-classes=true 190 | fail-under=100 191 | exclude = ["tests", "docs"] 192 | 193 | [tool.repo-review] 194 | ignore = ["GH102"] 195 | -------------------------------------------------------------------------------- /jupyter_server_terminals/terminalmanager.py: -------------------------------------------------------------------------------- 1 | """A MultiTerminalManager for use in the notebook webserver 2 | - raises HTTPErrors 3 | - creates REST API models 4 | """ 5 | # Copyright (c) Jupyter Development Team. 6 | # Distributed under the terms of the Modified BSD License. 7 | from __future__ import annotations 8 | 9 | import typing as t 10 | from datetime import timedelta 11 | 12 | from jupyter_server._tz import isoformat, utcnow 13 | from jupyter_server.prometheus import metrics 14 | from terminado.management import NamedTermManager, PtyWithClients 15 | from tornado import web 16 | from tornado.ioloop import IOLoop, PeriodicCallback 17 | from traitlets import Integer 18 | from traitlets.config import LoggingConfigurable 19 | 20 | RUNNING_TOTAL = metrics.TERMINAL_CURRENTLY_RUNNING_TOTAL 21 | 22 | MODEL = t.Dict[str, t.Any] 23 | 24 | 25 | class TerminalManager(LoggingConfigurable, NamedTermManager): # type:ignore[misc] 26 | """A MultiTerminalManager for use in the notebook webserver""" 27 | 28 | _culler_callback = None 29 | 30 | _initialized_culler = False 31 | 32 | cull_inactive_timeout = Integer( 33 | 0, 34 | config=True, 35 | help="""Timeout (in seconds) in which a terminal has been inactive and ready to be culled. 36 | Values of 0 or lower disable culling.""", 37 | ) 38 | 39 | cull_interval_default = 300 # 5 minutes 40 | cull_interval = Integer( 41 | cull_interval_default, 42 | config=True, 43 | help="""The interval (in seconds) on which to check for terminals exceeding the inactive timeout value.""", 44 | ) 45 | 46 | # ------------------------------------------------------------------------- 47 | # Methods for managing terminals 48 | # ------------------------------------------------------------------------- 49 | def create(self, **kwargs: t.Any) -> MODEL: 50 | """Create a new terminal.""" 51 | name, term = self.new_named_terminal(**kwargs) 52 | # Monkey-patch last-activity, similar to kernels. Should we need 53 | # more functionality per terminal, we can look into possible sub- 54 | # classing or containment then. 55 | term.last_activity = utcnow() # type:ignore[attr-defined] 56 | model = self.get_terminal_model(name) 57 | # Increase the metric by one because a new terminal was created 58 | RUNNING_TOTAL.inc() 59 | # Ensure culler is initialized 60 | self._initialize_culler() 61 | return model 62 | 63 | def get(self, name: str) -> MODEL: 64 | """Get terminal 'name'.""" 65 | return self.get_terminal_model(name) 66 | 67 | def list(self) -> list[MODEL]: 68 | """Get a list of all running terminals.""" 69 | models = [self.get_terminal_model(name) for name in self.terminals] 70 | 71 | # Update the metric below to the length of the list 'terms' 72 | RUNNING_TOTAL.set(len(models)) 73 | return models 74 | 75 | async def terminate(self, name: str, force: bool = False) -> None: 76 | """Terminate terminal 'name'.""" 77 | self._check_terminal(name) 78 | await super().terminate(name, force=force) 79 | 80 | # Decrease the metric below by one 81 | # because a terminal has been shutdown 82 | RUNNING_TOTAL.dec() 83 | 84 | async def terminate_all(self) -> None: 85 | """Terminate all terminals.""" 86 | terms = list(self.terminals) 87 | for term in terms: 88 | await self.terminate(term, force=True) 89 | 90 | def get_terminal_model(self, name: str) -> MODEL: 91 | """Return a JSON-safe dict representing a terminal. 92 | For use in representing terminals in the JSON APIs. 93 | """ 94 | self._check_terminal(name) 95 | term = self.terminals[name] 96 | return { 97 | "name": name, 98 | "last_activity": isoformat(term.last_activity), # type:ignore[attr-defined] 99 | } 100 | 101 | def _check_terminal(self, name: str) -> None: 102 | """Check a that terminal 'name' exists and raise 404 if not.""" 103 | if name not in self.terminals: 104 | raise web.HTTPError(404, "Terminal not found: %s" % name) 105 | 106 | def _initialize_culler(self) -> None: 107 | """Start culler if 'cull_inactive_timeout' is greater than zero. 108 | Regardless of that value, set flag that we've been here. 109 | """ 110 | if not self._initialized_culler and self.cull_inactive_timeout > 0: # noqa: SIM102 111 | if self._culler_callback is None: 112 | _ = IOLoop.current() 113 | if self.cull_interval <= 0: # handle case where user set invalid value 114 | self.log.warning( 115 | "Invalid value for 'cull_interval' detected (%s) - using default value (%s).", 116 | self.cull_interval, 117 | self.cull_interval_default, 118 | ) 119 | self.cull_interval = self.cull_interval_default 120 | self._culler_callback = PeriodicCallback( 121 | self._cull_terminals, 1000 * self.cull_interval 122 | ) 123 | self.log.info( 124 | "Culling terminals with inactivity > %s seconds at %s second intervals ...", 125 | self.cull_inactive_timeout, 126 | self.cull_interval, 127 | ) 128 | self._culler_callback.start() 129 | 130 | self._initialized_culler = True 131 | 132 | async def _cull_terminals(self) -> None: 133 | self.log.debug( 134 | "Polling every %s seconds for terminals inactive for > %s seconds...", 135 | self.cull_interval, 136 | self.cull_inactive_timeout, 137 | ) 138 | # Create a separate list of terminals to avoid conflicting updates while iterating 139 | for name in list(self.terminals): 140 | try: 141 | await self._cull_inactive_terminal(name) 142 | except Exception as e: 143 | self.log.exception( 144 | "The following exception was encountered while checking the " 145 | "activity of terminal %s: %s", 146 | name, 147 | e, 148 | ) 149 | 150 | async def _cull_inactive_terminal(self, name: str) -> None: 151 | try: 152 | term = self.terminals[name] 153 | except KeyError: 154 | return # KeyErrors are somewhat expected since the terminal can be terminated as the culling check is made. 155 | 156 | self.log.debug("name=%s, last_activity=%s", name, term.last_activity) # type:ignore[attr-defined] 157 | if hasattr(term, "last_activity"): 158 | dt_now = utcnow() 159 | dt_inactive = dt_now - term.last_activity 160 | # Compute idle properties 161 | is_time = dt_inactive > timedelta(seconds=self.cull_inactive_timeout) 162 | # Cull the kernel if all three criteria are met 163 | if is_time: 164 | inactivity = int(dt_inactive.total_seconds()) 165 | self.log.warning( 166 | "Culling terminal '%s' due to %s seconds of inactivity.", name, inactivity 167 | ) 168 | await self.terminate(name, force=True) 169 | 170 | def pre_pty_read_hook(self, ptywclients: PtyWithClients) -> None: 171 | """The pre-pty read hook.""" 172 | ptywclients.last_activity = utcnow() # type:ignore[attr-defined] 173 | -------------------------------------------------------------------------------- /tests/test_terminal.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import os 4 | import shutil 5 | import sys 6 | from pathlib import Path 7 | 8 | import pytest 9 | from tornado.httpclient import HTTPClientError 10 | from traitlets.config.loader import Config 11 | 12 | 13 | @pytest.fixture() 14 | def terminal_path(tmp_path): 15 | subdir = tmp_path.joinpath("terminal_path") 16 | subdir.mkdir() 17 | 18 | yield subdir 19 | 20 | shutil.rmtree(str(subdir), ignore_errors=True) 21 | 22 | 23 | @pytest.fixture() 24 | def terminal_root_dir(jp_root_dir): 25 | subdir = jp_root_dir.joinpath("terminal_path") 26 | subdir.mkdir() 27 | 28 | yield subdir 29 | 30 | shutil.rmtree(str(subdir), ignore_errors=True) 31 | 32 | 33 | CULL_TIMEOUT = 10 34 | CULL_INTERVAL = 3 35 | 36 | 37 | @pytest.fixture() 38 | def jp_server_config(): 39 | return Config( 40 | { 41 | "ServerApp": { 42 | "TerminalManager": { 43 | "cull_inactive_timeout": CULL_TIMEOUT, 44 | "cull_interval": CULL_INTERVAL, 45 | }, 46 | "jpserver_extensions": {"jupyter_server_terminals": True}, 47 | } 48 | } 49 | ) 50 | 51 | 52 | async def test_no_terminals(jp_fetch): 53 | resp_list = await jp_fetch( 54 | "api", 55 | "terminals", 56 | method="GET", 57 | allow_nonstandard_methods=True, 58 | ) 59 | 60 | data = json.loads(resp_list.body.decode()) 61 | 62 | assert len(data) == 0 63 | 64 | 65 | async def test_terminal_create(jp_fetch): 66 | resp = await jp_fetch( 67 | "api", 68 | "terminals", 69 | method="POST", 70 | allow_nonstandard_methods=True, 71 | ) 72 | term = json.loads(resp.body.decode()) 73 | assert term["name"] == "1" 74 | 75 | resp_list = await jp_fetch( 76 | "api", 77 | "terminals", 78 | method="GET", 79 | allow_nonstandard_methods=True, 80 | ) 81 | 82 | data = json.loads(resp_list.body.decode()) 83 | 84 | assert len(data) == 1 85 | del data[0]["last_activity"] 86 | del term["last_activity"] 87 | assert data[0] == term 88 | 89 | 90 | async def test_terminal_create_with_kwargs(jp_fetch, terminal_path): 91 | resp_create = await jp_fetch( 92 | "api", 93 | "terminals", 94 | method="POST", 95 | body=json.dumps({"cwd": str(terminal_path)}), 96 | allow_nonstandard_methods=True, 97 | ) 98 | 99 | data = json.loads(resp_create.body.decode()) 100 | term_name = data["name"] 101 | 102 | resp_get = await jp_fetch( 103 | "api", 104 | "terminals", 105 | term_name, 106 | method="GET", 107 | allow_nonstandard_methods=True, 108 | ) 109 | 110 | data = json.loads(resp_get.body.decode()) 111 | 112 | assert data["name"] == term_name 113 | 114 | 115 | async def test_terminal_create_with_cwd(jp_fetch, jp_ws_fetch, terminal_path): 116 | resp = await jp_fetch( 117 | "api", 118 | "terminals", 119 | method="POST", 120 | body=json.dumps({"cwd": str(terminal_path)}), 121 | allow_nonstandard_methods=True, 122 | ) 123 | 124 | data = json.loads(resp.body.decode()) 125 | term_name = data["name"] 126 | 127 | while True: 128 | try: 129 | ws = await jp_ws_fetch("terminals", "websocket", term_name) 130 | break 131 | except HTTPClientError as e: 132 | if e.code != 404: 133 | raise 134 | await asyncio.sleep(1) 135 | 136 | ws.write_message(json.dumps(["stdin", "pwd\r\n"])) 137 | 138 | message_stdout = "" 139 | while True: 140 | try: 141 | message = await asyncio.wait_for(ws.read_message(), timeout=5.0) 142 | except asyncio.TimeoutError: 143 | break 144 | 145 | message = json.loads(message) 146 | 147 | if message[0] == "stdout": 148 | message_stdout += message[1] 149 | 150 | ws.close() 151 | 152 | assert Path(terminal_path).name in message_stdout 153 | 154 | 155 | async def test_terminal_create_with_relative_cwd( 156 | jp_fetch, jp_ws_fetch, jp_root_dir, terminal_root_dir 157 | ): 158 | resp = await jp_fetch( 159 | "api", 160 | "terminals", 161 | method="POST", 162 | body=json.dumps({"cwd": str(terminal_root_dir.relative_to(jp_root_dir))}), 163 | allow_nonstandard_methods=True, 164 | ) 165 | 166 | data = json.loads(resp.body.decode()) 167 | term_name = data["name"] 168 | 169 | while True: 170 | try: 171 | ws = await jp_ws_fetch("terminals", "websocket", term_name) 172 | break 173 | except HTTPClientError as e: 174 | if e.code != 404: 175 | raise 176 | await asyncio.sleep(1) 177 | 178 | ws.write_message(json.dumps(["stdin", "pwd\r\n"])) 179 | 180 | message_stdout = "" 181 | while True: 182 | try: 183 | message = await asyncio.wait_for(ws.read_message(), timeout=5.0) 184 | except asyncio.TimeoutError: 185 | break 186 | 187 | message = json.loads(message) 188 | 189 | if message[0] == "stdout": 190 | message_stdout += message[1] 191 | 192 | ws.close() 193 | 194 | expected = terminal_root_dir.name if sys.platform == "win32" else str(terminal_root_dir) 195 | assert expected in message_stdout 196 | 197 | 198 | async def test_terminal_create_with_bad_cwd(jp_fetch, jp_ws_fetch): 199 | non_existing_path = "/tmp/path/to/nowhere" # noqa: S108 200 | resp = await jp_fetch( 201 | "api", 202 | "terminals", 203 | method="POST", 204 | body=json.dumps({"cwd": non_existing_path}), 205 | allow_nonstandard_methods=True, 206 | ) 207 | 208 | data = json.loads(resp.body.decode()) 209 | term_name = data["name"] 210 | 211 | while True: 212 | try: 213 | ws = await jp_ws_fetch("terminals", "websocket", term_name) 214 | break 215 | except HTTPClientError as e: 216 | if e.code != 404: 217 | raise 218 | await asyncio.sleep(1) 219 | 220 | ws.write_message(json.dumps(["stdin", "pwd\r\n"])) 221 | 222 | message_stdout = "" 223 | while True: 224 | try: 225 | message = await asyncio.wait_for(ws.read_message(), timeout=5.0) 226 | except asyncio.TimeoutError: 227 | break 228 | 229 | message = json.loads(message) 230 | 231 | if message[0] == "stdout": 232 | message_stdout += message[1] 233 | 234 | ws.close() 235 | 236 | assert non_existing_path not in message_stdout 237 | 238 | 239 | async def test_app_config(jp_configurable_serverapp): 240 | assert jp_configurable_serverapp().terminals_enabled is True 241 | assert jp_configurable_serverapp().web_app.settings["terminals_available"] is True 242 | assert jp_configurable_serverapp().web_app.settings["terminal_manager"] 243 | 244 | 245 | async def test_culling_config(jp_configurable_serverapp): 246 | terminal_mgr_config = jp_configurable_serverapp().config.ServerApp.TerminalManager 247 | assert terminal_mgr_config.cull_inactive_timeout == CULL_TIMEOUT 248 | assert terminal_mgr_config.cull_interval == CULL_INTERVAL 249 | terminal_mgr_settings = jp_configurable_serverapp().web_app.settings["terminal_manager"] 250 | assert terminal_mgr_settings.cull_inactive_timeout == CULL_TIMEOUT 251 | assert terminal_mgr_settings.cull_interval == CULL_INTERVAL 252 | 253 | 254 | @pytest.mark.skipif(os.name == "nt", reason="Not currently working on Windows") 255 | async def test_culling(jp_fetch): 256 | # POST request 257 | resp = await jp_fetch( 258 | "api", 259 | "terminals", 260 | method="POST", 261 | allow_nonstandard_methods=True, 262 | ) 263 | term = json.loads(resp.body.decode()) 264 | term_1 = term["name"] 265 | last_activity = term["last_activity"] 266 | 267 | culled = False 268 | for _ in range(CULL_TIMEOUT + CULL_INTERVAL * 2): 269 | try: 270 | resp = await jp_fetch( 271 | "api", 272 | "terminals", 273 | term_1, 274 | method="GET", 275 | allow_nonstandard_methods=True, 276 | ) 277 | except HTTPClientError as e: 278 | assert e.code == 404 # noqa: PT017 279 | culled = True 280 | break 281 | else: 282 | await asyncio.sleep(1) 283 | 284 | assert culled 285 | -------------------------------------------------------------------------------- /docs/source/_static/jupyter_server_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 14 | 16 | 17 | 19 | image/svg+xml 20 | 22 | logo.svg 23 | 24 | 25 | 26 | logo.svg 28 | Created using Figma 0.90 30 | 39 | 48 | 57 | 66 | 75 | 84 | 93 | 102 | 111 | 120 | 129 | 138 | 140 | 143 | 146 | 149 | 152 | 155 | 158 | 161 | 164 | 167 | 170 | 173 | 176 | 177 | server 191 | 192 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Jupyter Server documentation build configuration file, created by 2 | # sphinx-quickstart on Mon Apr 13 09:51:11 2015. 3 | # 4 | # This file is execfile()d with the current directory set to its 5 | # containing dir. 6 | # 7 | # Note that not all possible configuration values are present in this 8 | # autogenerated file. 9 | # 10 | # All configuration values have a default; values that are commented out 11 | # serve to show the default. 12 | import os 13 | import os.path as osp 14 | import shutil 15 | import sys 16 | 17 | from packaging.version import parse as parse_version 18 | 19 | HERE = osp.abspath(osp.dirname(__file__)) 20 | 21 | 22 | # If extensions (or modules to document with autodoc) are in another directory, 23 | # add these directories to sys.path here. If the directory is relative to the 24 | # documentation root, use os.path.abspath to make it absolute, like shown here. 25 | 26 | # DEBUG for RTD 27 | print("DEBUG:: sys.path") 28 | print("================") 29 | for item in sys.path: 30 | print(item) 31 | 32 | # add repo root to sys.path 33 | # here = root/docs/source 34 | here = os.path.abspath(os.path.dirname(__file__)) 35 | repo_root = os.path.dirname(os.path.dirname(here)) 36 | sys.path.insert(0, repo_root) 37 | 38 | print("repo_root") 39 | print("=====================") 40 | print(repo_root) 41 | 42 | # DEBUG for post insert on RTD 43 | print("DEBUG:: Post insert to sys.path") 44 | print("===============================") 45 | for item in sys.path: 46 | print(item) 47 | 48 | # -- General configuration ------------------------------------------------ 49 | 50 | # If your documentation needs a minimal Sphinx version, state it here. 51 | # needs_sphinx = '1.0' 52 | 53 | # Add any Sphinx extension module names here, as strings. They can be 54 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 55 | # ones. 56 | extensions = [ 57 | "myst_parser", 58 | "sphinx.ext.autodoc", 59 | "sphinx.ext.doctest", 60 | "sphinx.ext.intersphinx", 61 | "sphinx.ext.autosummary", 62 | "sphinx.ext.mathjax", 63 | "sphinxcontrib_github_alt", 64 | "sphinxcontrib.openapi", 65 | "sphinxemoji.sphinxemoji", 66 | ] 67 | 68 | try: 69 | import enchant # type:ignore[import] # noqa: F401 70 | 71 | extensions += ["sphinxcontrib.spelling"] 72 | except ImportError: 73 | pass 74 | 75 | myst_enable_extensions = ["html_image"] 76 | 77 | # Add any paths that contain templates here, relative to this directory. 78 | templates_path = ["_templates"] 79 | 80 | # The suffix(es) of source filenames. 81 | # You can specify multiple suffix as a list of string: 82 | # source_suffix = ['.rst', '.md'] 83 | source_suffix = [".rst", ".ipynb"] 84 | 85 | # The encoding of source files. 86 | # source_encoding = 'utf-8-sig' 87 | 88 | # The master toctree document. 89 | master_doc = "index" 90 | 91 | # General information about the project. 92 | project = "Jupyter Server Terminals" 93 | copyright = "2021, Jupyter Team, https://jupyter.org" 94 | author = "The Jupyter Server Team" 95 | 96 | # ghissue config 97 | github_project_url = "https://github.com/jupyter/jupyter_server_terminals" 98 | 99 | # The version info for the project you're documenting, acts as replacement for 100 | # |version| and |release|, also used in various other places throughout the 101 | # built documents. 102 | # 103 | __version__ = "0.3.1" 104 | # The short X.Y version. 105 | version_parsed = parse_version(__version__) 106 | version = f"{version_parsed.major}.{version_parsed.minor}" # type:ignore[union-attr] 107 | 108 | # The full version, including alpha/beta/rc tags. 109 | release = __version__ 110 | 111 | # The language for content autogenerated by Sphinx. Refer to documentation 112 | # for a list of supported languages. 113 | # 114 | # This is also used if you do content translation via gettext catalogs. 115 | # Usually you set "language" from the command line for these cases. 116 | language = "en" 117 | 118 | # There are two options for replacing |today|: either, you set today to some 119 | # non-false value, then it is used: 120 | # today = '' 121 | # Else, today_fmt is used as the format for a strftime call. 122 | # today_fmt = '%B %d, %Y' 123 | 124 | # List of patterns, relative to source directory, that match files and 125 | # directories to ignore when looking for source files. 126 | # exclude_patterns = [] 127 | 128 | # The reST default role (used for this markup: `text`) to use for all 129 | # documents. 130 | # default_role = None 131 | 132 | # If true, '()' will be appended to :func: etc. cross-reference text. 133 | # add_function_parentheses = True 134 | 135 | # If true, the current module name will be prepended to all description 136 | # unit titles (such as .. function::). 137 | # add_module_names = True 138 | 139 | # If true, sectionauthor and moduleauthor directives will be shown in the 140 | # output. They are ignored by default. 141 | # show_authors = False 142 | 143 | # The name of the Pygments (syntax highlighting) style to use. 144 | pygments_style = "default" 145 | # highlight_language = 'python3' 146 | 147 | # A list of ignored prefixes for module index sorting. 148 | # modindex_common_prefix = [] 149 | 150 | # If true, keep warnings as "system message" paragraphs in the built documents. 151 | # keep_warnings = False 152 | 153 | # If true, `todo` and `todoList` produce output, else they produce nothing. 154 | todo_include_todos = False 155 | 156 | # # Add custom note for each doc page 157 | 158 | # rst_prolog = "" 159 | 160 | # rst_prolog += """ 161 | # .. important:: 162 | # This documentation covers Jupyter Server, an **early developer preview**, 163 | # and is not suitable for general usage yet. Features and implementation are 164 | # subject to change. 165 | 166 | # For production use cases, please use the stable notebook server in the 167 | # `Jupyter Notebook repo `_ 168 | # and `Jupyter Notebook documentation `_. 169 | # """ 170 | 171 | # -- Options for HTML output ---------------------------------------------- 172 | 173 | # The theme to use for HTML and HTML Help pages. See the documentation for 174 | # a list of builtin themes. 175 | # html_theme = 'sphinx_rtd_theme' 176 | html_theme = "pydata_sphinx_theme" 177 | html_logo = "_static/jupyter_server_logo.svg" 178 | 179 | # Theme options are theme-specific and customize the look and feel of a theme 180 | # further. For a list of options available for each theme, see the 181 | # documentation. 182 | html_theme_options = {"navigation_with_keys": False} 183 | 184 | # Add any paths that contain custom themes here, relative to this directory. 185 | # html_theme_path = [] 186 | 187 | # The name for this set of Sphinx documents. If None, it defaults to 188 | # " v documentation". 189 | # html_title = None 190 | 191 | # A shorter title for the navigation bar. Default is the same as html_title. 192 | # html_short_title = None 193 | 194 | # The name of an image file (relative to this directory) to place at the top 195 | # of the sidebar. 196 | # html_logo = None 197 | 198 | # The name of an image file (within the static path) to use as favicon of the 199 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 200 | # pixels large. 201 | # html_favicon = None 202 | 203 | # Add any paths that contain custom static files (such as style sheets) here, 204 | # relative to this directory. They are copied after the builtin static files, 205 | # so a file named "default.css" will overwrite the builtin "default.css". 206 | # NOTE: Sphinx's 'make html' builder will throw a warning about an unfound 207 | # _static directory. Do not remove or comment out html_static_path 208 | # since it is needed to properly generate _static in the build directory 209 | html_static_path = ["_static"] 210 | 211 | # Add any extra paths that contain custom files (such as robots.txt or 212 | # .htaccess) here, relative to this directory. These files are copied 213 | # directly to the root of the documentation. 214 | # html_extra_path = [] 215 | 216 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 217 | # using the given strftime format. 218 | # html_last_updated_fmt = '%b %d, %Y' 219 | 220 | # If true, SmartyPants will be used to convert quotes and dashes to 221 | # typographically correct entities. 222 | # html_use_smartypants = True 223 | 224 | # Custom sidebar templates, maps document names to template names. 225 | # html_sidebars = {} 226 | 227 | # Additional templates that should be rendered to pages, maps page names to 228 | # template names. 229 | # html_additional_pages = {} 230 | 231 | # If false, no module index is generated. 232 | # html_domain_indices = True 233 | 234 | # If false, no index is generated. 235 | # html_use_index = True 236 | 237 | # If true, the index is split into individual pages for each letter. 238 | # html_split_index = False 239 | 240 | # If true, links to the reST sources are added to the pages. 241 | # html_show_sourcelink = True 242 | 243 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 244 | # html_show_sphinx = True 245 | 246 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 247 | # html_show_copyright = True 248 | 249 | # If true, an OpenSearch description file will be output, and all pages will 250 | # contain a tag referring to it. The value of this option must be the 251 | # base URL from which the finished HTML is served. 252 | # html_use_opensearch = '' 253 | 254 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 255 | # html_file_suffix = None 256 | 257 | # Language to be used for generating the HTML full-text search index. 258 | # Sphinx supports the following languages: 259 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' 260 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr' 261 | # html_search_language = 'en' 262 | 263 | # A dictionary with options for the search language support, empty by default. 264 | # Now only 'ja' uses this config value 265 | # html_search_options = {'type': 'default'} 266 | 267 | # The name of a javascript file (relative to the configuration directory) that 268 | # implements a search results scorer. If empty, the default will be used. 269 | # html_search_scorer = 'scorer.js' 270 | 271 | # Output file base name for HTML help builder. 272 | htmlhelp_basename = "JupyterServerTerminalsdoc" 273 | 274 | # -- Options for LaTeX output --------------------------------------------- 275 | 276 | # latex_elements = {} 277 | 278 | # Grouping the document tree into LaTeX files. List of tuples 279 | # (source start file, target name, title, 280 | # author, documentclass [howto, manual, or own class]). 281 | latex_documents = [ 282 | ( 283 | master_doc, 284 | "JupyterServerTerminals.tex", 285 | "Jupyter Server Terminals Documentation", 286 | "https://jupyter.org", 287 | "manual", 288 | ), 289 | ] 290 | 291 | # The name of an image file (relative to this directory) to place at the top of 292 | # the title page. 293 | # latex_logo = None 294 | 295 | # For "manual" documents, if this is true, then toplevel headings are parts, 296 | # not chapters. 297 | # latex_use_parts = False 298 | 299 | # If true, show page references after internal links. 300 | # latex_show_pagerefs = False 301 | 302 | # If true, show URL addresses after external links. 303 | # latex_show_urls = False 304 | 305 | # Documents to append as an appendix to all manuals. 306 | # latex_appendices = [] 307 | 308 | # If false, no module index is generated. 309 | # latex_domain_indices = True 310 | 311 | 312 | # -- Options for manual page output --------------------------------------- 313 | 314 | # One entry per manual page. List of tuples 315 | # (source start file, name, description, authors, manual section). 316 | man_pages = [ 317 | (master_doc, "jupyterserverterminals", "Jupyter Server Terminals Documentation", [author], 1) 318 | ] 319 | 320 | # If true, show URL addresses after external links. 321 | # man_show_urls = False 322 | 323 | 324 | # -- Options for link checks ---------------------------------------------- 325 | 326 | linkcheck_ignore = [r"http://127\.0\.0\.1/*"] 327 | 328 | 329 | # -- Options for Texinfo output ------------------------------------------- 330 | 331 | # Grouping the document tree into Texinfo files. List of tuples 332 | # (source start file, target name, title, author, 333 | # dir menu entry, description, category) 334 | texinfo_documents = [ 335 | ( 336 | master_doc, 337 | "JupyterServerTerminals", 338 | "Jupyter Server Terminals Documentation", 339 | author, 340 | "JupyterServerTerminals", 341 | "One line description of project.", 342 | "Miscellaneous", 343 | ), 344 | ] 345 | 346 | # Documents to append as an appendix to all manuals. 347 | # texinfo_appendices = [] 348 | 349 | # If false, no module index is generated. 350 | # texinfo_domain_indices = True 351 | 352 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 353 | # texinfo_show_urls = 'footnote' 354 | 355 | # If true, do not generate a @detailmenu in the "Top" node's menu. 356 | # texinfo_no_detailmenu = False 357 | 358 | intersphinx_mapping = { 359 | "jupyter": ("https://jupyter.readthedocs.io/en/latest/", None), 360 | "jupyter_server": ("https://jupyter-server.readthedocs.io/en/latest/", None), 361 | } 362 | 363 | spelling_lang = "en_US" 364 | spelling_word_list_filename = "spelling_wordlist.txt" 365 | 366 | 367 | def setup(app): 368 | dest = osp.join(HERE, "changelog.md") 369 | shutil.copy(osp.join(HERE, "..", "..", "CHANGELOG.md"), dest) 370 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | 6 | 7 | ## 0.5.3 8 | 9 | ([Full Changelog](https://github.com/jupyter-server/jupyter_server_terminals/compare/v0.5.2...4d5e3041fe8b24511d0b78b99a7678e353b78612)) 10 | 11 | ### Maintenance and upkeep improvements 12 | 13 | - Update Release Scripts [#108](https://github.com/jupyter-server/jupyter_server_terminals/pull/108) ([@blink1073](https://github.com/blink1073)) 14 | - chore: update pre-commit hooks [#107](https://github.com/jupyter-server/jupyter_server_terminals/pull/107) ([@pre-commit-ci](https://github.com/pre-commit-ci)) 15 | 16 | ### Contributors to this release 17 | 18 | ([GitHub contributors page for this release](https://github.com/jupyter-server/jupyter_server_terminals/graphs/contributors?from=2024-01-22&to=2024-03-12&type=c)) 19 | 20 | [@blink1073](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3Ablink1073+updated%3A2024-01-22..2024-03-12&type=Issues) | [@pre-commit-ci](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3Apre-commit-ci+updated%3A2024-01-22..2024-03-12&type=Issues) 21 | 22 | 23 | 24 | ## 0.5.2 25 | 26 | ([Full Changelog](https://github.com/jupyter-server/jupyter_server_terminals/compare/v0.5.1...1d47163a9c02f75ff24943faa6a69bf8639b3517)) 27 | 28 | ### Bugs fixed 29 | 30 | - Fix usage of await [#106](https://github.com/jupyter-server/jupyter_server_terminals/pull/106) ([@blink1073](https://github.com/blink1073)) 31 | - Set terminals_available to False when not enabled [#105](https://github.com/jupyter-server/jupyter_server_terminals/pull/105) ([@Wh1isper](https://github.com/Wh1isper)) 32 | 33 | ### Maintenance and upkeep improvements 34 | 35 | - chore: update pre-commit hooks [#104](https://github.com/jupyter-server/jupyter_server_terminals/pull/104) ([@pre-commit-ci](https://github.com/pre-commit-ci)) 36 | 37 | ### Contributors to this release 38 | 39 | ([GitHub contributors page for this release](https://github.com/jupyter-server/jupyter_server_terminals/graphs/contributors?from=2023-12-26&to=2024-01-22&type=c)) 40 | 41 | [@blink1073](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3Ablink1073+updated%3A2023-12-26..2024-01-22&type=Issues) | [@pre-commit-ci](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3Apre-commit-ci+updated%3A2023-12-26..2024-01-22&type=Issues) | [@Wh1isper](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3AWh1isper+updated%3A2023-12-26..2024-01-22&type=Issues) 42 | 43 | ## 0.5.1 44 | 45 | ([Full Changelog](https://github.com/jupyter-server/jupyter_server_terminals/compare/v0.5.0...58ad66a5ce0bca03f6c569b32a8bef9c1bdccd2b)) 46 | 47 | ### Bugs fixed 48 | 49 | - fix: support OPTIONS method for CORS [#102](https://github.com/jupyter-server/jupyter_server_terminals/pull/102) ([@zhanba](https://github.com/zhanba)) 50 | 51 | ### Contributors to this release 52 | 53 | ([GitHub contributors page for this release](https://github.com/jupyter-server/jupyter_server_terminals/graphs/contributors?from=2023-12-11&to=2023-12-26&type=c)) 54 | 55 | [@welcome](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3Awelcome+updated%3A2023-12-11..2023-12-26&type=Issues) | [@zhanba](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3Azhanba+updated%3A2023-12-11..2023-12-26&type=Issues) 56 | 57 | ## 0.5.0 58 | 59 | ([Full Changelog](https://github.com/jupyter-server/jupyter_server_terminals/compare/v0.4.4...00eb4ee20b8d5838e44de7a756823e4cf02949fa)) 60 | 61 | ### Bugs fixed 62 | 63 | - Fix respecting serverapp.terminals_enabled [#91](https://github.com/jupyter-server/jupyter_server_terminals/pull/91) ([@danielzgtg](https://github.com/danielzgtg)) 64 | 65 | ### Maintenance and upkeep improvements 66 | 67 | - Update ruff config [#101](https://github.com/jupyter-server/jupyter_server_terminals/pull/101) ([@blink1073](https://github.com/blink1073)) 68 | - Update typings for Server 2.10.1 and mypy 1.7 #425 [#99](https://github.com/jupyter-server/jupyter_server_terminals/pull/99) ([@blink1073](https://github.com/blink1073)) 69 | - Update types for terminado 0.18 [#98](https://github.com/jupyter-server/jupyter_server_terminals/pull/98) ([@blink1073](https://github.com/blink1073)) 70 | - Update ruff config [#97](https://github.com/jupyter-server/jupyter_server_terminals/pull/97) ([@blink1073](https://github.com/blink1073)) 71 | - Update typings for server 2.10 [#96](https://github.com/jupyter-server/jupyter_server_terminals/pull/96) ([@blink1073](https://github.com/blink1073)) 72 | - chore: update pre-commit hooks [#95](https://github.com/jupyter-server/jupyter_server_terminals/pull/95) ([@pre-commit-ci](https://github.com/pre-commit-ci)) 73 | - Clean up lint handling [#94](https://github.com/jupyter-server/jupyter_server_terminals/pull/94) ([@blink1073](https://github.com/blink1073)) 74 | - Adopt ruff format [#93](https://github.com/jupyter-server/jupyter_server_terminals/pull/93) ([@blink1073](https://github.com/blink1073)) 75 | - Update ruff and typing [#92](https://github.com/jupyter-server/jupyter_server_terminals/pull/92) ([@blink1073](https://github.com/blink1073)) 76 | - chore: update pre-commit hooks [#90](https://github.com/jupyter-server/jupyter_server_terminals/pull/90) ([@pre-commit-ci](https://github.com/pre-commit-ci)) 77 | - Fix typings for traitlets 5.10.1 [#89](https://github.com/jupyter-server/jupyter_server_terminals/pull/89) ([@blink1073](https://github.com/blink1073)) 78 | - Bump actions/checkout from 3 to 4 [#88](https://github.com/jupyter-server/jupyter_server_terminals/pull/88) ([@dependabot](https://github.com/dependabot)) 79 | - Adopt sp-repo-review [#87](https://github.com/jupyter-server/jupyter_server_terminals/pull/87) ([@blink1073](https://github.com/blink1073)) 80 | - Update mistune requirement from \<3.0 to \<4.0 [#83](https://github.com/jupyter-server/jupyter_server_terminals/pull/83) ([@dependabot](https://github.com/dependabot)) 81 | - Use local coverage [#80](https://github.com/jupyter-server/jupyter_server_terminals/pull/80) ([@blink1073](https://github.com/blink1073)) 82 | - Clean up license [#77](https://github.com/jupyter-server/jupyter_server_terminals/pull/77) ([@dcsaba89](https://github.com/dcsaba89)) 83 | - Add more linting [#75](https://github.com/jupyter-server/jupyter_server_terminals/pull/75) ([@blink1073](https://github.com/blink1073)) 84 | 85 | ### Contributors to this release 86 | 87 | ([GitHub contributors page for this release](https://github.com/jupyter-server/jupyter_server_terminals/graphs/contributors?from=2023-01-09&to=2023-12-11&type=c)) 88 | 89 | [@blink1073](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3Ablink1073+updated%3A2023-01-09..2023-12-11&type=Issues) | [@codecov](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3Acodecov+updated%3A2023-01-09..2023-12-11&type=Issues) | [@danielzgtg](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3Adanielzgtg+updated%3A2023-01-09..2023-12-11&type=Issues) | [@dcsaba89](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3Adcsaba89+updated%3A2023-01-09..2023-12-11&type=Issues) | [@dependabot](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3Adependabot+updated%3A2023-01-09..2023-12-11&type=Issues) | [@pre-commit-ci](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3Apre-commit-ci+updated%3A2023-01-09..2023-12-11&type=Issues) | [@welcome](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3Awelcome+updated%3A2023-01-09..2023-12-11&type=Issues) 90 | 91 | ## 0.4.4 92 | 93 | ([Full Changelog](https://github.com/jupyter-server/jupyter_server_terminals/compare/v0.4.3...6791413888e45d2aeab5d9d154c98ca9dbd828d8)) 94 | 95 | ### Maintenance and upkeep improvements 96 | 97 | - Add typing file [#74](https://github.com/jupyter-server/jupyter_server_terminals/pull/74) ([@blink1073](https://github.com/blink1073)) 98 | - Add spelling and docstring enforcement [#72](https://github.com/jupyter-server/jupyter_server_terminals/pull/72) ([@blink1073](https://github.com/blink1073)) 99 | 100 | ### Contributors to this release 101 | 102 | ([GitHub contributors page for this release](https://github.com/jupyter-server/jupyter_server_terminals/graphs/contributors?from=2022-12-19&to=2023-01-09&type=c)) 103 | 104 | [@blink1073](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3Ablink1073+updated%3A2022-12-19..2023-01-09&type=Issues) | [@codecov](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3Acodecov+updated%3A2022-12-19..2023-01-09&type=Issues) | [@pre-commit-ci](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3Apre-commit-ci+updated%3A2022-12-19..2023-01-09&type=Issues) 105 | 106 | ## 0.4.3 107 | 108 | ([Full Changelog](https://github.com/jupyter-server/jupyter_server_terminals/compare/v0.4.2...b1f2a99b062192e809d770c517ce02988d32d121)) 109 | 110 | ### Bugs fixed 111 | 112 | - Fix Server Version Handling and Clean up CI [#71](https://github.com/jupyter-server/jupyter_server_terminals/pull/71) ([@blink1073](https://github.com/blink1073)) 113 | 114 | ### Maintenance and upkeep improvements 115 | 116 | - Update mistune requirement from \<2.0 to \<3.0 [#70](https://github.com/jupyter-server/jupyter_server_terminals/pull/70) ([@dependabot](https://github.com/dependabot)) 117 | - Adopt ruff and address lint [#69](https://github.com/jupyter-server/jupyter_server_terminals/pull/69) ([@blink1073](https://github.com/blink1073)) 118 | 119 | ### Contributors to this release 120 | 121 | ([GitHub contributors page for this release](https://github.com/jupyter-server/jupyter_server_terminals/graphs/contributors?from=2022-12-01&to=2022-12-19&type=c)) 122 | 123 | [@blink1073](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3Ablink1073+updated%3A2022-12-01..2022-12-19&type=Issues) | [@codecov](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3Acodecov+updated%3A2022-12-01..2022-12-19&type=Issues) | [@dependabot](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3Adependabot+updated%3A2022-12-01..2022-12-19&type=Issues) | [@pre-commit-ci](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3Apre-commit-ci+updated%3A2022-12-01..2022-12-19&type=Issues) 124 | 125 | ## 0.4.2 126 | 127 | ([Full Changelog](https://github.com/jupyter-server/jupyter_server_terminals/compare/v0.4.1...744451298913d2c2d81698f94d16dfb595df897f)) 128 | 129 | ### Maintenance and upkeep improvements 130 | 131 | - Use pytest-jupyter [#67](https://github.com/jupyter-server/jupyter_server_terminals/pull/67) ([@blink1073](https://github.com/blink1073)) 132 | - Fixup workflows and add badges [#64](https://github.com/jupyter-server/jupyter_server_terminals/pull/64) ([@blink1073](https://github.com/blink1073)) 133 | 134 | ### Documentation improvements 135 | 136 | - Fixup workflows and add badges [#64](https://github.com/jupyter-server/jupyter_server_terminals/pull/64) ([@blink1073](https://github.com/blink1073)) 137 | 138 | ### Contributors to this release 139 | 140 | ([GitHub contributors page for this release](https://github.com/jupyter-server/jupyter_server_terminals/graphs/contributors?from=2022-11-21&to=2022-12-01&type=c)) 141 | 142 | [@blink1073](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3Ablink1073+updated%3A2022-11-21..2022-12-01&type=Issues) | [@codecov](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3Acodecov+updated%3A2022-11-21..2022-12-01&type=Issues) | [@pre-commit-ci](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3Apre-commit-ci+updated%3A2022-11-21..2022-12-01&type=Issues) 143 | 144 | ## 0.4.1 145 | 146 | ([Full Changelog](https://github.com/jupyter-server/jupyter_server_terminals/compare/v0.4.0...37da434a24475daf674e1711edc53af52dd6957d)) 147 | 148 | ### Maintenance and upkeep improvements 149 | 150 | - Switch away from deprecated zmqhandlers module in Jupyter Server 2.0 [#62](https://github.com/jupyter-server/jupyter_server_terminals/pull/62) ([@Zsailer](https://github.com/Zsailer)) 151 | - CI Cleanup [#61](https://github.com/jupyter-server/jupyter_server_terminals/pull/61) ([@blink1073](https://github.com/blink1073)) 152 | 153 | ### Contributors to this release 154 | 155 | ([GitHub contributors page for this release](https://github.com/jupyter-server/jupyter_server_terminals/graphs/contributors?from=2022-11-11&to=2022-11-21&type=c)) 156 | 157 | [@blink1073](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3Ablink1073+updated%3A2022-11-11..2022-11-21&type=Issues) | [@codecov](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3Acodecov+updated%3A2022-11-11..2022-11-21&type=Issues) | [@pre-commit-ci](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3Apre-commit-ci+updated%3A2022-11-11..2022-11-21&type=Issues) | [@Zsailer](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3AZsailer+updated%3A2022-11-11..2022-11-21&type=Issues) 158 | 159 | ## 0.4.0 160 | 161 | ([Full Changelog](https://github.com/jupyter-server/jupyter_server_terminals/compare/v0.3.2...6aa63b9c7cfcbe44da7d65964f2b84e0a3a7c83c)) 162 | 163 | ### Maintenance and upkeep improvements 164 | 165 | - Add ability to release from repo [#59](https://github.com/jupyter-server/jupyter_server_terminals/pull/59) ([@blink1073](https://github.com/blink1073)) 166 | - Handle jupyter core warning [#58](https://github.com/jupyter-server/jupyter_server_terminals/pull/58) ([@blink1073](https://github.com/blink1073)) 167 | - Bump actions/checkout from 2 to 3 [#57](https://github.com/jupyter-server/jupyter_server_terminals/pull/57) ([@dependabot](https://github.com/dependabot)) 168 | - Add dependabot [#56](https://github.com/jupyter-server/jupyter_server_terminals/pull/56) ([@blink1073](https://github.com/blink1073)) 169 | 170 | ### Contributors to this release 171 | 172 | ([GitHub contributors page for this release](https://github.com/jupyter-server/jupyter_server_terminals/graphs/contributors?from=2022-10-31&to=2022-11-11&type=c)) 173 | 174 | [@blink1073](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3Ablink1073+updated%3A2022-10-31..2022-11-11&type=Issues) | [@dependabot](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3Adependabot+updated%3A2022-10-31..2022-11-11&type=Issues) | [@pre-commit-ci](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3Apre-commit-ci+updated%3A2022-10-31..2022-11-11&type=Issues) 175 | 176 | ## 0.3.2 177 | 178 | ([Full Changelog](https://github.com/jupyter-server/jupyter_server_terminals/compare/v0.3.1...574e1018b7d924f09c1ca45389182f1fd314caee)) 179 | 180 | ### Maintenance and upkeep improvements 181 | 182 | - Use ensure_async function [#54](https://github.com/jupyter-server/jupyter_server_terminals/pull/54) ([@blink1073](https://github.com/blink1073)) 183 | - Maintenance cleanup [#51](https://github.com/jupyter-server/jupyter_server_terminals/pull/51) ([@blink1073](https://github.com/blink1073)) 184 | - Maintenance cleanup [#50](https://github.com/jupyter-server/jupyter_server_terminals/pull/50) ([@blink1073](https://github.com/blink1073)) 185 | - Ignore warnings in prerelease test [#47](https://github.com/jupyter-server/jupyter_server_terminals/pull/47) ([@blink1073](https://github.com/blink1073)) 186 | - Clean up pyproject and ci [#45](https://github.com/jupyter-server/jupyter_server_terminals/pull/45) ([@blink1073](https://github.com/blink1073)) 187 | 188 | ### Contributors to this release 189 | 190 | ([GitHub contributors page for this release](https://github.com/jupyter-server/jupyter_server_terminals/graphs/contributors?from=2022-09-08&to=2022-10-31&type=c)) 191 | 192 | [@blink1073](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3Ablink1073+updated%3A2022-09-08..2022-10-31&type=Issues) | [@codecov-commenter](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3Acodecov-commenter+updated%3A2022-09-08..2022-10-31&type=Issues) | [@pre-commit-ci](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3Apre-commit-ci+updated%3A2022-09-08..2022-10-31&type=Issues) 193 | 194 | ## 0.3.1 195 | 196 | ([Full Changelog](https://github.com/jupyter-server/jupyter_server_terminals/compare/v0.3.0...6d8b60bc758adc8656ff530a600fb7f57a34259e)) 197 | 198 | ### Enhancements made 199 | 200 | - Allow to pass string as a shell override [#42](https://github.com/jupyter-server/jupyter_server_terminals/pull/42) ([@krassowski](https://github.com/krassowski)) 201 | 202 | ### Maintenance and upkeep improvements 203 | 204 | - \[pre-commit.ci\] pre-commit autoupdate [#41](https://github.com/jupyter-server/jupyter_server_terminals/pull/41) ([@pre-commit-ci](https://github.com/pre-commit-ci)) 205 | - \[pre-commit.ci\] pre-commit autoupdate [#40](https://github.com/jupyter-server/jupyter_server_terminals/pull/40) ([@pre-commit-ci](https://github.com/pre-commit-ci)) 206 | - \[pre-commit.ci\] pre-commit autoupdate [#39](https://github.com/jupyter-server/jupyter_server_terminals/pull/39) ([@pre-commit-ci](https://github.com/pre-commit-ci)) 207 | - Fix flake8 v5 compat [#38](https://github.com/jupyter-server/jupyter_server_terminals/pull/38) ([@blink1073](https://github.com/blink1073)) 208 | - \[pre-commit.ci\] pre-commit autoupdate [#37](https://github.com/jupyter-server/jupyter_server_terminals/pull/37) ([@pre-commit-ci](https://github.com/pre-commit-ci)) 209 | - \[pre-commit.ci\] pre-commit autoupdate [#36](https://github.com/jupyter-server/jupyter_server_terminals/pull/36) ([@pre-commit-ci](https://github.com/pre-commit-ci)) 210 | - \[pre-commit.ci\] pre-commit autoupdate [#35](https://github.com/jupyter-server/jupyter_server_terminals/pull/35) ([@pre-commit-ci](https://github.com/pre-commit-ci)) 211 | - \[pre-commit.ci\] pre-commit autoupdate [#34](https://github.com/jupyter-server/jupyter_server_terminals/pull/34) ([@pre-commit-ci](https://github.com/pre-commit-ci)) 212 | - Suppress tornado 6.2 beta warnings [#33](https://github.com/jupyter-server/jupyter_server_terminals/pull/33) ([@blink1073](https://github.com/blink1073)) 213 | - \[pre-commit.ci\] pre-commit autoupdate [#32](https://github.com/jupyter-server/jupyter_server_terminals/pull/32) ([@pre-commit-ci](https://github.com/pre-commit-ci)) 214 | - \[pre-commit.ci\] pre-commit autoupdate [#31](https://github.com/jupyter-server/jupyter_server_terminals/pull/31) ([@pre-commit-ci](https://github.com/pre-commit-ci)) 215 | - \[pre-commit.ci\] pre-commit autoupdate [#30](https://github.com/jupyter-server/jupyter_server_terminals/pull/30) ([@pre-commit-ci](https://github.com/pre-commit-ci)) 216 | - Use hatch backend [#29](https://github.com/jupyter-server/jupyter_server_terminals/pull/29) ([@blink1073](https://github.com/blink1073)) 217 | - \[pre-commit.ci\] pre-commit autoupdate [#28](https://github.com/jupyter-server/jupyter_server_terminals/pull/28) ([@pre-commit-ci](https://github.com/pre-commit-ci)) 218 | - \[pre-commit.ci\] pre-commit autoupdate [#27](https://github.com/jupyter-server/jupyter_server_terminals/pull/27) ([@pre-commit-ci](https://github.com/pre-commit-ci)) 219 | 220 | ### Contributors to this release 221 | 222 | ([GitHub contributors page for this release](https://github.com/jupyter-server/jupyter_server_terminals/graphs/contributors?from=2022-05-03&to=2022-09-08&type=c)) 223 | 224 | [@blink1073](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3Ablink1073+updated%3A2022-05-03..2022-09-08&type=Issues) | [@codecov-commenter](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3Acodecov-commenter+updated%3A2022-05-03..2022-09-08&type=Issues) | [@krassowski](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3Akrassowski+updated%3A2022-05-03..2022-09-08&type=Issues) | [@pre-commit-ci](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3Apre-commit-ci+updated%3A2022-05-03..2022-09-08&type=Issues) | [@welcome](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3Awelcome+updated%3A2022-05-03..2022-09-08&type=Issues) 225 | 226 | ## 0.3.0 227 | 228 | ([Full Changelog](https://github.com/jupyter-server/jupyter_server_terminals/compare/v0.2.1...3e59b7ca4ebdbf3e1535b4be3a973f2f419ae49f)) 229 | 230 | ### Maintenance and upkeep improvements 231 | 232 | - Use alpha release of server 2.0 [#25](https://github.com/jupyter-server/jupyter_server_terminals/pull/25) ([@blink1073](https://github.com/blink1073)) 233 | - Switch to flit build backend [#24](https://github.com/jupyter-server/jupyter_server_terminals/pull/24) ([@blink1073](https://github.com/blink1073)) 234 | - Allow bot PRs to be auto-labeled [#23](https://github.com/jupyter-server/jupyter_server_terminals/pull/23) ([@blink1073](https://github.com/blink1073)) 235 | - \[pre-commit.ci\] pre-commit autoupdate [#22](https://github.com/jupyter-server/jupyter_server_terminals/pull/22) ([@pre-commit-ci](https://github.com/pre-commit-ci)) 236 | - \[pre-commit.ci\] pre-commit autoupdate [#21](https://github.com/jupyter-server/jupyter_server_terminals/pull/21) ([@pre-commit-ci](https://github.com/pre-commit-ci)) 237 | - Add mypy check [#20](https://github.com/jupyter-server/jupyter_server_terminals/pull/20) ([@blink1073](https://github.com/blink1073)) 238 | - \[pre-commit.ci\] pre-commit autoupdate [#19](https://github.com/jupyter-server/jupyter_server_terminals/pull/19) ([@pre-commit-ci](https://github.com/pre-commit-ci)) 239 | - Clean up pre-commit [#18](https://github.com/jupyter-server/jupyter_server_terminals/pull/18) ([@blink1073](https://github.com/blink1073)) 240 | - \[pre-commit.ci\] pre-commit autoupdate [#17](https://github.com/jupyter-server/jupyter_server_terminals/pull/17) ([@pre-commit-ci](https://github.com/pre-commit-ci)) 241 | 242 | ### Contributors to this release 243 | 244 | ([GitHub contributors page for this release](https://github.com/jupyter-server/jupyter_server_terminals/graphs/contributors?from=2022-04-03&to=2022-05-03&type=c)) 245 | 246 | [@blink1073](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3Ablink1073+updated%3A2022-04-03..2022-05-03&type=Issues) | [@pre-commit-ci](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3Apre-commit-ci+updated%3A2022-04-03..2022-05-03&type=Issues) 247 | 248 | ## 0.2.1 249 | 250 | ([Full Changelog](https://github.com/jupyter-server/jupyter_server_terminals/compare/v0.2.0...e33b367c81cfb42bf4b903a75f8cd2ae7f400f64)) 251 | 252 | ### Bugs fixed 253 | 254 | - Fix initialize method [#15](https://github.com/jupyter-server/jupyter_server_terminals/pull/15) ([@blink1073](https://github.com/blink1073)) 255 | 256 | ### Contributors to this release 257 | 258 | ([GitHub contributors page for this release](https://github.com/jupyter-server/jupyter_server_terminals/graphs/contributors?from=2022-04-03&to=2022-04-03&type=c)) 259 | 260 | [@blink1073](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3Ablink1073+updated%3A2022-04-03..2022-04-03&type=Issues) 261 | 262 | ## 0.2.0 263 | 264 | ([Full Changelog](https://github.com/jupyter-server/jupyter_server_terminals/compare/v0.1.0...22d210ea50fb27bcda9a2a781bef6790c509f9a8)) 265 | 266 | ### Enhancements made 267 | 268 | - More Cleanup [#12](https://github.com/jupyter-server/jupyter_server_terminals/pull/12) ([@blink1073](https://github.com/blink1073)) 269 | - Add authorization [#11](https://github.com/jupyter-server/jupyter_server_terminals/pull/11) ([@blink1073](https://github.com/blink1073)) 270 | 271 | ### Contributors to this release 272 | 273 | ([GitHub contributors page for this release](https://github.com/jupyter-server/jupyter_server_terminals/graphs/contributors?from=2022-04-02&to=2022-04-03&type=c)) 274 | 275 | [@blink1073](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3Ablink1073+updated%3A2022-04-02..2022-04-03&type=Issues) 276 | 277 | ## 0.1.0 278 | 279 | ([Full Changelog](https://github.com/jupyter-server/jupyter_server_terminals/compare/0.0.1...c849eb024b37e98004f9f2038a19d2227d0923a4)) 280 | 281 | ### Enhancements made 282 | 283 | - update last_activity when pty_read [#4](https://github.com/jupyter-server/jupyter_server_terminals/pull/4) ([@Wh1isper](https://github.com/Wh1isper)) 284 | - Refactor terminals into an ExtensionApp [#2](https://github.com/jupyter-server/jupyter_server_terminals/pull/2) ([@Zsailer](https://github.com/Zsailer)) 285 | 286 | ### Bugs fixed 287 | 288 | - Pin pywintpy and add missing documentation page [#6](https://github.com/jupyter-server/jupyter_server_terminals/pull/6) ([@blink1073](https://github.com/blink1073)) 289 | 290 | ### Maintenance and upkeep improvements 291 | 292 | - More Cleanup [#8](https://github.com/jupyter-server/jupyter_server_terminals/pull/8) ([@blink1073](https://github.com/blink1073)) 293 | - Clean up dependencies [#7](https://github.com/jupyter-server/jupyter_server_terminals/pull/7) ([@blink1073](https://github.com/blink1073)) 294 | - Initial setup [#1](https://github.com/jupyter-server/jupyter_server_terminals/pull/1) ([@blink1073](https://github.com/blink1073)) 295 | 296 | ### Documentation improvements 297 | 298 | - Fix links in readme [#3](https://github.com/jupyter-server/jupyter_server_terminals/pull/3) ([@jasonweill](https://github.com/jasonweill)) 299 | 300 | ### Contributors to this release 301 | 302 | ([GitHub contributors page for this release](https://github.com/jupyter-server/jupyter_server_terminals/graphs/contributors?from=2021-12-26&to=2022-04-02&type=c)) 303 | 304 | [@blink1073](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3Ablink1073+updated%3A2021-12-26..2022-04-02&type=Issues) | [@jasonweills](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3Ajasonweill+updated%3A2021-12-26..2022-04-02&type=Issues) | [@welcome](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3Awelcome+updated%3A2021-12-26..2022-04-02&type=Issues) | [@Wh1isper](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3AWh1isper+updated%3A2021-12-26..2022-04-02&type=Issues) | [@Zsailer](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server_terminals+involves%3AZsailer+updated%3A2021-12-26..2022-04-02&type=Issues) 305 | --------------------------------------------------------------------------------