diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 83c949e6ca..709a190ac6 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -18,12 +18,12 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] - python-version: ["3.8", "3.11"] + python-version: ["3.9", "3.11", "3.12"] include: - os: windows-latest python-version: "3.9" - os: ubuntu-latest - python-version: "pypy-3.8" + python-version: "pypy-3.9" - os: macos-latest python-version: "3.10" - os: ubuntu-latest @@ -180,7 +180,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 @@ -194,7 +194,7 @@ jobs: - uses: actions/checkout@v4 - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 with: - python_version: "pypy-3.8" + python_version: "pypy-3.9" - name: Run the tests run: hatch -v run test:nowarn --integration_tests=true diff --git a/CHANGELOG.md b/CHANGELOG.md index cadf2e8a93..73e03df6d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,104 @@ All notable changes to this project will be documented in this file. +## 2.15.0 + +([Full Changelog](https://github.com/jupyter-server/jupyter_server/compare/v2.9.1...f23b3392624001c8fba6623e19f526a98b4a07ba)) + +### Enhancements made + +- Better error message when starting kernel for session. [#1478](https://github.com/jupyter-server/jupyter_server/pull/1478) ([@Carreau](https://github.com/Carreau)) +- Add a traitlet to disable recording HTTP request metrics [#1472](https://github.com/jupyter-server/jupyter_server/pull/1472) ([@yuvipanda](https://github.com/yuvipanda)) +- prometheus: Expose 3 activity metrics [#1471](https://github.com/jupyter-server/jupyter_server/pull/1471) ([@yuvipanda](https://github.com/yuvipanda)) +- Add prometheus info metrics listing server extensions + versions [#1470](https://github.com/jupyter-server/jupyter_server/pull/1470) ([@yuvipanda](https://github.com/yuvipanda)) +- Add prometheus metric with version information [#1467](https://github.com/jupyter-server/jupyter_server/pull/1467) ([@yuvipanda](https://github.com/yuvipanda)) +- Better hash format error message [#1442](https://github.com/jupyter-server/jupyter_server/pull/1442) ([@fcollonval](https://github.com/fcollonval)) +- Removing excessive logging from reading local files [#1420](https://github.com/jupyter-server/jupyter_server/pull/1420) ([@lresende](https://github.com/lresende)) +- Do not include token in dashboard link, when available [#1406](https://github.com/jupyter-server/jupyter_server/pull/1406) ([@minrk](https://github.com/minrk)) +- Add an option to have authentication enabled for all endpoints by default [#1392](https://github.com/jupyter-server/jupyter_server/pull/1392) ([@krassowski](https://github.com/krassowski)) +- websockets: add configurations for ping interval and timeout [#1391](https://github.com/jupyter-server/jupyter_server/pull/1391) ([@oliver-sanders](https://github.com/oliver-sanders)) +- log extension import time at debug level unless it's actually slow [#1375](https://github.com/jupyter-server/jupyter_server/pull/1375) ([@minrk](https://github.com/minrk)) +- Add support for async Authorizers (part 2) [#1374](https://github.com/jupyter-server/jupyter_server/pull/1374) ([@Zsailer](https://github.com/Zsailer)) +- Support async Authorizers [#1373](https://github.com/jupyter-server/jupyter_server/pull/1373) ([@Zsailer](https://github.com/Zsailer)) +- Support get file(notebook) md5 [#1363](https://github.com/jupyter-server/jupyter_server/pull/1363) ([@Wh1isper](https://github.com/Wh1isper)) +- Update kernel env to reflect changes in session [#1354](https://github.com/jupyter-server/jupyter_server/pull/1354) ([@blink1073](https://github.com/blink1073)) + +### Bugs fixed + +- Return HTTP 400 when attempting to post an event with an unregistered schema [#1463](https://github.com/jupyter-server/jupyter_server/pull/1463) ([@afshin](https://github.com/afshin)) +- write server extension list to stdout [#1451](https://github.com/jupyter-server/jupyter_server/pull/1451) ([@minrk](https://github.com/minrk)) +- don't let ExtensionApp jpserver_extensions be overridden by config [#1447](https://github.com/jupyter-server/jupyter_server/pull/1447) ([@minrk](https://github.com/minrk)) +- Pass session_id during Websocket connect [#1440](https://github.com/jupyter-server/jupyter_server/pull/1440) ([@gogasca](https://github.com/gogasca)) +- Do not log environment variables passed to kernels [#1437](https://github.com/jupyter-server/jupyter_server/pull/1437) ([@krassowski](https://github.com/krassowski)) +- extensions: render default templates with default static_url [#1435](https://github.com/jupyter-server/jupyter_server/pull/1435) ([@minrk](https://github.com/minrk)) +- Improve the busy/idle execution state tracking for kernels. [#1429](https://github.com/jupyter-server/jupyter_server/pull/1429) ([@ojarjur](https://github.com/ojarjur)) +- Ignore zero-length page_config.json, restore previous behavior of crashing for invalid JSON [#1405](https://github.com/jupyter-server/jupyter_server/pull/1405) ([@holzman](https://github.com/holzman)) +- Don't crash on invalid JSON in page_config (#1403) [#1404](https://github.com/jupyter-server/jupyter_server/pull/1404) ([@holzman](https://github.com/holzman)) +- Fix color in windows log console with colorama [#1397](https://github.com/jupyter-server/jupyter_server/pull/1397) ([@hansepac](https://github.com/hansepac)) +- Fix log arguments for gateway client error [#1385](https://github.com/jupyter-server/jupyter_server/pull/1385) ([@minrk](https://github.com/minrk)) +- Import User unconditionally [#1384](https://github.com/jupyter-server/jupyter_server/pull/1384) ([@yuvipanda](https://github.com/yuvipanda)) +- Fix a typo in error message [#1381](https://github.com/jupyter-server/jupyter_server/pull/1381) ([@krassowski](https://github.com/krassowski)) +- avoid unhandled error on some invalid paths [#1369](https://github.com/jupyter-server/jupyter_server/pull/1369) ([@minrk](https://github.com/minrk)) +- Change md5 to hash and hash_algorithm, fix incompatibility [#1367](https://github.com/jupyter-server/jupyter_server/pull/1367) ([@Wh1isper](https://github.com/Wh1isper)) +- ContentsHandler return 404 rather than raise exc [#1357](https://github.com/jupyter-server/jupyter_server/pull/1357) ([@bloomsa](https://github.com/bloomsa)) +- Force legacy ws subprotocol when using gateway [#1311](https://github.com/jupyter-server/jupyter_server/pull/1311) ([@epignot](https://github.com/epignot)) + +### Maintenance and upkeep improvements + +- Donation link NF -> LF [#1485](https://github.com/jupyter-server/jupyter_server/pull/1485) ([@Carreau](https://github.com/Carreau)) +- Handle newer jupyter_events wants string version, drop 3.8 [#1481](https://github.com/jupyter-server/jupyter_server/pull/1481) ([@Carreau](https://github.com/Carreau)) +- Ignore unclosed sqlite connection in traits [#1477](https://github.com/jupyter-server/jupyter_server/pull/1477) ([@cjwatson](https://github.com/cjwatson)) +- chore: update pre-commit hooks [#1441](https://github.com/jupyter-server/jupyter_server/pull/1441) ([@pre-commit-ci](https://github.com/pre-commit-ci)) +- chore: update pre-commit hooks [#1427](https://github.com/jupyter-server/jupyter_server/pull/1427) ([@pre-commit-ci](https://github.com/pre-commit-ci)) +- Use hatch fmt command [#1424](https://github.com/jupyter-server/jupyter_server/pull/1424) ([@blink1073](https://github.com/blink1073)) +- chore: update pre-commit hooks [#1421](https://github.com/jupyter-server/jupyter_server/pull/1421) ([@pre-commit-ci](https://github.com/pre-commit-ci)) +- Fix jupytext and lint CI failures [#1413](https://github.com/jupyter-server/jupyter_server/pull/1413) ([@blink1073](https://github.com/blink1073)) +- Set all min deps [#1411](https://github.com/jupyter-server/jupyter_server/pull/1411) ([@blink1073](https://github.com/blink1073)) +- chore: update pre-commit hooks [#1409](https://github.com/jupyter-server/jupyter_server/pull/1409) ([@pre-commit-ci](https://github.com/pre-commit-ci)) +- Update pytest requirement from \<8,>=7.0 to >=7.0,\<9 [#1402](https://github.com/jupyter-server/jupyter_server/pull/1402) ([@dependabot](https://github.com/dependabot)) +- Pin to Pytest 7 [#1401](https://github.com/jupyter-server/jupyter_server/pull/1401) ([@blink1073](https://github.com/blink1073)) +- Update release workflows [#1399](https://github.com/jupyter-server/jupyter_server/pull/1399) ([@blink1073](https://github.com/blink1073)) +- chore: update pre-commit hooks [#1390](https://github.com/jupyter-server/jupyter_server/pull/1390) ([@pre-commit-ci](https://github.com/pre-commit-ci)) +- Improve warning handling [#1386](https://github.com/jupyter-server/jupyter_server/pull/1386) ([@blink1073](https://github.com/blink1073)) +- Simplify the jupytext downstream test [#1383](https://github.com/jupyter-server/jupyter_server/pull/1383) ([@mwouts](https://github.com/mwouts)) +- Fix test param for pytest-xdist [#1382](https://github.com/jupyter-server/jupyter_server/pull/1382) ([@tornaria](https://github.com/tornaria)) +- Update pre-commit deps [#1380](https://github.com/jupyter-server/jupyter_server/pull/1380) ([@blink1073](https://github.com/blink1073)) +- Use ruff docstring-code-format [#1377](https://github.com/jupyter-server/jupyter_server/pull/1377) ([@blink1073](https://github.com/blink1073)) +- Update for tornado 6.4 [#1372](https://github.com/jupyter-server/jupyter_server/pull/1372) ([@blink1073](https://github.com/blink1073)) +- chore: update pre-commit hooks [#1370](https://github.com/jupyter-server/jupyter_server/pull/1370) ([@pre-commit-ci](https://github.com/pre-commit-ci)) +- Update ruff and typings [#1365](https://github.com/jupyter-server/jupyter_server/pull/1365) ([@blink1073](https://github.com/blink1073)) +- Clean up ruff config [#1358](https://github.com/jupyter-server/jupyter_server/pull/1358) ([@blink1073](https://github.com/blink1073)) +- Add more typings [#1356](https://github.com/jupyter-server/jupyter_server/pull/1356) ([@blink1073](https://github.com/blink1073)) +- chore: update pre-commit hooks [#1355](https://github.com/jupyter-server/jupyter_server/pull/1355) ([@pre-commit-ci](https://github.com/pre-commit-ci)) +- Clean up config and address warnings [#1353](https://github.com/jupyter-server/jupyter_server/pull/1353) ([@blink1073](https://github.com/blink1073)) +- Clean up lint and typing [#1351](https://github.com/jupyter-server/jupyter_server/pull/1351) ([@blink1073](https://github.com/blink1073)) +- Update typing for traitlets 5.13 [#1350](https://github.com/jupyter-server/jupyter_server/pull/1350) ([@blink1073](https://github.com/blink1073)) +- Update typings and fix tests [#1344](https://github.com/jupyter-server/jupyter_server/pull/1344) ([@blink1073](https://github.com/blink1073)) + +### Documentation improvements + +- add comments to explain signal handling under jupyterhub [#1452](https://github.com/jupyter-server/jupyter_server/pull/1452) ([@oliver-sanders](https://github.com/oliver-sanders)) +- Update documentation for `cookie_secret` [#1433](https://github.com/jupyter-server/jupyter_server/pull/1433) ([@krassowski](https://github.com/krassowski)) +- Add Changelog for 2.14.1 [#1430](https://github.com/jupyter-server/jupyter_server/pull/1430) ([@blink1073](https://github.com/blink1073)) +- Update simple extension examples: \_jupyter_server_extension_points [#1426](https://github.com/jupyter-server/jupyter_server/pull/1426) ([@manics](https://github.com/manics)) +- Link to GitHub repo from the docs [#1415](https://github.com/jupyter-server/jupyter_server/pull/1415) ([@krassowski](https://github.com/krassowski)) +- docs: list server extensions [#1412](https://github.com/jupyter-server/jupyter_server/pull/1412) ([@oliver-sanders](https://github.com/oliver-sanders)) +- Update simple extension README to cd into correct subdirectory [#1410](https://github.com/jupyter-server/jupyter_server/pull/1410) ([@markypizz](https://github.com/markypizz)) +- Add deprecation note for `ServerApp.preferred_dir` [#1396](https://github.com/jupyter-server/jupyter_server/pull/1396) ([@krassowski](https://github.com/krassowski)) +- Replace \_jupyter_server_extension_paths in apidocs [#1393](https://github.com/jupyter-server/jupyter_server/pull/1393) ([@manics](https://github.com/manics)) +- fix "Shutdown" -> "Shut down" [#1389](https://github.com/jupyter-server/jupyter_server/pull/1389) ([@Timeroot](https://github.com/Timeroot)) +- Enable htmlzip and epub on readthedocs [#1379](https://github.com/jupyter-server/jupyter_server/pull/1379) ([@bollwyvl](https://github.com/bollwyvl)) +- Update api docs with md5 param [#1364](https://github.com/jupyter-server/jupyter_server/pull/1364) ([@Wh1isper](https://github.com/Wh1isper)) +- typo: ServerApp [#1361](https://github.com/jupyter-server/jupyter_server/pull/1361) ([@IITII](https://github.com/IITII)) + +### Contributors to this release + +([GitHub contributors page for this release](https://github.com/jupyter-server/jupyter_server/graphs/contributors?from=2023-10-25&to=2024-12-20&type=c)) + +[@afshin](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Aafshin+updated%3A2023-10-25..2024-12-20&type=Issues) | [@blink1073](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Ablink1073+updated%3A2023-10-25..2024-12-20&type=Issues) | [@bloomsa](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Abloomsa+updated%3A2023-10-25..2024-12-20&type=Issues) | [@bollwyvl](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Abollwyvl+updated%3A2023-10-25..2024-12-20&type=Issues) | [@Carreau](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3ACarreau+updated%3A2023-10-25..2024-12-20&type=Issues) | [@cjwatson](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Acjwatson+updated%3A2023-10-25..2024-12-20&type=Issues) | [@davidbrochart](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Adavidbrochart+updated%3A2023-10-25..2024-12-20&type=Issues) | [@dependabot](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Adependabot+updated%3A2023-10-25..2024-12-20&type=Issues) | [@epignot](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Aepignot+updated%3A2023-10-25..2024-12-20&type=Issues) | [@fcollonval](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Afcollonval+updated%3A2023-10-25..2024-12-20&type=Issues) | [@gogasca](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Agogasca+updated%3A2023-10-25..2024-12-20&type=Issues) | [@hansepac](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Ahansepac+updated%3A2023-10-25..2024-12-20&type=Issues) | [@holzman](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Aholzman+updated%3A2023-10-25..2024-12-20&type=Issues) | [@IITII](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3AIITII+updated%3A2023-10-25..2024-12-20&type=Issues) | [@krassowski](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Akrassowski+updated%3A2023-10-25..2024-12-20&type=Issues) | [@lresende](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Alresende+updated%3A2023-10-25..2024-12-20&type=Issues) | [@manics](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Amanics+updated%3A2023-10-25..2024-12-20&type=Issues) | [@markypizz](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Amarkypizz+updated%3A2023-10-25..2024-12-20&type=Issues) | [@minrk](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Aminrk+updated%3A2023-10-25..2024-12-20&type=Issues) | [@mwouts](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Amwouts+updated%3A2023-10-25..2024-12-20&type=Issues) | [@ojarjur](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Aojarjur+updated%3A2023-10-25..2024-12-20&type=Issues) | [@oliver-sanders](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Aoliver-sanders+updated%3A2023-10-25..2024-12-20&type=Issues) | [@pre-commit-ci](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Apre-commit-ci+updated%3A2023-10-25..2024-12-20&type=Issues) | [@Timeroot](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3ATimeroot+updated%3A2023-10-25..2024-12-20&type=Issues) | [@tornaria](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Atornaria+updated%3A2023-10-25..2024-12-20&type=Issues) | [@welcome](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Awelcome+updated%3A2023-10-25..2024-12-20&type=Issues) | [@Wh1isper](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3AWh1isper+updated%3A2023-10-25..2024-12-20&type=Issues) | [@yuvipanda](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Ayuvipanda+updated%3A2023-10-25..2024-12-20&type=Issues) | [@Zsailer](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3AZsailer+updated%3A2023-10-25..2024-12-20&type=Issues) + + + ## 2.14.2 ([Full Changelog](https://github.com/jupyter-server/jupyter_server/compare/v2.14.1...b961d4eb499071c0c60e24f429c20d1e6a908a32)) @@ -30,8 +128,6 @@ All notable changes to this project will be documented in this file. [@blink1073](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Ablink1073+updated%3A2024-05-31..2024-07-12&type=Issues) | [@gogasca](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Agogasca+updated%3A2024-05-31..2024-07-12&type=Issues) | [@krassowski](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Akrassowski+updated%3A2024-05-31..2024-07-12&type=Issues) | [@manics](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Amanics+updated%3A2024-05-31..2024-07-12&type=Issues) | [@pre-commit-ci](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Apre-commit-ci+updated%3A2024-05-31..2024-07-12&type=Issues) - - ## 2.14.1 ([Full Changelog](https://github.com/jupyter-server/jupyter_server/compare/v2.14.0...f1379164fa209bc4bfeadf43ab0e7f473b03a0ce)) diff --git a/examples/simple/pyproject.toml b/examples/simple/pyproject.toml index 38ae8e71a7..9dec3e55c7 100644 --- a/examples/simple/pyproject.toml +++ b/examples/simple/pyproject.toml @@ -6,8 +6,8 @@ build-backend = "hatchling.build" name = "jupyter-server-example" description = "Jupyter Server Example" readme = "README.md" -license = "" -requires-python = ">=3.8" +license = "MIT" +requires-python = ">=3.9" dependencies = [ "jinja2", "jupyter_server", diff --git a/examples/simple/simple_ext1/handlers.py b/examples/simple/simple_ext1/handlers.py index 72f54a8bfd..6ac99f64a4 100644 --- a/examples/simple/simple_ext1/handlers.py +++ b/examples/simple/simple_ext1/handlers.py @@ -18,7 +18,8 @@ def get(self): self.log.info(f"Extension Name in {self.name} Default Handler: {self.name}") # A method for getting the url to static files (prefixed with /static/). self.log.info( - "Static URL for / in simple_ext1 Default Handler: {}".format(self.static_url(path="/")) + "Static URL for / in simple_ext1 Default Handler: %s", + self.static_url(path="/"), ) self.write("

