Skip to content

Commit 21a242c

Browse files
committed
Merge remote-tracking branch 'upstream/main' into retry-gateway-errors
2 parents 9b6132d + 7623966 commit 21a242c

File tree

9 files changed

+180
-51
lines changed

9 files changed

+180
-51
lines changed

.pre-commit-config.yaml

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ repos:
3030
files: \.py$
3131

3232
- repo: https://github.com/pre-commit/mirrors-mypy
33-
rev: v0.961
33+
rev: v0.971
3434
hooks:
3535
- id: mypy
3636
additional_dependencies: [types-requests]
@@ -41,39 +41,35 @@ repos:
4141
- id: prettier
4242

4343
- repo: https://github.com/asottile/pyupgrade
44-
rev: v2.37.1
44+
rev: v2.37.3
4545
hooks:
4646
- id: pyupgrade
4747
args: [--py37-plus]
4848

4949
- repo: https://github.com/PyCQA/doc8
50-
rev: 0.11.2
50+
rev: v1.0.0
5151
hooks:
5252
- id: doc8
5353
args: [--max-line-length=200]
5454
exclude: docs/source/other/full-config.rst
5555
stages: [manual]
5656

5757
- repo: https://github.com/pycqa/flake8
58-
rev: 4.0.1
58+
rev: 5.0.4
5959
hooks:
6060
- id: flake8
6161
additional_dependencies:
62-
[
63-
"flake8-bugbear==20.1.4",
64-
"flake8-logging-format==0.6.0",
65-
"flake8-implicit-str-concat==0.2.0",
66-
]
62+
["flake8-bugbear==22.6.22", "flake8-implicit-str-concat==0.2.0"]
6763
stages: [manual]
6864

6965
- repo: https://github.com/pre-commit/mirrors-eslint
70-
rev: v8.19.0
66+
rev: v8.21.0
7167
hooks:
7268
- id: eslint
7369
stages: [manual]
7470

7571
- repo: https://github.com/sirosen/check-jsonschema
76-
rev: 0.17.0
72+
rev: 0.17.1
7773
hooks:
7874
- id: check-jsonschema
7975
name: "Check GitHub Workflows"

docs/source/operators/security.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ as all requests requiring _authorization_ must first complete _authentication_.
101101
Identity Providers
102102
******************
103103

104-
The :class:`.IdentityProvider` class is responsible for the "authorization" step,
104+
The :class:`.IdentityProvider` class is responsible for the "authentication" step,
105105
identifying the user making the request,
106106
and constructing information about them.
107107

jupyter_server/auth/identity.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import binascii
1111
import datetime
12+
import json
1213
import os
1314
import re
1415
import sys
@@ -24,6 +25,7 @@
2425
from jupyter_server.transutils import _i18n
2526

2627
from .security import passwd_check, set_password
28+
from .utils import get_anonymous_username
2729

2830
# circular imports for type checking
2931
if TYPE_CHECKING:
@@ -290,11 +292,28 @@ def user_to_cookie(self, user: User) -> str:
290292
Default is just the user's username.
291293
"""
292294
# default: username is enough
293-
return user.username
295+
cookie = json.dumps(
296+
{
297+
"username": user.username,
298+
"name": user.name,
299+
"display_name": user.display_name,
300+
"initials": user.initials,
301+
"color": user.color,
302+
}
303+
)
304+
return cookie
294305

295306
def user_from_cookie(self, cookie_value: str) -> User | None:
296307
"""Inverse of user_to_cookie"""
297-
return User(username=cookie_value)
308+
user = json.loads(cookie_value)
309+
return User(
310+
user["username"],
311+
user["name"],
312+
user["display_name"],
313+
user["initials"],
314+
None,
315+
user["color"],
316+
)
298317

299318
def get_cookie_name(self, handler: JupyterHandler) -> str:
300319
"""Return the login cookie name
@@ -396,7 +415,6 @@ def get_token(self, handler: JupyterHandler) -> str | None:
396415
- in URL parameters: ?token=<token>
397416
- in header: Authorization: token <token>
398417
"""
399-
400418
user_token = handler.get_argument("token", "")
401419
if not user_token:
402420
# get it from Authorization header
@@ -447,8 +465,12 @@ def generate_anonymous_user(self, handler: JupyterHandler) -> User:
447465
but does not identify a user.
448466
"""
449467
user_id = uuid.uuid4().hex
468+
moon = get_anonymous_username()
469+
name = display_name = f"Anonymous {moon}"
470+
initials = f"A{moon[0]}"
471+
color = None
450472
handler.log.info(f"Generating new user for token-authenticated request: {user_id}")
451-
return User(user_id)
473+
return User(user_id, name, display_name, initials, None, color)
452474

