Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-jupyter-server for 
openSUSE:Factory checked in at 2026-06-08 14:24:14
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-jupyter-server (Old)
 and      /work/SRC/openSUSE:Factory/.python-jupyter-server.new.2375 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-jupyter-server"

Mon Jun  8 14:24:14 2026 rev:47 rq:1357929 version:2.19.0

Changes:
--------
--- 
/work/SRC/openSUSE:Factory/python-jupyter-server/python-jupyter-server.changes  
    2026-05-06 19:20:36.651792482 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-jupyter-server.new.2375/python-jupyter-server.changes
    2026-06-08 14:28:54.385984049 +0200
@@ -1,0 +2,27 @@
+Mon Jun  8 09:33:44 UTC 2026 - Daniel Garcia <[email protected]>
+
+- Update to 2.19.0 (CVE-2026-5422, bsc#1267357):
+  # Enhancements made
+  - Return `unresolved` stanza when kernel scope is unavailable for
+    `resolvePath` (instead of failing with 404) [#1641]
+  # Bugs fixed
+  - Recreate notary store on failure to prevent save deadlock and data
+    loss [#1640]
+  # Maintenance and upkeep improvements
+  - Restore the test skip marks with updated compatibility reason [#1646]
+  - Drop Python 3.9 [#1644]
+  - Bump minimatch from 3.1.2 to 3.1.5 [#1606]
+  # Other merged PRs
+  - Update pytest requirement from \<9,>=7.0 to >=7.0,\<10 [#1647]
+  - fix: use percent-placeholder args in HTTPError to prevent Tornado
+    from doubling percent signs in gateway URLs [#1642]
+
+- 2.18.2:
+  # Bugs fixed
+  - Fix saving user avatar URL [#1637]
+  - Fix path resolution if `root_dir` is a filesystem root [#1636]
+  # Maintenance and upkeep improvements
+  - Add Zulip notification when a release is complete [#1634]
+  - chore: update pre-commit hooks [#1633]
+
+-------------------------------------------------------------------

Old:
----
  jupyter_server-2.18.1.tar.gz

New:
----
  jupyter_server-2.19.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-jupyter-server.spec ++++++
--- /var/tmp/diff_new_pack.SEhWjP/_old  2026-06-08 14:28:55.118014423 +0200
+++ /var/tmp/diff_new_pack.SEhWjP/_new  2026-06-08 14:28:55.122014590 +0200
@@ -33,7 +33,7 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-jupyter-server%{psuffix}
-Version:        2.18.1
+Version:        2.19.0
 Release:        0
 Summary:        The backend to Jupyter web applications
 License:        BSD-3-Clause

++++++ jupyter_server-2.18.1.tar.gz -> jupyter_server-2.19.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/jupyter_server-2.18.1/.github/workflows/publish-release.yml 
new/jupyter_server-2.19.0/.github/workflows/publish-release.yml
--- old/jupyter_server-2.18.1/.github/workflows/publish-release.yml     
2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server-2.19.0/.github/workflows/publish-release.yml     
2020-02-02 01:00:00.000000000 +0100
@@ -54,3 +54,17 @@
         run: |
           echo "Failed to Publish the Draft Release Url:"
           echo ${{ steps.populate-release.outputs.release_url }}
+      - name: Send Zulip notification
+        if: startsWith(github.ref, 'refs/tags/')
+        continue-on-error: true
+        uses: 
zulip/github-actions-zulip/send-message@08b6fbd07f5834e5b930a85bc7740e9fd44ab2e7
+        with:
+          api-key: ${{ secrets.ZULIP_API_KEY }}
+          email: ${{ secrets.ZULIP_EMAIL }}
+          organization-url: ${{ vars.ZULIP_ORGANIZATION_URL }}
+          to: "Releases"
+          type: "stream"
+          topic: "jupyter_server"
+          content: | # pypi strips the v from the tag name in urls
+            Jupyter Server ${{ github.ref_name }} was just released on PyPI! 🎉
+            https://pypi.org/project/jupyter-server/${{ github.ref_name }}/
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/jupyter_server-2.18.1/.github/workflows/python-tests.yml 
new/jupyter_server-2.19.0/.github/workflows/python-tests.yml
--- old/jupyter_server-2.18.1/.github/workflows/python-tests.yml        
2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server-2.19.0/.github/workflows/python-tests.yml        
2020-02-02 01:00:00.000000000 +0100
@@ -18,10 +18,7 @@
       fail-fast: false
       matrix:
         os: [ubuntu-latest, windows-latest, macos-latest]
-        python-version: ["3.9", "3.11", "3.12", "3.13", "3.14"]
-        exclude:
-          - os: windows-latest
-            python-version: "3.9"
+        python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
         include:
           - os: macos-latest
             python-version: "3.10"
@@ -196,7 +193,7 @@
       fail-fast: false
       matrix:
         os: [ubuntu-latest]
-        python-version: ["3.9", "3.10", "3.11", "3.12"]
+        python-version: ["3.10", "3.11", "3.12"]
     steps:
       - uses: actions/checkout@v6
       - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jupyter_server-2.18.1/.pre-commit-config.yaml 
new/jupyter_server-2.19.0/.pre-commit-config.yaml
--- old/jupyter_server-2.18.1/.pre-commit-config.yaml   2020-02-02 
01:00:00.000000000 +0100
+++ new/jupyter_server-2.19.0/.pre-commit-config.yaml   2020-02-02 
01:00:00.000000000 +0100
@@ -22,7 +22,7 @@
       - id: trailing-whitespace
 
   - repo: https://github.com/python-jsonschema/check-jsonschema
-    rev: 0.37.1
+    rev: 0.37.2
     hooks:
       - id: check-github-workflows
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jupyter_server-2.18.1/.readthedocs.yaml 
new/jupyter_server-2.19.0/.readthedocs.yaml
--- old/jupyter_server-2.18.1/.readthedocs.yaml 2020-02-02 01:00:00.000000000 
+0100
+++ new/jupyter_server-2.19.0/.readthedocs.yaml 2020-02-02 01:00:00.000000000 
+0100
@@ -2,7 +2,7 @@
 build:
   os: ubuntu-22.04
   tools:
-    python: "3.9"
+    python: "3.10"
 sphinx:
   configuration: docs/source/conf.py
 python:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jupyter_server-2.18.1/CHANGELOG.md 
new/jupyter_server-2.19.0/CHANGELOG.md
--- old/jupyter_server-2.18.1/CHANGELOG.md      2020-02-02 01:00:00.000000000 
+0100
+++ new/jupyter_server-2.19.0/CHANGELOG.md      2020-02-02 01:00:00.000000000 
+0100
@@ -4,6 +4,63 @@
 
 <!-- <START NEW CHANGELOG ENTRY> -->
 
+## 2.19.0
+
+([Full 
Changelog](https://github.com/jupyter-server/jupyter_server/compare/v2.18.2...664e2255c71efe963f397b9f803dbcf503b5a920))
+
+### Enhancements made
+
+- Return `unresolved` stanza when kernel scope is unavailable for 
`resolvePath` (instead of failing with 404) 
[#1641](https://github.com/jupyter-server/jupyter_server/pull/1641) 
([@MUFFANUJ](https://github.com/MUFFANUJ), 
[@Carreau](https://github.com/Carreau), 
[@krassowski](https://github.com/krassowski))
+
+### Bugs fixed
+
+- Recreate notary store on failure to prevent save deadlock and data loss 
[#1640](https://github.com/jupyter-server/jupyter_server/pull/1640) 
([@krassowski](https://github.com/krassowski), 
[@Carreau](https://github.com/Carreau))
+
+### Maintenance and upkeep improvements
+
+- Restore the test skip marks with updated compatibility reason 
[#1646](https://github.com/jupyter-server/jupyter_server/pull/1646) 
([@krassowski](https://github.com/krassowski), 
[@Carreau](https://github.com/Carreau))
+- Drop Python 3.9 
[#1644](https://github.com/jupyter-server/jupyter_server/pull/1644) 
([@krassowski](https://github.com/krassowski), 
[@Zsailer](https://github.com/Zsailer))
+- Bump minimatch from 3.1.2 to 3.1.5 
[#1606](https://github.com/jupyter-server/jupyter_server/pull/1606) 
([@Carreau](https://github.com/Carreau), [@minrk](https://github.com/minrk))
+
+### Other merged PRs
+
+- Update pytest requirement from \<9,>=7.0 to >=7.0,\<10 
[#1647](https://github.com/jupyter-server/jupyter_server/pull/1647) 
([@Carreau](https://github.com/Carreau))
+- fix: use percent-placeholder args in HTTPError to prevent Tornado from 
doubling percent signs in gateway URLs 
[#1642](https://github.com/jupyter-server/jupyter_server/pull/1642) 
([@terminalchai](https://github.com/terminalchai), 
[@Carreau](https://github.com/Carreau))
+
+### Contributors to this release
+
+The following people contributed discussions, new ideas, code and 
documentation contributions, and review.
+See [our definition of 
contributors](https://github-activity.readthedocs.io/en/latest/use/#how-does-this-tool-define-contributions-in-the-reports).
+
+([GitHub contributors page for this 
release](https://github.com/jupyter-server/jupyter_server/graphs/contributors?from=2026-05-06&to=2026-05-29&type=c))
+
+@Carreau 
([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3ACarreau+updated%3A2026-05-06..2026-05-29&type=Issues))
 | @ianthomas23 
([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Aianthomas23+updated%3A2026-05-06..2026-05-29&type=Issues))
 | @krassowski 
([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Akrassowski+updated%3A2026-05-06..2026-05-29&type=Issues))
 | @minrk 
([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Aminrk+updated%3A2026-05-06..2026-05-29&type=Issues))
 | @MUFFANUJ 
([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3AMUFFANUJ+updated%3A2026-05-06..2026-05-29&type=Issues))
 | @terminalchai 
([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Aterminalchai+updated%3A2026-05-06..2026-05-29&type=Issues))
 | @Zsailer ([activity](https://github.com
 
/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3AZsailer+updated%3A2026-05-06..2026-05-29&type=Issues))
+
+<!-- <END NEW CHANGELOG ENTRY> -->
+
+## 2.18.2
+
+([Full 
Changelog](https://github.com/jupyter-server/jupyter_server/compare/v2.18.1...0d829f2c35a481c3b24ecbe1e25a6f79954e88f2))
+
+### Bugs fixed
+
+- Fix saving user avatar URL 
[#1637](https://github.com/jupyter-server/jupyter_server/pull/1637) 
([@brichet](https://github.com/brichet), 
[@krassowski](https://github.com/krassowski))
+- Fix path resolution if `root_dir` is a filesystem root 
[#1636](https://github.com/jupyter-server/jupyter_server/pull/1636) 
([@Yann-P](https://github.com/Yann-P), 
[@krassowski](https://github.com/krassowski))
+
+### Maintenance and upkeep improvements
+
+- Add Zulip notification when a release is complete 
[#1634](https://github.com/jupyter-server/jupyter_server/pull/1634) 
([@Yann-P](https://github.com/Yann-P), [@Carreau](https://github.com/Carreau))
+- chore: update pre-commit hooks 
[#1633](https://github.com/jupyter-server/jupyter_server/pull/1633) 
([@Carreau](https://github.com/Carreau))
+
+### Contributors to this release
+
+The following people contributed discussions, new ideas, code and 
documentation contributions, and review.
+See [our definition of 
contributors](https://github-activity.readthedocs.io/en/latest/use/#how-does-this-tool-define-contributions-in-the-reports).
+
+([GitHub contributors page for this 
release](https://github.com/jupyter-server/jupyter_server/graphs/contributors?from=2026-05-05&to=2026-05-06&type=c))
+
+@brichet 
([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Abrichet+updated%3A2026-05-05..2026-05-06&type=Issues))
 | @Carreau 
([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3ACarreau+updated%3A2026-05-05..2026-05-06&type=Issues))
 | @krassowski 
([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Akrassowski+updated%3A2026-05-05..2026-05-06&type=Issues))
 | @Yann-P 
([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3AYann-P+updated%3A2026-05-05..2026-05-06&type=Issues))
+
 ## 2.18.1
 
 ([Full 
Changelog](https://github.com/jupyter-server/jupyter_server/compare/v2.18.0...8f0b2b31822b89046c66a9dd24cfd4cf7e0249c1))
@@ -21,8 +78,6 @@
 
 @Copilot 
([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3ACopilot+updated%3A2026-05-04..2026-05-05&type=Issues))
 | @jtpio 
([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Ajtpio+updated%3A2026-05-04..2026-05-05&type=Issues))
 | @krassowski 
([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Akrassowski+updated%3A2026-05-04..2026-05-05&type=Issues))
 | @tonyx93 
([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Atonyx93+updated%3A2026-05-04..2026-05-05&type=Issues))
 
-<!-- <END NEW CHANGELOG ENTRY> -->
-
 ## 2.18.0
 
 ([Full 
Changelog](https://github.com/jupyter-server/jupyter_server/compare/v2.9.1...49b34392feaa97735b3b777e3baf8f22f2a14ed8))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jupyter_server-2.18.1/PKG-INFO 
new/jupyter_server-2.19.0/PKG-INFO
--- old/jupyter_server-2.18.1/PKG-INFO  2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server-2.19.0/PKG-INFO  2020-02-02 01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: jupyter_server
-Version: 2.18.1
+Version: 2.19.0
 Summary: The backend—i.e. core services, APIs, and REST endpoints—to Jupyter 
web applications.
 Project-URL: Homepage, https://jupyter-server.readthedocs.io
 Project-URL: Documentation, https://jupyter-server.readthedocs.io
@@ -50,7 +50,7 @@
 Classifier: Programming Language :: Python
 Classifier: Programming Language :: Python :: 3
 Classifier: Programming Language :: Python :: 3 :: Only
-Requires-Python: >=3.9
+Requires-Python: >=3.10
 Requires-Dist: anyio>=3.1.0
 Requires-Dist: argon2-cffi>=21.1
 Requires-Dist: jinja2>=3.0.3
@@ -94,7 +94,7 @@
 Requires-Dist: pytest-console-scripts; extra == 'test'
 Requires-Dist: pytest-jupyter[server]>=0.7; extra == 'test'
 Requires-Dist: pytest-timeout; extra == 'test'
-Requires-Dist: pytest<9,>=7.0; extra == 'test'
+Requires-Dist: pytest<10,>=7.0; extra == 'test'
 Requires-Dist: requests; extra == 'test'
 Description-Content-Type: text/markdown
 
@@ -116,7 +116,7 @@
 pip install jupyter_server
 ```
 
-Jupyter Server currently supports Python>=3.9 on Linux, OSX and Windows.
+Jupyter Server currently supports Python>=3.10 on Linux, OSX and Windows.
 
 ### Versioning and Branches
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jupyter_server-2.18.1/README.md 
new/jupyter_server-2.19.0/README.md
--- old/jupyter_server-2.18.1/README.md 2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server-2.19.0/README.md 2020-02-02 01:00:00.000000000 +0100
@@ -16,7 +16,7 @@
 pip install jupyter_server
 ```
 
-Jupyter Server currently supports Python>=3.9 on Linux, OSX and Windows.
+Jupyter Server currently supports Python>=3.10 on Linux, OSX and Windows.
 
 ### Versioning and Branches
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jupyter_server-2.18.1/examples/simple/README.md 
new/jupyter_server-2.19.0/examples/simple/README.md
--- old/jupyter_server-2.18.1/examples/simple/README.md 2020-02-02 
01:00:00.000000000 +0100
+++ new/jupyter_server-2.19.0/examples/simple/README.md 2020-02-02 
01:00:00.000000000 +0100
@@ -10,7 +10,7 @@
 # Clone, create a conda env and install from source.
 git clone https://github.com/jupyter/jupyter_server && \
   cd jupyter_server/examples/simple && \
-  conda create -y -n jupyter-server-example python=3.9 && \
+  conda create -y -n jupyter-server-example python=3.10 && \
   conda activate jupyter-server-example && \
   pip install -e .[test]
 ```
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jupyter_server-2.18.1/examples/simple/pyproject.toml 
new/jupyter_server-2.19.0/examples/simple/pyproject.toml
--- old/jupyter_server-2.18.1/examples/simple/pyproject.toml    2020-02-02 
01:00:00.000000000 +0100
+++ new/jupyter_server-2.19.0/examples/simple/pyproject.toml    2020-02-02 
01:00:00.000000000 +0100
@@ -7,7 +7,7 @@
 description = "Jupyter Server Example"
 readme = "README.md"
 license = "BSD-3-Clause"
-requires-python = ">=3.9"
+requires-python = ">=3.10"
 dependencies = [
     "jinja2",
     "jupyter_server",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jupyter_server-2.18.1/jupyter_server/_version.py 
new/jupyter_server-2.19.0/jupyter_server/_version.py
--- old/jupyter_server-2.18.1/jupyter_server/_version.py        2020-02-02 
01:00:00.000000000 +0100
+++ new/jupyter_server-2.19.0/jupyter_server/_version.py        2020-02-02 
01:00:00.000000000 +0100
@@ -6,7 +6,7 @@
 import re
 
 # Version string must appear intact for automatic versioning
-__version__ = "2.18.1"
+__version__ = "2.19.0"
 
 # Build up version_info tuple for backwards compatibility
 pattern = r"(?P<major>\d+).(?P<minor>\d+).(?P<patch>\d+)(?P<rest>.*)"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/jupyter_server-2.18.1/jupyter_server/auth/decorator.py 
new/jupyter_server-2.19.0/jupyter_server/auth/decorator.py
--- old/jupyter_server-2.18.1/jupyter_server/auth/decorator.py  2020-02-02 
01:00:00.000000000 +0100
+++ new/jupyter_server-2.19.0/jupyter_server/auth/decorator.py  2020-02-02 
01:00:00.000000000 +0100
@@ -3,8 +3,9 @@
 # Copyright (c) Jupyter Development Team.
 # Distributed under the terms of the Modified BSD License.
 import asyncio
+from collections.abc import Callable
 from functools import wraps
-from typing import Any, Callable, Optional, TypeVar, Union, cast
+from typing import Any, TypeVar, cast
 
 from jupyter_core.utils import ensure_async
 from tornado.log import app_log
@@ -16,9 +17,9 @@
 
 
 def authorized(
-    action: Optional[Union[str, FuncT]] = None,
-    resource: Optional[str] = None,
-    message: Optional[str] = None,
+    action: str | FuncT | None = None,
+    resource: str | None = None,
+    message: str | None = None,
 ) -> FuncT:
     """A decorator for tornado.web.RequestHandler methods
     that verifies whether the current user is authorized
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/jupyter_server-2.18.1/jupyter_server/auth/identity.py 
new/jupyter_server-2.19.0/jupyter_server/auth/identity.py
--- old/jupyter_server-2.18.1/jupyter_server/auth/identity.py   2020-02-02 
01:00:00.000000000 +0100
+++ new/jupyter_server-2.19.0/jupyter_server/auth/identity.py   2020-02-02 
01:00:00.000000000 +0100
@@ -235,7 +235,7 @@
             raise TraitError(msg)
         return proposal["value"]
 
-    need_token: bool | Bool[bool, t.Union[bool, int]] = Bool(True)
+    need_token: bool | Bool[bool, bool | int] = Bool(True)
 
     def get_user(self, handler: web.RequestHandler) -> User | None | 
t.Awaitable[User | None]:
         """Get the authenticated user for a request
@@ -354,6 +354,7 @@
                 "display_name": user.display_name,
                 "initials": user.initials,
                 "color": user.color,
+                "avatar_url": user.avatar_url,
             }
         )
         return cookie
@@ -366,7 +367,7 @@
             user["name"],
             user["display_name"],
             user["initials"],
-            None,
+            user["avatar_url"],
             user["color"],
         )
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/jupyter_server-2.18.1/jupyter_server/base/websocket.py 
new/jupyter_server-2.19.0/jupyter_server/base/websocket.py
--- old/jupyter_server-2.18.1/jupyter_server/base/websocket.py  2020-02-02 
01:00:00.000000000 +0100
+++ new/jupyter_server-2.19.0/jupyter_server/base/websocket.py  2020-02-02 
01:00:00.000000000 +0100
@@ -1,7 +1,7 @@
 """Base websocket classes."""
 
 import warnings
-from typing import Optional, no_type_check
+from typing import no_type_check
 from urllib.parse import urlparse
 
 from tornado import ioloop, web
@@ -20,7 +20,7 @@
     ping_callback = None
     last_ping = 0.0
     last_pong = 0.0
-    stream: Optional[IOStream] = None
+    stream: IOStream | None = None
 
     @property
     def ping_interval(self):
@@ -41,7 +41,7 @@
         )
 
     @no_type_check
-    def check_origin(self, origin: Optional[str] = None) -> bool:
+    def check_origin(self, origin: str | None = None) -> bool:
         """Check Origin == Host or Access-Control-Allow-Origin.
 
         Tornado >= 4 calls this method automatically, raising 403 if it 
returns False.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/jupyter_server-2.18.1/jupyter_server/gateway/gateway_client.py 
new/jupyter_server-2.19.0/jupyter_server/gateway/gateway_client.py
--- old/jupyter_server-2.18.1/jupyter_server/gateway/gateway_client.py  
2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server-2.19.0/jupyter_server/gateway/gateway_client.py  
2020-02-02 01:00:00.000000000 +0100
@@ -60,7 +60,7 @@
     def get_token(
         self,
         auth_header_key: str,
-        auth_scheme: ty.Union[str, None],
+        auth_scheme: str | None,
         auth_token: str,
         **kwargs: ty.Any,
     ) -> str:
@@ -78,7 +78,7 @@
     def get_token(
         self,
         auth_header_key: str,
-        auth_scheme: ty.Union[str, None],
+        auth_scheme: str | None,
         auth_token: str,
         **kwargs: ty.Any,
     ) -> str:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/jupyter_server-2.18.1/jupyter_server/gateway/handlers.py 
new/jupyter_server-2.19.0/jupyter_server/gateway/handlers.py
--- old/jupyter_server-2.18.1/jupyter_server/gateway/handlers.py        
2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server-2.19.0/jupyter_server/gateway/handlers.py        
2020-02-02 01:00:00.000000000 +0100
@@ -10,7 +10,7 @@
 import os
 import random
 import warnings
-from typing import Any, Optional, cast
+from typing import Any, cast
 
 from jupyter_client.session import Session
 from tornado import web
@@ -290,7 +290,7 @@
     @web.authenticated
     async def get(self, kernel_name, path, include_body=True):
         """Get a gateway resource by name and path."""
-        mimetype: Optional[str] = None
+        mimetype: str | None = None
         ksm = self.kernel_spec_manager
         kernel_spec_res = await ksm.get_kernel_spec_resource(  # 
type:ignore[attr-defined]
             kernel_name, path
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/jupyter_server-2.18.1/jupyter_server/gateway/managers.py 
new/jupyter_server-2.19.0/jupyter_server/gateway/managers.py
--- old/jupyter_server-2.18.1/jupyter_server/gateway/managers.py        
2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server-2.19.0/jupyter_server/gateway/managers.py        
2020-02-02 01:00:00.000000000 +0100
@@ -11,7 +11,7 @@
 from queue import Empty, Queue
 from threading import Thread
 from time import monotonic
-from typing import TYPE_CHECKING, Any, Optional, cast
+from typing import TYPE_CHECKING, Any, cast
 
 import websocket
 from jupyter_client.asynchronous.client import AsyncKernelClient
@@ -361,7 +361,7 @@
 
     async def kernel_culled(self, kernel_id: str) -> bool:  # typing: ignore
         """Checks if the kernel is still considered alive and returns true if 
it's not found."""
-        km: Optional[GatewayKernelManager] = None
+        km: GatewayKernelManager | None = None
         try:
             # Since we keep the models up-to-date via client polling, use that 
state to determine
             # if this kernel no longer exists on the gateway server rather 
than perform a redundant
@@ -380,7 +380,7 @@
 class GatewayKernelManager(ServerKernelManager):
     """Manages a single kernel remotely via a Gateway Server."""
 
-    kernel_id: Optional[str] = None
+    kernel_id: str | None = None
     kernel = None
 
     @default("cache_ports")
@@ -596,7 +596,7 @@
 class ChannelQueue(Queue):  # type:ignore[type-arg]
     """A queue for a named channel."""
 
-    channel_name: Optional[str] = None
+    channel_name: str | None = None
     response_router_finished: bool
 
     def __init__(self, channel_name: str, channel_socket: websocket.WebSocket, 
log: Logger):
@@ -713,19 +713,19 @@
     # flag for whether execute requests should be allowed to call raw_input:
     allow_stdin = False
     _channels_stopped: bool
-    _channel_queues: Optional[dict[str, ChannelQueue]]
-    _control_channel: Optional[ChannelQueue]
-    _hb_channel: Optional[ChannelQueue]
-    _stdin_channel: Optional[ChannelQueue]
-    _iopub_channel: Optional[ChannelQueue]
-    _shell_channel: Optional[ChannelQueue]
+    _channel_queues: dict[str, ChannelQueue] | None
+    _control_channel: ChannelQueue | None
+    _hb_channel: ChannelQueue | None
+    _stdin_channel: ChannelQueue | None
+    _iopub_channel: ChannelQueue | None
+    _shell_channel: ChannelQueue | None
 
     def __init__(self, kernel_id, **kwargs):
         """Initialize a gateway kernel client."""
         super().__init__(**kwargs)
         self.kernel_id = kernel_id
-        self.channel_socket: Optional[websocket.WebSocket] = None
-        self.response_router: Optional[Thread] = None
+        self.channel_socket: websocket.WebSocket | None = None
+        self.response_router: Thread | None = None
         self._channels_stopped = False
         self._channel_queues = {}
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jupyter_server-2.18.1/jupyter_server/serverapp.py 
new/jupyter_server-2.19.0/jupyter_server/serverapp.py
--- old/jupyter_server-2.18.1/jupyter_server/serverapp.py       2020-02-02 
01:00:00.000000000 +0100
+++ new/jupyter_server-2.19.0/jupyter_server/serverapp.py       2020-02-02 
01:00:00.000000000 +0100
@@ -558,9 +558,7 @@
         sources.extend(self.settings["last_activity_times"].values())
         return max(sources)
 
-    def _check_handler_auth(
-        self, matcher: t.Union[str, Matcher], handler: type[web.RequestHandler]
-    ):
+    def _check_handler_auth(self, matcher: str | Matcher, handler: 
type[web.RequestHandler]):
         missing_authentication = []
         for method_name in handler.SUPPORTED_METHODS:
             method = getattr(handler, method_name.lower())
@@ -1216,7 +1214,7 @@
     )
 
     @default("min_open_files_limit")
-    def _default_min_open_files_limit(self) -> t.Optional[int]:
+    def _default_min_open_files_limit(self) -> int | None:
         if resource is None:
             # Ignoring min_open_files_limit because the limit cannot be 
adjusted (for example, on Windows)
             return None  # type:ignore[unreachable]
@@ -1276,7 +1274,7 @@
     )
 
     def _warn_deprecated_config(
-        self, change: t.Any, clsname: str, new_name: t.Optional[str] = None
+        self, change: t.Any, clsname: str, new_name: str | None = None
     ) -> None:
         """Warn on deprecated config."""
         if new_name is None:
@@ -1620,7 +1618,7 @@
     )
 
     @default("kernel_manager_class")
-    def _default_kernel_manager_class(self) -> t.Union[str, 
type[AsyncMappingKernelManager]]:
+    def _default_kernel_manager_class(self) -> str | 
type[AsyncMappingKernelManager]:
         if self.gateway_config.gateway_enabled:
             return 
"jupyter_server.gateway.managers.GatewayMappingKernelManager"
         return AsyncMappingKernelManager
@@ -1631,7 +1629,7 @@
     )
 
     @default("session_manager_class")
-    def _default_session_manager_class(self) -> t.Union[str, 
type[SessionManager]]:
+    def _default_session_manager_class(self) -> str | type[SessionManager]:
         if self.gateway_config.gateway_enabled:
             return "jupyter_server.gateway.managers.GatewaySessionManager"
         return SessionManager
@@ -1645,7 +1643,7 @@
     @default("kernel_websocket_connection_class")
     def _default_kernel_websocket_connection_class(
         self,
-    ) -> t.Union[str, type[ZMQChannelsWebsocketConnection]]:
+    ) -> str | type[ZMQChannelsWebsocketConnection]:
         if self.gateway_config.gateway_enabled:
             return 
"jupyter_server.gateway.connections.GatewayWebSocketConnection"
         return ZMQChannelsWebsocketConnection
@@ -1696,7 +1694,7 @@
     )
 
     @default("kernel_spec_manager_class")
-    def _default_kernel_spec_manager_class(self) -> t.Union[str, 
type[KernelSpecManager]]:
+    def _default_kernel_spec_manager_class(self) -> str | 
type[KernelSpecManager]:
         if self.gateway_config.gateway_enabled:
             return "jupyter_server.gateway.managers.GatewayKernelSpecManager"
         return KernelSpecManager
@@ -2049,7 +2047,7 @@
         """Get the Extension that started this server."""
         return self._starter_app
 
-    def parse_command_line(self, argv: t.Optional[list[str]] = None) -> None:
+    def parse_command_line(self, argv: list[str] | None = None) -> None:
         """Parse the command line options."""
         super().parse_command_line(argv)
 
@@ -2343,7 +2341,7 @@
             resource.setrlimit(resource.RLIMIT_NOFILE, (soft, hard))
 
     def _get_urlparts(
-        self, path: t.Optional[str] = None, include_token: bool = False
+        self, path: str | None = None, include_token: bool = False
     ) -> urllib.parse.ParseResult:
         """Constructs a urllib named tuple, ParseResult,
         with default values set by server config.
@@ -2782,7 +2780,7 @@
     @catch_config_error
     def initialize(
         self,
-        argv: t.Optional[list[str]] = None,
+        argv: list[str] | None = None,
         find_extensions: bool = True,
         new_httpserver: bool = True,
         starter_extension: t.Any = None,
@@ -3028,7 +3026,7 @@
             if e.errno != errno.ENOENT:
                 raise
 
-    def _prepare_browser_open(self) -> tuple[str, t.Optional[str]]:
+    def _prepare_browser_open(self) -> tuple[str, str | None]:
         """Prepare to open the browser."""
         if not self.use_redirect_file:
             uri = self.default_url[len(self.base_url) :]
@@ -3245,7 +3243,7 @@
 
 
 def list_running_servers(
-    runtime_dir: t.Optional[str] = None, log: t.Optional[logging.Logger] = None
+    runtime_dir: str | None = None, log: logging.Logger | None = None
 ) -> t.Generator[t.Any, None, None]:
     """Iterate over the server info files of running Jupyter servers.
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/jupyter_server-2.18.1/jupyter_server/services/api/api.yaml 
new/jupyter_server-2.19.0/jupyter_server/services/api/api.yaml
--- old/jupyter_server-2.18.1/jupyter_server/services/api/api.yaml      
2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server-2.19.0/jupyter_server/services/api/api.yaml      
2020-02-02 01:00:00.000000000 +0100
@@ -379,7 +379,7 @@
         in: query
         description: file path
         type: string
-      - name: kernel_id
+      - name: kernel
         required: false
         in: query
         description: kernel uuid
@@ -389,7 +389,7 @@
       summary: Resolve path to a file
       responses:
         200:
-          description: Resolved path with scope details
+          description: Resolved path with scope details and optional 
unresolved scopes
           schema:
             $ref: "#/definitions/ResolvedPath"
   /api/sessions/{session}:
@@ -1014,3 +1014,18 @@
             path:
               type: string
               description: fully specified path for file
+      unresolved:
+        type: array
+        description: scopes for which path resolution could not be completed
+        items:
+          type: object
+          required:
+            - scope
+            - reason
+          properties:
+            scope:
+              type: string
+              description: the scope that failed to resolve
+            reason:
+              type: string
+              description: human-readable reason for the unresolved scope
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/jupyter_server-2.18.1/jupyter_server/services/api/handlers.py 
new/jupyter_server-2.19.0/jupyter_server/services/api/handlers.py
--- old/jupyter_server-2.18.1/jupyter_server/services/api/handlers.py   
2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server-2.19.0/jupyter_server/services/api/handlers.py   
2020-02-02 01:00:00.000000000 +0100
@@ -153,14 +153,25 @@
         path = self.get_query_argument("path")
         kernel_uuid = self.get_query_argument("kernel", default=None)
         scopes: dict[str, Any] = {"server": self.contents_manager}
+        unresolved: list[dict[str, str]] = []
         if kernel_uuid:
-            scopes["kernel"] = self.kernel_manager.get_kernel(kernel_uuid)
+            try:
+                scopes["kernel"] = self.kernel_manager.get_kernel(kernel_uuid)
+            except web.HTTPError as e:
+                if e.status_code == 404:
+                    unresolved.append(
+                        {"scope": "kernel", "reason": f"Kernel {kernel_uuid} 
could not be found"}
+                    )
+                else:
+                    raise
         resolved = [
             {"scope": name, "path": await 
ensure_async(scope.resolve_path(path))}
             for name, scope in scopes.items()
             if hasattr(scope, "resolve_path")
         ]
         response = {"resolved": [entry for entry in resolved if entry["path"] 
is not None]}
+        if unresolved:
+            response["unresolved"] = unresolved
         self.finish(json.dumps(response))
 
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/jupyter_server-2.18.1/jupyter_server/services/contents/fileio.py 
new/jupyter_server-2.19.0/jupyter_server/services/contents/fileio.py
--- old/jupyter_server-2.18.1/jupyter_server/services/contents/fileio.py        
2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server-2.19.0/jupyter_server/services/contents/fileio.py        
2020-02-02 01:00:00.000000000 +0100
@@ -322,8 +322,10 @@
         except ValueError:
             raise HTTPError(404, f"{path} is not a valid path") from None
 
-        if not (os.path.abspath(os_path) + os.path.sep).startswith(root + 
os.path.sep):
-            raise HTTPError(404, "%s is outside root contents directory" % 
path)
+        # Corner case: when root_dir is the filesystem root, root + sep 
produces an invalid prefix
+        if os.path.dirname(root) != root:
+            if not (os.path.abspath(os_path) + os.path.sep).startswith(root + 
os.path.sep):
+                raise HTTPError(404, "%s is outside root contents directory" % 
path)
         return os_path
 
     def _read_notebook(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/jupyter_server-2.18.1/jupyter_server/services/contents/manager.py 
new/jupyter_server-2.19.0/jupyter_server/services/contents/manager.py
--- old/jupyter_server-2.18.1/jupyter_server/services/contents/manager.py       
2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server-2.19.0/jupyter_server/services/contents/manager.py       
2020-02-02 01:00:00.000000000 +0100
@@ -705,7 +705,7 @@
         self.notary.mark_cells(nb, True)
         self.check_and_sign(nb, path)
 
-    def check_and_sign(self, nb, path=""):
+    def check_and_sign(self, nb, path="", *, _retrying=False):
         """Check for trusted cells, and sign the notebook.
 
         Called as a part of saving notebooks.
@@ -717,10 +717,26 @@
         path : str
             The notebook's path (for logging)
         """
-        if self.notary.check_cells(nb):
-            self.notary.sign(nb)
-        else:
-            self.log.warning("Notebook %s is not trusted", path)
+        try:
+            if self.notary.check_cells(nb):
+                self.notary.sign(nb)
+            else:
+                self.log.warning("Notebook %s is not trusted", path)
+        except Exception:
+            if _retrying:
+                raise
+            self.log.warning(
+                "Signature store for notebook %s is corrupted or unavailable; "
+                "recreating the store.",
+                path,
+                exc_info=True,
+            )
+            # The default implementation uses SQLiteSignatureStore if SQLite3 
is available
+            # and falls back to MemorySignatureStore if not; 
SQLiteSignatureStore will
+            # attempt to recreate the database if it detects errors during 
initialization,
+            # and fallback to in-memory (`:memory:`) SQLite database if 
necessary.
+            self.notary.store = self.notary.store_factory()
+            self.check_and_sign(nb, path, _retrying=True)
 
     def mark_trusted_cells(self, nb, path=""):
         """Mark cells as trusted if the notebook signature matches.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/jupyter_server-2.18.1/jupyter_server/services/events/handlers.py 
new/jupyter_server-2.19.0/jupyter_server/services/events/handlers.py
--- old/jupyter_server-2.18.1/jupyter_server/services/events/handlers.py        
2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server-2.19.0/jupyter_server/services/events/handlers.py        
2020-02-02 01:00:00.000000000 +0100
@@ -7,7 +7,7 @@
 
 import json
 from datetime import datetime
-from typing import TYPE_CHECKING, Any, Optional, cast
+from typing import TYPE_CHECKING, Any, cast
 
 from jupyter_core.utils import ensure_async
 from tornado import web, websocket
@@ -92,7 +92,7 @@
         raise Exception(message)
 
 
-def get_timestamp(data: dict[str, Any]) -> Optional[datetime]:
+def get_timestamp(data: dict[str, Any]) -> datetime | None:
     """Parses timestamp from the JSON request body"""
     try:
         if "timestamp" in data:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/jupyter_server-2.18.1/jupyter_server/services/kernels/connection/base.py 
new/jupyter_server-2.19.0/jupyter_server/services/kernels/connection/base.py
--- 
old/jupyter_server-2.18.1/jupyter_server/services/kernels/connection/base.py    
    2020-02-02 01:00:00.000000000 +0100
+++ 
new/jupyter_server-2.19.0/jupyter_server/services/kernels/connection/base.py    
    2020-02-02 01:00:00.000000000 +0100
@@ -68,7 +68,7 @@
     offsets = list(struct.unpack("!" + "I" * nbufs, bmsg[4 : 4 * (nbufs + 1)]))
     offsets.append(None)
     bufs = []
-    for start, stop in zip(offsets[:-1], offsets[1:]):
+    for start, stop in zip(offsets[:-1], offsets[1:], strict=False):
         bufs.append(bmsg[start:stop])
     msg = json.loads(bufs[0].decode("utf8"))
     msg["header"] = extract_dates(msg["header"])
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/jupyter_server-2.18.1/jupyter_server/services/sessions/sessionmanager.py 
new/jupyter_server-2.19.0/jupyter_server/services/sessions/sessionmanager.py
--- 
old/jupyter_server-2.18.1/jupyter_server/services/sessions/sessionmanager.py    
    2020-02-02 01:00:00.000000000 +0100
+++ 
new/jupyter_server-2.19.0/jupyter_server/services/sessions/sessionmanager.py    
    2020-02-02 01:00:00.000000000 +0100
@@ -5,7 +5,7 @@
 import os
 import pathlib
 import uuid
-from typing import Any, NewType, Optional, Union, cast
+from typing import Any, NewType, cast
 
 KernelName = NewType("KernelName", str)
 ModelName = NewType("ModelName", str)
@@ -41,8 +41,8 @@
     associated with them.
     """
 
-    session_id: Optional[str] = None
-    kernel_id: Optional[str] = None
+    session_id: str | None = None
+    kernel_id: str | None = None
 
     def __eq__(self, other: object) -> bool:
         """Whether a record equals another."""
@@ -112,7 +112,7 @@
         """The string representation of a record list."""
         return str(self._records)
 
-    def __contains__(self, record: Union[KernelSessionRecord, str]) -> bool:
+    def __contains__(self, record: KernelSessionRecord | str) -> bool:
         """Search for records by kernel_id and session_id"""
         if isinstance(record, KernelSessionRecord) and record in self._records:
             return True
@@ -127,7 +127,7 @@
         """The length of the record list."""
         return len(self._records)
 
-    def get(self, record: Union[KernelSessionRecord, str]) -> 
KernelSessionRecord:
+    def get(self, record: KernelSessionRecord | str) -> KernelSessionRecord:
         """Return a full KernelSessionRecord from a session_id, kernel_id, or
         incomplete KernelSessionRecord.
         """
@@ -262,11 +262,11 @@
 
     async def create_session(
         self,
-        path: Optional[str] = None,
-        name: Optional[ModelName] = None,
-        type: Optional[str] = None,
-        kernel_name: Optional[KernelName] = None,
-        kernel_id: Optional[str] = None,
+        path: str | None = None,
+        name: ModelName | None = None,
+        type: str | None = None,
+        kernel_name: KernelName | None = None,
+        kernel_id: str | None = None,
     ) -> dict[str, Any]:
         """Creates a session and returns its model
 
@@ -293,9 +293,7 @@
         self._pending_sessions.remove(record)
         return cast("dict[str, Any]", result)
 
-    def get_kernel_env(
-        self, path: Optional[str], name: Optional[ModelName] = None
-    ) -> dict[str, str]:
+    def get_kernel_env(self, path: str | None, name: ModelName | None = None) 
-> dict[str, str]:
         """Return the environment variables that need to be set in the kernel
 
         Parameters
@@ -315,10 +313,10 @@
     async def start_kernel_for_session(
         self,
         session_id: str,
-        path: Optional[str],
-        name: Optional[ModelName],
-        type: Optional[str],
-        kernel_name: Optional[KernelName],
+        path: str | None,
+        name: ModelName | None,
+        type: str | None,
+        kernel_name: KernelName | None,
     ) -> str:
         """Start a new kernel for a given session.
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jupyter_server-2.18.1/package-lock.json 
new/jupyter_server-2.19.0/package-lock.json
--- old/jupyter_server-2.18.1/package-lock.json 2020-02-02 01:00:00.000000000 
+0100
+++ new/jupyter_server-2.19.0/package-lock.json 2020-02-02 01:00:00.000000000 
+0100
@@ -184,9 +184,9 @@
       "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
     },
     "node_modules/minimatch": {
-      "version": "3.1.2",
-      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz";,
-      "integrity": 
"sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "version": "3.1.5",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz";,
+      "integrity": 
"sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
       "dependencies": {
         "brace-expansion": "^1.1.7"
       },
@@ -538,9 +538,9 @@
       "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
     },
     "minimatch": {
-      "version": "3.1.2",
-      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz";,
-      "integrity": 
"sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "version": "3.1.5",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz";,
+      "integrity": 
"sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
       "requires": {
         "brace-expansion": "^1.1.7"
       }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jupyter_server-2.18.1/pyproject.toml 
new/jupyter_server-2.19.0/pyproject.toml
--- old/jupyter_server-2.18.1/pyproject.toml    2020-02-02 01:00:00.000000000 
+0100
+++ new/jupyter_server-2.19.0/pyproject.toml    2020-02-02 01:00:00.000000000 
+0100
@@ -21,7 +21,7 @@
     "Programming Language :: Python :: 3",
     "Programming Language :: Python :: 3 :: Only",
 ]
-requires-python = ">=3.9"
+requires-python = ">=3.10"
 dependencies = [
     "anyio>=3.1.0",
     "argon2-cffi>=21.1",
@@ -57,7 +57,7 @@
     "pytest-console-scripts",
     "pytest-timeout",
     "pytest-jupyter[server]>=0.7",
-    "pytest>=7.0,<9",
+    "pytest>=7.0,<10",
     "requests",
     "pre-commit",
     'flaky'
@@ -139,7 +139,7 @@
 docstring-code-format = true
 
 [tool.ruff.lint]
-ignore = ["ARG", "TRY", "RUF012", "TID252", "G", "INP001", "E402", "F401", 
"BLE001", "UP045", "PLC0415"]
+ignore = ["ARG", "TRY", "RUF012", "TID252", "G", "INP001", "E402", "F401", 
"BLE001", "PLC0415"]
 extend-select = [
   "B",  # flake8-bugbear
   "I",  # isort
@@ -157,7 +157,7 @@
 
 [tool.ruff.lint.extend-per-file-ignores]
 "jupyter_server/*" = ["S101", "RET", "S110", "UP031", "FBT", "FA100", 
"SLF001", "A002",
-                      "SIM105", "A001", "UP007", "PLR2004", "T201", "N818", 
"F403"]
+                      "SIM105", "A001", "PLR2004", "T201", "N818", "F403"]
 "jupyter_server/gateway/*" = ["TCH" ]
 "tests/*" = ["UP031", "PT", 'EM', "TRY", "RET", "SLF", "C408", "F841", "FBT", 
"A002", "FLY", "N",
             "PERF", "ASYNC", "T201", "FA100", "E711", "INP", "TCH", "SIM105", 
"A001", "PLW0603",
@@ -216,7 +216,7 @@
 post-version-spec = "dev"
 
 [tool.mypy]
-python_version = "3.9"
+python_version = "3.10"
 explicit_package_bases = true
 strict = true
 pretty = true
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jupyter_server-2.18.1/tests/auth/test_identity.py 
new/jupyter_server-2.19.0/tests/auth/test_identity.py
--- old/jupyter_server-2.18.1/tests/auth/test_identity.py       2020-02-02 
01:00:00.000000000 +0100
+++ new/jupyter_server-2.19.0/tests/auth/test_identity.py       2020-02-02 
01:00:00.000000000 +0100
@@ -178,6 +178,30 @@
         idp.validate_security(app, ssl_options=None)
 
 
[email protected](
+    "user_fields",
+    [
+        {"username": "user"},
+        {"username": "user", "avatar_url": "https://example.com/avatar.png"},
+        {
+            "username": "user",
+            "name": "Real Name",
+            "display_name": "RN",
+            "initials": "RN",
+            "avatar_url": "img/avatar.jpg",
+            "color": "#abc",
+        },
+    ],
+)
+def test_user_cookie_roundtrip(user_fields):
+    """All user fields must survive user_to_cookie -> user_from_cookie."""
+    idp = IdentityProvider()
+    user = User(**user_fields)
+    cookie = idp.user_to_cookie(user)
+    restored = idp.user_from_cookie(cookie)
+    assert restored == user
+
+
 async def test_auth_disabled(request, jp_serverapp, jp_fetch):
     idp = PasswordIdentityProvider(
         parent=jp_serverapp,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jupyter_server-2.18.1/tests/services/api/test_api.py 
new/jupyter_server-2.19.0/tests/services/api/test_api.py
--- old/jupyter_server-2.18.1/tests/services/api/test_api.py    2020-02-02 
01:00:00.000000000 +0100
+++ new/jupyter_server-2.19.0/tests/services/api/test_api.py    2020-02-02 
01:00:00.000000000 +0100
@@ -227,6 +227,7 @@
                 "name": "Updated Name",
                 "display_name": "Updated Display Name",
                 "color": "#000000",
+                "avatar_url": "some/url",
             },
         )
     ],
@@ -236,11 +237,12 @@
 ):
     """Test successful user update."""
     password_identity_provider.mock_user = MockUser(**identity)
-    identity_provider.updatable_fields = ["name", "display_name", "color"]
+    password_identity_provider.updatable_fields = ["name", "display_name", 
"color", "avatar_url"]
     payload = {
         "name": expected["name"],
         "display_name": expected["display_name"],
         "color": expected["color"],
+        "avatar_url": expected["avatar_url"],
     }
     r = await jp_fetch(
         "/api/me",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/jupyter_server-2.18.1/tests/services/contents/test_fileio.py 
new/jupyter_server-2.19.0/tests/services/contents/test_fileio.py
--- old/jupyter_server-2.18.1/tests/services/contents/test_fileio.py    
2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server-2.19.0/tests/services/contents/test_fileio.py    
2020-02-02 01:00:00.000000000 +0100
@@ -299,3 +299,48 @@
 
     assert err.value.status_code == 404
     assert "outside root contents directory" in str(err.value)
+
+
+def test_get_os_path_with_root_dir_slash(tmp_path):
+    """root_dir set to filesystem root should not raise for any valid path 
(issue #1635)."""
+
+    fs_root = os.path.abspath(os.sep)
+
+    class FileManagerMixinTest(FileManagerMixin):
+        root_dir = fs_root
+
+    mixin = FileManagerMixinTest()
+    mixin.log = logging.getLogger()
+
+    assert mixin._get_os_path("work/notebook.ipynb") == os.path.join(
+        fs_root, "work", "notebook.ipynb"
+    )
+
+
[email protected](os.name != "nt", reason="Windows UNC paths only")
[email protected]("root_dir", ["\\\\server\\share", 
"\\\\server\\share\\"])
+def test_get_os_path_with_unc_root(root_dir):
+    """UNC root_dir (with or without trailing backslash) should resolve paths 
correctly."""
+
+    class FileManagerMixinTest(FileManagerMixin):
+        pass
+
+    mixin = FileManagerMixinTest()
+    mixin.root_dir = root_dir
+    mixin.log = logging.getLogger()
+
+    assert mixin._get_os_path("work/notebook.ipynb") == 
"\\\\server\\share\\work\\notebook.ipynb"
+
+
[email protected](os.name != "nt", reason="Windows only")
+def test_get_os_path_with_non_current_drive_root():
+    """root_dir set to a non-current drive root should not raise"""
+
+    class FileManagerMixinTest(FileManagerMixin):
+        pass
+
+    mixin = FileManagerMixinTest()
+    mixin.root_dir = "W:\\"
+    mixin.log = logging.getLogger()
+
+    assert mixin._get_os_path("work/notebook.ipynb") == 
"W:\\work\\notebook.ipynb"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/jupyter_server-2.18.1/tests/services/contents/test_manager.py 
new/jupyter_server-2.19.0/tests/services/contents/test_manager.py
--- old/jupyter_server-2.18.1/tests/services/contents/test_manager.py   
2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server-2.19.0/tests/services/contents/test_manager.py   
2020-02-02 01:00:00.000000000 +0100
@@ -4,13 +4,13 @@
 import sys
 import time
 from itertools import combinations
-from typing import Optional
 from unittest.mock import MagicMock, patch
 
 import pytest
 from jupyter_core.utils import ensure_async
 from nbformat import ValidationError
 from nbformat import v4 as nbformat
+from nbformat.sign import NotebookNotary
 from tornado.web import HTTPError
 from traitlets import TraitError
 
@@ -113,7 +113,7 @@
 
 
 async def prepare_notebook(
-    jp_contents_manager: FileContentsManager, make_invalid: Optional[bool] = 
False
+    jp_contents_manager: FileContentsManager, make_invalid: bool | None = False
 ) -> tuple[dict, str]:
     cm = jp_contents_manager
     model = await ensure_async(cm.new_untitled(type="notebook"))
@@ -1152,6 +1152,77 @@
     assert updated_model["last_modified"] > model["last_modified"]
 
 
[email protected](params=[FileContentsManager, AsyncFileContentsManager])
+def file_manager_with_notary(request, tmp_path):
+    """Both sync and async file managers, each with a notary using a dedicated 
temp directory."""
+    notary_dir = tmp_path / "notary"
+    notary_dir.mkdir()
+    root_dir = tmp_path / "root"
+    root_dir.mkdir()
+    notary = NotebookNotary(data_dir=str(notary_dir))
+    cm = request.param(root_dir=str(root_dir))
+    cm.notary = notary
+    return cm
+
+
+async def 
test_save_before_and_after_signature_store_corruption(file_manager_with_notary, 
caplog):
+    cm = file_manager_with_notary
+    db_path = cm.notary.db_file
+
+    # First save
+    model = await ensure_async(cm.new_untitled(type="notebook"))
+    path = model["path"]
+    full_model = await ensure_async(cm.get(path))
+    result = await ensure_async(cm.save(full_model, path))
+    assert isinstance(result, dict)
+    assert result["path"] == path
+    assert os.path.exists(db_path)
+
+    # Fetch the model before corrupting so that cm.get() does not itself hit 
the DB
+    full_model = await ensure_async(cm.get(path))
+
+    # Corrupt the signature store by truncating to the SQLite file header size 
(100 bytes),
+    # leaving the magic bytes intact but removing all data pages.
+    with open(db_path, "r+b") as f:
+        f.truncate(100)
+
+    # Second save with the same notary: signature store corruption should be 
handled
+    # gracefully and the notebook should still be saved successfully.
+    with caplog.at_level("WARNING"):
+        result = await ensure_async(cm.save(full_model, path))
+    assert isinstance(result, dict)
+    assert result["path"] == path
+    assert "corrupted or unavailable" in caplog.text
+
+
+async def 
test_save_raises_when_signature_store_unrecoverable(file_manager_with_notary, 
caplog):
+    cm = file_manager_with_notary
+    db_path = cm.notary.db_file
+
+    # First save
+    model = await ensure_async(cm.new_untitled(type="notebook"))
+    path = model["path"]
+    full_model = await ensure_async(cm.get(path))
+    await ensure_async(cm.save(full_model, path))
+
+    # Fetch the model before corrupting so that cm.get() does not itself hit 
the DB
+    full_model = await ensure_async(cm.get(path))
+
+    # Corrupt the store, and also replace store_factory so that the recovery 
attempt
+    # returns a broken store, causing the retry to also fail.
+    with open(db_path, "r+b") as f:
+        f.truncate(100)
+    broken_store = MagicMock()
+    broken_store.store_signature.side_effect = RuntimeError("store is broken")
+    cm.notary.store_factory = lambda: broken_store
+
+    with caplog.at_level("WARNING"):
+        with pytest.raises(HTTPError) as exc_info:
+            await ensure_async(cm.save(full_model, path))
+    assert exc_info.value.status_code == 500
+    assert "corrupted or unavailable" in caplog.text
+
+
 @pytest.mark.skipif(sys.platform != "darwin", reason="macOS only - 
st_birthtime test")
 async def test_created_uses_birthtime_on_macos(jp_contents_manager):
     """Test that on macOS, created timestamp uses st_birthtime (actual 
creation time)."""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/jupyter_server-2.18.1/tests/services/kernels/test_api.py 
new/jupyter_server-2.19.0/tests/services/kernels/test_api.py
--- old/jupyter_server-2.18.1/tests/services/kernels/test_api.py        
2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server-2.19.0/tests/services/kernels/test_api.py        
2020-02-02 01:00:00.000000000 +0100
@@ -186,7 +186,7 @@
 
 @pytest.mark.skipif(
     ipykernel.version_info < (7, 0),
-    reason="requires ipykernel 7, which is not compatible with Python 3.9",
+    reason="requires ipykernel 7, which is not compatible with Python 3.10",
 )
 @pytest.mark.timeout(TEST_TIMEOUT)
 async def test_resolve_path_kernel(jp_fetch, jp_serverapp, jp_root_dir, 
pending_kernel_is_ready):
@@ -269,7 +269,7 @@
 
 @pytest.mark.skipif(
     ipykernel.version_info < (7, 0),
-    reason="requires ipykernel 7, which is not compatible with Python 3.9",
+    reason="requires ipykernel 7, which is not compatible with Python 3.10",
 )
 @pytest.mark.timeout(TEST_TIMEOUT)
 async def test_resolve_path_server_and_kernel(
@@ -306,7 +306,7 @@
     resolution = json.loads(r.body.decode())
 
     # Should present candidates for both server and kernel
-    assert len(resolution["resolved"]) == 2
+    assert len(resolution["resolved"]) == 2, resolution["resolved"]
     assert {k["scope"] for k in resolution["resolved"]} == {"server", "kernel"}
 
 
@@ -345,6 +345,23 @@
 
 
 @pytest.mark.timeout(TEST_TIMEOUT)
+async def test_resolve_path_missing_kernel_reports_unresolved(jp_fetch, 
jp_serverapp):
+    bad_id = "111-111-111-111-111"
+    r = await jp_fetch(
+        "api",
+        "resolvePath",
+        params={"kernel": bad_id, "path": "hello.py"},
+        method="GET",
+    )
+    assert r.code == 200
+    resolution = json.loads(r.body.decode())
+    assert resolution["resolved"] == []
+    assert resolution["unresolved"] == [
+        {"scope": "kernel", "reason": f"Kernel {bad_id} could not be found"}
+    ]
+
+
[email protected](TEST_TIMEOUT)
 async def test_kernel_handler(jp_fetch, jp_serverapp, pending_kernel_is_ready):
     # Create a kernel
     r = await jp_fetch(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jupyter_server-2.18.1/tests/test_gateway.py 
new/jupyter_server-2.19.0/tests/test_gateway.py
--- old/jupyter_server-2.18.1/tests/test_gateway.py     2020-02-02 
01:00:00.000000000 +0100
+++ new/jupyter_server-2.19.0/tests/test_gateway.py     2020-02-02 
01:00:00.000000000 +0100
@@ -8,7 +8,7 @@
 from datetime import datetime, timedelta, timezone
 from io import BytesIO
 from queue import Empty
-from typing import Any, Union
+from typing import Any
 from unittest.mock import MagicMock, patch
 
 import pytest
@@ -222,7 +222,7 @@
     config_var_2: str = Unicode(config=True)  # type:ignore[assignment]
 
     def get_token(
-        self, auth_header_key: str, auth_scheme: Union[str, None], auth_token: 
str, **kwargs: Any
+        self, auth_header_key: str, auth_scheme: str | None, auth_token: str, 
**kwargs: Any
     ) -> str:
         return f"{self.config_var_2}{self.config_var_1}"
 

Reply via email to