mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-07 18:14:10 +00:00
Compare commits
32 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
bbc3870803 | ||
![]() |
212d618978 | ||
![]() |
75673fc268 | ||
![]() |
332a393083 | ||
![]() |
fa538cfc65 | ||
![]() |
29ae082399 | ||
![]() |
463960edaf | ||
![]() |
d9c6e43508 | ||
![]() |
961d2fe878 | ||
![]() |
5636472ebf | ||
![]() |
fc02f9e2e6 | ||
![]() |
fd21b2fe94 | ||
![]() |
6051dc9fa7 | ||
![]() |
4ee5ee4e02 | ||
![]() |
745cad5058 | ||
![]() |
335803d19f | ||
![]() |
3924295650 | ||
![]() |
c135e109ab | ||
![]() |
7e098fa09f | ||
![]() |
49f88450d5 | ||
![]() |
de20379933 | ||
![]() |
8d406c398b | ||
![]() |
dbd3813a1c | ||
![]() |
df04596172 | ||
![]() |
12f96df4eb | ||
![]() |
aecb95cd26 | ||
![]() |
5fecb71265 | ||
![]() |
e0157ff5eb | ||
![]() |
5ae250506b | ||
![]() |
8d298922e5 | ||
![]() |
18707e24b3 | ||
![]() |
3580904e8a |
@@ -16,7 +16,7 @@ ci:
|
|||||||
repos:
|
repos:
|
||||||
# autoformat and lint Python code
|
# autoformat and lint Python code
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.3.5
|
rev: v0.4.3
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
types_or:
|
types_or:
|
||||||
@@ -42,13 +42,14 @@ repos:
|
|||||||
- id: djlint-reformat-jinja
|
- id: djlint-reformat-jinja
|
||||||
files: ".*templates/.*.html"
|
files: ".*templates/.*.html"
|
||||||
types_or: ["html"]
|
types_or: ["html"]
|
||||||
|
exclude: redoc.html
|
||||||
- id: djlint-jinja
|
- id: djlint-jinja
|
||||||
files: ".*templates/.*.html"
|
files: ".*templates/.*.html"
|
||||||
types_or: ["html"]
|
types_or: ["html"]
|
||||||
|
|
||||||
# Autoformat and linting, misc. details
|
# Autoformat and linting, misc. details
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.5.0
|
rev: v4.6.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
exclude: share/jupyterhub/static/js/admin-react.js
|
exclude: share/jupyterhub/static/js/admin-react.js
|
||||||
|
@@ -7,7 +7,7 @@ info:
|
|||||||
license:
|
license:
|
||||||
name: BSD-3-Clause
|
name: BSD-3-Clause
|
||||||
identifier: BSD-3-Clause
|
identifier: BSD-3-Clause
|
||||||
version: 5.0.0b1
|
version: 5.0.0b2
|
||||||
servers:
|
servers:
|
||||||
- url: /hub/api
|
- url: /hub/api
|
||||||
security:
|
security:
|
||||||
@@ -1714,12 +1714,31 @@ components:
|
|||||||
url:
|
url:
|
||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
The URL where the server can be accessed
|
The URL path where the server can be accessed
|
||||||
(typically /user/:name/:server.name/).
|
(typically /user/:name/:server.name/).
|
||||||
|
Will be a full URL if subdomains are configured.
|
||||||
progress_url:
|
progress_url:
|
||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
The URL for an event-stream to retrieve events during a spawn.
|
The URL path for an event-stream to retrieve events during a spawn.
|
||||||
|
full_url:
|
||||||
|
type:
|
||||||
|
- string
|
||||||
|
- "null"
|
||||||
|
description: |
|
||||||
|
The full URL of the server (`https://hub.example.org/user/:name/:servername`).
|
||||||
|
`null` unless JupyterHub.public_url or subdomains are configured.
|
||||||
|
|
||||||
|
Added in 5.0.
|
||||||
|
full_progress_url:
|
||||||
|
type:
|
||||||
|
- string
|
||||||
|
- "null"
|
||||||
|
description: |
|
||||||
|
The full URL for the progress events (`https://hub.example.org/hub/api/users/:name/servers/:servername/progress`).
|
||||||
|
`null` unless JupyterHub.public_url is configured.
|
||||||
|
|
||||||
|
Added in 5.0.
|
||||||
started:
|
started:
|
||||||
type: string
|
type: string
|
||||||
description: UTC timestamp when the server was last started.
|
description: UTC timestamp when the server was last started.
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
{# djlint: off #}
|
||||||
{%- extends "!layout.html" %}
|
{%- extends "!layout.html" %}
|
||||||
{# not sure why, but theme CSS prevents scrolling within redoc content
|
{# not sure why, but theme CSS prevents scrolling within redoc content
|
||||||
# If this were fixed, we could keep the navbar and footer
|
# If this were fixed, we could keep the navbar and footer
|
||||||
@@ -8,9 +9,7 @@
|
|||||||
{% endblock docs_navbar %}
|
{% endblock docs_navbar %}
|
||||||
{% block footer %}
|
{% block footer %}
|
||||||
{% endblock footer %}
|
{% endblock footer %}
|
||||||
{# djlint: off #}
|
|
||||||
{%- block body_tag -%}<body>{%- endblock body_tag %}
|
{%- block body_tag -%}<body>{%- endblock body_tag %}
|
||||||
{# djlint: on #}
|
|
||||||
{%- block extrahead %}
|
{%- block extrahead %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
<link href="{{ pathto('_static/redoc-fonts.css', 1) }}" rel="stylesheet" />
|
<link href="{{ pathto('_static/redoc-fonts.css', 1) }}" rel="stylesheet" />
|
||||||
@@ -23,14 +22,11 @@
|
|||||||
document.body.innerText = "Rendered API specification doesn't work with file: protocol. Use sphinx-autobuild to do local builds of the docs, served over HTTP."
|
document.body.innerText = "Rendered API specification doesn't work with file: protocol. Use sphinx-autobuild to do local builds of the docs, served over HTTP."
|
||||||
} else {
|
} else {
|
||||||
Redoc.init(
|
Redoc.init(
|
||||||
"{{ pathto('_static/rest-api.yml', 1) }}", {
|
"{{ pathto('_static/rest-api.yml', 1) }}",
|
||||||
{
|
{{ meta.redoc_options | default ({}) }},
|
||||||
meta.redoc_options |
|
|
||||||
default ({})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
document.getElementById("redoc-spec"),
|
document.getElementById("redoc-spec"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
{%- endblock content %}
|
{%- endblock content %}
|
||||||
|
{# djlint: on #}
|
||||||
|
@@ -289,6 +289,7 @@ linkcheck_ignore = [
|
|||||||
r"https://github.com/[^/]*$", # too many github usernames / searches in changelog
|
r"https://github.com/[^/]*$", # too many github usernames / searches in changelog
|
||||||
"https://github.com/jupyterhub/jupyterhub/pull/", # too many PRs in changelog
|
"https://github.com/jupyterhub/jupyterhub/pull/", # too many PRs in changelog
|
||||||
"https://github.com/jupyterhub/jupyterhub/compare/", # too many comparisons in changelog
|
"https://github.com/jupyterhub/jupyterhub/compare/", # too many comparisons in changelog
|
||||||
|
"https://schema.jupyter.org/jupyterhub/.*", # schemas are not published yet
|
||||||
r"https?://(localhost|127.0.0.1).*", # ignore localhost references in auto-links
|
r"https?://(localhost|127.0.0.1).*", # ignore localhost references in auto-links
|
||||||
r"https://linux.die.net/.*", # linux.die.net seems to block requests from CI with 403 sometimes
|
r"https://linux.die.net/.*", # linux.die.net seems to block requests from CI with 403 sometimes
|
||||||
# don't check links to unpublished advisories
|
# don't check links to unpublished advisories
|
||||||
|
File diff suppressed because one or more lines are too long
@@ -1,28 +1,23 @@
|
|||||||
# Event logging and telemetry
|
# Event logging and telemetry
|
||||||
|
|
||||||
JupyterHub can be configured to record structured events from a running server using Jupyter's [Telemetry System]. The types of events that JupyterHub emits are defined by [JSON schemas] listed at the bottom of this page.
|
JupyterHub can be configured to record structured events from a running server using Jupyter's [Events System]. The types of events that JupyterHub emits are defined by [JSON schemas] listed at the bottom of this page.
|
||||||
|
|
||||||
## How to emit events
|
## How to emit events
|
||||||
|
|
||||||
Event logging is handled by its `Eventlog` object. This leverages Python's standing [logging] library to emit, filter, and collect event data.
|
Event logging is handled by its `EventLogger` object. This leverages Python's standing [logging] library to emit, filter, and collect event data.
|
||||||
|
|
||||||
To begin recording events, you'll need to set two configurations:
|
To begin recording events, you'll need to set at least one configuration option:
|
||||||
|
|
||||||
> 1. `handlers`: tells the EventLog _where_ to route your events. This trait is a list of Python logging handlers that route events to the event log file.
|
> `EventLogger.handlers`: tells the EventLogger _where_ to route your events. This trait is a list of Python logging handlers that route events to e.g. an event log file.
|
||||||
> 2. `allows_schemas`: tells the EventLog _which_ events should be recorded. No events are emitted by default; all recorded events must be listed here.
|
|
||||||
|
|
||||||
Here's a basic example:
|
Here's a basic example:
|
||||||
|
|
||||||
```
|
```python
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
c.EventLog.handlers = [
|
c.EventLogger.handlers = [
|
||||||
logging.FileHandler('event.log'),
|
logging.FileHandler('event.log'),
|
||||||
]
|
]
|
||||||
|
|
||||||
c.EventLog.allowed_schemas = [
|
|
||||||
'hub.jupyter.org/server-action'
|
|
||||||
]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The output is a file, `"event.log"`, with events recorded as JSON data.
|
The output is a file, `"event.log"`, with events recorded as JSON data.
|
||||||
@@ -37,6 +32,15 @@ The output is a file, `"event.log"`, with events recorded as JSON data.
|
|||||||
server-actions
|
server-actions
|
||||||
```
|
```
|
||||||
|
|
||||||
|
:::{versionchanged} 5.0
|
||||||
|
JupyterHub 5.0 changes from the deprecated jupyter-telemetry to jupyter-events.
|
||||||
|
|
||||||
|
The main changes are:
|
||||||
|
|
||||||
|
- `EventLog` configuration is now called `EventLogger`
|
||||||
|
- The `hub.jupyter.org/server-action` schema is now called `https://schema.jupyter.org/jupyterhub/events/server-action`
|
||||||
|
:::
|
||||||
|
|
||||||
[json schemas]: https://json-schema.org/
|
[json schemas]: https://json-schema.org/
|
||||||
[logging]: https://docs.python.org/3/library/logging.html
|
[logging]: https://docs.python.org/3/library/logging.html
|
||||||
[telemetry system]: https://github.com/jupyter/telemetry
|
[events system]: https://jupyter-events.readthedocs.io
|
||||||
|
@@ -514,7 +514,7 @@ For example, using flask:
|
|||||||
:language: python
|
:language: python
|
||||||
```
|
```
|
||||||
|
|
||||||
We recommend looking at the [`HubOAuth`][huboauth] class implementation for reference,
|
We recommend looking at the {class}`.HubOAuth` class implementation for reference,
|
||||||
and taking note of the following process:
|
and taking note of the following process:
|
||||||
|
|
||||||
1. retrieve the token from the request.
|
1. retrieve the token from the request.
|
||||||
|
@@ -7,5 +7,5 @@ import httpx
|
|||||||
def get_client():
|
def get_client():
|
||||||
base_url = os.environ["JUPYTERHUB_API_URL"]
|
base_url = os.environ["JUPYTERHUB_API_URL"]
|
||||||
token = os.environ["JUPYTERHUB_API_TOKEN"]
|
token = os.environ["JUPYTERHUB_API_TOKEN"]
|
||||||
headers = {"Authorization": "Bearer %s" % token}
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
return httpx.AsyncClient(base_url=base_url, headers=headers)
|
return httpx.AsyncClient(base_url=base_url, headers=headers)
|
||||||
|
@@ -38,7 +38,7 @@ def authenticated(f):
|
|||||||
else:
|
else:
|
||||||
# redirect to login url on failed auth
|
# redirect to login url on failed auth
|
||||||
state = auth.generate_state(next_url=request.path)
|
state = auth.generate_state(next_url=request.path)
|
||||||
response = make_response(redirect(auth.login_url + '&state=%s' % state))
|
response = make_response(redirect(auth.login_url + f'&state={state}'))
|
||||||
response.set_cookie(auth.state_cookie_name, state)
|
response.set_cookie(auth.state_cookie_name, state)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@@ -7,6 +7,7 @@ c.JupyterHub.services = [
|
|||||||
'name': 'whoami-api',
|
'name': 'whoami-api',
|
||||||
'url': 'http://127.0.0.1:10101',
|
'url': 'http://127.0.0.1:10101',
|
||||||
'command': [sys.executable, './whoami.py'],
|
'command': [sys.executable, './whoami.py'],
|
||||||
|
'display': False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'whoami-oauth',
|
'name': 'whoami-oauth',
|
||||||
@@ -36,3 +37,5 @@ c.JupyterHub.load_roles = [
|
|||||||
c.JupyterHub.authenticator_class = 'dummy'
|
c.JupyterHub.authenticator_class = 'dummy'
|
||||||
c.JupyterHub.spawner_class = 'simple'
|
c.JupyterHub.spawner_class = 'simple'
|
||||||
c.JupyterHub.ip = '127.0.0.1' # let's just run on localhost while dummy auth is enabled
|
c.JupyterHub.ip = '127.0.0.1' # let's just run on localhost while dummy auth is enabled
|
||||||
|
# default to home page, since we don't want to start servers for this demo
|
||||||
|
c.JupyterHub.default_url = "/hub/home"
|
||||||
|
@@ -11,7 +11,7 @@ c = get_config() # noqa
|
|||||||
|
|
||||||
class DemoFormSpawner(LocalProcessSpawner):
|
class DemoFormSpawner(LocalProcessSpawner):
|
||||||
def _options_form_default(self):
|
def _options_form_default(self):
|
||||||
default_env = "YOURNAME=%s\n" % self.user.name
|
default_env = f"YOURNAME={self.user.name}\n"
|
||||||
return f"""
|
return f"""
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="args">Extra notebook CLI arguments</label>
|
<label for="args">Extra notebook CLI arguments</label>
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
# Copyright (c) Jupyter Development Team.
|
# Copyright (c) Jupyter Development Team.
|
||||||
# Distributed under the terms of the Modified BSD License.
|
# Distributed under the terms of the Modified BSD License.
|
||||||
# version_info updated by running `tbump`
|
# version_info updated by running `tbump`
|
||||||
version_info = (5, 0, 0, "b1", "")
|
version_info = (5, 0, 0, "b2", "")
|
||||||
|
|
||||||
# pep 440 version: no dot before beta/rc, but before .dev
|
# pep 440 version: no dot before beta/rc, but before .dev
|
||||||
# 0.1.0rc1
|
# 0.1.0rc1
|
||||||
@@ -70,6 +70,4 @@ def _check_version(hub_version, singleuser_version, log):
|
|||||||
singleuser_version,
|
singleuser_version,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
log.debug(
|
log.debug(f"jupyterhub and jupyterhub-singleuser both on version {hub_version}")
|
||||||
"jupyterhub and jupyterhub-singleuser both on version %s" % hub_version
|
|
||||||
)
|
|
||||||
|
@@ -46,7 +46,7 @@ class TokenAPIHandler(APIHandler):
|
|||||||
elif orm_token.service:
|
elif orm_token.service:
|
||||||
model = self.service_model(orm_token.service)
|
model = self.service_model(orm_token.service)
|
||||||
else:
|
else:
|
||||||
self.log.warning("%s has no user or service. Deleting..." % orm_token)
|
self.log.warning(f"{orm_token} has no user or service. Deleting...")
|
||||||
self.db.delete(orm_token)
|
self.db.delete(orm_token)
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
raise web.HTTPError(404)
|
raise web.HTTPError(404)
|
||||||
|
@@ -202,7 +202,22 @@ class APIHandler(BaseHandler):
|
|||||||
'url': url_path_join(user.url, url_escape_path(spawner.name), '/'),
|
'url': url_path_join(user.url, url_escape_path(spawner.name), '/'),
|
||||||
'user_options': spawner.user_options,
|
'user_options': spawner.user_options,
|
||||||
'progress_url': user.progress_url(spawner.name),
|
'progress_url': user.progress_url(spawner.name),
|
||||||
|
'full_url': None,
|
||||||
|
'full_progress_url': None,
|
||||||
}
|
}
|
||||||
|
# fill out full_url fields
|
||||||
|
public_url = self.settings.get("public_url")
|
||||||
|
if urlparse(model["url"]).netloc:
|
||||||
|
# if using subdomains, this is already a full URL
|
||||||
|
model["full_url"] = model["url"]
|
||||||
|
if public_url:
|
||||||
|
model["full_progress_url"] = urlunparse(
|
||||||
|
public_url._replace(path=model["progress_url"])
|
||||||
|
)
|
||||||
|
if not model["full_url"]:
|
||||||
|
# set if not defined already by subdomain
|
||||||
|
model["full_url"] = urlunparse(public_url._replace(path=model["url"]))
|
||||||
|
|
||||||
scope_filter = self.get_scope_filter('admin:server_state')
|
scope_filter = self.get_scope_filter('admin:server_state')
|
||||||
if scope_filter(spawner, kind='server'):
|
if scope_filter(spawner, kind='server'):
|
||||||
model['state'] = state
|
model['state'] = state
|
||||||
@@ -446,9 +461,9 @@ class APIHandler(BaseHandler):
|
|||||||
name (str): name of the model, used in error messages
|
name (str): name of the model, used in error messages
|
||||||
"""
|
"""
|
||||||
if not isinstance(model, dict):
|
if not isinstance(model, dict):
|
||||||
raise web.HTTPError(400, "Invalid JSON data: %r" % model)
|
raise web.HTTPError(400, f"Invalid JSON data: {model!r}")
|
||||||
if not set(model).issubset(set(model_types)):
|
if not set(model).issubset(set(model_types)):
|
||||||
raise web.HTTPError(400, "Invalid JSON keys: %r" % model)
|
raise web.HTTPError(400, f"Invalid JSON keys: {model!r}")
|
||||||
for key, value in model.items():
|
for key, value in model.items():
|
||||||
if not isinstance(value, model_types[key]):
|
if not isinstance(value, model_types[key]):
|
||||||
raise web.HTTPError(
|
raise web.HTTPError(
|
||||||
|
@@ -19,7 +19,7 @@ class _GroupAPIHandler(APIHandler):
|
|||||||
username = self.authenticator.normalize_username(username)
|
username = self.authenticator.normalize_username(username)
|
||||||
user = self.find_user(username)
|
user = self.find_user(username)
|
||||||
if user is None:
|
if user is None:
|
||||||
raise web.HTTPError(400, "No such user: %s" % username)
|
raise web.HTTPError(400, f"No such user: {username}")
|
||||||
users.append(user.orm_user)
|
users.append(user.orm_user)
|
||||||
return users
|
return users
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ class GroupListAPIHandler(_GroupAPIHandler):
|
|||||||
for name in groupnames:
|
for name in groupnames:
|
||||||
existing = orm.Group.find(self.db, name=name)
|
existing = orm.Group.find(self.db, name=name)
|
||||||
if existing is not None:
|
if existing is not None:
|
||||||
raise web.HTTPError(409, "Group %s already exists" % name)
|
raise web.HTTPError(409, f"Group {name} already exists")
|
||||||
|
|
||||||
usernames = model.get('users', [])
|
usernames = model.get('users', [])
|
||||||
# check that users exist
|
# check that users exist
|
||||||
@@ -124,7 +124,7 @@ class GroupAPIHandler(_GroupAPIHandler):
|
|||||||
|
|
||||||
existing = orm.Group.find(self.db, name=group_name)
|
existing = orm.Group.find(self.db, name=group_name)
|
||||||
if existing is not None:
|
if existing is not None:
|
||||||
raise web.HTTPError(409, "Group %s already exists" % group_name)
|
raise web.HTTPError(409, f"Group {group_name} already exists")
|
||||||
|
|
||||||
usernames = model.get('users', [])
|
usernames = model.get('users', [])
|
||||||
# check that users exist
|
# check that users exist
|
||||||
|
@@ -32,14 +32,14 @@ class ShutdownAPIHandler(APIHandler):
|
|||||||
proxy = data['proxy']
|
proxy = data['proxy']
|
||||||
if proxy not in {True, False}:
|
if proxy not in {True, False}:
|
||||||
raise web.HTTPError(
|
raise web.HTTPError(
|
||||||
400, "proxy must be true or false, got %r" % proxy
|
400, f"proxy must be true or false, got {proxy!r}"
|
||||||
)
|
)
|
||||||
app.cleanup_proxy = proxy
|
app.cleanup_proxy = proxy
|
||||||
if 'servers' in data:
|
if 'servers' in data:
|
||||||
servers = data['servers']
|
servers = data['servers']
|
||||||
if servers not in {True, False}:
|
if servers not in {True, False}:
|
||||||
raise web.HTTPError(
|
raise web.HTTPError(
|
||||||
400, "servers must be true or false, got %r" % servers
|
400, f"servers must be true or false, got {servers!r}"
|
||||||
)
|
)
|
||||||
app.cleanup_servers = servers
|
app.cleanup_servers = servers
|
||||||
|
|
||||||
|
@@ -5,9 +5,14 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import inspect
|
import inspect
|
||||||
import json
|
import json
|
||||||
|
import sys
|
||||||
from datetime import timedelta, timezone
|
from datetime import timedelta, timezone
|
||||||
|
|
||||||
from async_generator import aclosing
|
if sys.version_info >= (3, 10):
|
||||||
|
from contextlib import aclosing
|
||||||
|
else:
|
||||||
|
from async_generator import aclosing
|
||||||
|
|
||||||
from dateutil.parser import parse as parse_date
|
from dateutil.parser import parse as parse_date
|
||||||
from sqlalchemy import func, or_
|
from sqlalchemy import func, or_
|
||||||
from sqlalchemy.orm import joinedload, raiseload, selectinload # noqa
|
from sqlalchemy.orm import joinedload, raiseload, selectinload # noqa
|
||||||
@@ -156,7 +161,7 @@ class UserListAPIHandler(APIHandler):
|
|||||||
.having(func.count(orm.Server.id) == 0)
|
.having(func.count(orm.Server.id) == 0)
|
||||||
)
|
)
|
||||||
elif state_filter:
|
elif state_filter:
|
||||||
raise web.HTTPError(400, "Unrecognized state filter: %r" % state_filter)
|
raise web.HTTPError(400, f"Unrecognized state filter: {state_filter!r}")
|
||||||
|
|
||||||
# apply eager load options
|
# apply eager load options
|
||||||
query = query.options(
|
query = query.options(
|
||||||
@@ -241,15 +246,15 @@ class UserListAPIHandler(APIHandler):
|
|||||||
continue
|
continue
|
||||||
user = self.find_user(name)
|
user = self.find_user(name)
|
||||||
if user is not None:
|
if user is not None:
|
||||||
self.log.warning("User %s already exists" % name)
|
self.log.warning(f"User {name} already exists")
|
||||||
else:
|
else:
|
||||||
to_create.append(name)
|
to_create.append(name)
|
||||||
|
|
||||||
if invalid_names:
|
if invalid_names:
|
||||||
if len(invalid_names) == 1:
|
if len(invalid_names) == 1:
|
||||||
msg = "Invalid username: %s" % invalid_names[0]
|
msg = f"Invalid username: {invalid_names[0]}"
|
||||||
else:
|
else:
|
||||||
msg = "Invalid usernames: %s" % ', '.join(invalid_names)
|
msg = "Invalid usernames: {}".format(', '.join(invalid_names))
|
||||||
raise web.HTTPError(400, msg)
|
raise web.HTTPError(400, msg)
|
||||||
|
|
||||||
if not to_create:
|
if not to_create:
|
||||||
@@ -265,7 +270,7 @@ class UserListAPIHandler(APIHandler):
|
|||||||
try:
|
try:
|
||||||
await maybe_future(self.authenticator.add_user(user))
|
await maybe_future(self.authenticator.add_user(user))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.error("Failed to create user: %s" % name, exc_info=True)
|
self.log.error(f"Failed to create user: {name}", exc_info=True)
|
||||||
self.users.delete(user)
|
self.users.delete(user)
|
||||||
raise web.HTTPError(400, f"Failed to create user {name}: {e}")
|
raise web.HTTPError(400, f"Failed to create user {name}: {e}")
|
||||||
else:
|
else:
|
||||||
@@ -302,7 +307,7 @@ class UserAPIHandler(APIHandler):
|
|||||||
data = self.get_json_body()
|
data = self.get_json_body()
|
||||||
user = self.find_user(user_name)
|
user = self.find_user(user_name)
|
||||||
if user is not None:
|
if user is not None:
|
||||||
raise web.HTTPError(409, "User %s already exists" % user_name)
|
raise web.HTTPError(409, f"User {user_name} already exists")
|
||||||
|
|
||||||
user = self.user_from_username(user_name)
|
user = self.user_from_username(user_name)
|
||||||
if data:
|
if data:
|
||||||
@@ -315,10 +320,10 @@ class UserAPIHandler(APIHandler):
|
|||||||
try:
|
try:
|
||||||
await maybe_future(self.authenticator.add_user(user))
|
await maybe_future(self.authenticator.add_user(user))
|
||||||
except Exception:
|
except Exception:
|
||||||
self.log.error("Failed to create user: %s" % user_name, exc_info=True)
|
self.log.error(f"Failed to create user: {user_name}", exc_info=True)
|
||||||
# remove from registry
|
# remove from registry
|
||||||
self.users.delete(user)
|
self.users.delete(user)
|
||||||
raise web.HTTPError(400, "Failed to create user: %s" % user_name)
|
raise web.HTTPError(400, f"Failed to create user: {user_name}")
|
||||||
|
|
||||||
self.write(json.dumps(self.user_model(user)))
|
self.write(json.dumps(self.user_model(user)))
|
||||||
self.set_status(201)
|
self.set_status(201)
|
||||||
@@ -333,15 +338,14 @@ class UserAPIHandler(APIHandler):
|
|||||||
if user.spawner._stop_pending:
|
if user.spawner._stop_pending:
|
||||||
raise web.HTTPError(
|
raise web.HTTPError(
|
||||||
400,
|
400,
|
||||||
"%s's server is in the process of stopping, please wait." % user_name,
|
f"{user_name}'s server is in the process of stopping, please wait.",
|
||||||
)
|
)
|
||||||
if user.running:
|
if user.running:
|
||||||
await self.stop_single_user(user)
|
await self.stop_single_user(user)
|
||||||
if user.spawner._stop_pending:
|
if user.spawner._stop_pending:
|
||||||
raise web.HTTPError(
|
raise web.HTTPError(
|
||||||
400,
|
400,
|
||||||
"%s's server is in the process of stopping, please wait."
|
f"{user_name}'s server is in the process of stopping, please wait.",
|
||||||
% user_name,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
await maybe_future(self.authenticator.delete_user(user))
|
await maybe_future(self.authenticator.delete_user(user))
|
||||||
@@ -365,7 +369,9 @@ class UserAPIHandler(APIHandler):
|
|||||||
if self.find_user(data['name']):
|
if self.find_user(data['name']):
|
||||||
raise web.HTTPError(
|
raise web.HTTPError(
|
||||||
400,
|
400,
|
||||||
"User %s already exists, username must be unique" % data['name'],
|
"User {} already exists, username must be unique".format(
|
||||||
|
data['name']
|
||||||
|
),
|
||||||
)
|
)
|
||||||
for key, value in data.items():
|
for key, value in data.items():
|
||||||
if key == 'auth_state':
|
if key == 'auth_state':
|
||||||
@@ -397,7 +403,7 @@ class UserTokenListAPIHandler(APIHandler):
|
|||||||
"""Get tokens for a given user"""
|
"""Get tokens for a given user"""
|
||||||
user = self.find_user(user_name)
|
user = self.find_user(user_name)
|
||||||
if not user:
|
if not user:
|
||||||
raise web.HTTPError(404, "No such user: %s" % user_name)
|
raise web.HTTPError(404, f"No such user: {user_name}")
|
||||||
|
|
||||||
now = utcnow(with_tz=False)
|
now = utcnow(with_tz=False)
|
||||||
api_tokens = []
|
api_tokens = []
|
||||||
@@ -619,7 +625,7 @@ class UserServerAPIHandler(APIHandler):
|
|||||||
finally:
|
finally:
|
||||||
spawner._spawn_pending = False
|
spawner._spawn_pending = False
|
||||||
if state is None:
|
if state is None:
|
||||||
raise web.HTTPError(400, "%s is already running" % spawner._log_name)
|
raise web.HTTPError(400, f"{spawner._log_name} is already running")
|
||||||
|
|
||||||
options = self.get_json_body()
|
options = self.get_json_body()
|
||||||
await self.spawn_single_user(user, server_name, options=options)
|
await self.spawn_single_user(user, server_name, options=options)
|
||||||
|
@@ -21,6 +21,7 @@ from datetime import datetime, timedelta, timezone
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
from getpass import getuser
|
from getpass import getuser
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
|
from pathlib import Path
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from urllib.parse import unquote, urlparse, urlunparse
|
from urllib.parse import unquote, urlparse, urlunparse
|
||||||
@@ -29,7 +30,7 @@ import tornado.httpserver
|
|||||||
import tornado.options
|
import tornado.options
|
||||||
from dateutil.parser import parse as parse_date
|
from dateutil.parser import parse as parse_date
|
||||||
from jinja2 import ChoiceLoader, Environment, FileSystemLoader, PrefixLoader
|
from jinja2 import ChoiceLoader, Environment, FileSystemLoader, PrefixLoader
|
||||||
from jupyter_telemetry.eventlog import EventLog
|
from jupyter_events.logger import EventLogger
|
||||||
from sqlalchemy.exc import OperationalError, SQLAlchemyError
|
from sqlalchemy.exc import OperationalError, SQLAlchemyError
|
||||||
from sqlalchemy.orm import joinedload
|
from sqlalchemy.orm import joinedload
|
||||||
from tornado import gen, web
|
from tornado import gen, web
|
||||||
@@ -213,7 +214,7 @@ class NewToken(Application):
|
|||||||
ThreadPoolExecutor(1).submit(init_roles_and_users).result()
|
ThreadPoolExecutor(1).submit(init_roles_and_users).result()
|
||||||
user = orm.User.find(hub.db, self.name)
|
user = orm.User.find(hub.db, self.name)
|
||||||
if user is None:
|
if user is None:
|
||||||
print("No such user: %s" % self.name, file=sys.stderr)
|
print(f"No such user: {self.name}", file=sys.stderr)
|
||||||
self.exit(1)
|
self.exit(1)
|
||||||
token = user.new_api_token(note="command-line generated")
|
token = user.new_api_token(note="command-line generated")
|
||||||
print(token)
|
print(token)
|
||||||
@@ -1475,7 +1476,7 @@ class JupyterHub(Application):
|
|||||||
new = change['new']
|
new = change['new']
|
||||||
if '://' not in new:
|
if '://' not in new:
|
||||||
# assume sqlite, if given as a plain filename
|
# assume sqlite, if given as a plain filename
|
||||||
self.db_url = 'sqlite:///%s' % new
|
self.db_url = f'sqlite:///{new}'
|
||||||
|
|
||||||
db_kwargs = Dict(
|
db_kwargs = Dict(
|
||||||
help="""Include any kwargs to pass to the database connection.
|
help="""Include any kwargs to pass to the database connection.
|
||||||
@@ -1778,10 +1779,10 @@ class JupyterHub(Application):
|
|||||||
[
|
[
|
||||||
# add trailing / to ``/user|services/:name`
|
# add trailing / to ``/user|services/:name`
|
||||||
(
|
(
|
||||||
r"%s(user|services)/([^/]+)" % self.base_url,
|
rf"{self.base_url}(user|services)/([^/]+)",
|
||||||
handlers.AddSlashHandler,
|
handlers.AddSlashHandler,
|
||||||
),
|
),
|
||||||
(r"(?!%s).*" % self.hub_prefix, handlers.PrefixRedirectHandler),
|
(rf"(?!{self.hub_prefix}).*", handlers.PrefixRedirectHandler),
|
||||||
(r'(.*)', handlers.Template404),
|
(r'(.*)', handlers.Template404),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@@ -1910,7 +1911,7 @@ class JupyterHub(Application):
|
|||||||
default_alt_names = ["IP:127.0.0.1", "DNS:localhost"]
|
default_alt_names = ["IP:127.0.0.1", "DNS:localhost"]
|
||||||
if self.subdomain_host:
|
if self.subdomain_host:
|
||||||
default_alt_names.append(
|
default_alt_names.append(
|
||||||
"DNS:%s" % urlparse(self.subdomain_host).hostname
|
f"DNS:{urlparse(self.subdomain_host).hostname}"
|
||||||
)
|
)
|
||||||
# The signed certs used by hub-internal components
|
# The signed certs used by hub-internal components
|
||||||
try:
|
try:
|
||||||
@@ -2095,7 +2096,7 @@ class JupyterHub(Application):
|
|||||||
ck.check_available()
|
ck.check_available()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.exit(
|
self.exit(
|
||||||
"auth_state is enabled, but encryption is not available: %s" % e
|
f"auth_state is enabled, but encryption is not available: {e}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# give the authenticator a chance to check its own config
|
# give the authenticator a chance to check its own config
|
||||||
@@ -2114,7 +2115,7 @@ class JupyterHub(Application):
|
|||||||
self.authenticator.admin_users = set(admin_users) # force normalization
|
self.authenticator.admin_users = set(admin_users) # force normalization
|
||||||
for username in admin_users:
|
for username in admin_users:
|
||||||
if not self.authenticator.validate_username(username):
|
if not self.authenticator.validate_username(username):
|
||||||
raise ValueError("username %r is not valid" % username)
|
raise ValueError(f"username {username!r} is not valid")
|
||||||
|
|
||||||
new_users = []
|
new_users = []
|
||||||
|
|
||||||
@@ -2138,7 +2139,7 @@ class JupyterHub(Application):
|
|||||||
self.authenticator.allowed_users = set(allowed_users) # force normalization
|
self.authenticator.allowed_users = set(allowed_users) # force normalization
|
||||||
for username in allowed_users:
|
for username in allowed_users:
|
||||||
if not self.authenticator.validate_username(username):
|
if not self.authenticator.validate_username(username):
|
||||||
raise ValueError("username %r is not valid" % username)
|
raise ValueError(f"username {username!r} is not valid")
|
||||||
|
|
||||||
if self.authenticator.allowed_users and self.authenticator.admin_users:
|
if self.authenticator.allowed_users and self.authenticator.admin_users:
|
||||||
# make sure admin users are in the allowed_users set, if defined,
|
# make sure admin users are in the allowed_users set, if defined,
|
||||||
@@ -2206,7 +2207,7 @@ class JupyterHub(Application):
|
|||||||
user = orm.User.find(self.db, name=username)
|
user = orm.User.find(self.db, name=username)
|
||||||
if user is None:
|
if user is None:
|
||||||
if not self.authenticator.validate_username(username):
|
if not self.authenticator.validate_username(username):
|
||||||
raise ValueError("Username %r is not valid" % username)
|
raise ValueError(f"Username {username!r} is not valid")
|
||||||
self.log.info(f"Creating user {username} found in {hint}")
|
self.log.info(f"Creating user {username} found in {hint}")
|
||||||
user = orm.User(name=username)
|
user = orm.User(name=username)
|
||||||
self.db.add(user)
|
self.db.add(user)
|
||||||
@@ -2317,9 +2318,7 @@ class JupyterHub(Application):
|
|||||||
old_role = orm.Role.find(self.db, name=role_name)
|
old_role = orm.Role.find(self.db, name=role_name)
|
||||||
if old_role:
|
if old_role:
|
||||||
if not set(role_spec.get('scopes', [])).issubset(old_role.scopes):
|
if not set(role_spec.get('scopes', [])).issubset(old_role.scopes):
|
||||||
self.log.warning(
|
self.log.warning(f"Role {role_name} has obtained extra permissions")
|
||||||
"Role %s has obtained extra permissions" % role_name
|
|
||||||
)
|
|
||||||
roles_with_new_permissions.append(role_name)
|
roles_with_new_permissions.append(role_name)
|
||||||
|
|
||||||
# make sure we load any default roles not overridden
|
# make sure we load any default roles not overridden
|
||||||
@@ -2583,14 +2582,14 @@ class JupyterHub(Application):
|
|||||||
elif kind == 'service':
|
elif kind == 'service':
|
||||||
Class = orm.Service
|
Class = orm.Service
|
||||||
else:
|
else:
|
||||||
raise ValueError("kind must be user or service, not %r" % kind)
|
raise ValueError(f"kind must be user or service, not {kind!r}")
|
||||||
|
|
||||||
db = self.db
|
db = self.db
|
||||||
for token, name in token_dict.items():
|
for token, name in token_dict.items():
|
||||||
if kind == 'user':
|
if kind == 'user':
|
||||||
name = self.authenticator.normalize_username(name)
|
name = self.authenticator.normalize_username(name)
|
||||||
if not self.authenticator.validate_username(name):
|
if not self.authenticator.validate_username(name):
|
||||||
raise ValueError("Token user name %r is not valid" % name)
|
raise ValueError(f"Token user name {name!r} is not valid")
|
||||||
if kind == 'service':
|
if kind == 'service':
|
||||||
if not any(service_name == name for service_name in self._service_map):
|
if not any(service_name == name for service_name in self._service_map):
|
||||||
self.log.warning(
|
self.log.warning(
|
||||||
@@ -2790,7 +2789,7 @@ class JupyterHub(Application):
|
|||||||
for key, value in spec.items():
|
for key, value in spec.items():
|
||||||
trait = traits.get(key)
|
trait = traits.get(key)
|
||||||
if trait is None:
|
if trait is None:
|
||||||
raise AttributeError("No such service field: %s" % key)
|
raise AttributeError(f"No such service field: {key}")
|
||||||
setattr(service, key, value)
|
setattr(service, key, value)
|
||||||
# also set the value on the orm object
|
# also set the value on the orm object
|
||||||
# unless it's marked as not in the db
|
# unless it's marked as not in the db
|
||||||
@@ -2863,7 +2862,7 @@ class JupyterHub(Application):
|
|||||||
client_id=service.oauth_client_id,
|
client_id=service.oauth_client_id,
|
||||||
client_secret=service.api_token,
|
client_secret=service.api_token,
|
||||||
redirect_uri=service.oauth_redirect_uri,
|
redirect_uri=service.oauth_redirect_uri,
|
||||||
description="JupyterHub service %s" % service.name,
|
description=f"JupyterHub service {service.name}",
|
||||||
)
|
)
|
||||||
service.orm.oauth_client = oauth_client
|
service.orm.oauth_client = oauth_client
|
||||||
# add access-scopes, derived from OAuthClient itself
|
# add access-scopes, derived from OAuthClient itself
|
||||||
@@ -3251,13 +3250,10 @@ class JupyterHub(Application):
|
|||||||
|
|
||||||
def init_eventlog(self):
|
def init_eventlog(self):
|
||||||
"""Set up the event logging system."""
|
"""Set up the event logging system."""
|
||||||
self.eventlog = EventLog(parent=self)
|
self.eventlog = EventLogger(parent=self)
|
||||||
|
|
||||||
for dirname, _, files in os.walk(os.path.join(here, 'event-schemas')):
|
for schema in (Path(here) / "event-schemas").glob("**/*.yaml"):
|
||||||
for file in files:
|
self.eventlog.register_event_schema(schema)
|
||||||
if not file.endswith('.yaml'):
|
|
||||||
continue
|
|
||||||
self.eventlog.register_schema_file(os.path.join(dirname, file))
|
|
||||||
|
|
||||||
def write_pid_file(self):
|
def write_pid_file(self):
|
||||||
pid = os.getpid()
|
pid = os.getpid()
|
||||||
@@ -3456,7 +3452,7 @@ class JupyterHub(Application):
|
|||||||
answer = ''
|
answer = ''
|
||||||
|
|
||||||
def ask():
|
def ask():
|
||||||
prompt = "Overwrite %s with default config? [y/N]" % self.config_file
|
prompt = f"Overwrite {self.config_file} with default config? [y/N]"
|
||||||
try:
|
try:
|
||||||
return input(prompt).lower() or 'n'
|
return input(prompt).lower() or 'n'
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
@@ -3473,7 +3469,7 @@ class JupyterHub(Application):
|
|||||||
config_text = self.generate_config_file()
|
config_text = self.generate_config_file()
|
||||||
if isinstance(config_text, bytes):
|
if isinstance(config_text, bytes):
|
||||||
config_text = config_text.decode('utf8')
|
config_text = config_text.decode('utf8')
|
||||||
print("Writing default config to: %s" % self.config_file)
|
print(f"Writing default config to: {self.config_file}")
|
||||||
with open(self.config_file, mode='w') as f:
|
with open(self.config_file, mode='w') as f:
|
||||||
f.write(config_text)
|
f.write(config_text)
|
||||||
|
|
||||||
|
@@ -332,7 +332,7 @@ class Authenticator(LoggingConfigurable):
|
|||||||
if short_names:
|
if short_names:
|
||||||
sorted_names = sorted(short_names)
|
sorted_names = sorted(short_names)
|
||||||
single = ''.join(sorted_names)
|
single = ''.join(sorted_names)
|
||||||
string_set_typo = "set('%s')" % single
|
string_set_typo = f"set('{single}')"
|
||||||
self.log.warning(
|
self.log.warning(
|
||||||
"Allowed set contains single-character names: %s; did you mean set([%r]) instead of %s?",
|
"Allowed set contains single-character names: %s; did you mean set([%r]) instead of %s?",
|
||||||
sorted_names[:8],
|
sorted_names[:8],
|
||||||
@@ -663,7 +663,7 @@ class Authenticator(LoggingConfigurable):
|
|||||||
return
|
return
|
||||||
if isinstance(authenticated, dict):
|
if isinstance(authenticated, dict):
|
||||||
if 'name' not in authenticated:
|
if 'name' not in authenticated:
|
||||||
raise ValueError("user missing a name: %r" % authenticated)
|
raise ValueError(f"user missing a name: {authenticated!r}")
|
||||||
else:
|
else:
|
||||||
authenticated = {'name': authenticated}
|
authenticated = {'name': authenticated}
|
||||||
authenticated.setdefault('auth_state', None)
|
authenticated.setdefault('auth_state', None)
|
||||||
@@ -850,7 +850,7 @@ class Authenticator(LoggingConfigurable):
|
|||||||
user (User): The User wrapper object
|
user (User): The User wrapper object
|
||||||
"""
|
"""
|
||||||
if not self.validate_username(user.name):
|
if not self.validate_username(user.name):
|
||||||
raise ValueError("Invalid username: %s" % user.name)
|
raise ValueError(f"Invalid username: {user.name}")
|
||||||
if self.allow_existing_users and not self.allow_all:
|
if self.allow_existing_users and not self.allow_all:
|
||||||
self.allowed_users.add(user.name)
|
self.allowed_users.add(user.name)
|
||||||
|
|
||||||
@@ -1115,13 +1115,16 @@ class LocalAuthenticator(Authenticator):
|
|||||||
"""
|
"""
|
||||||
if not self.allowed_groups:
|
if not self.allowed_groups:
|
||||||
return False
|
return False
|
||||||
|
user_group_gids = set(
|
||||||
|
self._getgrouplist(username, self._getpwnam(username).pw_gid)
|
||||||
|
)
|
||||||
for grnam in self.allowed_groups:
|
for grnam in self.allowed_groups:
|
||||||
try:
|
try:
|
||||||
group = self._getgrnam(grnam)
|
group = self._getgrnam(grnam)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
self.log.error('No such group: [%s]' % grnam)
|
self.log.error(f'No such group: [{grnam}]')
|
||||||
continue
|
continue
|
||||||
if username in group.gr_mem:
|
if group.gr_gid in user_group_gids:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -1190,7 +1193,7 @@ class LocalAuthenticator(Authenticator):
|
|||||||
uid = self.uids[name]
|
uid = self.uids[name]
|
||||||
cmd += ['--uid', '%d' % uid]
|
cmd += ['--uid', '%d' % uid]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
self.log.debug("No UID for user %s" % name)
|
self.log.debug(f"No UID for user {name}")
|
||||||
cmd += [name]
|
cmd += [name]
|
||||||
self.log.info("Creating user: %s", ' '.join(map(shlex.quote, cmd)))
|
self.log.info("Creating user: %s", ' '.join(map(shlex.quote, cmd)))
|
||||||
p = Popen(cmd, stdout=PIPE, stderr=STDOUT)
|
p = Popen(cmd, stdout=PIPE, stderr=STDOUT)
|
||||||
|
@@ -33,7 +33,7 @@ class CryptographyUnavailable(EncryptionUnavailable):
|
|||||||
|
|
||||||
class NoEncryptionKeys(EncryptionUnavailable):
|
class NoEncryptionKeys(EncryptionUnavailable):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "Encryption keys must be specified in %s env" % KEY_ENV
|
return f"Encryption keys must be specified in {KEY_ENV} env"
|
||||||
|
|
||||||
|
|
||||||
def _validate_key(key):
|
def _validate_key(key):
|
||||||
|
@@ -95,7 +95,7 @@ def backup_db_file(db_file, log=None):
|
|||||||
backup_db_file = f'{db_file}.{timestamp}.{i}'
|
backup_db_file = f'{db_file}.{timestamp}.{i}'
|
||||||
#
|
#
|
||||||
if os.path.exists(backup_db_file):
|
if os.path.exists(backup_db_file):
|
||||||
raise OSError("backup db file already exists: %s" % backup_db_file)
|
raise OSError(f"backup db file already exists: {backup_db_file}")
|
||||||
if log:
|
if log:
|
||||||
log.info("Backing up %s => %s", db_file, backup_db_file)
|
log.info("Backing up %s => %s", db_file, backup_db_file)
|
||||||
shutil.copy(db_file, backup_db_file)
|
shutil.copy(db_file, backup_db_file)
|
||||||
@@ -167,7 +167,7 @@ def main(args=None):
|
|||||||
# to subcommands
|
# to subcommands
|
||||||
choices = ['shell', 'alembic']
|
choices = ['shell', 'alembic']
|
||||||
if not args or args[0] not in choices:
|
if not args or args[0] not in choices:
|
||||||
print("Select a command from: %s" % ', '.join(choices))
|
print("Select a command from: {}".format(', '.join(choices)))
|
||||||
return 1
|
return 1
|
||||||
cmd, args = args[0], args[1:]
|
cmd, args = args[0], args[1:]
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
"$id": hub.jupyter.org/server-action
|
"$id": https://schema.jupyter.org/jupyterhub/events/server-action
|
||||||
version: 1
|
version: 1
|
||||||
title: JupyterHub server events
|
title: JupyterHub server events
|
||||||
description: |
|
description: |
|
||||||
|
@@ -1128,10 +1128,13 @@ class BaseHandler(RequestHandler):
|
|||||||
SERVER_SPAWN_DURATION_SECONDS.labels(
|
SERVER_SPAWN_DURATION_SECONDS.labels(
|
||||||
status=ServerSpawnStatus.success
|
status=ServerSpawnStatus.success
|
||||||
).observe(time.perf_counter() - spawn_start_time)
|
).observe(time.perf_counter() - spawn_start_time)
|
||||||
self.eventlog.record_event(
|
self.eventlog.emit(
|
||||||
'hub.jupyter.org/server-action',
|
schema_id='https://schema.jupyter.org/jupyterhub/events/server-action',
|
||||||
1,
|
data={
|
||||||
{'action': 'start', 'username': user.name, 'servername': server_name},
|
'action': 'start',
|
||||||
|
'username': user.name,
|
||||||
|
'servername': server_name,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
proxy_add_start_time = time.perf_counter()
|
proxy_add_start_time = time.perf_counter()
|
||||||
spawner._proxy_pending = True
|
spawner._proxy_pending = True
|
||||||
@@ -1334,10 +1337,9 @@ class BaseHandler(RequestHandler):
|
|||||||
SERVER_STOP_DURATION_SECONDS.labels(
|
SERVER_STOP_DURATION_SECONDS.labels(
|
||||||
status=ServerStopStatus.success
|
status=ServerStopStatus.success
|
||||||
).observe(toc - tic)
|
).observe(toc - tic)
|
||||||
self.eventlog.record_event(
|
self.eventlog.emit(
|
||||||
'hub.jupyter.org/server-action',
|
schema_id='https://schema.jupyter.org/jupyterhub/events/server-action',
|
||||||
1,
|
data={
|
||||||
{
|
|
||||||
'action': 'stop',
|
'action': 'stop',
|
||||||
'username': user.name,
|
'username': user.name,
|
||||||
'servername': server_name,
|
'servername': server_name,
|
||||||
@@ -1512,7 +1514,7 @@ class BaseHandler(RequestHandler):
|
|||||||
# so we run it sync here, instead of making a sync version of render_template
|
# so we run it sync here, instead of making a sync version of render_template
|
||||||
|
|
||||||
try:
|
try:
|
||||||
html = self.render_template('%s.html' % status_code, sync=True, **ns)
|
html = self.render_template(f'{status_code}.html', sync=True, **ns)
|
||||||
except TemplateNotFound:
|
except TemplateNotFound:
|
||||||
self.log.debug("Using default error template for %d", status_code)
|
self.log.debug("Using default error template for %d", status_code)
|
||||||
try:
|
try:
|
||||||
@@ -1537,6 +1539,16 @@ class PrefixRedirectHandler(BaseHandler):
|
|||||||
"""Redirect anything outside a prefix inside.
|
"""Redirect anything outside a prefix inside.
|
||||||
|
|
||||||
Redirects /foo to /prefix/foo, etc.
|
Redirects /foo to /prefix/foo, etc.
|
||||||
|
|
||||||
|
Redirect specifies hub domain when public_url or subdomains are enabled.
|
||||||
|
|
||||||
|
Mainly handles requests for non-running servers, e.g. to
|
||||||
|
|
||||||
|
/user/tree/ -> /hub/user/tree/
|
||||||
|
|
||||||
|
UserUrlHandler will handle the request after redirect.
|
||||||
|
Don't do anything but redirect here because cookies, etc. won't be available to this request,
|
||||||
|
due to not being on the hub's path or possibly domain.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
@@ -1554,7 +1566,19 @@ class PrefixRedirectHandler(BaseHandler):
|
|||||||
# default / -> /hub/ redirect
|
# default / -> /hub/ redirect
|
||||||
# avoiding extra hop through /hub
|
# avoiding extra hop through /hub
|
||||||
path = '/'
|
path = '/'
|
||||||
self.redirect(url_path_join(self.hub.base_url, path), permanent=False)
|
|
||||||
|
redirect_url = redirect_path = url_path_join(self.hub.base_url, path)
|
||||||
|
|
||||||
|
# when using subdomains,
|
||||||
|
# make sure we redirect `user.domain/user/foo` -> `hub.domain/hub/user/foo/...`
|
||||||
|
# so that the Hub handles it properly with cookies and all
|
||||||
|
public_url = self.settings.get("public_url")
|
||||||
|
subdomain_host = self.settings.get("subdomain_host")
|
||||||
|
if public_url:
|
||||||
|
redirect_url = urlunparse(public_url._replace(path=redirect_path))
|
||||||
|
elif subdomain_host:
|
||||||
|
redirect_url = url_path_join(subdomain_host, redirect_path)
|
||||||
|
self.redirect(redirect_url, permanent=False)
|
||||||
|
|
||||||
|
|
||||||
class UserUrlHandler(BaseHandler):
|
class UserUrlHandler(BaseHandler):
|
||||||
|
@@ -236,12 +236,12 @@ class SpawnHandler(BaseHandler):
|
|||||||
if for_user != user.name:
|
if for_user != user.name:
|
||||||
user = self.find_user(for_user)
|
user = self.find_user(for_user)
|
||||||
if user is None:
|
if user is None:
|
||||||
raise web.HTTPError(404, "No such user: %s" % for_user)
|
raise web.HTTPError(404, f"No such user: {for_user}")
|
||||||
|
|
||||||
spawner = user.get_spawner(server_name, replace_failed=True)
|
spawner = user.get_spawner(server_name, replace_failed=True)
|
||||||
|
|
||||||
if spawner.ready:
|
if spawner.ready:
|
||||||
raise web.HTTPError(400, "%s is already running" % (spawner._log_name))
|
raise web.HTTPError(400, f"{spawner._log_name} is already running")
|
||||||
elif spawner.pending:
|
elif spawner.pending:
|
||||||
raise web.HTTPError(
|
raise web.HTTPError(
|
||||||
400, f"{spawner._log_name} is pending {spawner.pending}"
|
400, f"{spawner._log_name} is pending {spawner.pending}"
|
||||||
@@ -251,7 +251,7 @@ class SpawnHandler(BaseHandler):
|
|||||||
for key, byte_list in self.request.body_arguments.items():
|
for key, byte_list in self.request.body_arguments.items():
|
||||||
form_options[key] = [bs.decode('utf8') for bs in byte_list]
|
form_options[key] = [bs.decode('utf8') for bs in byte_list]
|
||||||
for key, byte_list in self.request.files.items():
|
for key, byte_list in self.request.files.items():
|
||||||
form_options["%s_file" % key] = byte_list
|
form_options[f"{key}_file"] = byte_list
|
||||||
try:
|
try:
|
||||||
self.log.debug(
|
self.log.debug(
|
||||||
"Triggering spawn with supplied form options for %s", spawner._log_name
|
"Triggering spawn with supplied form options for %s", spawner._log_name
|
||||||
@@ -345,7 +345,7 @@ class SpawnPendingHandler(BaseHandler):
|
|||||||
if for_user != current_user.name:
|
if for_user != current_user.name:
|
||||||
user = self.find_user(for_user)
|
user = self.find_user(for_user)
|
||||||
if user is None:
|
if user is None:
|
||||||
raise web.HTTPError(404, "No such user: %s" % for_user)
|
raise web.HTTPError(404, f"No such user: {for_user}")
|
||||||
|
|
||||||
if server_name and server_name not in user.spawners:
|
if server_name and server_name not in user.spawners:
|
||||||
raise web.HTTPError(404, f"{user.name} has no such server {server_name}")
|
raise web.HTTPError(404, f"{user.name} has no such server {server_name}")
|
||||||
@@ -642,7 +642,7 @@ class ProxyErrorHandler(BaseHandler):
|
|||||||
message_html = ' '.join(
|
message_html = ' '.join(
|
||||||
[
|
[
|
||||||
"Your server appears to be down.",
|
"Your server appears to be down.",
|
||||||
"Try restarting it <a href='%s'>from the hub</a>" % hub_home,
|
f"Try restarting it <a href='{hub_home}'>from the hub</a>",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
ns = dict(
|
ns = dict(
|
||||||
@@ -655,7 +655,7 @@ class ProxyErrorHandler(BaseHandler):
|
|||||||
self.set_header('Content-Type', 'text/html')
|
self.set_header('Content-Type', 'text/html')
|
||||||
# render the template
|
# render the template
|
||||||
try:
|
try:
|
||||||
html = await self.render_template('%s.html' % status_code, **ns)
|
html = await self.render_template(f'{status_code}.html', **ns)
|
||||||
except TemplateNotFound:
|
except TemplateNotFound:
|
||||||
self.log.debug("Using default error template for %d", status_code)
|
self.log.debug("Using default error template for %d", status_code)
|
||||||
html = await self.render_template('error.html', **ns)
|
html = await self.render_template('error.html', **ns)
|
||||||
|
@@ -156,7 +156,7 @@ class JupyterHubRequestValidator(RequestValidator):
|
|||||||
self.db.query(orm.OAuthClient).filter_by(identifier=client_id).first()
|
self.db.query(orm.OAuthClient).filter_by(identifier=client_id).first()
|
||||||
)
|
)
|
||||||
if orm_client is None:
|
if orm_client is None:
|
||||||
raise ValueError("No such client: %s" % client_id)
|
raise ValueError(f"No such client: {client_id}")
|
||||||
scopes = set(orm_client.allowed_scopes)
|
scopes = set(orm_client.allowed_scopes)
|
||||||
if 'inherit' not in scopes:
|
if 'inherit' not in scopes:
|
||||||
# add identify-user scope
|
# add identify-user scope
|
||||||
@@ -255,7 +255,7 @@ class JupyterHubRequestValidator(RequestValidator):
|
|||||||
self.db.query(orm.OAuthClient).filter_by(identifier=client_id).first()
|
self.db.query(orm.OAuthClient).filter_by(identifier=client_id).first()
|
||||||
)
|
)
|
||||||
if orm_client is None:
|
if orm_client is None:
|
||||||
raise ValueError("No such client: %s" % client_id)
|
raise ValueError(f"No such client: {client_id}")
|
||||||
|
|
||||||
orm_code = orm.OAuthCode(
|
orm_code = orm.OAuthCode(
|
||||||
code=code['code'],
|
code=code['code'],
|
||||||
@@ -345,7 +345,7 @@ class JupyterHubRequestValidator(RequestValidator):
|
|||||||
app_log.debug("Saving bearer token %s", log_token)
|
app_log.debug("Saving bearer token %s", log_token)
|
||||||
|
|
||||||
if request.user is None:
|
if request.user is None:
|
||||||
raise ValueError("No user for access token: %s" % request.user)
|
raise ValueError(f"No user for access token: {request.user}")
|
||||||
client = (
|
client = (
|
||||||
self.db.query(orm.OAuthClient)
|
self.db.query(orm.OAuthClient)
|
||||||
.filter_by(identifier=request.client.client_id)
|
.filter_by(identifier=request.client.client_id)
|
||||||
|
@@ -1113,7 +1113,7 @@ class APIToken(Hashed, Base):
|
|||||||
elif kind == 'service':
|
elif kind == 'service':
|
||||||
prefix_match = prefix_match.filter(cls.service_id != None)
|
prefix_match = prefix_match.filter(cls.service_id != None)
|
||||||
elif kind is not None:
|
elif kind is not None:
|
||||||
raise ValueError("kind must be 'user', 'service', or None, not %r" % kind)
|
raise ValueError(f"kind must be 'user', 'service', or None, not {kind!r}")
|
||||||
for orm_token in prefix_match:
|
for orm_token in prefix_match:
|
||||||
if orm_token.match(token):
|
if orm_token.match(token):
|
||||||
if not orm_token.client_id:
|
if not orm_token.client_id:
|
||||||
|
@@ -221,11 +221,11 @@ class Proxy(LoggingConfigurable):
|
|||||||
host_route = not routespec.startswith('/')
|
host_route = not routespec.startswith('/')
|
||||||
if host_route and not self.host_routing:
|
if host_route and not self.host_routing:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Cannot add host-based route %r, not using host-routing" % routespec
|
f"Cannot add host-based route {routespec!r}, not using host-routing"
|
||||||
)
|
)
|
||||||
if self.host_routing and not host_route:
|
if self.host_routing and not host_route:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Cannot add route without host %r, using host-routing" % routespec
|
f"Cannot add route without host {routespec!r}, using host-routing"
|
||||||
)
|
)
|
||||||
# add trailing slash
|
# add trailing slash
|
||||||
if not routespec.endswith('/'):
|
if not routespec.endswith('/'):
|
||||||
@@ -613,8 +613,8 @@ class ConfigurableHTTPProxy(Proxy):
|
|||||||
# check for required token if proxy is external
|
# check for required token if proxy is external
|
||||||
if not self.auth_token and not self.should_start:
|
if not self.auth_token and not self.should_start:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"%s.auth_token or CONFIGPROXY_AUTH_TOKEN env is required"
|
f"{self.__class__.__name__}.auth_token or CONFIGPROXY_AUTH_TOKEN env is required"
|
||||||
" if Proxy.should_start is False" % self.__class__.__name__
|
" if Proxy.should_start is False"
|
||||||
)
|
)
|
||||||
|
|
||||||
def _check_previous_process(self):
|
def _check_previous_process(self):
|
||||||
@@ -758,11 +758,11 @@ class ConfigurableHTTPProxy(Proxy):
|
|||||||
)
|
)
|
||||||
except FileNotFoundError as e:
|
except FileNotFoundError as e:
|
||||||
self.log.error(
|
self.log.error(
|
||||||
"Failed to find proxy %r\n"
|
f"Failed to find proxy {self.command!r}\n"
|
||||||
"The proxy can be installed with `npm install -g configurable-http-proxy`."
|
"The proxy can be installed with `npm install -g configurable-http-proxy`."
|
||||||
"To install `npm`, install nodejs which includes `npm`."
|
"To install `npm`, install nodejs which includes `npm`."
|
||||||
"If you see an `EACCES` error or permissions error, refer to the `npm` "
|
"If you see an `EACCES` error or permissions error, refer to the `npm` "
|
||||||
"documentation on How To Prevent Permissions Errors." % self.command
|
"documentation on How To Prevent Permissions Errors."
|
||||||
)
|
)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
@@ -1257,7 +1257,7 @@ def define_custom_scopes(scopes):
|
|||||||
The keys are the scopes,
|
The keys are the scopes,
|
||||||
while the values are dictionaries with at least a `description` field,
|
while the values are dictionaries with at least a `description` field,
|
||||||
and optional `subscopes` field.
|
and optional `subscopes` field.
|
||||||
%s
|
CUSTOM_SCOPE_DESCRIPTION
|
||||||
Examples::
|
Examples::
|
||||||
|
|
||||||
define_custom_scopes(
|
define_custom_scopes(
|
||||||
@@ -1274,7 +1274,7 @@ def define_custom_scopes(scopes):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
""" % indent(_custom_scope_description, " " * 8)
|
""".replace("CUSTOM_SCOPE_DESCRIPTION", indent(_custom_scope_description, " " * 8))
|
||||||
for scope, scope_definition in scopes.items():
|
for scope, scope_definition in scopes.items():
|
||||||
if scope in scope_definitions and scope_definitions[scope] != scope_definition:
|
if scope in scope_definitions and scope_definitions[scope] != scope_definition:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
|
@@ -250,7 +250,6 @@ class HubAuth(SingletonConfigurable):
|
|||||||
fetched from JUPYTERHUB_API_URL by default.
|
fetched from JUPYTERHUB_API_URL by default.
|
||||||
- cookie_cache_max_age: the number of seconds responses
|
- cookie_cache_max_age: the number of seconds responses
|
||||||
from the Hub should be cached.
|
from the Hub should be cached.
|
||||||
- login_url (the *public* ``/hub/login`` URL of the Hub).
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
hub_host = Unicode(
|
hub_host = Unicode(
|
||||||
@@ -331,17 +330,18 @@ class HubAuth(SingletonConfigurable):
|
|||||||
return url_path_join(os.getenv('JUPYTERHUB_BASE_URL') or '/', 'hub') + '/'
|
return url_path_join(os.getenv('JUPYTERHUB_BASE_URL') or '/', 'hub') + '/'
|
||||||
|
|
||||||
login_url = Unicode(
|
login_url = Unicode(
|
||||||
'/hub/login',
|
'',
|
||||||
help="""The login URL to use
|
help="""The login URL to use, if any.
|
||||||
|
|
||||||
Typically /hub/login
|
The base HubAuth class doesn't support login via URL,
|
||||||
|
and will raise 403 on `@web.authenticated` requests without a valid token.
|
||||||
|
|
||||||
|
An empty string here raises 403 errors instead of redirecting.
|
||||||
|
|
||||||
|
HubOAuth will redirect to /hub/api/oauth2/authorize.
|
||||||
""",
|
""",
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
|
|
||||||
@default('login_url')
|
|
||||||
def _default_login_url(self):
|
|
||||||
return self.hub_host + url_path_join(self.hub_prefix, 'login')
|
|
||||||
|
|
||||||
keyfile = Unicode(
|
keyfile = Unicode(
|
||||||
os.getenv('JUPYTERHUB_SSL_KEYFILE', ''),
|
os.getenv('JUPYTERHUB_SSL_KEYFILE', ''),
|
||||||
help="""The ssl key to use for requests
|
help="""The ssl key to use for requests
|
||||||
@@ -613,11 +613,8 @@ class HubAuth(SingletonConfigurable):
|
|||||||
r = await AsyncHTTPClient().fetch(req, raise_error=False)
|
r = await AsyncHTTPClient().fetch(req, raise_error=False)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
app_log.error("Error connecting to %s: %s", self.api_url, e)
|
app_log.error("Error connecting to %s: %s", self.api_url, e)
|
||||||
msg = "Failed to connect to Hub API at %r." % self.api_url
|
msg = f"Failed to connect to Hub API at {self.api_url!r}."
|
||||||
msg += (
|
msg += f" Is the Hub accessible at this URL (from host: {socket.gethostname()})?"
|
||||||
" Is the Hub accessible at this URL (from host: %s)?"
|
|
||||||
% socket.gethostname()
|
|
||||||
)
|
|
||||||
if '127.0.0.1' in self.api_url:
|
if '127.0.0.1' in self.api_url:
|
||||||
msg += (
|
msg += (
|
||||||
" Make sure to set c.JupyterHub.hub_ip to an IP accessible to"
|
" Make sure to set c.JupyterHub.hub_ip to an IP accessible to"
|
||||||
@@ -1045,7 +1042,7 @@ class HubOAuth(HubAuth):
|
|||||||
@validate('oauth_client_id', 'api_token')
|
@validate('oauth_client_id', 'api_token')
|
||||||
def _ensure_not_empty(self, proposal):
|
def _ensure_not_empty(self, proposal):
|
||||||
if not proposal.value:
|
if not proposal.value:
|
||||||
raise ValueError("%s cannot be empty." % proposal.trait.name)
|
raise ValueError(f"{proposal.trait.name} cannot be empty.")
|
||||||
return proposal.value
|
return proposal.value
|
||||||
|
|
||||||
oauth_redirect_uri = Unicode(
|
oauth_redirect_uri = Unicode(
|
||||||
@@ -1385,6 +1382,12 @@ class HubAuthenticated:
|
|||||||
if self._hub_login_url is not None:
|
if self._hub_login_url is not None:
|
||||||
# cached value, don't call this more than once per handler
|
# cached value, don't call this more than once per handler
|
||||||
return self._hub_login_url
|
return self._hub_login_url
|
||||||
|
|
||||||
|
if not self.hub_auth.login_url:
|
||||||
|
# HubOAuth is required for login via redirect,
|
||||||
|
# base class can only raise to avoid redirect loops
|
||||||
|
raise HTTPError(403)
|
||||||
|
|
||||||
# temporary override at setting level,
|
# temporary override at setting level,
|
||||||
# to allow any subclass overrides of get_login_url to preserve their effect
|
# to allow any subclass overrides of get_login_url to preserve their effect
|
||||||
# for example, APIHandler raises 403 to prevent redirects
|
# for example, APIHandler raises 403 to prevent redirects
|
||||||
@@ -1555,7 +1558,7 @@ class HubOAuthCallbackHandler(HubOAuthenticated, RequestHandler):
|
|||||||
error = self.get_argument("error", False)
|
error = self.get_argument("error", False)
|
||||||
if error:
|
if error:
|
||||||
msg = self.get_argument("error_description", error)
|
msg = self.get_argument("error_description", error)
|
||||||
raise HTTPError(400, "Error in oauth: %s" % msg)
|
raise HTTPError(400, f"Error in oauth: {msg}")
|
||||||
|
|
||||||
code = self.get_argument("code", False)
|
code = self.get_argument("code", False)
|
||||||
if not code:
|
if not code:
|
||||||
|
@@ -167,6 +167,12 @@ class Service(LoggingConfigurable):
|
|||||||
- url: str (None)
|
- url: str (None)
|
||||||
The URL where the service is/should be.
|
The URL where the service is/should be.
|
||||||
If specified, the service will be added to the proxy at /services/:name
|
If specified, the service will be added to the proxy at /services/:name
|
||||||
|
- oauth_redirect_url: str ('/services/:name/oauth_redirect')
|
||||||
|
The URI for the oauth redirect.
|
||||||
|
|
||||||
|
Not usually needed, but must be set for external services that are not accessed through the proxy,
|
||||||
|
or any service which have a redirect URI different from the default of `/services/:name/oauth_redirect`.
|
||||||
|
|
||||||
- oauth_no_confirm: bool(False)
|
- oauth_no_confirm: bool(False)
|
||||||
Whether this service should be allowed to complete oauth
|
Whether this service should be allowed to complete oauth
|
||||||
with logged-in users without prompting for confirmation.
|
with logged-in users without prompting for confirmation.
|
||||||
@@ -321,7 +327,7 @@ class Service(LoggingConfigurable):
|
|||||||
|
|
||||||
@default('oauth_client_id')
|
@default('oauth_client_id')
|
||||||
def _default_client_id(self):
|
def _default_client_id(self):
|
||||||
return 'service-%s' % self.name
|
return f'service-{self.name}'
|
||||||
|
|
||||||
@validate("oauth_client_id")
|
@validate("oauth_client_id")
|
||||||
def _validate_client_id(self, proposal):
|
def _validate_client_id(self, proposal):
|
||||||
@@ -335,7 +341,9 @@ class Service(LoggingConfigurable):
|
|||||||
oauth_redirect_uri = Unicode(
|
oauth_redirect_uri = Unicode(
|
||||||
help="""OAuth redirect URI for this service.
|
help="""OAuth redirect URI for this service.
|
||||||
|
|
||||||
You shouldn't generally need to change this.
|
Must be set for external services that are not accessed through the proxy,
|
||||||
|
or any service which have a redirect URI different from the default.
|
||||||
|
|
||||||
Default: `/services/:name/oauth_callback`
|
Default: `/services/:name/oauth_callback`
|
||||||
"""
|
"""
|
||||||
).tag(input=True, in_db=False)
|
).tag(input=True, in_db=False)
|
||||||
@@ -411,7 +419,7 @@ class Service(LoggingConfigurable):
|
|||||||
async def start(self):
|
async def start(self):
|
||||||
"""Start a managed service"""
|
"""Start a managed service"""
|
||||||
if not self.managed:
|
if not self.managed:
|
||||||
raise RuntimeError("Cannot start unmanaged service %s" % self)
|
raise RuntimeError(f"Cannot start unmanaged service {self}")
|
||||||
self.log.info("Starting service %r: %r", self.name, self.command)
|
self.log.info("Starting service %r: %r", self.name, self.command)
|
||||||
env = {}
|
env = {}
|
||||||
env.update(self.environment)
|
env.update(self.environment)
|
||||||
@@ -465,7 +473,7 @@ class Service(LoggingConfigurable):
|
|||||||
"""Stop a managed service"""
|
"""Stop a managed service"""
|
||||||
self.log.debug("Stopping service %s", self.name)
|
self.log.debug("Stopping service %s", self.name)
|
||||||
if not self.managed:
|
if not self.managed:
|
||||||
raise RuntimeError("Cannot stop unmanaged service %s" % self)
|
raise RuntimeError(f"Cannot stop unmanaged service {self}")
|
||||||
if self.spawner:
|
if self.spawner:
|
||||||
if self.orm.server:
|
if self.orm.server:
|
||||||
self.db.delete(self.orm.server)
|
self.db.delete(self.orm.server)
|
||||||
|
@@ -341,7 +341,7 @@ class SingleUserNotebookAppMixin(Configurable):
|
|||||||
# If we receive a non-absolute path, make it absolute.
|
# If we receive a non-absolute path, make it absolute.
|
||||||
value = os.path.abspath(value)
|
value = os.path.abspath(value)
|
||||||
if not os.path.isdir(value):
|
if not os.path.isdir(value):
|
||||||
raise TraitError("No such notebook dir: %r" % value)
|
raise TraitError(f"No such notebook dir: {value!r}")
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@default('log_level')
|
@default('log_level')
|
||||||
|
@@ -18,7 +18,11 @@ from tempfile import mkdtemp
|
|||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from async_generator import aclosing
|
if sys.version_info >= (3, 10):
|
||||||
|
from contextlib import aclosing
|
||||||
|
else:
|
||||||
|
from async_generator import aclosing
|
||||||
|
|
||||||
from sqlalchemy import inspect
|
from sqlalchemy import inspect
|
||||||
from tornado.ioloop import PeriodicCallback
|
from tornado.ioloop import PeriodicCallback
|
||||||
from traitlets import (
|
from traitlets import (
|
||||||
@@ -990,7 +994,7 @@ class Spawner(LoggingConfigurable):
|
|||||||
env = {}
|
env = {}
|
||||||
if self.env:
|
if self.env:
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"Spawner.env is deprecated, found %s" % self.env, DeprecationWarning
|
f"Spawner.env is deprecated, found {self.env}", DeprecationWarning
|
||||||
)
|
)
|
||||||
env.update(self.env)
|
env.update(self.env)
|
||||||
|
|
||||||
@@ -1490,7 +1494,7 @@ def _try_setcwd(path):
|
|||||||
path, _ = os.path.split(path)
|
path, _ = os.path.split(path)
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
print("Couldn't set CWD at all (%s), using temp dir" % exc, file=sys.stderr)
|
print(f"Couldn't set CWD at all ({exc}), using temp dir", file=sys.stderr)
|
||||||
td = mkdtemp()
|
td = mkdtemp()
|
||||||
os.chdir(td)
|
os.chdir(td)
|
||||||
|
|
||||||
@@ -1520,7 +1524,7 @@ def set_user_setuid(username, chdir=True):
|
|||||||
try:
|
try:
|
||||||
os.setgroups(gids)
|
os.setgroups(gids)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print('Failed to set groups %s' % e, file=sys.stderr)
|
print(f'Failed to set groups {e}', file=sys.stderr)
|
||||||
os.setuid(uid)
|
os.setuid(uid)
|
||||||
|
|
||||||
# start in the user's home dir
|
# start in the user's home dir
|
||||||
|
@@ -32,6 +32,20 @@ async def login(browser, username, password=None):
|
|||||||
await browser.get_by_role("button", name="Sign in").click()
|
await browser.get_by_role("button", name="Sign in").click()
|
||||||
|
|
||||||
|
|
||||||
|
async def login_home(browser, app, username):
|
||||||
|
"""Visit login page, login, go home
|
||||||
|
|
||||||
|
A good way to start a session
|
||||||
|
"""
|
||||||
|
login_url = url_concat(
|
||||||
|
url_path_join(public_url(app), "hub/login"),
|
||||||
|
{"next": ujoin(app.hub.base_url, "home")},
|
||||||
|
)
|
||||||
|
await browser.goto(login_url)
|
||||||
|
async with browser.expect_navigation(url=re.compile(".*/hub/home")):
|
||||||
|
await login(browser, username)
|
||||||
|
|
||||||
|
|
||||||
async def test_open_login_page(app, browser):
|
async def test_open_login_page(app, browser):
|
||||||
login_url = url_path_join(public_host(app), app.hub.base_url, "login")
|
login_url = url_path_join(public_host(app), app.hub.base_url, "login")
|
||||||
await browser.goto(login_url)
|
await browser.goto(login_url)
|
||||||
@@ -1367,6 +1381,17 @@ async def test_login_xsrf_initial_cookies(app, browser, case, username):
|
|||||||
await login(browser, username, username)
|
await login(browser, username, username)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_prefix_redirect_not_running(browser, app, user):
|
||||||
|
# tests PrefixRedirectHandler for stopped servers
|
||||||
|
await login_home(browser, app, user.name)
|
||||||
|
# visit user url (includes subdomain, if enabled)
|
||||||
|
url = public_url(app, user, "/tree/")
|
||||||
|
await browser.goto(url)
|
||||||
|
# make sure we end up on the Hub (domain included)
|
||||||
|
expected_url = url_path_join(public_url(app), f"hub/user/{user.name}/tree/")
|
||||||
|
await expect(browser).to_have_url(expected_url)
|
||||||
|
|
||||||
|
|
||||||
def _cookie_dict(cookie_list):
|
def _cookie_dict(cookie_list):
|
||||||
"""Convert list of cookies to dict of the form
|
"""Convert list of cookies to dict of the form
|
||||||
|
|
||||||
|
@@ -83,7 +83,7 @@ async def app(request, io_loop, ssl_tmpdir):
|
|||||||
try:
|
try:
|
||||||
mocked_app.stop()
|
mocked_app.stop()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Error stopping Hub: %s" % e, file=sys.stderr)
|
print(f"Error stopping Hub: {e}", file=sys.stderr)
|
||||||
|
|
||||||
request.addfinalizer(fin)
|
request.addfinalizer(fin)
|
||||||
await mocked_app.initialize([])
|
await mocked_app.initialize([])
|
||||||
|
@@ -54,7 +54,7 @@ class APIHandler(web.RequestHandler):
|
|||||||
api_token = os.environ['JUPYTERHUB_API_TOKEN']
|
api_token = os.environ['JUPYTERHUB_API_TOKEN']
|
||||||
api_url = os.environ['JUPYTERHUB_API_URL']
|
api_url = os.environ['JUPYTERHUB_API_URL']
|
||||||
r = requests.get(
|
r = requests.get(
|
||||||
api_url + path, headers={'Authorization': 'token %s' % api_token}
|
api_url + path, headers={'Authorization': f'token {api_token}'}
|
||||||
)
|
)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
self.set_header('Content-Type', 'application/json')
|
self.set_header('Content-Type', 'application/json')
|
||||||
|
@@ -63,7 +63,7 @@ async def test_auth_api(app):
|
|||||||
app,
|
app,
|
||||||
'authorizations/token',
|
'authorizations/token',
|
||||||
api_token,
|
api_token,
|
||||||
headers={'Authorization': 'token: %s' % user.cookie_id},
|
headers={'Authorization': f'token: {user.cookie_id}'},
|
||||||
)
|
)
|
||||||
assert r.status_code == 403
|
assert r.status_code == 403
|
||||||
|
|
||||||
@@ -965,7 +965,7 @@ async def test_spawn(app):
|
|||||||
status = await app_user.spawner.poll()
|
status = await app_user.spawner.poll()
|
||||||
assert status is None
|
assert status is None
|
||||||
|
|
||||||
assert spawner.server.base_url == ujoin(app.base_url, 'user/%s' % name) + '/'
|
assert spawner.server.base_url == ujoin(app.base_url, f'user/{name}') + '/'
|
||||||
url = public_url(app, user)
|
url = public_url(app, user)
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
if app.internal_ssl:
|
if app.internal_ssl:
|
||||||
@@ -1412,7 +1412,7 @@ async def test_progress_bad_slow(request, app, no_patience, slow_bad_spawn):
|
|||||||
async def progress_forever():
|
async def progress_forever():
|
||||||
"""progress function that yields messages forever"""
|
"""progress function that yields messages forever"""
|
||||||
for i in range(1, 10):
|
for i in range(1, 10):
|
||||||
yield {'progress': i, 'message': 'Stage %s' % i}
|
yield {'progress': i, 'message': f'Stage {i}'}
|
||||||
# wait a long time before the next event
|
# wait a long time before the next event
|
||||||
await asyncio.sleep(10)
|
await asyncio.sleep(10)
|
||||||
|
|
||||||
@@ -1741,7 +1741,7 @@ async def test_token_for_user(app, as_user, for_user, status):
|
|||||||
if for_user != 'missing':
|
if for_user != 'missing':
|
||||||
for_user_obj = add_user(app.db, app, name=for_user)
|
for_user_obj = add_user(app.db, app, name=for_user)
|
||||||
data = {'username': for_user}
|
data = {'username': for_user}
|
||||||
headers = {'Authorization': 'token %s' % u.new_api_token()}
|
headers = {'Authorization': f'token {u.new_api_token()}'}
|
||||||
r = await api_request(
|
r = await api_request(
|
||||||
app,
|
app,
|
||||||
'users',
|
'users',
|
||||||
@@ -1765,7 +1765,7 @@ async def test_token_for_user(app, as_user, for_user, status):
|
|||||||
if for_user == as_user:
|
if for_user == as_user:
|
||||||
note = 'Requested via api'
|
note = 'Requested via api'
|
||||||
else:
|
else:
|
||||||
note = 'Requested via api by user %s' % as_user
|
note = f'Requested via api by user {as_user}'
|
||||||
assert reply['note'] == note
|
assert reply['note'] == note
|
||||||
|
|
||||||
# delete the token
|
# delete the token
|
||||||
@@ -1836,7 +1836,7 @@ async def test_token_list(app, as_user, for_user, status):
|
|||||||
u = add_user(app.db, app, name=as_user)
|
u = add_user(app.db, app, name=as_user)
|
||||||
if for_user != 'missing':
|
if for_user != 'missing':
|
||||||
for_user_obj = add_user(app.db, app, name=for_user)
|
for_user_obj = add_user(app.db, app, name=for_user)
|
||||||
headers = {'Authorization': 'token %s' % u.new_api_token()}
|
headers = {'Authorization': f'token {u.new_api_token()}'}
|
||||||
r = await api_request(app, 'users', for_user, 'tokens', headers=headers)
|
r = await api_request(app, 'users', for_user, 'tokens', headers=headers)
|
||||||
assert r.status_code == status
|
assert r.status_code == status
|
||||||
if status != 200:
|
if status != 200:
|
||||||
@@ -2214,7 +2214,7 @@ async def test_get_service(app, mockservice_url):
|
|||||||
r = await api_request(
|
r = await api_request(
|
||||||
app,
|
app,
|
||||||
f"services/{mockservice.name}",
|
f"services/{mockservice.name}",
|
||||||
headers={'Authorization': 'token %s' % mockservice.api_token},
|
headers={'Authorization': f'token {mockservice.api_token}'},
|
||||||
)
|
)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
|
||||||
|
@@ -165,21 +165,35 @@ async def test_pam_auth_allowed():
|
|||||||
|
|
||||||
|
|
||||||
async def test_pam_auth_allowed_groups():
|
async def test_pam_auth_allowed_groups():
|
||||||
def getgrnam(name):
|
class TestAuthenticator(MockPAMAuthenticator):
|
||||||
return MockStructGroup('grp', ['kaylee'])
|
@staticmethod
|
||||||
|
def _getpwnam(name):
|
||||||
|
return MockStructPasswd(name=name)
|
||||||
|
|
||||||
authenticator = MockPAMAuthenticator(allowed_groups={'group'}, allow_all=False)
|
@staticmethod
|
||||||
|
def _getgrnam(name):
|
||||||
|
if name == "group":
|
||||||
|
return MockStructGroup('grp', ['kaylee'], gid=1234)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
with mock.patch.object(authenticator, '_getgrnam', getgrnam):
|
@staticmethod
|
||||||
authorized = await authenticator.get_authenticated_user(
|
def _getgrouplist(username, gid):
|
||||||
None, {'username': 'kaylee', 'password': 'kaylee'}
|
gids = [gid]
|
||||||
)
|
if username == "kaylee":
|
||||||
|
gids.append(1234)
|
||||||
|
return gids
|
||||||
|
|
||||||
|
authenticator = TestAuthenticator(allowed_groups={'group'}, allow_all=False)
|
||||||
|
|
||||||
|
authorized = await authenticator.get_authenticated_user(
|
||||||
|
None, {'username': 'kaylee', 'password': 'kaylee'}
|
||||||
|
)
|
||||||
assert authorized['name'] == 'kaylee'
|
assert authorized['name'] == 'kaylee'
|
||||||
|
|
||||||
with mock.patch.object(authenticator, '_getgrnam', getgrnam):
|
authorized = await authenticator.get_authenticated_user(
|
||||||
authorized = await authenticator.get_authenticated_user(
|
None, {'username': 'mal', 'password': 'mal'}
|
||||||
None, {'username': 'mal', 'password': 'mal'}
|
)
|
||||||
)
|
|
||||||
assert authorized is None
|
assert authorized is None
|
||||||
|
|
||||||
|
|
||||||
@@ -270,6 +284,7 @@ async def test_pam_auth_no_such_group():
|
|||||||
authenticator = MockPAMAuthenticator(
|
authenticator = MockPAMAuthenticator(
|
||||||
allowed_groups={'nosuchcrazygroup'},
|
allowed_groups={'nosuchcrazygroup'},
|
||||||
)
|
)
|
||||||
|
authenticator._getpwnam = MockStructPasswd
|
||||||
authorized = await authenticator.get_authenticated_user(
|
authorized = await authenticator.get_authenticated_user(
|
||||||
None, {'username': 'kaylee', 'password': 'kaylee'}
|
None, {'username': 'kaylee', 'password': 'kaylee'}
|
||||||
)
|
)
|
||||||
|
@@ -57,7 +57,7 @@ async def test_upgrade(tmpdir, hub_version):
|
|||||||
|
|
||||||
# use persistent temp env directory
|
# use persistent temp env directory
|
||||||
# to reuse across multiple runs
|
# to reuse across multiple runs
|
||||||
env_dir = os.path.join(tempfile.gettempdir(), 'test-hub-upgrade-%s' % hub_version)
|
env_dir = os.path.join(tempfile.gettempdir(), f'test-hub-upgrade-{hub_version}')
|
||||||
|
|
||||||
generate_old_db(env_dir, hub_version, db_url)
|
generate_old_db(env_dir, hub_version, db_url)
|
||||||
|
|
||||||
|
@@ -19,20 +19,22 @@ from traitlets.config import Config
|
|||||||
# and `invalid_events` dictionary below.
|
# and `invalid_events` dictionary below.
|
||||||
|
|
||||||
# To test valid events, add event item with the form:
|
# To test valid events, add event item with the form:
|
||||||
# { ( '<schema id>', <version> ) : { <event_data> } }
|
# ( '<schema id>', { <event_data> } )
|
||||||
valid_events = [
|
valid_events = [
|
||||||
(
|
(
|
||||||
'hub.jupyter.org/server-action',
|
'https://schema.jupyter.org/jupyterhub/events/server-action',
|
||||||
1,
|
|
||||||
dict(action='start', username='test-username', servername='test-servername'),
|
dict(action='start', username='test-username', servername='test-servername'),
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
# To test invalid events, add event item with the form:
|
# To test invalid events, add event item with the form:
|
||||||
# { ( '<schema id>', <version> ) : { <event_data> } }
|
# ( '<schema id>', { <event_data> } )
|
||||||
invalid_events = [
|
invalid_events = [
|
||||||
# Missing required keys
|
# Missing required keys
|
||||||
('hub.jupyter.org/server-action', 1, dict(action='start'))
|
(
|
||||||
|
'https://schema.jupyter.org/jupyterhub/events/server-action',
|
||||||
|
dict(action='start'),
|
||||||
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -41,11 +43,11 @@ def eventlog_sink(app):
|
|||||||
"""Return eventlog and sink objects"""
|
"""Return eventlog and sink objects"""
|
||||||
sink = io.StringIO()
|
sink = io.StringIO()
|
||||||
handler = logging.StreamHandler(sink)
|
handler = logging.StreamHandler(sink)
|
||||||
# Update the EventLog config with handler
|
# Update the EventLogger config with handler
|
||||||
cfg = Config()
|
cfg = Config()
|
||||||
cfg.EventLog.handlers = [handler]
|
cfg.EventLogger.handlers = [handler]
|
||||||
|
|
||||||
with mock.patch.object(app.config, 'EventLog', cfg.EventLog):
|
with mock.patch.object(app.config, 'EventLogger', cfg.EventLogger):
|
||||||
# recreate the eventlog object with our config
|
# recreate the eventlog object with our config
|
||||||
app.init_eventlog()
|
app.init_eventlog()
|
||||||
# return the sink from the fixture
|
# return the sink from the fixture
|
||||||
@@ -54,12 +56,12 @@ def eventlog_sink(app):
|
|||||||
app.init_eventlog()
|
app.init_eventlog()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('schema, version, event', valid_events)
|
@pytest.mark.parametrize('schema, event', valid_events)
|
||||||
def test_valid_events(eventlog_sink, schema, version, event):
|
def test_valid_events(eventlog_sink, schema, event):
|
||||||
eventlog, sink = eventlog_sink
|
eventlog, sink = eventlog_sink
|
||||||
eventlog.allowed_schemas = [schema]
|
eventlog.allowed_schemas = [schema]
|
||||||
# Record event
|
# Record event
|
||||||
eventlog.record_event(schema, version, event)
|
eventlog.emit(schema_id=schema, data=event)
|
||||||
# Inspect consumed event
|
# Inspect consumed event
|
||||||
output = sink.getvalue()
|
output = sink.getvalue()
|
||||||
assert output
|
assert output
|
||||||
@@ -68,11 +70,11 @@ def test_valid_events(eventlog_sink, schema, version, event):
|
|||||||
assert data is not None
|
assert data is not None
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('schema, version, event', invalid_events)
|
@pytest.mark.parametrize('schema, event', invalid_events)
|
||||||
def test_invalid_events(eventlog_sink, schema, version, event):
|
def test_invalid_events(eventlog_sink, schema, event):
|
||||||
eventlog, sink = eventlog_sink
|
eventlog, sink = eventlog_sink
|
||||||
eventlog.allowed_schemas = [schema]
|
eventlog.allowed_schemas = [schema]
|
||||||
|
|
||||||
# Make sure an error is thrown when bad events are recorded
|
# Make sure an error is thrown when bad events are recorded
|
||||||
with pytest.raises(jsonschema.ValidationError):
|
with pytest.raises(jsonschema.ValidationError):
|
||||||
recorded_event = eventlog.record_event(schema, version, event)
|
recorded_event = eventlog.emit(schema_id=schema, data=event)
|
||||||
|
@@ -69,6 +69,12 @@ async def test_default_server(app, named_servers):
|
|||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
|
||||||
user_model = normalize_user(r.json())
|
user_model = normalize_user(r.json())
|
||||||
|
full_progress_url = None
|
||||||
|
if app.public_url:
|
||||||
|
full_progress_url = url_path_join(
|
||||||
|
app.public_url,
|
||||||
|
f'hub/api/users/{username}/server/progress',
|
||||||
|
)
|
||||||
assert user_model == fill_user(
|
assert user_model == fill_user(
|
||||||
{
|
{
|
||||||
'name': username,
|
'name': username,
|
||||||
@@ -88,6 +94,8 @@ async def test_default_server(app, named_servers):
|
|||||||
'progress_url': f'PREFIX/hub/api/users/{username}/server/progress',
|
'progress_url': f'PREFIX/hub/api/users/{username}/server/progress',
|
||||||
'state': {'pid': 0},
|
'state': {'pid': 0},
|
||||||
'user_options': {},
|
'user_options': {},
|
||||||
|
'full_url': user.public_url() or None,
|
||||||
|
'full_progress_url': full_progress_url,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -157,6 +165,14 @@ async def test_create_named_server(
|
|||||||
assert db_server_names == {"", servername}
|
assert db_server_names == {"", servername}
|
||||||
|
|
||||||
user_model = normalize_user(r.json())
|
user_model = normalize_user(r.json())
|
||||||
|
|
||||||
|
full_progress_url = None
|
||||||
|
if app.public_url:
|
||||||
|
full_progress_url = url_path_join(
|
||||||
|
app.public_url,
|
||||||
|
f'hub/api/users/{username}/servers/{escapedname}/progress',
|
||||||
|
)
|
||||||
|
|
||||||
assert user_model == fill_user(
|
assert user_model == fill_user(
|
||||||
{
|
{
|
||||||
'name': username,
|
'name': username,
|
||||||
@@ -175,6 +191,8 @@ async def test_create_named_server(
|
|||||||
'progress_url': f'PREFIX/hub/api/users/{username}/servers/{escapedname}/progress',
|
'progress_url': f'PREFIX/hub/api/users/{username}/servers/{escapedname}/progress',
|
||||||
'state': {'pid': 0},
|
'state': {'pid': 0},
|
||||||
'user_options': {},
|
'user_options': {},
|
||||||
|
'full_url': user.public_url(name) or None,
|
||||||
|
'full_progress_url': full_progress_url,
|
||||||
}
|
}
|
||||||
for name in [servername]
|
for name in [servername]
|
||||||
},
|
},
|
||||||
|
@@ -120,7 +120,7 @@ async def test_admin_version(app):
|
|||||||
@pytest.mark.parametrize('sort', ['running', 'last_activity', 'admin', 'name'])
|
@pytest.mark.parametrize('sort', ['running', 'last_activity', 'admin', 'name'])
|
||||||
async def test_admin_sort(app, sort):
|
async def test_admin_sort(app, sort):
|
||||||
cookies = await app.login_user('admin')
|
cookies = await app.login_user('admin')
|
||||||
r = await get_page('admin?sort=%s' % sort, app, cookies=cookies)
|
r = await get_page(f'admin?sort={sort}', app, cookies=cookies)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
|
|
||||||
@@ -170,7 +170,7 @@ async def test_spawn_redirect(app, last_failed):
|
|||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
print(urlparse(r.url))
|
print(urlparse(r.url))
|
||||||
path = urlparse(r.url).path
|
path = urlparse(r.url).path
|
||||||
assert path == ujoin(app.base_url, '/user/%s/' % name)
|
assert path == ujoin(app.base_url, f'/user/{name}/')
|
||||||
|
|
||||||
# stop server to ensure /user/name is handled by the Hub
|
# stop server to ensure /user/name is handled by the Hub
|
||||||
r = await api_request(
|
r = await api_request(
|
||||||
@@ -181,7 +181,7 @@ async def test_spawn_redirect(app, last_failed):
|
|||||||
# test handing of trailing slash on `/user/name`
|
# test handing of trailing slash on `/user/name`
|
||||||
r = await get_page('user/' + name, app, hub=False, cookies=cookies)
|
r = await get_page('user/' + name, app, hub=False, cookies=cookies)
|
||||||
path = urlparse(r.url).path
|
path = urlparse(r.url).path
|
||||||
assert path == ujoin(app.base_url, 'hub/user/%s/' % name)
|
assert path == ujoin(app.base_url, f'hub/user/{name}/')
|
||||||
assert r.status_code == 424
|
assert r.status_code == 424
|
||||||
|
|
||||||
|
|
||||||
@@ -586,7 +586,7 @@ async def test_user_redirect(app, username):
|
|||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
r = await async_requests.get(r.url, cookies=cookies)
|
r = await async_requests.get(r.url, cookies=cookies)
|
||||||
path = urlparse(r.url).path
|
path = urlparse(r.url).path
|
||||||
assert path == ujoin(app.base_url, '/user/%s/notebooks/test.ipynb' % name)
|
assert path == ujoin(app.base_url, f'/user/{name}/notebooks/test.ipynb')
|
||||||
|
|
||||||
|
|
||||||
async def test_user_redirect_hook(app, username):
|
async def test_user_redirect_hook(app, username):
|
||||||
@@ -1240,7 +1240,7 @@ async def test_token_page(app):
|
|||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
body = extract_body(r)
|
body = extract_body(r)
|
||||||
assert "API Tokens" in body, body
|
assert "API Tokens" in body, body
|
||||||
assert "Server at %s" % user.base_url in body, body
|
assert f"Server at {user.base_url}" in body, body
|
||||||
assert "Authorized Applications" in body, body
|
assert "Authorized Applications" in body, body
|
||||||
|
|
||||||
|
|
||||||
@@ -1299,7 +1299,7 @@ async def test_pre_spawn_start_exc_options_form(app):
|
|||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
assert FormSpawner.options_form in r.text
|
assert FormSpawner.options_form in r.text
|
||||||
# spawning the user server should throw the pre_spawn_start error
|
# spawning the user server should throw the pre_spawn_start error
|
||||||
with pytest.raises(Exception, match="%s" % exc):
|
with pytest.raises(Exception, match=str(exc)):
|
||||||
await user.spawn()
|
await user.spawn()
|
||||||
|
|
||||||
|
|
||||||
|
@@ -171,7 +171,7 @@ async def test_external_proxy(request):
|
|||||||
async def test_check_routes(app, username, disable_check_routes):
|
async def test_check_routes(app, username, disable_check_routes):
|
||||||
proxy = app.proxy
|
proxy = app.proxy
|
||||||
test_user = add_user(app.db, app, name=username)
|
test_user = add_user(app.db, app, name=username)
|
||||||
r = await api_request(app, 'users/%s/server' % username, method='post')
|
r = await api_request(app, f'users/{username}/server', method='post')
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
|
||||||
# check a valid route exists for user
|
# check a valid route exists for user
|
||||||
|
@@ -956,7 +956,7 @@ async def test_user_group_roles(app, create_temp_role):
|
|||||||
# jack's API token
|
# jack's API token
|
||||||
token = user.new_api_token()
|
token = user.new_api_token()
|
||||||
|
|
||||||
headers = {'Authorization': 'token %s' % token}
|
headers = {'Authorization': f'token {token}'}
|
||||||
r = await api_request(app, f'users/{user.name}', method='get', headers=headers)
|
r = await api_request(app, f'users/{user.name}', method='get', headers=headers)
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
@@ -968,7 +968,7 @@ async def test_user_group_roles(app, create_temp_role):
|
|||||||
assert len(reply['roles']) == 1
|
assert len(reply['roles']) == 1
|
||||||
assert group_role.name not in reply['roles']
|
assert group_role.name not in reply['roles']
|
||||||
|
|
||||||
headers = {'Authorization': 'token %s' % token}
|
headers = {'Authorization': f'token {token}'}
|
||||||
r = await api_request(app, 'groups', method='get', headers=headers)
|
r = await api_request(app, 'groups', method='get', headers=headers)
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
@@ -978,7 +978,7 @@ async def test_user_group_roles(app, create_temp_role):
|
|||||||
assert len(reply) == 1
|
assert len(reply) == 1
|
||||||
assert reply[0]['name'] == 'A'
|
assert reply[0]['name'] == 'A'
|
||||||
|
|
||||||
headers = {'Authorization': 'token %s' % token}
|
headers = {'Authorization': f'token {token}'}
|
||||||
r = await api_request(app, f'users/{user.name}', method='get', headers=headers)
|
r = await api_request(app, f'users/{user.name}', method='get', headers=headers)
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
@@ -289,7 +289,7 @@ async def test_exceeding_user_permissions(
|
|||||||
orm_api_token = orm.APIToken.find(app.db, token=api_token)
|
orm_api_token = orm.APIToken.find(app.db, token=api_token)
|
||||||
# store scopes user does not have
|
# store scopes user does not have
|
||||||
orm_api_token.scopes = list(orm_api_token.scopes) + ['list:users', 'read:users']
|
orm_api_token.scopes = list(orm_api_token.scopes) + ['list:users', 'read:users']
|
||||||
headers = {'Authorization': 'token %s' % api_token}
|
headers = {'Authorization': f'token {api_token}'}
|
||||||
r = await api_request(app, 'users', headers=headers)
|
r = await api_request(app, 'users', headers=headers)
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
keys = {key for user in r.json() for key in user.keys()}
|
keys = {key for user in r.json() for key in user.keys()}
|
||||||
@@ -307,7 +307,7 @@ async def test_user_service_separation(app, mockservice_url, create_temp_role):
|
|||||||
roles.update_roles(app.db, mockservice_url.orm, roles=['reader_role'])
|
roles.update_roles(app.db, mockservice_url.orm, roles=['reader_role'])
|
||||||
user.roles.remove(orm.Role.find(app.db, name='user'))
|
user.roles.remove(orm.Role.find(app.db, name='user'))
|
||||||
api_token = user.new_api_token()
|
api_token = user.new_api_token()
|
||||||
headers = {'Authorization': 'token %s' % api_token}
|
headers = {'Authorization': f'token {api_token}'}
|
||||||
r = await api_request(app, 'users', headers=headers)
|
r = await api_request(app, 'users', headers=headers)
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
keys = {key for user in r.json() for key in user.keys()}
|
keys = {key for user in r.json() for key in user.keys()}
|
||||||
@@ -551,7 +551,7 @@ async def test_server_state_access(
|
|||||||
)
|
)
|
||||||
service = create_service_with_scopes("read:users:name!user=bianca", *scopes)
|
service = create_service_with_scopes("read:users:name!user=bianca", *scopes)
|
||||||
api_token = service.new_api_token()
|
api_token = service.new_api_token()
|
||||||
headers = {'Authorization': 'token %s' % api_token}
|
headers = {'Authorization': f'token {api_token}'}
|
||||||
|
|
||||||
# can I get the user model?
|
# can I get the user model?
|
||||||
r = await api_request(app, 'users', user.name, headers=headers)
|
r = await api_request(app, 'users', user.name, headers=headers)
|
||||||
|
@@ -3,10 +3,9 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from binascii import hexlify
|
from binascii import hexlify
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
from subprocess import Popen
|
from subprocess import Popen
|
||||||
|
|
||||||
from async_generator import asynccontextmanager
|
|
||||||
|
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
exponential_backoff,
|
exponential_backoff,
|
||||||
maybe_future,
|
maybe_future,
|
||||||
|
@@ -88,14 +88,10 @@ async def test_hubauth_token(app, mockservice_url, create_user_with_scopes):
|
|||||||
|
|
||||||
# token in ?token parameter is not allowed by default
|
# token in ?token parameter is not allowed by default
|
||||||
r = await async_requests.get(
|
r = await async_requests.get(
|
||||||
public_url(app, mockservice_url) + '/whoami/?token=%s' % token,
|
public_url(app, mockservice_url) + f'/whoami/?token={token}',
|
||||||
allow_redirects=False,
|
allow_redirects=False,
|
||||||
)
|
)
|
||||||
assert r.status_code == 302
|
assert r.status_code == 403
|
||||||
assert 'Location' in r.headers
|
|
||||||
location = r.headers['Location']
|
|
||||||
path = urlparse(location).path
|
|
||||||
assert path.endswith('/hub/login')
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@@ -154,7 +150,7 @@ async def test_hubauth_service_token(request, app, mockservice_url, scopes, allo
|
|||||||
# token in Authorization header
|
# token in Authorization header
|
||||||
r = await async_requests.get(
|
r = await async_requests.get(
|
||||||
public_url(app, mockservice_url) + 'whoami/',
|
public_url(app, mockservice_url) + 'whoami/',
|
||||||
headers={'Authorization': 'token %s' % token},
|
headers={'Authorization': f'token {token}'},
|
||||||
allow_redirects=False,
|
allow_redirects=False,
|
||||||
)
|
)
|
||||||
service_model = {
|
service_model = {
|
||||||
@@ -174,14 +170,10 @@ async def test_hubauth_service_token(request, app, mockservice_url, scopes, allo
|
|||||||
|
|
||||||
# token in ?token parameter is not allowed by default
|
# token in ?token parameter is not allowed by default
|
||||||
r = await async_requests.get(
|
r = await async_requests.get(
|
||||||
public_url(app, mockservice_url) + 'whoami/?token=%s' % token,
|
public_url(app, mockservice_url) + f'whoami/?token={token}',
|
||||||
allow_redirects=False,
|
allow_redirects=False,
|
||||||
)
|
)
|
||||||
assert r.status_code == 302
|
assert r.status_code == 403
|
||||||
assert 'Location' in r.headers
|
|
||||||
location = r.headers['Location']
|
|
||||||
path = urlparse(location).path
|
|
||||||
assert path.endswith('/hub/login')
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@@ -311,7 +303,7 @@ async def test_oauth_service_roles(
|
|||||||
# we should be looking at the oauth confirmation page
|
# we should be looking at the oauth confirmation page
|
||||||
assert urlparse(r.url).path == app.base_url + 'hub/api/oauth2/authorize'
|
assert urlparse(r.url).path == app.base_url + 'hub/api/oauth2/authorize'
|
||||||
# verify oauth state cookie was set at some point
|
# verify oauth state cookie was set at some point
|
||||||
assert set(r.history[0].cookies.keys()) == {'service-%s-oauth-state' % service.name}
|
assert set(r.history[0].cookies.keys()) == {f'service-{service.name}-oauth-state'}
|
||||||
|
|
||||||
page = BeautifulSoup(r.text, "html.parser")
|
page = BeautifulSoup(r.text, "html.parser")
|
||||||
scope_inputs = page.find_all("input", {"name": "scopes"})
|
scope_inputs = page.find_all("input", {"name": "scopes"})
|
||||||
@@ -326,9 +318,9 @@ async def test_oauth_service_roles(
|
|||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
assert r.url == url
|
assert r.url == url
|
||||||
# verify oauth cookie is set
|
# verify oauth cookie is set
|
||||||
assert 'service-%s' % service.name in set(s.cookies.keys())
|
assert f'service-{service.name}' in set(s.cookies.keys())
|
||||||
# verify oauth state cookie has been consumed
|
# verify oauth state cookie has been consumed
|
||||||
assert 'service-%s-oauth-state' % service.name not in set(s.cookies.keys())
|
assert f'service-{service.name}-oauth-state' not in set(s.cookies.keys())
|
||||||
|
|
||||||
# second request should be authenticated, which means no redirects
|
# second request should be authenticated, which means no redirects
|
||||||
r = await s.get(url, allow_redirects=False)
|
r = await s.get(url, allow_redirects=False)
|
||||||
@@ -410,16 +402,16 @@ async def test_oauth_access_scopes(
|
|||||||
# we should be looking at the oauth confirmation page
|
# we should be looking at the oauth confirmation page
|
||||||
assert urlparse(r.url).path == app.base_url + 'hub/api/oauth2/authorize'
|
assert urlparse(r.url).path == app.base_url + 'hub/api/oauth2/authorize'
|
||||||
# verify oauth state cookie was set at some point
|
# verify oauth state cookie was set at some point
|
||||||
assert set(r.history[0].cookies.keys()) == {'service-%s-oauth-state' % service.name}
|
assert set(r.history[0].cookies.keys()) == {f'service-{service.name}-oauth-state'}
|
||||||
|
|
||||||
# submit the oauth form to complete authorization
|
# submit the oauth form to complete authorization
|
||||||
r = await s.post(r.url, data={"_xsrf": s.cookies["_xsrf"]})
|
r = await s.post(r.url, data={"_xsrf": s.cookies["_xsrf"]})
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
assert r.url == url
|
assert r.url == url
|
||||||
# verify oauth cookie is set
|
# verify oauth cookie is set
|
||||||
assert 'service-%s' % service.name in set(s.cookies.keys())
|
assert f'service-{service.name}' in set(s.cookies.keys())
|
||||||
# verify oauth state cookie has been consumed
|
# verify oauth state cookie has been consumed
|
||||||
assert 'service-%s-oauth-state' % service.name not in set(s.cookies.keys())
|
assert f'service-{service.name}-oauth-state' not in set(s.cookies.keys())
|
||||||
|
|
||||||
# second request should be authenticated, which means no redirects
|
# second request should be authenticated, which means no redirects
|
||||||
r = await s.get(url, allow_redirects=False)
|
r = await s.get(url, allow_redirects=False)
|
||||||
@@ -507,8 +499,8 @@ async def test_oauth_cookie_collision(
|
|||||||
name = 'mypha'
|
name = 'mypha'
|
||||||
create_user_with_scopes("access:services", name=name)
|
create_user_with_scopes("access:services", name=name)
|
||||||
s.cookies = await app.login_user(name)
|
s.cookies = await app.login_user(name)
|
||||||
state_cookie_name = 'service-%s-oauth-state' % service.name
|
state_cookie_name = f'service-{service.name}-oauth-state'
|
||||||
service_cookie_name = 'service-%s' % service.name
|
service_cookie_name = f'service-{service.name}'
|
||||||
url_1 = url + "?oauth_test=1"
|
url_1 = url + "?oauth_test=1"
|
||||||
oauth_1 = await s.get(url_1)
|
oauth_1 = await s.get(url_1)
|
||||||
assert state_cookie_name in s.cookies
|
assert state_cookie_name in s.cookies
|
||||||
@@ -590,7 +582,7 @@ async def test_oauth_logout(app, mockservice_url, create_user_with_scopes):
|
|||||||
4. cache hit
|
4. cache hit
|
||||||
"""
|
"""
|
||||||
service = mockservice_url
|
service = mockservice_url
|
||||||
service_cookie_name = 'service-%s' % service.name
|
service_cookie_name = f'service-{service.name}'
|
||||||
url = url_path_join(public_url(app, mockservice_url), 'owhoami/?foo=bar')
|
url = url_path_join(public_url(app, mockservice_url), 'owhoami/?foo=bar')
|
||||||
# first request is only going to set login cookie
|
# first request is only going to set login cookie
|
||||||
s = AsyncSession()
|
s = AsyncSession()
|
||||||
|
@@ -221,8 +221,8 @@ def test_string_formatting(db):
|
|||||||
name = s.user.name
|
name = s.user.name
|
||||||
assert s.notebook_dir == 'user/{username}/'
|
assert s.notebook_dir == 'user/{username}/'
|
||||||
assert s.default_url == '/base/{username}'
|
assert s.default_url == '/base/{username}'
|
||||||
assert s.format_string(s.notebook_dir) == 'user/%s/' % name
|
assert s.format_string(s.notebook_dir) == f'user/{name}/'
|
||||||
assert s.format_string(s.default_url) == '/base/%s' % name
|
assert s.format_string(s.default_url) == f'/base/{name}'
|
||||||
|
|
||||||
|
|
||||||
async def test_popen_kwargs(db):
|
async def test_popen_kwargs(db):
|
||||||
@@ -496,7 +496,7 @@ async def test_hub_connect_url(db):
|
|||||||
assert env["JUPYTERHUB_API_URL"] == "https://example.com/api"
|
assert env["JUPYTERHUB_API_URL"] == "https://example.com/api"
|
||||||
assert (
|
assert (
|
||||||
env["JUPYTERHUB_ACTIVITY_URL"]
|
env["JUPYTERHUB_ACTIVITY_URL"]
|
||||||
== "https://example.com/api/users/%s/activity" % name
|
== f"https://example.com/api/users/{name}/activity"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -1,12 +1,17 @@
|
|||||||
"""Tests for utilities"""
|
"""Tests for utilities"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from unittest.mock import Mock
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 10):
|
||||||
|
from contextlib import aclosing
|
||||||
|
else:
|
||||||
|
from async_generator import aclosing
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from async_generator import aclosing
|
|
||||||
from tornado import gen
|
from tornado import gen
|
||||||
from tornado.concurrent import run_on_executor
|
from tornado.concurrent import run_on_executor
|
||||||
from tornado.httpserver import HTTPRequest
|
from tornado.httpserver import HTTPRequest
|
||||||
|
@@ -149,7 +149,7 @@ def auth_header(db, name):
|
|||||||
if user is None:
|
if user is None:
|
||||||
raise KeyError(f"No such user: {name}")
|
raise KeyError(f"No such user: {name}")
|
||||||
token = user.new_api_token()
|
token = user.new_api_token()
|
||||||
return {'Authorization': 'token %s' % token}
|
return {'Authorization': f'token {token}'}
|
||||||
|
|
||||||
|
|
||||||
@check_db_locks
|
@check_db_locks
|
||||||
@@ -198,7 +198,7 @@ async def api_request(
|
|||||||
def get_page(path, app, hub=True, **kw):
|
def get_page(path, app, hub=True, **kw):
|
||||||
if "://" in path:
|
if "://" in path:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Not a hub page path: %r. Did you mean async_requests.get?" % path
|
f"Not a hub page path: {path!r}. Did you mean async_requests.get?"
|
||||||
)
|
)
|
||||||
if hub:
|
if hub:
|
||||||
prefix = app.hub.base_url
|
prefix = app.hub.base_url
|
||||||
|
@@ -125,7 +125,7 @@ class UserDict(dict):
|
|||||||
elif isinstance(key, str):
|
elif isinstance(key, str):
|
||||||
orm_user = self.db.query(orm.User).filter(orm.User.name == key).first()
|
orm_user = self.db.query(orm.User).filter(orm.User.name == key).first()
|
||||||
if orm_user is None:
|
if orm_user is None:
|
||||||
raise KeyError("No such user: %s" % key)
|
raise KeyError(f"No such user: {key}")
|
||||||
else:
|
else:
|
||||||
key = orm_user.id
|
key = orm_user.id
|
||||||
if isinstance(key, orm.User):
|
if isinstance(key, orm.User):
|
||||||
@@ -142,7 +142,7 @@ class UserDict(dict):
|
|||||||
if id not in self:
|
if id not in self:
|
||||||
orm_user = self.db.query(orm.User).filter(orm.User.id == id).first()
|
orm_user = self.db.query(orm.User).filter(orm.User.id == id).first()
|
||||||
if orm_user is None:
|
if orm_user is None:
|
||||||
raise KeyError("No such user: %s" % id)
|
raise KeyError(f"No such user: {id}")
|
||||||
user = self.add(orm_user)
|
user = self.add(orm_user)
|
||||||
else:
|
else:
|
||||||
user = super().__getitem__(id)
|
user = super().__getitem__(id)
|
||||||
@@ -505,7 +505,7 @@ class User:
|
|||||||
|
|
||||||
# use fully quoted name for client_id because it will be used in cookie-name
|
# use fully quoted name for client_id because it will be used in cookie-name
|
||||||
# self.escaped_name may contain @ which is legal in URLs but not cookie keys
|
# self.escaped_name may contain @ which is legal in URLs but not cookie keys
|
||||||
client_id = 'jupyterhub-user-%s' % quote(self.name)
|
client_id = f'jupyterhub-user-{quote(self.name)}'
|
||||||
if server_name:
|
if server_name:
|
||||||
client_id = f'{client_id}-{quote(server_name)}'
|
client_id = f'{client_id}-{quote(server_name)}'
|
||||||
|
|
||||||
@@ -790,7 +790,7 @@ class User:
|
|||||||
|
|
||||||
orm_server = orm.Server(base_url=base_url)
|
orm_server = orm.Server(base_url=base_url)
|
||||||
db.add(orm_server)
|
db.add(orm_server)
|
||||||
note = "Server at %s" % base_url
|
note = f"Server at {base_url}"
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
spawner = self.get_spawner(server_name, replace_failed=True)
|
spawner = self.get_spawner(server_name, replace_failed=True)
|
||||||
@@ -962,7 +962,7 @@ class User:
|
|||||||
)
|
)
|
||||||
self.db.delete(found)
|
self.db.delete(found)
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
raise ValueError("Invalid token for %s!" % self.name)
|
raise ValueError(f"Invalid token for {self.name}!")
|
||||||
else:
|
else:
|
||||||
# Spawner.api_token has changed, but isn't in the db.
|
# Spawner.api_token has changed, but isn't in the db.
|
||||||
# What happened? Maybe something unclean in a resumed container.
|
# What happened? Maybe something unclean in a resumed container.
|
||||||
@@ -975,7 +975,7 @@ class User:
|
|||||||
self.new_api_token(
|
self.new_api_token(
|
||||||
spawner.api_token,
|
spawner.api_token,
|
||||||
generated=False,
|
generated=False,
|
||||||
note="retrieved from spawner %s" % server_name,
|
note=f"retrieved from spawner {server_name}",
|
||||||
scopes=resolved_scopes,
|
scopes=resolved_scopes,
|
||||||
)
|
)
|
||||||
# update OAuth client secret with updated API token
|
# update OAuth client secret with updated API token
|
||||||
|
@@ -27,8 +27,12 @@ from hmac import compare_digest
|
|||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 10):
|
||||||
|
from contextlib import aclosing
|
||||||
|
else:
|
||||||
|
from async_generator import aclosing
|
||||||
|
|
||||||
import idna
|
import idna
|
||||||
from async_generator import aclosing
|
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
from tornado import gen, ioloop, web
|
from tornado import gen, ioloop, web
|
||||||
from tornado.httpclient import AsyncHTTPClient, HTTPError
|
from tornado.httpclient import AsyncHTTPClient, HTTPError
|
||||||
@@ -500,7 +504,7 @@ def print_ps_info(file=sys.stderr):
|
|||||||
if cpu >= 10:
|
if cpu >= 10:
|
||||||
cpu_s = "%i" % cpu
|
cpu_s = "%i" % cpu
|
||||||
else:
|
else:
|
||||||
cpu_s = "%.1f" % cpu
|
cpu_s = f"{cpu:.1f}"
|
||||||
|
|
||||||
# format memory (only resident set)
|
# format memory (only resident set)
|
||||||
rss = p.memory_info().rss
|
rss = p.memory_info().rss
|
||||||
@@ -558,7 +562,7 @@ def print_stacks(file=sys.stderr):
|
|||||||
|
|
||||||
print("Active threads: %i" % threading.active_count(), file=file)
|
print("Active threads: %i" % threading.active_count(), file=file)
|
||||||
for thread in threading.enumerate():
|
for thread in threading.enumerate():
|
||||||
print("Thread %s:" % thread.name, end='', file=file)
|
print(f"Thread {thread.name}:", end='', file=file)
|
||||||
frame = sys._current_frames()[thread.ident]
|
frame = sys._current_frames()[thread.ident]
|
||||||
stack = traceback.extract_stack(frame)
|
stack = traceback.extract_stack(frame)
|
||||||
if thread is threading.current_thread():
|
if thread is threading.current_thread():
|
||||||
|
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
# ref: https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html
|
# ref: https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html
|
||||||
[project]
|
[project]
|
||||||
name = "jupyterhub"
|
name = "jupyterhub"
|
||||||
version = "5.0.0b1"
|
version = "5.0.0b2"
|
||||||
dynamic = ["readme", "dependencies"]
|
dynamic = ["readme", "dependencies"]
|
||||||
description = "JupyterHub: A multi-user server for Jupyter notebooks"
|
description = "JupyterHub: A multi-user server for Jupyter notebooks"
|
||||||
authors = [
|
authors = [
|
||||||
@@ -147,7 +147,7 @@ indent_size = 2
|
|||||||
github_url = "https://github.com/jupyterhub/jupyterhub"
|
github_url = "https://github.com/jupyterhub/jupyterhub"
|
||||||
|
|
||||||
[tool.tbump.version]
|
[tool.tbump.version]
|
||||||
current = "5.0.0b1"
|
current = "5.0.0b2"
|
||||||
|
|
||||||
# Example of a semver regexp.
|
# Example of a semver regexp.
|
||||||
# Make sure this matches current_version before
|
# Make sure this matches current_version before
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
alembic>=1.4
|
alembic>=1.4
|
||||||
async_generator>=1.9
|
async_generator>=1.9; python_version < '3.10'
|
||||||
certipy>=0.1.2
|
certipy>=0.1.2
|
||||||
idna
|
idna
|
||||||
importlib_metadata>=3.6; python_version < '3.10'
|
importlib_metadata>=3.6; python_version < '3.10'
|
||||||
jinja2>=2.11.0
|
jinja2>=2.11.0
|
||||||
jupyter_telemetry>=0.1.0
|
jupyter_events
|
||||||
oauthlib>=3.0
|
oauthlib>=3.0
|
||||||
packaging
|
packaging
|
||||||
pamela>=1.1.0; sys_platform != 'win32'
|
pamela>=1.1.0; sys_platform != 'win32'
|
||||||
|
2
setup.py
2
setup.py
@@ -157,7 +157,7 @@ class CSS(BaseCommand):
|
|||||||
try:
|
try:
|
||||||
check_call(args, cwd=here, shell=shell)
|
check_call(args, cwd=here, shell=shell)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
print("Failed to build css: %s" % e, file=sys.stderr)
|
print(f"Failed to build css: {e}", file=sys.stderr)
|
||||||
print("You can install js dependencies with `npm install`", file=sys.stderr)
|
print("You can install js dependencies with `npm install`", file=sys.stderr)
|
||||||
raise
|
raise
|
||||||
# update data-files in case this created new files
|
# update data-files in case this created new files
|
||||||
|
Reference in New Issue
Block a user