From f2d40f692c29e155349b179d57a454e7f23ea898 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Jul 2024 19:33:03 -0500 Subject: [PATCH 01/23] chore: update pre-commit hooks (#1441) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Steven Silvester --- .pre-commit-config.yaml | 6 +++--- tests/test_utils.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 39cf8836fe..0d72a2d239 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.28.4 + rev: 0.28.6 hooks: - id: check-github-workflows @@ -52,7 +52,7 @@ repos: - id: rst-inline-touching-normal - repo: https://github.com/pre-commit/mirrors-mypy - rev: "v1.10.0" + rev: "v1.10.1" hooks: - id: mypy files: jupyter_server @@ -61,7 +61,7 @@ repos: ["traitlets>=5.13", "jupyter_core>=5.5", "jupyter_client>=8.5"] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.4.7 + rev: v0.5.0 hooks: - id: ruff types_or: [python, jupyter] diff --git a/tests/test_utils.py b/tests/test_utils.py index fdbfd60587..90f9dcd340 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -160,7 +160,7 @@ def test_filefind(tmp_path, filename, result): if isinstance(result, str): found = filefind(filename, [str(a), str(b)]) found_relative = Path(found).relative_to(tmp_path) - assert str(found_relative) == result + assert str(found_relative).replace(os.sep, "/") == result else: with pytest.raises(result): filefind(filename, [str(a), str(b)]) From 5be0a9aae6979dd375cbcfff89df9cdc40504230 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Krassowski?= <5832902+krassowski@users.noreply.github.com> Date: Wed, 10 Jul 2024 10:35:22 +0100 Subject: [PATCH 02/23] Update documentation for `cookie_secret` (#1433) --- jupyter_server/serverapp.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/jupyter_server/serverapp.py b/jupyter_server/serverapp.py index ed0738976a..3dedd5634f 100644 --- a/jupyter_server/serverapp.py +++ b/jupyter_server/serverapp.py @@ -1139,8 +1139,9 @@ def _default_cookie_secret_file(self) -> str: b"", config=True, help="""The random bytes used to secure cookies. - By default this is a new random number every time you start the server. - Set it to a value in a config file to enable logins to persist across server sessions. + By default this is generated on first start of the server and persisted across server + sessions by writing the cookie secret into the `cookie_secret_file` file. + When using an executable config file you can override this to be random at each server restart. Note: Cookie secrets should be kept private, do not share config files with cookie_secret stored in plaintext (you can read the value from a file). From 094067107897ef8d7cd452c3a5beecb8c2a1a43e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Krassowski?= <5832902+krassowski@users.noreply.github.com> Date: Wed, 10 Jul 2024 10:36:32 +0100 Subject: [PATCH 03/23] Do not log environment variables passed to kernels (#1437) --- jupyter_server/services/kernels/kernelmanager.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/jupyter_server/services/kernels/kernelmanager.py b/jupyter_server/services/kernels/kernelmanager.py index 5b0d09aab2..cc6ec8edc6 100644 --- a/jupyter_server/services/kernels/kernelmanager.py +++ b/jupyter_server/services/kernels/kernelmanager.py @@ -243,7 +243,12 @@ async def _async_start_kernel( # type:ignore[override] kernel.reason = "" # type:ignore[attr-defined] kernel.last_activity = utcnow() # type:ignore[attr-defined] self.log.info("Kernel started: %s", kernel_id) - self.log.debug("Kernel args: %r", kwargs) + self.log.debug( + "Kernel args (excluding env): %r", {k: v for k, v in kwargs.items() if k != "env"} + ) + env = kwargs.get("env", None) + if env and isinstance(env, dict): # type:ignore[unreachable] + self.log.debug("Kernel argument 'env' passed with: %r", list(env.keys())) # type:ignore[unreachable] # Increase the metric of number of kernels running # for the relevant kernel type by 1 From b961d4eb499071c0c60e24f429c20d1e6a908a32 Mon Sep 17 00:00:00 2001 From: Gonzalo Gasca Meza Date: Fri, 12 Jul 2024 09:42:06 -0700 Subject: [PATCH 04/23] Pass session_id during Websocket connect (#1440) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- jupyter_server/gateway/connections.py | 2 ++ tests/test_gateway.py | 36 +++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/jupyter_server/gateway/connections.py b/jupyter_server/gateway/connections.py index 8027a822cc..d4dde730fa 100644 --- a/jupyter_server/gateway/connections.py +++ b/jupyter_server/gateway/connections.py @@ -47,6 +47,8 @@ async def connect(self): url_escape(self.kernel_id), "channels", ) + if self.session_id: + ws_url += f"?session_id={url_escape(self.session_id)}" self.log.info(f"Connecting to {ws_url}") kwargs: dict[str, Any] = {} kwargs = GatewayClient.instance().load_connection_args(**kwargs) diff --git a/tests/test_gateway.py b/tests/test_gateway.py index f0033c278e..569268d833 100644 --- a/tests/test_gateway.py +++ b/tests/test_gateway.py @@ -200,6 +200,7 @@ async def mock_gateway_request(url, **kwargs): mocked_gateway = patch("jupyter_server.gateway.managers.gateway_request", mock_gateway_request) +mock_gateway_ws_url = "ws://mock-gateway-server:8889" mock_gateway_url = "http://mock-gateway-server:8889" mock_http_user = "alice" @@ -733,6 +734,41 @@ async def test_websocket_connection_closed(init_gateway, jp_serverapp, jp_fetch, pytest.fail(f"Logs contain an error: {message}") +@patch("tornado.websocket.websocket_connect", mock_websocket_connect()) +async def test_websocket_connection_with_session_id(init_gateway, jp_serverapp, jp_fetch, caplog): + # Create the session and kernel and get the kernel manager... + kernel_id = await create_kernel(jp_fetch, "kspec_foo") + km: GatewayKernelManager = jp_serverapp.kernel_manager.get_kernel(kernel_id) + + # Create the KernelWebsocketHandler... + request = HTTPServerRequest("foo", "GET") + request.connection = MagicMock() + handler = KernelWebsocketHandler(jp_serverapp.web_app, request) + # Create the GatewayWebSocketConnection and attach it to the handler... + with mocked_gateway: + conn = GatewayWebSocketConnection(parent=km, websocket_handler=handler) + handler.connection = conn + await conn.connect() + assert conn.session_id != None + expected_ws_url = ( + f"{mock_gateway_ws_url}/api/kernels/{kernel_id}/channels?session_id={conn.session_id}" + ) + assert ( + expected_ws_url in caplog.text + ), "WebSocket URL does not contain the expected session_id." + + # Processing websocket messages happens in separate coroutines and any + # errors in that process will show up in logs, but not bubble up to the + # caller. + # + # To check for these, we wait for the server to stop and then check the + # logs for errors. + await jp_serverapp._cleanup() + for _, level, message in caplog.record_tuples: + if level >= logging.ERROR: + pytest.fail(f"Logs contain an error: {message}") + + # # Test methods below... # From 535d7df3c0b358e31f514842c5509ca6ce64f056 Mon Sep 17 00:00:00 2001 From: Zsailer Date: Fri, 12 Jul 2024 18:31:30 +0000 Subject: [PATCH 05/23] Publish 2.14.2 SHA256 hashes: jupyter_server-2.14.2-py3-none-any.whl: 47ff506127c2f7851a17bf4713434208fc490955d0e8632e95014a9a9afbeefd jupyter_server-2.14.2.tar.gz: 66095021aa9638ced276c248b1d81862e4c50f292d575920bbe960de1c56b12b --- CHANGELOG.md | 30 ++++++++++++++++++++++++++++-- jupyter_server/_version.py | 2 +- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fa88c9b00..cadf2e8a93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,34 @@ All notable changes to this project will be documented in this file. +## 2.14.2 + +([Full Changelog](https://github.com/jupyter-server/jupyter_server/compare/v2.14.1...b961d4eb499071c0c60e24f429c20d1e6a908a32)) + +### Bugs fixed + +- 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)) + +### Maintenance and upkeep improvements + +- 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)) + +### Documentation improvements + +- 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)) + +### Contributors to this release + +([GitHub contributors page for this release](https://github.com/jupyter-server/jupyter_server/graphs/contributors?from=2024-05-31&to=2024-07-12&type=c)) + +[@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)) @@ -27,8 +55,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-04-11..2024-05-31&type=Issues) | [@lresende](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Alresende+updated%3A2024-04-11..2024-05-31&type=Issues) | [@pre-commit-ci](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Apre-commit-ci+updated%3A2024-04-11..2024-05-31&type=Issues) - - ## 2.14.0 ([Full Changelog](https://github.com/jupyter-server/jupyter_server/compare/v2.13.0...074628806d6b2ec3304d60ab5cfba1c326f67730)) diff --git a/jupyter_server/_version.py b/jupyter_server/_version.py index fa814fbb2f..141e68282f 100644 --- a/jupyter_server/_version.py +++ b/jupyter_server/_version.py @@ -7,7 +7,7 @@ from typing import List # Version string must appear intact for automatic versioning -__version__ = "2.15.0.dev0" +__version__ = "2.14.2" # Build up version_info tuple for backwards compatibility pattern = r"(?P\d+).(?P\d+).(?P\d+)(?P.*)" From 8fe5b58791472ae5c02de6448813d49b40c9bc85 Mon Sep 17 00:00:00 2001 From: Zsailer Date: Fri, 12 Jul 2024 18:31:32 +0000 Subject: [PATCH 06/23] Bump to 2.15.0.dev0 --- jupyter_server/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyter_server/_version.py b/jupyter_server/_version.py index 141e68282f..fa814fbb2f 100644 --- a/jupyter_server/_version.py +++ b/jupyter_server/_version.py @@ -7,7 +7,7 @@ from typing import List # Version string must appear intact for automatic versioning -__version__ = "2.14.2" +__version__ = "2.15.0.dev0" # Build up version_info tuple for backwards compatibility pattern = r"(?P\d+).(?P\d+).(?P\d+)(?P.*)" From a6d2d350486b73d8f5de053dc547d781d6431d1c Mon Sep 17 00:00:00 2001 From: Omar Jarjur Date: Thu, 18 Jul 2024 09:34:00 -0700 Subject: [PATCH 07/23] Improve the busy/idle execution state tracking for kernels. (#1429) --- .../services/kernels/kernelmanager.py | 64 +++++++- tests/services/kernels/test_cull.py | 79 ++++++++++ .../services/kernels/test_execution_state.py | 146 ++++++++++++++++++ 3 files changed, 281 insertions(+), 8 deletions(-) create mode 100644 tests/services/kernels/test_execution_state.py diff --git a/jupyter_server/services/kernels/kernelmanager.py b/jupyter_server/services/kernels/kernelmanager.py index cc6ec8edc6..cd8a9de71f 100644 --- a/jupyter_server/services/kernels/kernelmanager.py +++ b/jupyter_server/services/kernels/kernelmanager.py @@ -232,11 +232,7 @@ async def _async_start_kernel( # type:ignore[override] kwargs["kernel_id"] = kernel_id kernel_id = await self.pinned_superclass._async_start_kernel(self, **kwargs) self._kernel_connections[kernel_id] = 0 - task = asyncio.create_task(self._finish_kernel_start(kernel_id)) - if not getattr(self, "use_pending_kernels", None): - await task - else: - self._pending_kernel_tasks[kernel_id] = task + # add busy/activity markers: kernel = self.get_kernel(kernel_id) kernel.execution_state = "starting" # type:ignore[attr-defined] @@ -250,6 +246,12 @@ async def _async_start_kernel( # type:ignore[override] if env and isinstance(env, dict): # type:ignore[unreachable] self.log.debug("Kernel argument 'env' passed with: %r", list(env.keys())) # type:ignore[unreachable] + task = asyncio.create_task(self._finish_kernel_start(kernel_id)) + if not getattr(self, "use_pending_kernels", None): + await task + else: + self._pending_kernel_tasks[kernel_id] = task + # Increase the metric of number of kernels running # for the relevant kernel type by 1 KERNEL_CURRENTLY_RUNNING_TOTAL.labels(type=self._kernels[kernel_id].kernel_name).inc() @@ -537,6 +539,40 @@ def _check_kernel_id(self, kernel_id): raise web.HTTPError(404, "Kernel does not exist: %s" % kernel_id) # monitoring activity: + untracked_message_types = List( + trait=Unicode(), + config=True, + default_value=[ + "comm_info_request", + "comm_info_reply", + "kernel_info_request", + "kernel_info_reply", + "shutdown_request", + "shutdown_reply", + "interrupt_request", + "interrupt_reply", + "debug_request", + "debug_reply", + "stream", + "display_data", + "update_display_data", + "execute_input", + "execute_result", + "error", + "status", + "clear_output", + "debug_event", + "input_request", + "input_reply", + ], + help="""List of kernel message types excluded from user activity tracking. + + This should be a superset of the message types sent on any channel other + than the shell channel.""", + ) + + def track_message_type(self, message_type): + return message_type not in self.untracked_message_types def start_watching_activity(self, kernel_id): """Start watching IOPub messages on a kernel for activity. @@ -557,15 +593,27 @@ def start_watching_activity(self, kernel_id): def record_activity(msg_list): """Record an IOPub message arriving from a kernel""" - self.last_kernel_activity = kernel.last_activity = utcnow() - idents, fed_msg_list = session.feed_identities(msg_list) msg = session.deserialize(fed_msg_list, content=False) msg_type = msg["header"]["msg_type"] + parent_msg_type = msg.get("parent_header", {}).get("msg_type", None) + if ( + self.track_message_type(msg_type) + or self.track_message_type(parent_msg_type) + or kernel.execution_state == "busy" + ): + self.last_kernel_activity = kernel.last_activity = utcnow() if msg_type == "status": msg = session.deserialize(fed_msg_list) - kernel.execution_state = msg["content"]["execution_state"] + execution_state = msg["content"]["execution_state"] + if self.track_message_type(parent_msg_type): + kernel.execution_state = execution_state + elif kernel.execution_state == "starting" and execution_state != "starting": + # We always normalize post-starting execution state to "idle" + # unless we know that the status is in response to one of our + # tracked message types. + kernel.execution_state = "idle" self.log.debug( "activity on %s: %s (%s)", kernel_id, diff --git a/tests/services/kernels/test_cull.py b/tests/services/kernels/test_cull.py index 50ecbf2b96..5b0b8fd9a0 100644 --- a/tests/services/kernels/test_cull.py +++ b/tests/services/kernels/test_cull.py @@ -1,7 +1,9 @@ import asyncio +import datetime import json import os import platform +import uuid import warnings import jupyter_client @@ -94,6 +96,83 @@ async def test_cull_idle(jp_fetch, jp_ws_fetch): assert culled +@pytest.mark.parametrize( + "jp_server_config", + [ + # Test the synchronous case + Config( + { + "ServerApp": { + "kernel_manager_class": "jupyter_server.services.kernels.kernelmanager.MappingKernelManager", + "MappingKernelManager": { + "cull_idle_timeout": CULL_TIMEOUT, + "cull_interval": CULL_INTERVAL, + "cull_connected": True, + }, + } + } + ), + # Test the async case + Config( + { + "ServerApp": { + "kernel_manager_class": "jupyter_server.services.kernels.kernelmanager.AsyncMappingKernelManager", + "AsyncMappingKernelManager": { + "cull_idle_timeout": CULL_TIMEOUT, + "cull_interval": CULL_INTERVAL, + "cull_connected": True, + }, + } + } + ), + ], +) +async def test_cull_connected(jp_fetch, jp_ws_fetch): + r = await jp_fetch("api", "kernels", method="POST", allow_nonstandard_methods=True) + kernel = json.loads(r.body.decode()) + kid = kernel["id"] + + # Open a websocket connection. + ws = await jp_ws_fetch("api", "kernels", kid, "channels") + session_id = uuid.uuid1().hex + message_id = uuid.uuid1().hex + await ws.write_message( + json.dumps( + { + "channel": "shell", + "header": { + "date": datetime.datetime.now(tz=datetime.timezone.utc).isoformat(), + "session": session_id, + "msg_id": message_id, + "msg_type": "execute_request", + "username": "", + "version": "5.2", + }, + "parent_header": {}, + "metadata": {}, + "content": { + "code": f"import time\ntime.sleep({CULL_TIMEOUT-1})", + "silent": False, + "allow_stdin": False, + "stop_on_error": True, + }, + "buffers": [], + } + ) + ) + + r = await jp_fetch("api", "kernels", kid, method="GET") + model = json.loads(r.body.decode()) + assert model["connections"] == 1 + culled = await get_cull_status( + kid, jp_fetch + ) # connected, but code cell still running. Should not be culled + assert not culled + culled = await get_cull_status(kid, jp_fetch) # still connected, but idle... should be culled + assert culled + ws.close() + + async def test_cull_idle_disable(jp_fetch, jp_ws_fetch, jp_kernelspec_with_metadata): r = await jp_fetch("api", "kernels", method="POST", allow_nonstandard_methods=True) kernel = json.loads(r.body.decode()) diff --git a/tests/services/kernels/test_execution_state.py b/tests/services/kernels/test_execution_state.py new file mode 100644 index 0000000000..50155ec76f --- /dev/null +++ b/tests/services/kernels/test_execution_state.py @@ -0,0 +1,146 @@ +import asyncio +import datetime +import json +import os +import platform +import time +import uuid +import warnings + +import jupyter_client +import pytest +from flaky import flaky +from tornado.httpclient import HTTPClientError +from traitlets.config import Config + +MAX_POLL_ATTEMPTS = 10 +POLL_INTERVAL = 1 +MINIMUM_CONSISTENT_COUNT = 4 + + +@flaky +async def test_execution_state(jp_fetch, jp_ws_fetch): + r = await jp_fetch("api", "kernels", method="POST", allow_nonstandard_methods=True) + kernel = json.loads(r.body.decode()) + kid = kernel["id"] + + # Open a websocket connection. + ws = await jp_ws_fetch("api", "kernels", kid, "channels") + session_id = uuid.uuid1().hex + message_id = uuid.uuid1().hex + await ws.write_message( + json.dumps( + { + "channel": "shell", + "header": { + "date": datetime.datetime.now(tz=datetime.timezone.utc).isoformat(), + "session": session_id, + "msg_id": message_id, + "msg_type": "execute_request", + "username": "", + "version": "5.2", + }, + "parent_header": {}, + "metadata": {}, + "content": { + "code": "while True:\n\tpass", + "silent": False, + "allow_stdin": False, + "stop_on_error": True, + }, + "buffers": [], + } + ) + ) + await poll_for_parent_message_status(kid, message_id, "busy", ws) + es = await get_execution_state(kid, jp_fetch) + assert es == "busy" + + message_id_2 = uuid.uuid1().hex + await ws.write_message( + json.dumps( + { + "channel": "control", + "header": { + "date": datetime.datetime.now(tz=datetime.timezone.utc).isoformat(), + "session": session_id, + "msg_id": message_id_2, + "msg_type": "debug_request", + "username": "", + "version": "5.2", + }, + "parent_header": {}, + "metadata": {}, + "content": { + "type": "request", + "command": "debugInfo", + }, + "buffers": [], + } + ) + ) + await poll_for_parent_message_status(kid, message_id_2, "idle", ws) + es = await get_execution_state(kid, jp_fetch) + + # Verify that the overall kernel status is still "busy" even though one + # "idle" response was already seen for the second execute request. + assert es == "busy" + + await jp_fetch( + "api", + "kernels", + kid, + "interrupt", + method="POST", + allow_nonstandard_methods=True, + ) + + await poll_for_parent_message_status(kid, message_id, "idle", ws) + es = await get_execution_state(kid, jp_fetch) + assert es == "idle" + ws.close() + + +async def get_execution_state(kid, jp_fetch): + # There is an inherent race condition when getting the kernel execution status + # where we might fetch the status right before an expected state change occurs. + # + # To work-around this, we don't return the status until we've been able to fetch + # it twice in a row and get the same result both times. + last_execution_states = [] + + for _ in range(MAX_POLL_ATTEMPTS): + r = await jp_fetch("api", "kernels", kid, method="GET") + model = json.loads(r.body.decode()) + execution_state = model["execution_state"] + last_execution_states.append(execution_state) + consistent_count = 0 + last_execution_state = None + for es in last_execution_states: + if es != last_execution_state: + consistent_count = 0 + last_execution_state = es + consistent_count += 1 + if consistent_count >= MINIMUM_CONSISTENT_COUNT: + return es + time.sleep(POLL_INTERVAL) + + raise AssertionError("failed to get a consistent execution state") + + +async def poll_for_parent_message_status(kid, parent_message_id, target_status, ws): + while True: + resp = await ws.read_message() + resp_json = json.loads(resp) + print(resp_json) + parent_message = resp_json.get("parent_header", {}).get("msg_id", None) + if parent_message != parent_message_id: + continue + + response_type = resp_json.get("header", {}).get("msg_type", None) + if response_type != "status": + continue + + execution_state = resp_json.get("content", {}).get("execution_state", "") + if execution_state == target_status: + return From f37967ba7472d0cab4afeda3cc946ac244a3164e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Collonval?= Date: Thu, 18 Jul 2024 18:46:56 +0200 Subject: [PATCH 08/23] Better hash format error message (#1442) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- jupyter_server/services/contents/handlers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/jupyter_server/services/contents/handlers.py b/jupyter_server/services/contents/handlers.py index ad25a2d3f7..13e987809b 100644 --- a/jupyter_server/services/contents/handlers.py +++ b/jupyter_server/services/contents/handlers.py @@ -140,7 +140,9 @@ async def get(self, path=""): hash_str = self.get_query_argument("hash", default="0") if hash_str not in {"0", "1"}: - raise web.HTTPError(400, f"Content {hash_str!r} is invalid") + raise web.HTTPError( + 400, f"Hash argument {hash_str!r} is invalid. It must be '0' or '1'." + ) require_hash = int(hash_str) if not cm.allow_hidden and await ensure_async(cm.is_hidden(path)): From 74655ce66f36ed85a83591e6658e70ba91232580 Mon Sep 17 00:00:00 2001 From: Min RK Date: Thu, 1 Aug 2024 16:54:29 +0200 Subject: [PATCH 09/23] don't let ExtensionApp jpserver_extensions be overridden by config (#1447) --- jupyter_server/serverapp.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/jupyter_server/serverapp.py b/jupyter_server/serverapp.py index 3dedd5634f..bf375cbef6 100644 --- a/jupyter_server/serverapp.py +++ b/jupyter_server/serverapp.py @@ -2724,7 +2724,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, From 32679b9cbbc666dd4e5636755fcc70b7e04a809d Mon Sep 17 00:00:00 2001 From: Yuvi Panda Date: Mon, 4 Nov 2024 08:35:34 -0800 Subject: [PATCH 10/23] Add prometheus metric with version information (#1467) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- jupyter_server/prometheus/metrics.py | 31 ++++++++++++++++++++++------ jupyter_server/serverapp.py | 8 +++++++ 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/jupyter_server/prometheus/metrics.py b/jupyter_server/prometheus/metrics.py index 1a02f86209..0afcf6e23e 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,12 @@ ["type"], ) +# New prometheus metrics that do not exist in notebook v6 go here +SERVER_INFO = Info("jupyter_server", "Jupyter Server Version information") __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 bf375cbef6..26499c410e 100644 --- a/jupyter_server/serverapp.py +++ b/jupyter_server/serverapp.py @@ -110,6 +110,7 @@ GatewaySessionManager, ) from jupyter_server.log import log_request +from jupyter_server.prometheus.metrics import SERVER_INFO from jupyter_server.services.config import ConfigManager from jupyter_server.services.contents.filemanager import ( AsyncFileContentsManager, @@ -2696,6 +2697,12 @@ 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__}) + @catch_config_error def initialize( self, @@ -2763,6 +2770,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() From 0d3eeb07b0420100a71790198ccf36abd9ee83c0 Mon Sep 17 00:00:00 2001 From: Yuvi Panda Date: Mon, 4 Nov 2024 11:58:49 -0800 Subject: [PATCH 11/23] Add prometheus info metrics listing server extensions + versions (#1470) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- jupyter_server/prometheus/metrics.py | 5 +++++ jupyter_server/serverapp.py | 7 ++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/jupyter_server/prometheus/metrics.py b/jupyter_server/prometheus/metrics.py index 0afcf6e23e..ff71523299 100644 --- a/jupyter_server/prometheus/metrics.py +++ b/jupyter_server/prometheus/metrics.py @@ -53,6 +53,11 @@ # 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"], +) __all__ = [ "HTTP_REQUEST_DURATION_SECONDS", diff --git a/jupyter_server/serverapp.py b/jupyter_server/serverapp.py index 26499c410e..ca8bc98743 100644 --- a/jupyter_server/serverapp.py +++ b/jupyter_server/serverapp.py @@ -110,7 +110,7 @@ GatewaySessionManager, ) from jupyter_server.log import log_request -from jupyter_server.prometheus.metrics import SERVER_INFO +from jupyter_server.prometheus.metrics import SERVER_EXTENSION_INFO, SERVER_INFO from jupyter_server.services.config import ConfigManager from jupyter_server.services.contents.filemanager import ( AsyncFileContentsManager, @@ -2703,6 +2703,11 @@ def init_metrics(self) -> None: """ 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() + ) + @catch_config_error def initialize( self, From d3a6c60e266f958f05e0af453e78f11547b42790 Mon Sep 17 00:00:00 2001 From: Yuvi Panda Date: Mon, 4 Nov 2024 14:14:46 -0800 Subject: [PATCH 12/23] prometheus: Expose 3 activity metrics (#1471) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- jupyter_server/prometheus/metrics.py | 11 +++++++++++ jupyter_server/serverapp.py | 18 +++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/jupyter_server/prometheus/metrics.py b/jupyter_server/prometheus/metrics.py index ff71523299..c0aeb37568 100644 --- a/jupyter_server/prometheus/metrics.py +++ b/jupyter_server/prometheus/metrics.py @@ -58,6 +58,17 @@ "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", diff --git a/jupyter_server/serverapp.py b/jupyter_server/serverapp.py index ca8bc98743..2766509abc 100644 --- a/jupyter_server/serverapp.py +++ b/jupyter_server/serverapp.py @@ -110,7 +110,13 @@ GatewaySessionManager, ) from jupyter_server.log import log_request -from jupyter_server.prometheus.metrics import SERVER_EXTENSION_INFO, SERVER_INFO +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, @@ -2708,6 +2714,16 @@ def init_metrics(self) -> None: 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, From 432a9cc91b41ec3c2b7bef1620469b6d412de899 Mon Sep 17 00:00:00 2001 From: Afshin Taylor Darian Date: Mon, 4 Nov 2024 22:49:23 +0000 Subject: [PATCH 13/23] Return HTTP 400 when attempting to post an event with an unregistered schema (#1463) Co-authored-by: Zachary Sailer Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- jupyter_server/services/events/handlers.py | 27 +++++++++++++----- tests/services/events/test_api.py | 32 ++++++++++++---------- 2 files changed, 38 insertions(+), 21 deletions(-) diff --git a/jupyter_server/services/events/handlers.py b/jupyter_server/services/events/handlers.py index 82265ae0e4..41e3d0d53f 100644 --- a/jupyter_server/services/events/handlers.py +++ b/jupyter_server/services/events/handlers.py @@ -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 = int(cast(int, data.get("version"))) + if schema.version != version: + message = f"Unregistered version: {version}≠{schema.version} for `{schema_id}`" + raise Exception(message) def get_timestamp(data: dict[str, Any]) -> Optional[datetime]: @@ -111,7 +124,7 @@ 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")), @@ -119,10 +132,10 @@ async def post(self): ) 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/tests/services/events/test_api.py b/tests/services/events/test_api.py index 5311f0860b..49599e8380 100644 --- a/tests/services/events/test_api.py +++ b/tests/services/events/test_api.py @@ -117,16 +117,17 @@ 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, @@ -136,20 +137,23 @@ async def test_post_event_400(jp_fetch, event_logger, payload): } """ -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) From 045dc4631977952c84660f732bef9c21d0b6fa00 Mon Sep 17 00:00:00 2001 From: Yuvi Panda Date: Mon, 4 Nov 2024 16:32:01 -0800 Subject: [PATCH 14/23] Add a traitlet to disable recording HTTP request metrics (#1472) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Zachary Sailer --- jupyter_server/log.py | 8 ++++++-- jupyter_server/serverapp.py | 17 ++++++++++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/jupyter_server/log.py b/jupyter_server/log.py index aed024bb32..b1211114e9 100644 --- a/jupyter_server/log.py +++ b/jupyter_server/log.py @@ -41,13 +41,16 @@ def _scrub_uri(uri: str) -> 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/serverapp.py b/jupyter_server/serverapp.py index 2766509abc..8dd9d02515 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 @@ -410,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, @@ -1993,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=""" From e544fa1c8355fbdfb6437e99968367e56d10b7f3 Mon Sep 17 00:00:00 2001 From: Oliver Sanders Date: Tue, 5 Nov 2024 01:22:18 +0000 Subject: [PATCH 15/23] add comments to explain signal handling under jupyterhub (#1452) --- jupyter_server/serverapp.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/jupyter_server/serverapp.py b/jupyter_server/serverapp.py index 8dd9d02515..13fa256397 100644 --- a/jupyter_server/serverapp.py +++ b/jupyter_server/serverapp.py @@ -2410,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 @@ -2468,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) From 111e104f2a23bf72c9dd247e254940d6c714b1d0 Mon Sep 17 00:00:00 2001 From: M Bussonnier Date: Thu, 19 Dec 2024 04:14:01 -0800 Subject: [PATCH 16/23] Handle newer jupyter_events wants string version, drop 3.8 (#1481) --- .github/workflows/python-tests.yml | 8 ++++---- examples/simple/pyproject.toml | 4 ++-- jupyter_server/_version.py | 3 +-- jupyter_server/auth/authorizer.py | 4 +++- jupyter_server/base/call_context.py | 8 ++++---- jupyter_server/base/handlers.py | 3 ++- jupyter_server/config_manager.py | 2 +- .../event_schemas/contents_service/v1.yaml | 2 +- .../event_schemas/gateway_client/v1.yaml | 2 +- .../event_schemas/kernel_actions/v1.yaml | 2 +- jupyter_server/files/handlers.py | 5 ++++- jupyter_server/serverapp.py | 4 +--- jupyter_server/services/api/handlers.py | 6 +++--- jupyter_server/services/config/manager.py | 2 +- jupyter_server/services/contents/handlers.py | 4 ++-- jupyter_server/services/events/handlers.py | 8 ++++---- .../services/kernels/connection/abc.py | 4 ++-- .../services/kernels/connection/base.py | 6 +++--- .../services/sessions/sessionmanager.py | 10 +++++----- jupyter_server/utils.py | 19 +++++++------------ pyproject.toml | 6 +++--- tests/auth/test_authorizer.py | 2 +- tests/extension/mockextensions/app.py | 2 +- tests/extension/test_app.py | 1 + tests/services/api/test_api.py | 4 ++-- tests/services/contents/test_manager.py | 11 ++++++----- tests/services/events/mock_event.yaml | 2 +- .../mockextension/mock_extension_event.yaml | 2 +- tests/services/events/test_api.py | 14 +++++++------- tests/test_gateway.py | 4 ++-- tests/test_serverapp.py | 5 +++-- 31 files changed, 80 insertions(+), 79 deletions(-) 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/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/jupyter_server/_version.py b/jupyter_server/_version.py index fa814fbb2f..20f829fe92 100644 --- a/jupyter_server/_version.py +++ b/jupyter_server/_version.py @@ -4,7 +4,6 @@ """ import re -from typing import List # Version string must appear intact for automatic versioning __version__ = "2.15.0.dev0" @@ -13,7 +12,7 @@ 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 770fff1866..6f4977145d 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/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/serverapp.py b/jupyter_server/serverapp.py index 13fa256397..8aa3dbd082 100644 --- a/jupyter_server/serverapp.py +++ b/jupyter_server/serverapp.py @@ -2552,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.""" @@ -2718,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: 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..0a6110ee3d 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) """ diff --git a/jupyter_server/services/events/handlers.py b/jupyter_server/services/events/handlers.py index 41e3d0d53f..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 @@ -86,9 +86,9 @@ def validate_model( # jupyter_events raises a useful error, so there's no need to # handle that case here. schema = registry.get(schema_id) - version = int(cast(int, data.get("version"))) + version = str(cast(str, data.get("version"))) if schema.version != version: - message = f"Unregistered version: {version}≠{schema.version} for `{schema_id}`" + message = f"Unregistered version: {version!r}≠{schema.version!r} for `{schema_id}`" raise Exception(message) @@ -127,7 +127,7 @@ async def post(self): 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) 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/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..21f701a220 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,7 +40,7 @@ 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" ] @@ -208,7 +208,7 @@ pydist_resource_paths = ["jupyter_server/static/style/bootstrap.min.css", "jupyt post-version-spec = "dev" [tool.mypy] -python_version = "3.8" +python_version = "3.9" explicit_package_bases = true strict = true pretty = true diff --git a/tests/auth/test_authorizer.py b/tests/auth/test_authorizer.py index 3ba6bef9f1..59a6aa7428 100644 --- a/tests/auth/test_authorizer.py +++ b/tests/auth/test_authorizer.py @@ -3,7 +3,7 @@ import asyncio import json import os -from typing import Awaitable +from collections.abc import Awaitable import pytest from jupyter_client.kernelspec import NATIVE_KERNEL_NAME diff --git a/tests/extension/mockextensions/app.py b/tests/extension/mockextensions/app.py index 26f38464cd..c4f7af099c 100644 --- a/tests/extension/mockextensions/app.py +++ b/tests/extension/mockextensions/app.py @@ -14,7 +14,7 @@ EVENT_SCHEMA = """\ $id: https://events.jupyter.org/mockapp/v1/test -version: 1 +version: '1' properties: msg: type: string diff --git a/tests/extension/test_app.py b/tests/extension/test_app.py index d1add54344..965fe2ca16 100644 --- a/tests/extension/test_app.py +++ b/tests/extension/test_app.py @@ -32,6 +32,7 @@ def mock_extension(extension_manager): pkg = extension_manager.extensions[name] point = pkg.extension_points["mockextension"] app = point.app + app.initialize() return app diff --git a/tests/services/api/test_api.py b/tests/services/api/test_api.py index 900280f67d..8339f6e4af 100644 --- a/tests/services/api/test_api.py +++ b/tests/services/api/test_api.py @@ -1,5 +1,5 @@ import json -from typing import Awaitable, Dict, List +from collections.abc import Awaitable from unittest import mock import pytest @@ -31,7 +31,7 @@ async def test_get_status(jp_fetch): class MockUser(User): - permissions: Dict[str, List[str]] + permissions: dict[str, list[str]] class MockIdentityProvider(IdentityProvider): diff --git a/tests/services/contents/test_manager.py b/tests/services/contents/test_manager.py index be79011be8..5c3e2eca50 100644 --- a/tests/services/contents/test_manager.py +++ b/tests/services/contents/test_manager.py @@ -3,7 +3,7 @@ import sys import time from itertools import combinations -from typing import Dict, Optional, Tuple +from typing import Optional from unittest.mock import patch import pytest @@ -112,7 +112,7 @@ def add_invalid_cell(notebook): async def prepare_notebook( jp_contents_manager: FileContentsManager, make_invalid: Optional[bool] = False -) -> 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 49599e8380..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!" }, @@ -120,7 +120,7 @@ async def test_post_event(jp_fetch, event_logger_sink, payload): payload_7 = """\ { "schema_id": "http://event.mock.jupyter.org/UNREGISTERED-SCHEMA", - "version": 1, + "version": "1", "data": { "event_message": "Hello, world!" } @@ -130,7 +130,7 @@ async def test_post_event(jp_fetch, event_logger_sink, payload): payload_8 = """\ { "schema_id": "http://event.mock.jupyter.org/message", - "version": 1, + "version": "1", "data": { "message": "Hello, world!" } 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([]) From dff1599d490a0a1ee70327a2ce0dde80b473907d Mon Sep 17 00:00:00 2001 From: M Bussonnier Date: Thu, 19 Dec 2024 04:34:36 -0800 Subject: [PATCH 17/23] Better error message when starting kernel for session. (#1478) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michał Krassowski <5832902+krassowski@users.noreply.github.com> Co-authored-by: David Brochart --- examples/simple/simple_ext1/handlers.py | 3 +- jupyter_server/gateway/managers.py | 14 +++++---- jupyter_server/services/contents/handlers.py | 7 ++--- jupyter_server/services/sessions/handlers.py | 32 +++++++++++++++----- pyproject.toml | 1 + 5 files changed, 38 insertions(+), 19 deletions(-) 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/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(" Date: Thu, 19 Dec 2024 07:08:03 -0800 Subject: [PATCH 18/23] Donation link NF -> LF (#1485) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 90006c7d01..050b390522 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ dependencies = [ [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" From e74da85b1ed78151e62311f3610660315847145d Mon Sep 17 00:00:00 2001 From: Colin Watson Date: Fri, 20 Dec 2024 12:06:13 +0000 Subject: [PATCH 19/23] Ignore unclosed sqlite connection in traits (#1477) --- pyproject.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 050b390522..e04983380d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -179,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 Date: Fri, 20 Dec 2024 13:23:45 +0100 Subject: [PATCH 20/23] extensions: render default templates with default static_url (#1435) --- jupyter_server/extension/handler.py | 22 ++++++- tests/extension/mockextensions/__init__.py | 6 +- tests/extension/mockextensions/app.py | 27 ++++++++- tests/extension/test_app.py | 4 +- tests/extension/test_handler.py | 69 ++++++++++++++++++++++ 5 files changed, 122 insertions(+), 6 deletions(-) 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/tests/extension/mockextensions/__init__.py b/tests/extension/mockextensions/__init__.py index a25d9fc670..2c01cfe266 100644 --- a/tests/extension/mockextensions/__init__.py +++ b/tests/extension/mockextensions/__init__.py @@ -2,7 +2,7 @@ to load in various tests. """ -from .app import MockExtensionApp +from .app import MockExtensionApp, MockExtensionNoTemplateApp # Function that makes these extensions discoverable @@ -13,6 +13,10 @@ def _jupyter_server_extension_points(): "module": "tests.extension.mockextensions.app", "app": MockExtensionApp, }, + { + "module": "tests.extension.mockextensions.app", + "app": MockExtensionNoTemplateApp, + }, {"module": "tests.extension.mockextensions.mock1"}, {"module": "tests.extension.mockextensions.mock2"}, {"module": "tests.extension.mockextensions.mock3"}, diff --git a/tests/extension/mockextensions/app.py b/tests/extension/mockextensions/app.py index c4f7af099c..5546195593 100644 --- a/tests/extension/mockextensions/app.py +++ b/tests/extension/mockextensions/app.py @@ -4,6 +4,7 @@ from jupyter_events import EventLogger from jupyter_events.schema_registry import SchemaRegistryException +from tornado import web from traitlets import List, Unicode from jupyter_server.base.handlers import JupyterHandler @@ -44,6 +45,11 @@ def get(self): self.write(self.render_template("index.html")) +class MockExtensionErrorHandler(ExtensionHandlerMixin, JupyterHandler): + def get(self): + raise web.HTTPError(418) + + class MockExtensionApp(ExtensionAppJinjaMixin, ExtensionApp): name = "mockextension" template_paths: List[str] = List().tag(config=True) # type:ignore[assignment] @@ -51,7 +57,12 @@ class MockExtensionApp(ExtensionAppJinjaMixin, ExtensionApp): mock_trait = Unicode("mock trait", config=True) loaded = False - serverapp_config = {"jpserver_extensions": {"tests.extension.mockextensions.mock1": True}} + serverapp_config = { + "jpserver_extensions": { + "tests.extension.mockextensions.mock1": True, + "tests.extension.mockextensions.app.mockextension_notemplate": True, + } + } @staticmethod def get_extension_package(): @@ -69,6 +80,20 @@ def initialize_settings(self): def initialize_handlers(self): self.handlers.append(("/mock", MockExtensionHandler)) self.handlers.append(("/mock_template", MockExtensionTemplateHandler)) + self.handlers.append(("/mock_error_template", MockExtensionErrorHandler)) + self.loaded = True + + +class MockExtensionNoTemplateApp(ExtensionApp): + name = "mockextension_notemplate" + loaded = False + + @staticmethod + def get_extension_package(): + return "tests.extension.mockextensions" + + def initialize_handlers(self): + self.handlers.append(("/mock_error_notemplate", MockExtensionErrorHandler)) self.loaded = True diff --git a/tests/extension/test_app.py b/tests/extension/test_app.py index 965fe2ca16..ae324756ec 100644 --- a/tests/extension/test_app.py +++ b/tests/extension/test_app.py @@ -171,12 +171,14 @@ async def _stop(*args): "Shutting down 2 extensions", "jupyter_server_terminals | extension app 'jupyter_server_terminals' stopping", f"{extension_name} | extension app 'mockextension' stopping", + f"{extension_name} | extension app 'mockextension_notemplate' stopping", "jupyter_server_terminals | extension app 'jupyter_server_terminals' stopped", f"{extension_name} | extension app 'mockextension' stopped", + f"{extension_name} | extension app 'mockextension_notemplate' stopped", } # check the shutdown method was called twice - assert calls == 2 + assert calls == 3 async def test_events(jp_serverapp, jp_fetch): diff --git a/tests/extension/test_handler.py b/tests/extension/test_handler.py index af4d0568ec..050f734eb4 100644 --- a/tests/extension/test_handler.py +++ b/tests/extension/test_handler.py @@ -1,4 +1,7 @@ +from html.parser import HTMLParser + import pytest +from tornado.httpclient import HTTPClientError @pytest.fixture @@ -118,3 +121,69 @@ async def test_base_url(jp_fetch, jp_server_config, jp_base_url): assert r.code == 200 body = r.body.decode() assert "mock static content" in body + + +class StylesheetFinder(HTMLParser): + """Minimal HTML parser to find iframe.src attr""" + + def __init__(self): + super().__init__() + self.stylesheets = [] + self.body_chunks = [] + self.in_head = False + self.in_body = False + self.in_script = False + + def handle_starttag(self, tag, attrs): + tag = tag.lower() + if tag == "head": + self.in_head = True + elif tag == "body": + self.in_body = True + elif tag == "script": + self.in_script = True + elif self.in_head and tag.lower() == "link": + attr_dict = dict(attrs) + if attr_dict.get("rel", "").lower() == "stylesheet": + self.stylesheets.append(attr_dict["href"]) + + def handle_endtag(self, tag): + if tag == "head": + self.in_head = False + if tag == "body": + self.in_body = False + if tag == "script": + self.in_script = False + + def handle_data(self, data): + if self.in_body and not self.in_script: + data = data.strip() + if data: + self.body_chunks.append(data) + + +def find_stylesheets_body(html): + """Find the href= attr of stylesheets + + and body text of an HTML document + + stylesheets are used to test static_url prefix + """ + finder = StylesheetFinder() + finder.feed(html) + return (finder.stylesheets, "\n".join(finder.body_chunks)) + + +@pytest.mark.parametrize("error_url", ["mock_error_template", "mock_error_notemplate"]) +async def test_error_render(jp_fetch, jp_serverapp, jp_base_url, error_url): + with pytest.raises(HTTPClientError) as e: + await jp_fetch(error_url, method="GET") + r = e.value.response + assert r.code == 418 + assert r.headers["Content-Type"] == "text/html" + html = r.body.decode("utf8") + stylesheets, body = find_stylesheets_body(html) + static_prefix = f"{jp_base_url}static/" + assert stylesheets + assert all(stylesheet.startswith(static_prefix) for stylesheet in stylesheets) + assert str(r.code) in body From f23b3392624001c8fba6623e19f526a98b4a07ba Mon Sep 17 00:00:00 2001 From: Min RK Date: Fri, 20 Dec 2024 13:52:39 +0100 Subject: [PATCH 21/23] write server extension list to stdout (#1451) --- jupyter_server/extension/serverextension.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) 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("") From 8a6f60774599b4d72ad449f98ab2214d2d035bb4 Mon Sep 17 00:00:00 2001 From: minrk Date: Fri, 20 Dec 2024 13:02:26 +0000 Subject: [PATCH 22/23] Publish 2.15.0 SHA256 hashes: jupyter_server-2.15.0-py3-none-any.whl: 872d989becf83517012ee669f09604aa4a28097c0bd90b2f424310156c2cdae3 jupyter_server-2.15.0.tar.gz: 9d446b8697b4f7337a1b7cdcac40778babdd93ba614b6d68ab1c0c918f1c4084 --- CHANGELOG.md | 100 ++++++++++++++++++++++++++++++++++++- jupyter_server/_version.py | 2 +- 2 files changed, 99 insertions(+), 3 deletions(-) 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/jupyter_server/_version.py b/jupyter_server/_version.py index 20f829fe92..9c3ae5f28e 100644 --- a/jupyter_server/_version.py +++ b/jupyter_server/_version.py @@ -6,7 +6,7 @@ import re # Version string must appear intact for automatic versioning -__version__ = "2.15.0.dev0" +__version__ = "2.15.0" # Build up version_info tuple for backwards compatibility pattern = r"(?P\d+).(?P\d+).(?P\d+)(?P.*)" From 952782bc9636baa74f4a03966e1c14b43808d703 Mon Sep 17 00:00:00 2001 From: minrk Date: Fri, 20 Dec 2024 13:02:28 +0000 Subject: [PATCH 23/23] Bump to 2.16.0.dev0 --- jupyter_server/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyter_server/_version.py b/jupyter_server/_version.py index 9c3ae5f28e..b5b33991ed 100644 --- a/jupyter_server/_version.py +++ b/jupyter_server/_version.py @@ -6,7 +6,7 @@ import re # Version string must appear intact for automatic versioning -__version__ = "2.15.0" +__version__ = "2.16.0.dev0" # Build up version_info tuple for backwards compatibility pattern = r"(?P\d+).(?P\d+).(?P\d+)(?P.*)"