Hello Simple 1 - I am the default...

") self.write(f"Config in {self.name} Default Handler: {self.config}") diff --git a/jupyter_server/_version.py b/jupyter_server/_version.py index fa814fbb2f..b5b33991ed 100644 --- a/jupyter_server/_version.py +++ b/jupyter_server/_version.py @@ -4,16 +4,15 @@ """ import re -from typing import List # Version string must appear intact for automatic versioning -__version__ = "2.15.0.dev0" +__version__ = "2.16.0.dev0" # Build up version_info tuple for backwards compatibility pattern = r"(?P\d+).(?P\d+).(?P\d+)(?P.*)" match = re.match(pattern, __version__) assert match is not None -parts: List[object] = [int(match[part]) for part in ["major", "minor", "patch"]] +parts: list[object] = [int(match[part]) for part in ["major", "minor", "patch"]] if match["rest"]: parts.append(match["rest"]) version_info = tuple(parts) diff --git a/jupyter_server/auth/authorizer.py b/jupyter_server/auth/authorizer.py index 10414e2c39..fcebc3404b 100644 --- a/jupyter_server/auth/authorizer.py +++ b/jupyter_server/auth/authorizer.py @@ -10,7 +10,7 @@ # Distributed under the terms of the Modified BSD License. from __future__ import annotations -from typing import TYPE_CHECKING, Awaitable +from typing import TYPE_CHECKING from traitlets import Instance from traitlets.config import LoggingConfigurable @@ -18,6 +18,8 @@ from .identity import IdentityProvider, User if TYPE_CHECKING: + from collections.abc import Awaitable + from jupyter_server.base.handlers import JupyterHandler diff --git a/jupyter_server/base/call_context.py b/jupyter_server/base/call_context.py index cf71256235..4e80be8a7d 100644 --- a/jupyter_server/base/call_context.py +++ b/jupyter_server/base/call_context.py @@ -3,7 +3,7 @@ # Distributed under the terms of the Modified BSD License. from contextvars import Context, ContextVar, copy_context -from typing import Any, Dict, List +from typing import Any class CallContext: @@ -22,7 +22,7 @@ class CallContext: # easier management over maintaining a set of ContextVar instances, since the Context is a # map of ContextVar instances to their values, and the "name" is no longer a lookup key. _NAME_VALUE_MAP = "_name_value_map" - _name_value_map: ContextVar[Dict[str, Any]] = ContextVar(_NAME_VALUE_MAP) + _name_value_map: ContextVar[dict[str, Any]] = ContextVar(_NAME_VALUE_MAP) @classmethod def get(cls, name: str) -> Any: @@ -65,7 +65,7 @@ def set(cls, name: str, value: Any) -> None: name_value_map[name] = value @classmethod - def context_variable_names(cls) -> List[str]: + def context_variable_names(cls) -> list[str]: """Returns a list of variable names set for this call context. Returns @@ -77,7 +77,7 @@ def context_variable_names(cls) -> List[str]: return list(name_value_map.keys()) @classmethod - def _get_map(cls) -> Dict[str, Any]: + def _get_map(cls) -> dict[str, Any]: """Get the map of names to their values from the _NAME_VALUE_MAP context var. If the map does not exist in the current context, an empty map is created and returned. diff --git a/jupyter_server/base/handlers.py b/jupyter_server/base/handlers.py index 9a47b2f68c..3909c70638 100644 --- a/jupyter_server/base/handlers.py +++ b/jupyter_server/base/handlers.py @@ -13,9 +13,10 @@ import re import types import warnings +from collections.abc import Awaitable, Coroutine, Sequence from http.client import responses from logging import Logger -from typing import TYPE_CHECKING, Any, Awaitable, Coroutine, Sequence, cast +from typing import TYPE_CHECKING, Any, cast from urllib.parse import urlparse import prometheus_client diff --git a/jupyter_server/config_manager.py b/jupyter_server/config_manager.py index 4a0bff4015..8f49cb7bd6 100644 --- a/jupyter_server/config_manager.py +++ b/jupyter_server/config_manager.py @@ -14,7 +14,7 @@ from traitlets.config import LoggingConfigurable from traitlets.traitlets import Bool, Unicode -StrDict = t.Dict[str, t.Any] +StrDict = dict[str, t.Any] def recursive_update(target: StrDict, new: StrDict) -> None: diff --git a/jupyter_server/event_schemas/contents_service/v1.yaml b/jupyter_server/event_schemas/contents_service/v1.yaml index a787f9b2b0..d049005e01 100644 --- a/jupyter_server/event_schemas/contents_service/v1.yaml +++ b/jupyter_server/event_schemas/contents_service/v1.yaml @@ -1,5 +1,5 @@ "$id": https://events.jupyter.org/jupyter_server/contents_service/v1 -version: 1 +version: "1" title: Contents Manager activities personal-data: true description: | diff --git a/jupyter_server/event_schemas/gateway_client/v1.yaml b/jupyter_server/event_schemas/gateway_client/v1.yaml index 0a35d2464d..0257ce071a 100644 --- a/jupyter_server/event_schemas/gateway_client/v1.yaml +++ b/jupyter_server/event_schemas/gateway_client/v1.yaml @@ -1,5 +1,5 @@ "$id": https://events.jupyter.org/jupyter_server/gateway_client/v1 -version: 1 +version: "1" title: Gateway Client activities. personal-data: true description: | diff --git a/jupyter_server/event_schemas/kernel_actions/v1.yaml b/jupyter_server/event_schemas/kernel_actions/v1.yaml index e0375e5aaa..66c13802c2 100644 --- a/jupyter_server/event_schemas/kernel_actions/v1.yaml +++ b/jupyter_server/event_schemas/kernel_actions/v1.yaml @@ -1,5 +1,5 @@ "$id": https://events.jupyter.org/jupyter_server/kernel_actions/v1 -version: 1 +version: "1" title: Kernel Manager activities personal-data: true description: | diff --git a/jupyter_server/extension/handler.py b/jupyter_server/extension/handler.py index 31377cc367..4285c415b0 100644 --- a/jupyter_server/extension/handler.py +++ b/jupyter_server/extension/handler.py @@ -5,6 +5,7 @@ from logging import Logger from typing import TYPE_CHECKING, Any, cast +from jinja2 import Template from jinja2.exceptions import TemplateNotFound from jupyter_server.base.handlers import FileFindHandler @@ -21,13 +22,14 @@ class ExtensionHandlerJinjaMixin: template rendering. """ - def get_template(self, name: str) -> str: + def get_template(self, name: str) -> Template: """Return the jinja template object for a given name""" try: env = f"{self.name}_jinja2_env" # type:ignore[attr-defined] - return cast(str, self.settings[env].get_template(name)) # type:ignore[attr-defined] + template = cast(Template, self.settings[env].get_template(name)) # type:ignore[attr-defined] + return template except TemplateNotFound: - return cast(str, super().get_template(name)) # type:ignore[misc] + return cast(Template, super().get_template(name)) # type:ignore[misc] class ExtensionHandlerMixin: @@ -81,6 +83,20 @@ def server_config(self) -> Config: def base_url(self) -> str: return cast(str, self.settings.get("base_url", "/")) + def render_template(self, name: str, **ns) -> str: + """Override render template to handle static_paths + + If render_template is called with a template from the base environment + (e.g. default error pages) + make sure our extension-specific static_url is _not_ used. + """ + template = cast(Template, self.get_template(name)) # type:ignore[attr-defined] + ns.update(self.template_namespace) # type:ignore[attr-defined] + if template.environment is self.settings["jinja2_env"]: + # default template environment, use default static_url + ns["static_url"] = super().static_url # type:ignore[misc] + return cast(str, template.render(**ns)) + @property def static_url_prefix(self) -> str: return self.extensionapp.static_url_prefix diff --git a/jupyter_server/extension/serverextension.py b/jupyter_server/extension/serverextension.py index b4c9dacbc2..5978933acb 100644 --- a/jupyter_server/extension/serverextension.py +++ b/jupyter_server/extension/serverextension.py @@ -276,7 +276,7 @@ def toggle_server_extension(self, import_name: str) -> None: # If successful, let's log. self.log.info(f" - Extension successfully {self._toggle_post_message}.") except Exception as err: - self.log.info(f" {RED_X} Validation failed: {err}") + self.log.error(f" {RED_X} Validation failed: {err}") def start(self) -> None: """Perform the App's actions as configured""" @@ -336,7 +336,7 @@ def list_server_extensions(self) -> None: for option in configurations: config_dir = _get_config_dir(**option) - self.log.info(f"Config dir: {config_dir}") + print(f"Config dir: {config_dir}") write_dir = "jupyter_server_config.d" config_manager = ExtensionConfigManager( read_config_path=[config_dir], @@ -345,20 +345,18 @@ def list_server_extensions(self) -> None: jpserver_extensions = config_manager.get_jpserver_extensions() for name, enabled in jpserver_extensions.items(): # Attempt to get extension metadata - self.log.info(f" {name} {GREEN_ENABLED if enabled else RED_DISABLED}") + print(f" {name} {GREEN_ENABLED if enabled else RED_DISABLED}") try: - self.log.info(f" - Validating {name}...") + print(f" - Validating {name}...") extension = ExtensionPackage(name=name, enabled=enabled) if not extension.validate(): msg = "validation failed" raise ValueError(msg) version = extension.version - self.log.info(f" {name} {version} {GREEN_OK}") + print(f" {name} {version} {GREEN_OK}") except Exception as err: - exc_info = False - if int(self.log_level) <= logging.DEBUG: # type:ignore[call-overload] - exc_info = True - self.log.warning(f" {RED_X} {err}", exc_info=exc_info) + self.log.debug("", exc_info=True) + print(f" {RED_X} {err}") # Add a blank line between paths. self.log.info("") diff --git a/jupyter_server/files/handlers.py b/jupyter_server/files/handlers.py index 2c1dc5adf6..749328438e 100644 --- a/jupyter_server/files/handlers.py +++ b/jupyter_server/files/handlers.py @@ -6,7 +6,7 @@ import mimetypes from base64 import decodebytes -from typing import Awaitable +from typing import TYPE_CHECKING from jupyter_core.utils import ensure_async from tornado import web @@ -14,6 +14,9 @@ from jupyter_server.auth.decorator import authorized from jupyter_server.base.handlers import JupyterHandler +if TYPE_CHECKING: + from collections.abc import Awaitable + AUTH_RESOURCE = "contents" diff --git a/jupyter_server/gateway/managers.py b/jupyter_server/gateway/managers.py index 0ac47f8f57..daa6f99213 100644 --- a/jupyter_server/gateway/managers.py +++ b/jupyter_server/gateway/managers.py @@ -632,9 +632,10 @@ async def get_msg(self, *args: Any, **kwargs: Any) -> dict[str, Any]: timeout = kwargs.get("timeout", 1) msg = await self._async_get(timeout=timeout) self.log.debug( - "Received message on channel: {}, msg_id: {}, msg_type: {}".format( - self.channel_name, msg["msg_id"], msg["msg_type"] if msg else "null" - ) + "Received message on channel: %s, msg_id: %s, msg_type: %s", + self.channel_name, + msg["msg_id"], + msg["msg_type"] if msg else "null", ) self.task_done() return cast("dict[str, Any]", msg) @@ -643,9 +644,10 @@ def send(self, msg: dict[str, Any]) -> None: """Send a message to the queue.""" message = json.dumps(msg, default=ChannelQueue.serialize_datetime).replace(" str: return uri -def log_request(handler): +def log_request(handler, record_prometheus_metrics=True): """log a bit more information about each request than tornado's default - move static file get success to debug-level (reduces noise) - get proxied IP instead of proxy IP - log referer for redirect and failed requests - log user-agent for failed requests + + if record_prometheus_metrics is true, will record a histogram prometheus + metric (http_request_duration_seconds) for each request handler """ status = handler.get_status() request = handler.request @@ -97,4 +100,5 @@ def log_request(handler): headers[header] = request.headers[header] log_method(json.dumps(headers, indent=2)) log_method(msg.format(**ns)) - prometheus_log_method(handler) + if record_prometheus_metrics: + prometheus_log_method(handler) diff --git a/jupyter_server/prometheus/metrics.py b/jupyter_server/prometheus/metrics.py index 1a02f86209..c0aeb37568 100644 --- a/jupyter_server/prometheus/metrics.py +++ b/jupyter_server/prometheus/metrics.py @@ -5,19 +5,35 @@ conventions for metrics & labels. """ +from prometheus_client import Gauge, Histogram, Info + +from jupyter_server._version import version_info as server_version_info + try: - # Jupyter Notebook also defines these metrics. Re-defining them results in a ValueError. - # Try to de-duplicate by using the ones in Notebook if available. + from notebook._version import version_info as notebook_version_info +except ImportError: + notebook_version_info = None + + +if ( + notebook_version_info is not None # No notebook package found + and notebook_version_info < (7,) # Notebook package found, is version 6 + # Notebook package found, but its version is the same as jupyter_server + # version. This means some package (looking at you, nbclassic) has shimmed + # the notebook package to instead be imports from the jupyter_server package. + # In such cases, notebook.prometheus.metrics is actually *this file*, so + # trying to import it will cause a circular import. So we don't. + and notebook_version_info != server_version_info +): + # Jupyter Notebook v6 also defined these metrics. Re-defining them results in a ValueError, + # so we simply re-export them if we are co-existing with the notebook v6 package. # See https://github.com/jupyter/jupyter_server/issues/209 from notebook.prometheus.metrics import ( HTTP_REQUEST_DURATION_SECONDS, KERNEL_CURRENTLY_RUNNING_TOTAL, TERMINAL_CURRENTLY_RUNNING_TOTAL, ) - -except ImportError: - from prometheus_client import Gauge, Histogram - +else: HTTP_REQUEST_DURATION_SECONDS = Histogram( "http_request_duration_seconds", "duration in seconds for all HTTP requests", @@ -35,9 +51,28 @@ ["type"], ) +# New prometheus metrics that do not exist in notebook v6 go here +SERVER_INFO = Info("jupyter_server", "Jupyter Server Version information") +SERVER_EXTENSION_INFO = Info( + "jupyter_server_extension", + "Jupyter Server Extensiom Version Information", + ["name", "version", "enabled"], +) +LAST_ACTIVITY = Gauge( + "jupyter_server_last_activity_timestamp_seconds", + "Timestamp of last seen activity on this Jupyter Server", +) +SERVER_STARTED = Gauge( + "jupyter_server_started_timestamp_seconds", "Timestamp of when this Jupyter Server was started" +) +ACTIVE_DURATION = Gauge( + "jupyter_server_active_duration_seconds", + "Number of seconds this Jupyter Server has been active", +) __all__ = [ "HTTP_REQUEST_DURATION_SECONDS", "TERMINAL_CURRENTLY_RUNNING_TOTAL", "KERNEL_CURRENTLY_RUNNING_TOTAL", + "SERVER_INFO", ] diff --git a/jupyter_server/serverapp.py b/jupyter_server/serverapp.py index 3dedd5634f..8aa3dbd082 100644 --- a/jupyter_server/serverapp.py +++ b/jupyter_server/serverapp.py @@ -28,6 +28,7 @@ import urllib import warnings from base64 import encodebytes +from functools import partial from pathlib import Path import jupyter_client @@ -110,6 +111,13 @@ GatewaySessionManager, ) from jupyter_server.log import log_request +from jupyter_server.prometheus.metrics import ( + ACTIVE_DURATION, + LAST_ACTIVITY, + SERVER_EXTENSION_INFO, + SERVER_INFO, + SERVER_STARTED, +) from jupyter_server.services.config import ConfigManager from jupyter_server.services.contents.filemanager import ( AsyncFileContentsManager, @@ -403,7 +411,9 @@ def init_settings( settings = { # basics - "log_function": log_request, + "log_function": partial( + log_request, record_prometheus_metrics=jupyter_app.record_http_request_metrics + ), "base_url": base_url, "default_url": default_url, "template_path": template_path, @@ -1986,6 +1996,18 @@ def _default_terminals_enabled(self) -> bool: config=True, ) + record_http_request_metrics = Bool( + True, + help=""" + Record http_request_duration_seconds metric in the metrics endpoint. + + Since a histogram is exposed for each request handler, this can create a + *lot* of metrics, creating operational challenges for multitenant deployments. + + Set to False to disable recording the http_request_duration_seconds metric. + """, + ) + static_immutable_cache = List( Unicode(), help=""" @@ -2388,7 +2410,14 @@ def init_signal(self) -> None: signal.signal(signal.SIGINFO, self._signal_info) def _handle_sigint(self, sig: t.Any, frame: t.Any) -> None: - """SIGINT handler spawns confirmation dialog""" + """SIGINT handler spawns confirmation dialog + + Note: + JupyterHub replaces this method with _signal_stop + in order to bypass the interactive prompt. + https://github.com/jupyterhub/jupyterhub/pull/4864 + + """ # register more forceful signal handler for ^C^C case signal.signal(signal.SIGINT, self._signal_stop) # request confirmation dialog in bg thread, to avoid @@ -2446,7 +2475,13 @@ def _confirm_exit(self) -> None: self.io_loop.add_callback_from_signal(self._restore_sigint_handler) def _signal_stop(self, sig: t.Any, frame: t.Any) -> None: - """Handle a stop signal.""" + """Handle a stop signal. + + Note: + JupyterHub configures this method to be called for SIGINT. + https://github.com/jupyterhub/jupyterhub/pull/4864 + + """ self.log.critical(_i18n("received signal %s, stopping"), sig) self.stop(from_signal=True) @@ -2517,8 +2552,6 @@ def init_mime_overrides(self) -> None: # ensure css, js are correct, which are required for pages to function mimetypes.add_type("text/css", ".css") mimetypes.add_type("application/javascript", ".js") - # for python <3.8 - mimetypes.add_type("application/wasm", ".wasm") def shutdown_no_activity(self) -> None: """Shutdown server on timeout when there are no kernels or terminals.""" @@ -2683,7 +2716,7 @@ def _init_asyncio_patch() -> None: at least until asyncio adds *_reader methods to proactor. """ - if sys.platform.startswith("win") and sys.version_info >= (3, 8): + if sys.platform.startswith("win"): import asyncio try: @@ -2696,6 +2729,27 @@ def _init_asyncio_patch() -> None: # prefer Selector to Proactor for tornado + pyzmq asyncio.set_event_loop_policy(WindowsSelectorEventLoopPolicy()) + def init_metrics(self) -> None: + """ + Initialize any prometheus metrics that need to be set up on server startup + """ + SERVER_INFO.info({"version": __version__}) + + for ext in self.extension_manager.extensions.values(): + SERVER_EXTENSION_INFO.labels( + name=ext.name, version=ext.version, enabled=str(ext.enabled).lower() + ) + + started = self.web_app.settings["started"] + SERVER_STARTED.set(started.timestamp()) + + LAST_ACTIVITY.set_function(lambda: self.web_app.last_activity().timestamp()) + ACTIVE_DURATION.set_function( + lambda: ( + self.web_app.last_activity() - self.web_app.settings["started"] + ).total_seconds() + ) + @catch_config_error def initialize( self, @@ -2724,7 +2778,11 @@ def initialize( self._init_asyncio_patch() # Parse command line, load ServerApp config files, # and update ServerApp config. + # preserve jpserver_extensions, which may have been set by starter_extension + # don't let config clobber this value + jpserver_extensions = self.jpserver_extensions.copy() super().initialize(argv=argv) + self.jpserver_extensions.update(jpserver_extensions) if self._dispatching: return # initialize io loop as early as possible, @@ -2759,6 +2817,7 @@ def initialize( self.load_server_extensions() self.init_mime_overrides() self.init_shutdown_no_activity() + self.init_metrics() if new_httpserver: self.init_httpserver() diff --git a/jupyter_server/services/api/handlers.py b/jupyter_server/services/api/handlers.py index 22904fdb07..f61d9dd10f 100644 --- a/jupyter_server/services/api/handlers.py +++ b/jupyter_server/services/api/handlers.py @@ -4,7 +4,7 @@ # Distributed under the terms of the Modified BSD License. import json import os -from typing import Any, Dict, List +from typing import Any from jupyter_core.utils import ensure_async from tornado import web @@ -87,7 +87,7 @@ async def get(self): else: permissions_to_check = {} - permissions: Dict[str, List[str]] = {} + permissions: dict[str, list[str]] = {} user = self.current_user for resource, actions in permissions_to_check.items(): @@ -106,7 +106,7 @@ async def get(self): if authorized: allowed.append(action) - identity: Dict[str, Any] = self.identity_provider.identity_model(user) + identity: dict[str, Any] = self.identity_provider.identity_model(user) model = { "identity": identity, "permissions": permissions, diff --git a/jupyter_server/services/config/manager.py b/jupyter_server/services/config/manager.py index d4e207e247..223d4fed2c 100644 --- a/jupyter_server/services/config/manager.py +++ b/jupyter_server/services/config/manager.py @@ -23,7 +23,7 @@ class ConfigManager(LoggingConfigurable): def get(self, section_name): """Get the config from all config sections.""" - config: t.Dict[str, t.Any] = {} + config: dict[str, t.Any] = {} # step through back to front, to ensure front of the list is top priority for p in self.read_config_path[::-1]: cm = BaseJSONConfigManager(config_dir=p) diff --git a/jupyter_server/services/contents/handlers.py b/jupyter_server/services/contents/handlers.py index 13e987809b..ae160e6707 100644 --- a/jupyter_server/services/contents/handlers.py +++ b/jupyter_server/services/contents/handlers.py @@ -7,7 +7,7 @@ # Distributed under the terms of the Modified BSD License. import json from http import HTTPStatus -from typing import Any, Dict, List +from typing import Any try: from jupyter_client.jsonutil import json_default @@ -24,7 +24,7 @@ AUTH_RESOURCE = "contents" -def _validate_keys(expect_defined: bool, model: Dict[str, Any], keys: List[str]): +def _validate_keys(expect_defined: bool, model: dict[str, Any], keys: list[str]): """ Validate that the keys are defined (i.e. not None) or not (i.e. None) """ @@ -210,10 +210,9 @@ async def patch(self, path=""): async def _copy(self, copy_from, copy_to=None): """Copy a file, optionally specifying a target directory.""" self.log.info( - "Copying {copy_from} to {copy_to}".format( - copy_from=copy_from, - copy_to=copy_to or "", - ) + "Copying %r to %r", + copy_from, + copy_to or "", ) model = await ensure_async(self.contents_manager.copy(copy_from, copy_to)) self.set_status(201) diff --git a/jupyter_server/services/events/handlers.py b/jupyter_server/services/events/handlers.py index 82265ae0e4..fbc007341d 100644 --- a/jupyter_server/services/events/handlers.py +++ b/jupyter_server/services/events/handlers.py @@ -7,7 +7,7 @@ import json from datetime import datetime -from typing import TYPE_CHECKING, Any, Dict, Optional, cast +from typing import TYPE_CHECKING, Any, Optional, cast from jupyter_core.utils import ensure_async from tornado import web, websocket @@ -71,12 +71,25 @@ def on_close(self): self.event_logger.remove_listener(listener=self.event_listener) -def validate_model(data: dict[str, Any]) -> None: - """Validates for required fields in the JSON request body""" +def validate_model( + data: dict[str, Any], registry: jupyter_events.schema_registry.SchemaRegistry +) -> None: + """Validates for required fields in the JSON request body and verifies that + a registered schema/version exists""" required_keys = {"schema_id", "version", "data"} for key in required_keys: if key not in data: - raise web.HTTPError(400, f"Missing `{key}` in the JSON request body.") + message = f"Missing `{key}` in the JSON request body." + raise Exception(message) + schema_id = cast(str, data.get("schema_id")) + # The case where a given schema_id isn't found, + # jupyter_events raises a useful error, so there's no need to + # handle that case here. + schema = registry.get(schema_id) + version = str(cast(str, data.get("version"))) + if schema.version != version: + message = f"Unregistered version: {version!r}≠{schema.version!r} for `{schema_id}`" + raise Exception(message) def get_timestamp(data: dict[str, Any]) -> Optional[datetime]: @@ -111,18 +124,18 @@ async def post(self): raise web.HTTPError(400, "No JSON data provided") try: - validate_model(payload) + validate_model(payload, self.event_logger.schemas) self.event_logger.emit( schema_id=cast(str, payload.get("schema_id")), - data=cast("Dict[str, Any]", payload.get("data")), + data=cast("dict[str, Any]", payload.get("data")), timestamp_override=get_timestamp(payload), ) self.set_status(204) self.finish() - except web.HTTPError: - raise except Exception as e: - raise web.HTTPError(500, str(e)) from e + # All known exceptions are raised by bad requests, e.g., bad + # version, unregistered schema, invalid emission data payload, etc. + raise web.HTTPError(400, str(e)) from e default_handlers = [ diff --git a/jupyter_server/services/kernels/connection/abc.py b/jupyter_server/services/kernels/connection/abc.py index 71f9e8254f..61e11a948e 100644 --- a/jupyter_server/services/kernels/connection/abc.py +++ b/jupyter_server/services/kernels/connection/abc.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from typing import Any, List +from typing import Any class KernelWebsocketConnectionABC(ABC): @@ -25,5 +25,5 @@ def handle_incoming_message(self, incoming_msg: str) -> None: """Broker the incoming websocket message to the appropriate ZMQ channel.""" @abstractmethod - def handle_outgoing_message(self, stream: str, outgoing_msg: List[Any]) -> None: + def handle_outgoing_message(self, stream: str, outgoing_msg: list[Any]) -> None: """Broker outgoing ZMQ messages to the kernel websocket.""" diff --git a/jupyter_server/services/kernels/connection/base.py b/jupyter_server/services/kernels/connection/base.py index a0e0bae8b8..6af10444b5 100644 --- a/jupyter_server/services/kernels/connection/base.py +++ b/jupyter_server/services/kernels/connection/base.py @@ -2,7 +2,7 @@ import json import struct -from typing import Any, List +from typing import Any from jupyter_client.session import Session from tornado.websocket import WebSocketHandler @@ -89,7 +89,7 @@ def serialize_msg_to_ws_v1(msg_or_list, channel, pack=None): else: msg_list = msg_or_list channel = channel.encode("utf-8") - offsets: List[Any] = [] + offsets: list[Any] = [] offsets.append(8 * (1 + 1 + len(msg_list) + 1)) offsets.append(len(channel) + offsets[-1]) for msg in msg_list: @@ -173,7 +173,7 @@ def handle_incoming_message(self, incoming_msg: str) -> None: """Handle an incoming message.""" raise NotImplementedError - def handle_outgoing_message(self, stream: str, outgoing_msg: List[Any]) -> None: + def handle_outgoing_message(self, stream: str, outgoing_msg: list[Any]) -> None: """Handle an outgoing message.""" raise NotImplementedError diff --git a/jupyter_server/services/sessions/handlers.py b/jupyter_server/services/sessions/handlers.py index 3e013c0335..2d62c21b5a 100644 --- a/jupyter_server/services/sessions/handlers.py +++ b/jupyter_server/services/sessions/handlers.py @@ -161,14 +161,30 @@ async def patch(self, session_id): changes["kernel_id"] = kernel_id elif model["kernel"].get("name") is not None: kernel_name = model["kernel"]["name"] - kernel_id = await sm.start_kernel_for_session( - session_id, - kernel_name=kernel_name, - name=before["name"], - path=before["path"], - type=before["type"], - ) - changes["kernel_id"] = kernel_id + + try: + kernel_id = await sm.start_kernel_for_session( + session_id, + kernel_name=kernel_name, + name=before["name"], + path=before["path"], + type=before["type"], + ) + changes["kernel_id"] = kernel_id + except Exception as e: + # the error message may contain sensitive information, so we want to + # be careful with it, thus we only give the short repr of the exception + # and the full traceback. + # this should be fine as we are exposing here the same info as when we start a new kernel + msg = "The '%s' kernel could not be started: %s" % ( + kernel_name, + repr(str(e)), + ) + status_msg = "Error starting kernel %s" % kernel_name + self.log.error("Error starting kernel: %s", kernel_name) + self.set_status(501) + self.finish(json.dumps({"message": msg, "short_message": status_msg})) + return await sm.update_session(session_id, **changes) s_model = await sm.get_session(session_id=session_id) diff --git a/jupyter_server/services/sessions/sessionmanager.py b/jupyter_server/services/sessions/sessionmanager.py index 8b392b4e1b..3aac78a0a9 100644 --- a/jupyter_server/services/sessions/sessionmanager.py +++ b/jupyter_server/services/sessions/sessionmanager.py @@ -5,7 +5,7 @@ import os import pathlib import uuid -from typing import Any, Dict, List, NewType, Optional, Union, cast +from typing import Any, NewType, Optional, Union, cast KernelName = NewType("KernelName", str) ModelName = NewType("ModelName", str) @@ -100,7 +100,7 @@ class KernelSessionRecordList: it will be appended. """ - _records: List[KernelSessionRecord] + _records: list[KernelSessionRecord] def __init__(self, *records: KernelSessionRecord): """Initialize a record list.""" @@ -267,7 +267,7 @@ async def create_session( type: Optional[str] = None, kernel_name: Optional[KernelName] = None, kernel_id: Optional[str] = None, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Creates a session and returns its model Parameters @@ -291,11 +291,11 @@ async def create_session( session_id, path=path, name=name, type=type, kernel_id=kernel_id ) self._pending_sessions.remove(record) - return cast(Dict[str, Any], result) + return cast(dict[str, Any], result) def get_kernel_env( self, path: Optional[str], name: Optional[ModelName] = None - ) -> Dict[str, str]: + ) -> dict[str, str]: """Return the environment variables that need to be set in the kernel Parameters diff --git a/jupyter_server/utils.py b/jupyter_server/utils.py index 0c987bff25..d83e1be880 100644 --- a/jupyter_server/utils.py +++ b/jupyter_server/utils.py @@ -13,7 +13,7 @@ from _frozen_importlib_external import _NamespacePath from contextlib import contextmanager from pathlib import Path -from typing import Any, Generator, NewType, Sequence +from typing import TYPE_CHECKING, Any, NewType from urllib.parse import ( SplitResult, quote, @@ -32,6 +32,9 @@ from tornado.httpclient import AsyncHTTPClient, HTTPClient, HTTPRequest, HTTPResponse from tornado.netutil import Resolver +if TYPE_CHECKING: + from collections.abc import Generator, Sequence + ApiPath = NewType("ApiPath", str) # Re-export @@ -378,17 +381,9 @@ def filefind(filename: str, path_dirs: Sequence[str]) -> str: # os.path.abspath resolves '..', but Path.absolute() doesn't # Path.resolve() does, but traverses symlinks, which we don't want test_path = Path(os.path.abspath(test_path)) - if sys.version_info >= (3, 9): - if not test_path.is_relative_to(path): - # points outside root, e.g. via `filename='../foo'` - continue - else: - # is_relative_to is new in 3.9 - try: - test_path.relative_to(path) - except ValueError: - # points outside root, e.g. via `filename='../foo'` - continue + if not test_path.is_relative_to(path): + # points outside root, e.g. via `filename='../foo'` + continue # make sure we don't call is_file before we know it's a file within a prefix # GHSA-hrw6-wg82-cm62 - can leak password hash on windows. if test_path.is_file(): diff --git a/pyproject.toml b/pyproject.toml index 9bfcc74eef..e04983380d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ classifiers = [ "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", ] -requires-python = ">=3.8" +requires-python = ">=3.9" dependencies = [ "anyio>=3.1.0", "argon2-cffi>=21.1", @@ -40,14 +40,14 @@ dependencies = [ "tornado>=6.2.0", "traitlets>=5.6.0", "websocket-client>=1.7", - "jupyter_events>=0.9.0", + "jupyter_events>=0.11.0", "overrides>=5.0" ] [project.urls] Homepage = "https://jupyter-server.readthedocs.io" Documentation = "https://jupyter-server.readthedocs.io" -Funding = "https://numfocus.org/donate" +Funding = "https://jupyter.org/about#donate" Source = "https://github.com/jupyter-server/jupyter_server" Tracker = "https://github.com/jupyter-server/jupyter_server/issues" @@ -142,6 +142,7 @@ extend-select = [ "B", # flake8-bugbear "I", # isort "UP", # pyupgrade + "G001", # no % or f formatting in logs, prevents sttructured logging ] unfixable = [ # Don't touch print statements @@ -178,7 +179,10 @@ filterwarnings = [ "error", "ignore:datetime.datetime.utc:DeprecationWarning", "module:add_callback_from_signal is deprecated:DeprecationWarning", - "ignore::jupyter_server.utils.JupyterServerAuthWarning" + "ignore::jupyter_server.utils.JupyterServerAuthWarning", + + # ignore unclosed sqlite in traits + "ignore:unclosed database in Tuple[Dict, str]: +) -> tuple[dict, str]: cm = jp_contents_manager model = await ensure_async(cm.new_untitled(type="notebook")) name = model["name"] @@ -983,9 +983,10 @@ async def test_nb_validation(jp_contents_manager): # successful methods and ensure that calls to the aliased "validate_nb" are # zero. Note that since patching side-effects the validation error case, we'll # skip call-count assertions for that portion of the test. - with patch("nbformat.validate") as mock_validate, patch( - "jupyter_server.services.contents.manager.validate_nb" - ) as mock_validate_nb: + with ( + patch("nbformat.validate") as mock_validate, + patch("jupyter_server.services.contents.manager.validate_nb") as mock_validate_nb, + ): # Valid notebook, save, then get model = await ensure_async(cm.save(model, path)) assert "message" not in model diff --git a/tests/services/events/mock_event.yaml b/tests/services/events/mock_event.yaml index dabaa23db5..bf73915fb0 100644 --- a/tests/services/events/mock_event.yaml +++ b/tests/services/events/mock_event.yaml @@ -1,5 +1,5 @@ $id: http://event.mock.jupyter.org/message -version: 1 +version: "1" title: Message description: | Emit a message diff --git a/tests/services/events/mockextension/mock_extension_event.yaml b/tests/services/events/mockextension/mock_extension_event.yaml index b7c03d1a48..7354d6a094 100644 --- a/tests/services/events/mockextension/mock_extension_event.yaml +++ b/tests/services/events/mockextension/mock_extension_event.yaml @@ -1,5 +1,5 @@ $id: http://event.mockextension.jupyter.org/message -version: 1 +version: "1" title: Message description: | Emit a message diff --git a/tests/services/events/test_api.py b/tests/services/events/test_api.py index 5311f0860b..40ad8b137b 100644 --- a/tests/services/events/test_api.py +++ b/tests/services/events/test_api.py @@ -45,7 +45,7 @@ async def test_subscribe_websocket(event_logger, jp_ws_fetch): payload_1 = """\ { "schema_id": "http://event.mock.jupyter.org/message", - "version": 1, + "version": "1", "data": { "event_message": "Hello, world!" }, @@ -56,7 +56,7 @@ async def test_subscribe_websocket(event_logger, jp_ws_fetch): payload_2 = """\ { "schema_id": "http://event.mock.jupyter.org/message", - "version": 1, + "version": "1", "data": { "event_message": "Hello, world!" } @@ -92,7 +92,7 @@ async def test_post_event(jp_fetch, event_logger_sink, payload): payload_4 = """\ { - "version": 1, + "version": "1", "data": { "event_message": "Hello, world!" } @@ -102,14 +102,14 @@ async def test_post_event(jp_fetch, event_logger_sink, payload): payload_5 = """\ { "schema_id": "http://event.mock.jupyter.org/message", - "version": 1 + "version": "1" } """ payload_6 = """\ { "schema_id": "event.mock.jupyter.org/message", - "version": 1, + "version": "1", "data": { "event_message": "Hello, world!" }, @@ -117,39 +117,43 @@ async def test_post_event(jp_fetch, event_logger_sink, payload): } """ - -@pytest.mark.parametrize("payload", [payload_3, payload_4, payload_5, payload_6]) -async def test_post_event_400(jp_fetch, event_logger, payload): - with pytest.raises(tornado.httpclient.HTTPClientError) as e: - await jp_fetch("api", "events", method="POST", body=payload) - - assert expected_http_error(e, 400) - - payload_7 = """\ +{ + "schema_id": "http://event.mock.jupyter.org/UNREGISTERED-SCHEMA", + "version": "1", + "data": { + "event_message": "Hello, world!" + } +} +""" + +payload_8 = """\ { "schema_id": "http://event.mock.jupyter.org/message", - "version": 1, + "version": "1", "data": { "message": "Hello, world!" } } """ -payload_8 = """\ +payload_9 = """\ { "schema_id": "http://event.mock.jupyter.org/message", "version": 2, "data": { - "message": "Hello, world!" + "event_message": "Hello, world!" } } """ -@pytest.mark.parametrize("payload", [payload_7, payload_8]) -async def test_post_event_500(jp_fetch, event_logger, payload): +@pytest.mark.parametrize( + "payload", + [payload_3, payload_4, payload_5, payload_6, payload_7, payload_8, payload_9], +) +async def test_post_event_400(jp_fetch, event_logger, payload): with pytest.raises(tornado.httpclient.HTTPClientError) as e: await jp_fetch("api", "events", method="POST", body=payload) - assert expected_http_error(e, 500) + assert expected_http_error(e, 400) diff --git a/tests/test_gateway.py b/tests/test_gateway.py index 569268d833..00aa64f111 100644 --- a/tests/test_gateway.py +++ b/tests/test_gateway.py @@ -10,7 +10,7 @@ from http.cookies import SimpleCookie from io import BytesIO from queue import Empty -from typing import Any, Dict, Union +from typing import Any, Union from unittest.mock import MagicMock, patch import pytest @@ -74,7 +74,7 @@ def generate_kernelspec(name): # # This is used to simulate inconsistency in list results from the Gateway server # due to issues like race conditions, bugs, etc. -omitted_kernels: Dict[str, bool] = {} +omitted_kernels: dict[str, bool] = {} def generate_model(name): diff --git a/tests/test_serverapp.py b/tests/test_serverapp.py index 91fa33230c..f836d1b2f8 100644 --- a/tests/test_serverapp.py +++ b/tests/test_serverapp.py @@ -154,8 +154,9 @@ async def test_generate_config(tmp_path, jp_configurable_serverapp): def test_server_password(tmp_path, jp_configurable_serverapp): password = "secret" - with patch.dict("os.environ", {"JUPYTER_CONFIG_DIR": str(tmp_path)}), patch.object( - getpass, "getpass", return_value=password + with ( + patch.dict("os.environ", {"JUPYTER_CONFIG_DIR": str(tmp_path)}), + patch.object(getpass, "getpass", return_value=password), ): app = JupyterPasswordApp(log_level=logging.ERROR) app.initialize([])