453475
def should_check_origin(self, handler: JupyterHandler) -> bool:
454476
"""Should the Handler check for CORS origin validation?

jupyter_server/auth/utils.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# Copyright (c) Jupyter Development Team.
44
# Distributed under the terms of the Modified BSD License.
55
import importlib
6+
import random
67
import re
78
import warnings
89

@@ -74,3 +75,95 @@ def match_url_to_resource(url, regex_mapping=None):
7475
pattern = re.compile(regex)
7576
if pattern.fullmatch(url):
7677
return auth_resource
78+
79+
80+
# From https://en.wikipedia.org/wiki/Moons_of_Jupiter
81+
moons_of_jupyter = [
82+
"Metis",
83+
"Adrastea",
84+
"Amalthea",
85+
"Thebe",
86+
"Io",
87+
"Europa",
88+
"Ganymede",
89+
"Callisto",
90+
"Themisto",
91+
"Leda",
92+
"Ersa",
93+
"Pandia",
94+
"Himalia",
95+
"Lysithea",
96+
"Elara",
97+
"Dia",
98+
"Carpo",
99+
"Valetudo",
100+
"Euporie",
101+
"Eupheme",
102+
# 'S/2003 J 18',
103+
# 'S/2010 J 2',
104+
"Helike",
105+
# 'S/2003 J 16',
106+
# 'S/2003 J 2',
107+
"Euanthe",
108+
# 'S/2017 J 7',
109+
"Hermippe",
110+
"Praxidike",
111+
"Thyone",
112+
"Thelxinoe",
113+
# 'S/2017 J 3',
114+
"Ananke",
115+
"Mneme",
116+
# 'S/2016 J 1',
117+
"Orthosie",
118+
"Harpalyke",
119+
"Iocaste",
120+
# 'S/2017 J 9',
121+
# 'S/2003 J 12',
122+
# 'S/2003 J 4',
123+
"Erinome",
124+
"Aitne",
125+
"Herse",
126+
"Taygete",
127+
# 'S/2017 J 2',
128+
# 'S/2017 J 6',
129+
"Eukelade",
130+
"Carme",
131+
# 'S/2003 J 19',
132+
"Isonoe",
133+
# 'S/2003 J 10',
134+
"Autonoe",
135+
"Philophrosyne",
136+
"Cyllene",
137+
"Pasithee",
138+
# 'S/2010 J 1',
139+
"Pasiphae",
140+
"Sponde",
141+
# 'S/2017 J 8',
142+
"Eurydome",
143+
# 'S/2017 J 5',
144+
"Kalyke",
145+
"Hegemone",
146+
"Kale",
147+
"Kallichore",
148+
# 'S/2011 J 1',
149+
# 'S/2017 J 1',
150+
"Chaldene",
151+
"Arche",
152+
"Eirene",
153+
"Kore",
154+
# 'S/2011 J 2',
155+
# 'S/2003 J 9',
156+
"Megaclite",
157+
"Aoede",
158+
# 'S/2003 J 23',
159+
"Callirrhoe",
160+
"Sinope",
161+
]
162+
163+
164+
def get_anonymous_username() -> str:
165+
"""
166+
Get a random user-name based on the moons of Jupyter.
167+
This function returns names like "Anonymous Io" or "Anonymous Metis".
168+
"""
169+
return moons_of_jupyter[random.randint(0, len(moons_of_jupyter) - 1)]

jupyter_server/services/contents/filemanager.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -904,3 +904,13 @@ async def is_hidden(self, path):
904904
path = path.strip("/")
905905
os_path = self._get_os_path(path=path)
906906
return is_hidden(os_path, self.root_dir)
907+
908+
async def get_kernel_path(self, path, model=None):
909+
"""Return the initial API path of a kernel associated with a given notebook"""
910+
if await self.dir_exists(path):
911+
return path
912+
if "/" in path:
913+
parent_dir = path.rsplit("/", 1)[0]
914+
else:
915+
parent_dir = ""
916+
return parent_dir

jupyter_server/services/contents/handlers.py

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,8 @@ async def get(self, path=""):
9999

100100
type = self.get_query_argument("type", default=None)
101101
if type not in {None, "directory", "file", "notebook"}:
102-
raise web.HTTPError(400, "Type %r is invalid" % type)
102+
# fall back to file if unknown type
103+
type = "file"
103104

104105
format = self.get_query_argument("format", default=None)
105106
if format not in {None, "text", "base64"}:
@@ -207,24 +208,22 @@ async def post(self, path=""):
207208
raise web.HTTPError(400, "Cannot POST to files, use PUT instead.")
208209

209210
model = self.get_json_body()
210-
copy_from = model.get("copy_from")
211-
if (
212-
copy_from
213-
and (
214-
await ensure_async(cm.is_hidden(path))
215-
or await ensure_async(cm.is_hidden(copy_from))
216-
)
217-
and not cm.allow_hidden
218-
):
219-
raise web.HTTPError(400, f"Cannot copy file or directory {path!r}")
220-
221-
if model is not None:
211+
if model:
222212
copy_from = model.get("copy_from")
223-
ext = model.get("ext", "")
224-
type = model.get("type", "")
225213
if copy_from:
226-
await self._copy(copy_from, path)
214+
if not cm.allow_hidden and (
215+
await ensure_async(cm.is_hidden(path))
216+
or await ensure_async(cm.is_hidden(copy_from))
217+
):
218+
raise web.HTTPError(400, f"Cannot copy file or directory {path!r}")
219+
else:
220+
await self._copy(copy_from, path)
227221
else:
222+
ext = model.get("ext", "")
223+
type = model.get("type", "")
224+
if type not in {None, "", "directory", "file", "notebook"}:
225+
# fall back to file if unknown type
226+
type = "file"
228227
await self._new_untitled(path, type=type, ext=ext)
229228
else:
230229
await self._new_untitled(path)

jupyter_server/services/nbconvert/handlers.py

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,16 @@
1111
AUTH_RESOURCE = "nbconvert"
1212

1313

14-
LOCK = asyncio.Lock()
15-
16-
1714
class NbconvertRootHandler(APIHandler):
1815
auth_resource = AUTH_RESOURCE
16+
_exporter_lock: asyncio.Lock
17+
18+
def initialize(self, **kwargs):
19+
super().initialize(**kwargs)
20+
# share lock across instances of this handler class
21+
if not hasattr(self.__class__, "_exporter_lock"):
22+
self.__class__._exporter_lock = asyncio.Lock()
23+
self._exporter_lock = self.__class__._exporter_lock
1924

2025
@web.authenticated
2126
@authorized
@@ -28,22 +33,22 @@ async def get(self):
2833
# Some exporters use the filesystem when instantiating, delegate that
2934
# to a thread so we don't block the event loop for it.
3035
exporters = await run_sync(base.get_export_names)
31-
for exporter_name in exporters:
32-
try:
33-
async with LOCK:
36+
async with self._exporter_lock:
37+
for exporter_name in exporters:
38+
try:
3439
exporter_class = await run_sync(base.get_exporter, exporter_name)
35-
except ValueError:
36-
# I think the only way this will happen is if the entrypoint
37-
# is uninstalled while this method is running
38-
continue
39-
# XXX: According to the docs, it looks like this should be set to None
40-
# if the exporter shouldn't be exposed to the front-end and a friendly
41-
# name if it should. However, none of the built-in exports have it defined.
42-
# if not exporter_class.export_from_notebook:
43-
# continue
44-
res[exporter_name] = {
45-
"output_mimetype": exporter_class.output_mimetype,
46-
}
40+
except ValueError:
41+
# I think the only way this will happen is if the entrypoint
42+
# is uninstalled while this method is running
43+
continue
44+
# XXX: According to the docs, it looks like this should be set to None
45+
# if the exporter shouldn't be exposed to the front-end and a friendly
46+
# name if it should. However, none of the built-in exports have it defined.
47+
# if not exporter_class.export_from_notebook:
48+
# continue
49+
res[exporter_name] = {
50+
"output_mimetype": exporter_class.output_mimetype,
51+
}
4752

4853
self.finish(json.dumps(res))
4954

jupyter_server/services/sessions/sessionmanager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ async def create_session(
271271
async def start_kernel_for_session(self, session_id, path, name, type, kernel_name):
272272
"""Start a new kernel for a given session."""
273273
# allow contents manager to specify kernels cwd
274-
kernel_path = self.contents_manager.get_kernel_path(path=path)
274+
kernel_path = await ensure_async(self.contents_manager.get_kernel_path(path=path))
275275
kernel_id = await self.kernel_manager.start_kernel(
276276
path=kernel_path,
277277
kernel_name=kernel_name,

tests/services/contents/test_api.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,10 @@ async def test_create_untitled(jp_fetch, contents, contents_dir, _check_created)
349349
r = await jp_fetch("api", "contents", path, method="POST", body=json.dumps({"ext": ".ipynb"}))
350350
_check_created(r, str(contents_dir), path, name, type="notebook")
351351

352+
name = "untitled"
353+
r = await jp_fetch("api", "contents", path, method="POST", allow_nonstandard_methods=True)
354+
_check_created(r, str(contents_dir), path, name=name, type="file")
355+
352356

353357
async def test_create_untitled_txt(jp_fetch, contents, contents_dir, _check_created):
354358
name = "untitled.txt"

0 commit comments

Comments
 (0)