mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-15 05:53:00 +00:00
Merge branch 'jupyterhub:main' into contributing
This commit is contained in:
23
.github/workflows/test.yml
vendored
23
.github/workflows/test.yml
vendored
@@ -28,7 +28,6 @@ on:
|
|||||||
env:
|
env:
|
||||||
# UTF-8 content may be interpreted as ascii and causes errors without this.
|
# UTF-8 content may be interpreted as ascii and causes errors without this.
|
||||||
LANG: C.UTF-8
|
LANG: C.UTF-8
|
||||||
PYTEST_ADDOPTS: "--verbose --color=yes"
|
|
||||||
SQLALCHEMY_WARN_20: "1"
|
SQLALCHEMY_WARN_20: "1"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@@ -79,21 +78,30 @@ jobs:
|
|||||||
oldest_dependencies: oldest_dependencies
|
oldest_dependencies: oldest_dependencies
|
||||||
legacy_notebook: legacy_notebook
|
legacy_notebook: legacy_notebook
|
||||||
- python: "3.8"
|
- python: "3.8"
|
||||||
legacy_notebook: legacy_notebook
|
jupyter_server: "1.*"
|
||||||
|
subset: singleuser
|
||||||
- python: "3.9"
|
- python: "3.9"
|
||||||
db: mysql
|
db: mysql
|
||||||
- python: "3.10"
|
- python: "3.10"
|
||||||
db: postgres
|
db: postgres
|
||||||
- python: "3.11"
|
- python: "3.11"
|
||||||
subdomain: subdomain
|
subdomain: subdomain
|
||||||
|
serverextension: serverextension
|
||||||
- python: "3.11"
|
- python: "3.11"
|
||||||
ssl: ssl
|
ssl: ssl
|
||||||
|
serverextension: serverextension
|
||||||
|
- python: "3.11"
|
||||||
|
subdomain: subdomain
|
||||||
|
noextension: noextension
|
||||||
|
subset: singleuser
|
||||||
|
- python: "3.11"
|
||||||
|
ssl: ssl
|
||||||
|
noextension: noextension
|
||||||
|
subset: singleuser
|
||||||
- python: "3.11"
|
- python: "3.11"
|
||||||
selenium: selenium
|
selenium: selenium
|
||||||
- python: "3.11"
|
- python: "3.11"
|
||||||
main_dependencies: main_dependencies
|
main_dependencies: main_dependencies
|
||||||
- python: "3.10"
|
|
||||||
serverextension: serverextension
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
# NOTE: In GitHub workflows, environment variables are set by writing
|
# NOTE: In GitHub workflows, environment variables are set by writing
|
||||||
@@ -119,6 +127,8 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
if [ "${{ matrix.serverextension }}" != "" ]; then
|
if [ "${{ matrix.serverextension }}" != "" ]; then
|
||||||
echo "JUPYTERHUB_SINGLEUSER_EXTENSION=1" >> $GITHUB_ENV
|
echo "JUPYTERHUB_SINGLEUSER_EXTENSION=1" >> $GITHUB_ENV
|
||||||
|
elif [ "${{ matrix.noextension }}" != "" ]; then
|
||||||
|
echo "JUPYTERHUB_SINGLEUSER_EXTENSION=0" >> $GITHUB_ENV
|
||||||
fi
|
fi
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
# NOTE: actions/setup-node@v3 make use of a cache within the GitHub base
|
# NOTE: actions/setup-node@v3 make use of a cache within the GitHub base
|
||||||
@@ -161,6 +171,9 @@ jobs:
|
|||||||
pip uninstall jupyter_server --yes
|
pip uninstall jupyter_server --yes
|
||||||
pip install 'notebook<7'
|
pip install 'notebook<7'
|
||||||
fi
|
fi
|
||||||
|
if [ "${{ matrix.jupyter_server }}" != "" ]; then
|
||||||
|
pip install "jupyter_server==${{ matrix.jupyter_server }}"
|
||||||
|
fi
|
||||||
if [ "${{ matrix.db }}" == "mysql" ]; then
|
if [ "${{ matrix.db }}" == "mysql" ]; then
|
||||||
pip install mysql-connector-python
|
pip install mysql-connector-python
|
||||||
fi
|
fi
|
||||||
@@ -220,7 +233,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Run pytest
|
- name: Run pytest
|
||||||
run: |
|
run: |
|
||||||
pytest --maxfail=2 --cov=jupyterhub jupyterhub/tests
|
pytest -k "${{ matrix.subset }}" --maxfail=2 --cov=jupyterhub jupyterhub/tests
|
||||||
|
|
||||||
- uses: codecov/codecov-action@v3
|
- uses: codecov/codecov-action@v3
|
||||||
|
|
||||||
|
@@ -105,11 +105,16 @@ def _scrub_headers(headers):
|
|||||||
auth_type = ''
|
auth_type = ''
|
||||||
headers['Authorization'] = f'{auth_type} [secret]'
|
headers['Authorization'] = f'{auth_type} [secret]'
|
||||||
if 'Cookie' in headers:
|
if 'Cookie' in headers:
|
||||||
c = SimpleCookie(headers['Cookie'])
|
try:
|
||||||
redacted = []
|
c = SimpleCookie(headers['Cookie'])
|
||||||
for name in c.keys():
|
except Exception as e:
|
||||||
redacted.append(f"{name}=[secret]")
|
# it's possible for browsers to send invalid cookies
|
||||||
headers['Cookie'] = '; '.join(redacted)
|
headers['Cookie'] = f"Invalid Cookie: {e}"
|
||||||
|
else:
|
||||||
|
redacted = []
|
||||||
|
for name in c.keys():
|
||||||
|
redacted.append(f"{name}=[secret]")
|
||||||
|
headers['Cookie'] = '; '.join(redacted)
|
||||||
return headers
|
return headers
|
||||||
|
|
||||||
|
|
||||||
|
@@ -1,40 +1,75 @@
|
|||||||
"""JupyterHub single-user server entrypoints
|
"""JupyterHub single-user server entrypoints
|
||||||
|
|
||||||
Contains default notebook-app subclass and mixins
|
Contains default notebook-app subclass and mixins
|
||||||
|
|
||||||
|
Defaults to:
|
||||||
|
|
||||||
|
- Jupyter server extension with Jupyter Server >=2
|
||||||
|
- Subclass with Jupyter Server <2 or clasic notebook
|
||||||
|
|
||||||
|
Application subclass can be controlled with environment variables:
|
||||||
|
|
||||||
|
- JUPYTERHUB_SINGLEUSER_EXTENSION=1 to opt-in to the extension (requires Jupyter Server 2)
|
||||||
|
- JUPYTERHUB_SINGLEUSER_APP=notebook (or jupyter-server) to opt-in
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from .mixins import HubAuthenticatedHandler, make_singleuser_app
|
from .mixins import HubAuthenticatedHandler, make_singleuser_app
|
||||||
|
|
||||||
if os.environ.get("JUPYTERHUB_SINGLEUSER_EXTENSION", "") not in ("", "0"):
|
_as_extension = False
|
||||||
|
_extension_env = os.environ.get("JUPYTERHUB_SINGLEUSER_EXTENSION", "")
|
||||||
|
_app_env = os.environ.get("JUPYTERHUB_SINGLEUSER_APP", "")
|
||||||
|
|
||||||
|
if not _extension_env:
|
||||||
|
# extension env not set, check app env
|
||||||
|
if not _app_env or 'jupyter_server' in _app_env.replace("-", "_"):
|
||||||
|
# no app env set or using jupyter-server, this is the default branch
|
||||||
|
# default behavior:
|
||||||
|
# - extension, if jupyter server 2
|
||||||
|
# - older subclass app, otherwise
|
||||||
|
try:
|
||||||
|
import jupyter_server
|
||||||
|
|
||||||
|
_server_major = int(jupyter_server.__version__.split(".", 1)[0])
|
||||||
|
except Exception:
|
||||||
|
# don't have jupyter-server, assume classic notebook
|
||||||
|
_as_extension = False
|
||||||
|
else:
|
||||||
|
# default to extension if jupyter-server >=2
|
||||||
|
_as_extension = _server_major >= 2
|
||||||
|
|
||||||
|
elif _app_env == "extension":
|
||||||
|
_as_extension = True
|
||||||
|
else:
|
||||||
|
# app env set and not to jupyter-server, that opts out of extension
|
||||||
|
_as_extension = False
|
||||||
|
elif _extension_env == "0":
|
||||||
|
_as_extension = False
|
||||||
|
else:
|
||||||
|
# extension env set to anything non-empty other than '0' enables the extension
|
||||||
_as_extension = True
|
_as_extension = True
|
||||||
|
|
||||||
|
if _as_extension:
|
||||||
# check for conflict in singleuser entrypoint environment variables
|
# check for conflict in singleuser entrypoint environment variables
|
||||||
if os.environ.get("JUPYTERHUB_SINGLEUSER_APP", "") not in {
|
if _app_env not in {
|
||||||
"",
|
"",
|
||||||
"jupyter_server",
|
"jupyter_server",
|
||||||
"jupyter-server",
|
"jupyter-server",
|
||||||
"extension",
|
"extension",
|
||||||
"jupyter_server.serverapp.ServerApp",
|
"jupyter_server.serverapp.ServerApp",
|
||||||
}:
|
}:
|
||||||
ext = os.environ["JUPYTERHUB_SINGLEUSER_EXTENSION"]
|
|
||||||
app = os.environ["JUPYTERHUB_SINGLEUSER_APP"]
|
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Cannot use JUPYTERHUB_SINGLEUSER_EXTENSION={ext} with JUPYTERHUB_SINGLEUSER_APP={app}."
|
f"Cannot use JUPYTERHUB_SINGLEUSER_EXTENSION={_extension_env} with JUPYTERHUB_SINGLEUSER_APP={_app_env}."
|
||||||
" Please pick one or the other."
|
" Please pick one or the other."
|
||||||
)
|
)
|
||||||
from .extension import main
|
from .extension import main
|
||||||
else:
|
else:
|
||||||
_as_extension = False
|
from .app import SingleUserNotebookApp, main
|
||||||
try:
|
|
||||||
from .app import SingleUserNotebookApp, main
|
# backward-compatibility
|
||||||
except ImportError:
|
JupyterHubLoginHandler = SingleUserNotebookApp.login_handler_class
|
||||||
# check for Jupyter Server 2.0 ?
|
JupyterHubLogoutHandler = SingleUserNotebookApp.logout_handler_class
|
||||||
from .extension import main
|
OAuthCallbackHandler = SingleUserNotebookApp.oauth_callback_handler_class
|
||||||
else:
|
|
||||||
# backward-compatibility
|
|
||||||
JupyterHubLoginHandler = SingleUserNotebookApp.login_handler_class
|
|
||||||
JupyterHubLogoutHandler = SingleUserNotebookApp.logout_handler_class
|
|
||||||
OAuthCallbackHandler = SingleUserNotebookApp.oauth_callback_handler_class
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
@@ -475,6 +475,7 @@ class JupyterHubSingleUser(ExtensionApp):
|
|||||||
cfg.identity_provider_class = JupyterHubIdentityProvider
|
cfg.identity_provider_class = JupyterHubIdentityProvider
|
||||||
|
|
||||||
# disable some single-user features
|
# disable some single-user features
|
||||||
|
cfg.allow_remote_access = True
|
||||||
cfg.open_browser = False
|
cfg.open_browser = False
|
||||||
cfg.trust_xheaders = True
|
cfg.trust_xheaders = True
|
||||||
cfg.quit_button = False
|
cfg.quit_button = False
|
||||||
@@ -501,6 +502,13 @@ class JupyterHubSingleUser(ExtensionApp):
|
|||||||
# to make sure it has the desired effect
|
# to make sure it has the desired effect
|
||||||
cfg.default_url = self.default_url = self.get_default_url()
|
cfg.default_url = self.default_url = self.get_default_url()
|
||||||
|
|
||||||
|
# load internal SSL configuration
|
||||||
|
cfg.keyfile = os.environ.get('JUPYTERHUB_SSL_KEYFILE') or ''
|
||||||
|
cfg.certfile = os.environ.get('JUPYTERHUB_SSL_CERTFILE') or ''
|
||||||
|
cfg.client_ca = os.environ.get('JUPYTERHUB_SSL_CLIENT_CA') or ''
|
||||||
|
if cfg.certfile:
|
||||||
|
self.serverapp.log.info(f"Using SSL cert {cfg.certfile}")
|
||||||
|
|
||||||
# Jupyter Server default: config files have higher priority than extensions,
|
# Jupyter Server default: config files have higher priority than extensions,
|
||||||
# by:
|
# by:
|
||||||
# 1. load config files
|
# 1. load config files
|
||||||
|
@@ -660,6 +660,7 @@ class SingleUserNotebookAppMixin(Configurable):
|
|||||||
certfile=self.certfile,
|
certfile=self.certfile,
|
||||||
client_ca=self.client_ca,
|
client_ca=self.client_ca,
|
||||||
)
|
)
|
||||||
|
self.hub_host = self.hub_auth.hub_host
|
||||||
# smoke check
|
# smoke check
|
||||||
if not self.hub_auth.oauth_client_id:
|
if not self.hub_auth.oauth_client_id:
|
||||||
raise ValueError("Missing OAuth client ID")
|
raise ValueError("Missing OAuth client ID")
|
||||||
|
@@ -283,4 +283,8 @@ async def test_nbclassic_control_panel(app, user, full_spawn):
|
|||||||
page = BeautifulSoup(r.text, "html.parser")
|
page = BeautifulSoup(r.text, "html.parser")
|
||||||
link = page.find("a", id="jupyterhub-control-panel-link")
|
link = page.find("a", id="jupyterhub-control-panel-link")
|
||||||
assert link, f"Missing jupyterhub-control-panel-link in {page}"
|
assert link, f"Missing jupyterhub-control-panel-link in {page}"
|
||||||
assert link["href"] == url_path_join(app.base_url, "hub/home")
|
if app.subdomain_host:
|
||||||
|
prefix = public_url(app)
|
||||||
|
else:
|
||||||
|
prefix = app.base_url
|
||||||
|
assert link["href"] == url_path_join(prefix, "hub/home")
|
||||||
|
@@ -7,7 +7,7 @@
|
|||||||
asyncio_mode = auto
|
asyncio_mode = auto
|
||||||
|
|
||||||
# jupyter_server plugin is incompatible with notebook imports
|
# jupyter_server plugin is incompatible with notebook imports
|
||||||
addopts = -p no:jupyter_server -m 'not selenium'
|
addopts = -p no:jupyter_server -m 'not selenium' --color yes --durations 10 --verbose
|
||||||
|
|
||||||
python_files = test_*.py
|
python_files = test_*.py
|
||||||
markers =
|
markers =
|
||||||
|
Reference in New Issue
Block a user