mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-09 11:03:00 +00:00
Compare commits
35 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
8b583cb445 | ||
![]() |
038a85af43 | ||
![]() |
9165beb41c | ||
![]() |
b285de4412 | ||
![]() |
5826035fe9 | ||
![]() |
b953ac295b | ||
![]() |
8a95066b2e | ||
![]() |
00a4aef607 | ||
![]() |
e01ce7b665 | ||
![]() |
a57df48f28 | ||
![]() |
5d7e008055 | ||
![]() |
ba31b3ecb7 | ||
![]() |
3c5eb934bf | ||
![]() |
82e15df6e9 | ||
![]() |
e3c83c0c29 | ||
![]() |
94542334c4 | ||
![]() |
95494b3ace | ||
![]() |
a131cfb79e | ||
![]() |
f002c67343 | ||
![]() |
b9caf95c72 | ||
![]() |
5356954240 | ||
![]() |
126c73002e | ||
![]() |
65b4502a78 | ||
![]() |
3406161d75 | ||
![]() |
e45f00f0f7 | ||
![]() |
71f4a30562 | ||
![]() |
20ba414b41 | ||
![]() |
f5250f04c5 | ||
![]() |
c2ea20a87a | ||
![]() |
b14989d4a5 | ||
![]() |
04578e329c | ||
![]() |
be05e438ca | ||
![]() |
24d9215029 | ||
![]() |
54dcca7ba9 | ||
![]() |
056a7351a3 |
@@ -52,7 +52,8 @@ ENV PATH=/opt/conda/bin:$PATH
|
|||||||
ADD . /src/jupyterhub
|
ADD . /src/jupyterhub
|
||||||
WORKDIR /src/jupyterhub
|
WORKDIR /src/jupyterhub
|
||||||
|
|
||||||
RUN python setup.py js && pip install . && \
|
RUN npm install --unsafe-perm && \
|
||||||
|
pip install . && \
|
||||||
rm -rf $PWD ~/.cache ~/.npm
|
rm -rf $PWD ~/.cache ~/.npm
|
||||||
|
|
||||||
RUN mkdir -p /srv/jupyterhub/
|
RUN mkdir -p /srv/jupyterhub/
|
||||||
|
13
MANIFEST.in
13
MANIFEST.in
@@ -1,7 +1,7 @@
|
|||||||
include README.md
|
include README.md
|
||||||
include COPYING.md
|
include COPYING.md
|
||||||
include setupegg.py
|
include setupegg.py
|
||||||
include bower.json
|
include bower-lite
|
||||||
include package.json
|
include package.json
|
||||||
include *requirements.txt
|
include *requirements.txt
|
||||||
include Dockerfile
|
include Dockerfile
|
||||||
@@ -18,12 +18,13 @@ graft docs
|
|||||||
prune docs/node_modules
|
prune docs/node_modules
|
||||||
|
|
||||||
# prune some large unused files from components
|
# prune some large unused files from components
|
||||||
prune share/jupyter/hub/static/components/bootstrap/css
|
prune share/jupyter/hub/static/components/bootstrap/dist/css
|
||||||
exclude share/jupyter/hub/static/components/components/fonts/*.svg
|
exclude share/jupyter/hub/static/components/bootstrap/dist/fonts/*.svg
|
||||||
exclude share/jupyter/hub/static/components/bootstrap/less/*.js
|
prune share/jupyter/hub/static/components/font-awesome/css
|
||||||
exclude share/jupyter/hub/static/components/font-awesome/css
|
prune share/jupyter/hub/static/components/font-awesome/scss
|
||||||
exclude share/jupyter/hub/static/components/font-awesome/fonts/*.svg
|
exclude share/jupyter/hub/static/components/font-awesome/fonts/*.svg
|
||||||
exclude share/jupyter/hub/static/components/jquery/*migrate*.js
|
prune share/jupyter/hub/static/components/jquery/external
|
||||||
|
prune share/jupyter/hub/static/components/jquery/src
|
||||||
prune share/jupyter/hub/static/components/moment/lang
|
prune share/jupyter/hub/static/components/moment/lang
|
||||||
prune share/jupyter/hub/static/components/moment/min
|
prune share/jupyter/hub/static/components/moment/min
|
||||||
|
|
||||||
|
36
bower-lite
Executable file
36
bower-lite
Executable file
@@ -0,0 +1,36 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
# Copyright (c) Jupyter Development Team.
|
||||||
|
# Distributed under the terms of the Modified BSD License.
|
||||||
|
|
||||||
|
"""
|
||||||
|
bower-lite
|
||||||
|
|
||||||
|
Since Bower's on its way out,
|
||||||
|
stage frontend dependencies from node_modules into components
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from os.path import join
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
HERE = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
|
||||||
|
|
||||||
|
components = join(HERE, "share", "jupyter", "hub", "static", "components")
|
||||||
|
node_modules = join(HERE, "node_modules")
|
||||||
|
|
||||||
|
if os.path.exists(components):
|
||||||
|
shutil.rmtree(components)
|
||||||
|
os.mkdir(components)
|
||||||
|
|
||||||
|
with open(join(HERE, 'package.json')) as f:
|
||||||
|
package_json = json.load(f)
|
||||||
|
|
||||||
|
dependencies = package_json['dependencies']
|
||||||
|
for dep in dependencies:
|
||||||
|
src = join(node_modules, dep)
|
||||||
|
dest = join(components, dep)
|
||||||
|
print("%s -> %s" % (src, dest))
|
||||||
|
shutil.copytree(src, dest)
|
11
bower.json
11
bower.json
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "jupyterhub-deps",
|
|
||||||
"version": "0.0.0",
|
|
||||||
"dependencies": {
|
|
||||||
"bootstrap": "components/bootstrap#~3.3",
|
|
||||||
"font-awesome": "components/font-awesome#~4.7",
|
|
||||||
"jquery": "components/jquery#~3.2",
|
|
||||||
"moment": "~2.18",
|
|
||||||
"requirejs": "~2.3"
|
|
||||||
}
|
|
||||||
}
|
|
@@ -7,7 +7,32 @@ command line for details.
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
## [0.8.0] 2017-10-03
|
## 0.8
|
||||||
|
|
||||||
|
### [0.8.1] 2017-11-07
|
||||||
|
|
||||||
|
JupyterHub 0.8.1 is a collection of bugfixes and small improvements on 0.8.
|
||||||
|
|
||||||
|
#### Added
|
||||||
|
|
||||||
|
- Run tornado with AsyncIO by default
|
||||||
|
- Add `jupyterhub --upgrade-db` flag for automatically upgrading the database as part of startup.
|
||||||
|
This is useful for cases where manually running `jupyterhub upgrade-db`
|
||||||
|
as a separate step is unwieldy.
|
||||||
|
- Avoid creating backups of the database when no changes are to be made by
|
||||||
|
`jupyterhub upgrade-db`.
|
||||||
|
|
||||||
|
#### Fixed
|
||||||
|
|
||||||
|
- Add some further validation to usernames - `/` is not allowed in usernames.
|
||||||
|
- Fix empty logout page when using auto_login
|
||||||
|
- Fix autofill of username field in default login form.
|
||||||
|
- Fix listing of users on the admin page who have not yet started their server.
|
||||||
|
- Fix ever-growing traceback when re-raising Exceptions from spawn failures.
|
||||||
|
- Remove use of deprecated `bower` for javascript client dependencies.
|
||||||
|
|
||||||
|
|
||||||
|
### [0.8.0] 2017-10-03
|
||||||
|
|
||||||
JupyterHub 0.8 is a big release!
|
JupyterHub 0.8 is a big release!
|
||||||
|
|
||||||
@@ -235,7 +260,8 @@ Fix removal of `/login` page in 0.4.0, breaking some OAuth providers.
|
|||||||
First preview release
|
First preview release
|
||||||
|
|
||||||
|
|
||||||
[Unreleased]: https://github.com/jupyterhub/jupyterhub/compare/0.8.0...HEAD
|
[Unreleased]: https://github.com/jupyterhub/jupyterhub/compare/0.8.1...HEAD
|
||||||
|
[0.8.1]: https://github.com/jupyterhub/jupyterhub/compare/0.8.0...0.8.1
|
||||||
[0.8.0]: https://github.com/jupyterhub/jupyterhub/compare/0.7.2...0.8.0
|
[0.8.0]: https://github.com/jupyterhub/jupyterhub/compare/0.7.2...0.8.0
|
||||||
[0.7.2]: https://github.com/jupyterhub/jupyterhub/compare/0.7.1...0.7.2
|
[0.7.2]: https://github.com/jupyterhub/jupyterhub/compare/0.7.1...0.7.2
|
||||||
[0.7.1]: https://github.com/jupyterhub/jupyterhub/compare/0.7.0...0.7.1
|
[0.7.1]: https://github.com/jupyterhub/jupyterhub/compare/0.7.0...0.7.1
|
||||||
|
@@ -190,7 +190,7 @@ class MyAuthenticator(Authenticator):
|
|||||||
username = yield identify_user(handler, data)
|
username = yield identify_user(handler, data)
|
||||||
upstream_token = yield token_for_user(username)
|
upstream_token = yield token_for_user(username)
|
||||||
return {
|
return {
|
||||||
'username': username,
|
'name': username,
|
||||||
'auth_state': {
|
'auth_state': {
|
||||||
'upstream_token': upstream_token,
|
'upstream_token': upstream_token,
|
||||||
},
|
},
|
||||||
|
@@ -114,10 +114,11 @@ r.raise_for_status()
|
|||||||
r.json()
|
r.json()
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that the API token authorizes **JupyterHub** REST API requests. The same
|
The same API token can also authorize access to the [Jupyter Notebook REST API][]
|
||||||
token does **not** authorize access to the [Jupyter Notebook REST API][]
|
provided by notebook servers managed by JupyterHub if one of the following is true:
|
||||||
provided by notebook servers managed by JupyterHub. A different token is used
|
|
||||||
to access the **Jupyter Notebook** API.
|
1. The token is for the same user as the owner of the notebook
|
||||||
|
2. The token is tied to an admin user or service **and** `c.JupyterHub.admin_access` is set to `True`
|
||||||
|
|
||||||
## Enabling users to spawn multiple named-servers via the API
|
## Enabling users to spawn multiple named-servers via the API
|
||||||
|
|
||||||
|
@@ -178,7 +178,13 @@ When you run a service that has a url, it will be accessible under a
|
|||||||
your service to route proxied requests properly, it must take
|
your service to route proxied requests properly, it must take
|
||||||
`JUPYTERHUB_SERVICE_PREFIX` into account when routing requests. For example, a
|
`JUPYTERHUB_SERVICE_PREFIX` into account when routing requests. For example, a
|
||||||
web service would normally service its root handler at `'/'`, but the proxied
|
web service would normally service its root handler at `'/'`, but the proxied
|
||||||
service would need to serve `JUPYTERHUB_SERVICE_PREFIX + '/'`.
|
service would need to serve `JUPYTERHUB_SERVICE_PREFIX`.
|
||||||
|
|
||||||
|
Note that `JUPYTERHUB_SERVICE_PREFIX` will contain a trailing slash. This must
|
||||||
|
be taken into consideration when creating the service routes. If you include an
|
||||||
|
extra slash you might get unexpected behavior. For example if your service has a
|
||||||
|
`/foo` endpoint, the route would be `JUPYTERHUB_SERVICE_PREFIX + foo`, and
|
||||||
|
`/foo/bar` would be `JUPYTERHUB_SERVICE_PREFIX + foo/bar`.
|
||||||
|
|
||||||
## Hub Authentication and Services
|
## Hub Authentication and Services
|
||||||
|
|
||||||
@@ -269,7 +275,7 @@ def authenticated(f):
|
|||||||
return decorated
|
return decorated
|
||||||
|
|
||||||
|
|
||||||
@app.route(prefix + '/')
|
@app.route(prefix)
|
||||||
@authenticated
|
@authenticated
|
||||||
def whoami(user):
|
def whoami(user):
|
||||||
return Response(
|
return Response(
|
||||||
|
@@ -8,7 +8,7 @@ Uses `jupyterhub.services.HubAuth` to authenticate requests with the Hub in a [f
|
|||||||
|
|
||||||
jupyterhub --ip=127.0.0.1
|
jupyterhub --ip=127.0.0.1
|
||||||
|
|
||||||
2. Visit http://127.0.0.1:8000/services/whoami or http://127.0.0.1:8000/services/whoami-oauth
|
2. Visit http://127.0.0.1:8000/services/whoami/ or http://127.0.0.1:8000/services/whoami-oauth/
|
||||||
|
|
||||||
After logging in with your local-system credentials, you should see a JSON dump of your user info:
|
After logging in with your local-system credentials, you should see a JSON dump of your user info:
|
||||||
|
|
||||||
|
@@ -43,7 +43,7 @@ def authenticated(f):
|
|||||||
return decorated
|
return decorated
|
||||||
|
|
||||||
|
|
||||||
@app.route(prefix + '/')
|
@app.route(prefix)
|
||||||
@authenticated
|
@authenticated
|
||||||
def whoami(user):
|
def whoami(user):
|
||||||
return Response(
|
return Response(
|
||||||
|
@@ -13,7 +13,8 @@ def get_data_files():
|
|||||||
# walk up, looking for prefix/share/jupyter
|
# walk up, looking for prefix/share/jupyter
|
||||||
while path != '/':
|
while path != '/':
|
||||||
share_jupyter = join(path, 'share', 'jupyter', 'hub')
|
share_jupyter = join(path, 'share', 'jupyter', 'hub')
|
||||||
if exists(join(share_jupyter, 'static', 'components')):
|
static = join(share_jupyter, 'static')
|
||||||
|
if all(exists(join(static, f)) for f in ['components', 'css']):
|
||||||
return share_jupyter
|
return share_jupyter
|
||||||
path, _ = split(path)
|
path, _ = split(path)
|
||||||
# didn't find it, give up
|
# didn't find it, give up
|
||||||
|
@@ -6,7 +6,8 @@
|
|||||||
version_info = (
|
version_info = (
|
||||||
0,
|
0,
|
||||||
8,
|
8,
|
||||||
0,
|
1,
|
||||||
|
# 'dev',
|
||||||
)
|
)
|
||||||
|
|
||||||
__version__ = '.'.join(map(str, version_info))
|
__version__ = '.'.join(map(str, version_info))
|
||||||
|
@@ -12,7 +12,6 @@ import logging
|
|||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import shutil
|
|
||||||
import signal
|
import signal
|
||||||
import sys
|
import sys
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
@@ -23,7 +22,6 @@ if sys.version_info[:2] < (3, 3):
|
|||||||
|
|
||||||
from jinja2 import Environment, FileSystemLoader
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
|
||||||
from sqlalchemy import create_engine
|
|
||||||
from sqlalchemy.exc import OperationalError
|
from sqlalchemy.exc import OperationalError
|
||||||
|
|
||||||
from tornado.httpclient import AsyncHTTPClient
|
from tornado.httpclient import AsyncHTTPClient
|
||||||
@@ -32,6 +30,8 @@ from tornado.ioloop import IOLoop, PeriodicCallback
|
|||||||
from tornado.log import app_log, access_log, gen_log
|
from tornado.log import app_log, access_log, gen_log
|
||||||
import tornado.options
|
import tornado.options
|
||||||
from tornado import gen, web
|
from tornado import gen, web
|
||||||
|
from tornado.platform.asyncio import AsyncIOMainLoop
|
||||||
|
AsyncIOMainLoop().install()
|
||||||
|
|
||||||
from traitlets import (
|
from traitlets import (
|
||||||
Unicode, Integer, Dict, TraitError, List, Bool, Any,
|
Unicode, Integer, Dict, TraitError, List, Bool, Any,
|
||||||
@@ -99,6 +99,13 @@ flags = {
|
|||||||
'no-db': ({'JupyterHub': {'db_url': 'sqlite:///:memory:'}},
|
'no-db': ({'JupyterHub': {'db_url': 'sqlite:///:memory:'}},
|
||||||
"disable persisting state database to disk"
|
"disable persisting state database to disk"
|
||||||
),
|
),
|
||||||
|
'upgrade-db': ({'JupyterHub': {'upgrade_db': True}},
|
||||||
|
"""Automatically upgrade the database if needed on startup.
|
||||||
|
|
||||||
|
Only safe if the database has been backed up.
|
||||||
|
Only SQLite database files will be backed up automatically.
|
||||||
|
"""
|
||||||
|
),
|
||||||
'no-ssl': ({'JupyterHub': {'confirm_no_ssl': True}},
|
'no-ssl': ({'JupyterHub': {'confirm_no_ssl': True}},
|
||||||
"[DEPRECATED in 0.7: does nothing]"
|
"[DEPRECATED in 0.7: does nothing]"
|
||||||
),
|
),
|
||||||
@@ -165,39 +172,11 @@ class UpgradeDB(Application):
|
|||||||
aliases = common_aliases
|
aliases = common_aliases
|
||||||
classes = []
|
classes = []
|
||||||
|
|
||||||
def _backup_db_file(self, db_file):
|
|
||||||
"""Backup a database file"""
|
|
||||||
if not os.path.exists(db_file):
|
|
||||||
return
|
|
||||||
|
|
||||||
timestamp = datetime.now().strftime('.%Y-%m-%d-%H%M%S')
|
|
||||||
backup_db_file = db_file + timestamp
|
|
||||||
for i in range(1, 10):
|
|
||||||
if not os.path.exists(backup_db_file):
|
|
||||||
break
|
|
||||||
backup_db_file = '{}.{}.{}'.format(db_file, timestamp, i)
|
|
||||||
if os.path.exists(backup_db_file):
|
|
||||||
self.exit("backup db file already exists: %s" % backup_db_file)
|
|
||||||
|
|
||||||
self.log.info("Backing up %s => %s", db_file, backup_db_file)
|
|
||||||
shutil.copy(db_file, backup_db_file)
|
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
hub = JupyterHub(parent=self)
|
hub = JupyterHub(parent=self)
|
||||||
hub.load_config_file(hub.config_file)
|
hub.load_config_file(hub.config_file)
|
||||||
self.log = hub.log
|
self.log = hub.log
|
||||||
if (hub.db_url.startswith('sqlite:///')):
|
dbutil.upgrade_if_needed(hub.db_url, log=self.log)
|
||||||
db_file = hub.db_url.split(':///', 1)[1]
|
|
||||||
self._backup_db_file(db_file)
|
|
||||||
self.log.info("Upgrading %s", hub.db_url)
|
|
||||||
# run check-db-revision first
|
|
||||||
engine = create_engine(hub.db_url)
|
|
||||||
try:
|
|
||||||
orm.check_db_revision(engine)
|
|
||||||
except orm.DatabaseSchemaMismatch:
|
|
||||||
# ignore mismatch error because that's what we are here for!
|
|
||||||
pass
|
|
||||||
dbutil.upgrade(hub.db_url)
|
|
||||||
|
|
||||||
|
|
||||||
class JupyterHub(Application):
|
class JupyterHub(Application):
|
||||||
@@ -634,6 +613,12 @@ class JupyterHub(Application):
|
|||||||
"""
|
"""
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
|
|
||||||
|
upgrade_db = Bool(False,
|
||||||
|
help="""Upgrade the database automatically on start.
|
||||||
|
|
||||||
|
Only safe if database is regularly backed up.
|
||||||
|
Only SQLite databases will be backed up to a local file automatically.
|
||||||
|
""").tag(config=True)
|
||||||
reset_db = Bool(False,
|
reset_db = Bool(False,
|
||||||
help="Purge and reset the database."
|
help="Purge and reset the database."
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
@@ -897,7 +882,11 @@ class JupyterHub(Application):
|
|||||||
|
|
||||||
def init_db(self):
|
def init_db(self):
|
||||||
"""Create the database connection"""
|
"""Create the database connection"""
|
||||||
|
|
||||||
self.log.debug("Connecting to db: %s", self.db_url)
|
self.log.debug("Connecting to db: %s", self.db_url)
|
||||||
|
if self.upgrade_db:
|
||||||
|
dbutil.upgrade_if_needed(self.db_url, log=self.log)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.session_factory = orm.new_session_factory(
|
self.session_factory = orm.new_session_factory(
|
||||||
self.db_url,
|
self.db_url,
|
||||||
|
@@ -144,6 +144,12 @@ class Authenticator(LoggingConfigurable):
|
|||||||
|
|
||||||
Return True if username is valid, False otherwise.
|
Return True if username is valid, False otherwise.
|
||||||
"""
|
"""
|
||||||
|
if '/' in username:
|
||||||
|
# / is not allowed in usernames
|
||||||
|
return False
|
||||||
|
if not username:
|
||||||
|
# empty usernames are not allowed
|
||||||
|
return False
|
||||||
if not self.username_regex:
|
if not self.username_regex:
|
||||||
return True
|
return True
|
||||||
return bool(self.username_regex.match(username))
|
return bool(self.username_regex.match(username))
|
||||||
|
@@ -5,11 +5,17 @@
|
|||||||
# Based on pgcontents.utils.migrate, used under the Apache license.
|
# Based on pgcontents.utils.migrate, used under the Apache license.
|
||||||
|
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
from datetime import datetime
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
from subprocess import check_call
|
from subprocess import check_call
|
||||||
import sys
|
import sys
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
|
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
|
||||||
|
from . import orm
|
||||||
|
|
||||||
_here = os.path.abspath(os.path.dirname(__file__))
|
_here = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
|
||||||
ALEMBIC_INI_TEMPLATE_PATH = os.path.join(_here, 'alembic.ini')
|
ALEMBIC_INI_TEMPLATE_PATH = os.path.join(_here, 'alembic.ini')
|
||||||
@@ -84,6 +90,46 @@ def upgrade(db_url, revision='head'):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def backup_db_file(db_file, log=None):
|
||||||
|
"""Backup a database file if it exists"""
|
||||||
|
timestamp = datetime.now().strftime('.%Y-%m-%d-%H%M%S')
|
||||||
|
backup_db_file = db_file + timestamp
|
||||||
|
for i in range(1, 10):
|
||||||
|
if not os.path.exists(backup_db_file):
|
||||||
|
break
|
||||||
|
backup_db_file = '{}.{}.{}'.format(db_file, timestamp, i)
|
||||||
|
#
|
||||||
|
if os.path.exists(backup_db_file):
|
||||||
|
raise OSError("backup db file already exists: %s" % backup_db_file)
|
||||||
|
if log:
|
||||||
|
log.info("Backing up %s => %s", db_file, backup_db_file)
|
||||||
|
shutil.copy(db_file, backup_db_file)
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade_if_needed(db_url, backup=True, log=None):
|
||||||
|
"""Upgrade a database if needed
|
||||||
|
|
||||||
|
If the database is sqlite, a backup file will be created with a timestamp.
|
||||||
|
Other database systems should perform their own backups prior to calling this.
|
||||||
|
"""
|
||||||
|
# run check-db-revision first
|
||||||
|
engine = create_engine(db_url)
|
||||||
|
try:
|
||||||
|
orm.check_db_revision(engine)
|
||||||
|
except orm.DatabaseSchemaMismatch:
|
||||||
|
# ignore mismatch error because that's what we are here for!
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# nothing to do
|
||||||
|
return
|
||||||
|
log.info("Upgrading %s", db_url)
|
||||||
|
# we need to upgrade, backup the database
|
||||||
|
if backup and db_url.startswith('sqlite:///'):
|
||||||
|
db_file = db_url.split(':///', 1)[1]
|
||||||
|
backup_db_file(db_file, log=log)
|
||||||
|
upgrade(db_url)
|
||||||
|
|
||||||
|
|
||||||
def _alembic(*args):
|
def _alembic(*args):
|
||||||
"""Run an alembic command with a temporary alembic.ini"""
|
"""Run an alembic command with a temporary alembic.ini"""
|
||||||
with _temp_alembic_ini('sqlite:///jupyterhub.sqlite') as alembic_ini:
|
with _temp_alembic_ini('sqlite:///jupyterhub.sqlite') as alembic_ini:
|
||||||
|
@@ -3,6 +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.
|
||||||
|
|
||||||
|
import copy
|
||||||
import re
|
import re
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from http.client import responses
|
from http.client import responses
|
||||||
@@ -705,9 +706,11 @@ class UserSpawnHandler(BaseHandler):
|
|||||||
# Condition: spawner not active and _spawn_future exists and contains an Exception
|
# Condition: spawner not active and _spawn_future exists and contains an Exception
|
||||||
# Implicit spawn on /user/:name is not allowed if the user's last spawn failed.
|
# Implicit spawn on /user/:name is not allowed if the user's last spawn failed.
|
||||||
# We should point the user to Home if the most recent spawn failed.
|
# We should point the user to Home if the most recent spawn failed.
|
||||||
|
exc = spawner._spawn_future.exception()
|
||||||
self.log.error("Preventing implicit spawn for %s because last spawn failed: %s",
|
self.log.error("Preventing implicit spawn for %s because last spawn failed: %s",
|
||||||
spawner._log_name, spawner._spawn_future.exception())
|
spawner._log_name, exc)
|
||||||
raise spawner._spawn_future.exception()
|
# raise a copy because each time an Exception object is re-raised, its traceback grows
|
||||||
|
raise copy.copy(exc).with_traceback(exc.__traceback__)
|
||||||
|
|
||||||
# check for pending spawn
|
# check for pending spawn
|
||||||
if spawner.pending and spawner._spawn_future:
|
if spawner.pending and spawner._spawn_future:
|
||||||
|
@@ -20,7 +20,8 @@ class LogoutHandler(BaseHandler):
|
|||||||
self.clear_login_cookie()
|
self.clear_login_cookie()
|
||||||
self.statsd.incr('logout')
|
self.statsd.incr('logout')
|
||||||
if self.authenticator.auto_login:
|
if self.authenticator.auto_login:
|
||||||
self.render_template('logout.html')
|
html = self.render_template('logout.html')
|
||||||
|
self.finish(html)
|
||||||
else:
|
else:
|
||||||
self.redirect(self.settings['login_url'], permanent=False)
|
self.redirect(self.settings['login_url'], permanent=False)
|
||||||
|
|
||||||
|
@@ -201,7 +201,7 @@ class AdminHandler(BaseHandler):
|
|||||||
# get User.col.desc() order objects
|
# get User.col.desc() order objects
|
||||||
ordered = [ getattr(c, o)() for c, o in zip(cols, orders) ]
|
ordered = [ getattr(c, o)() for c, o in zip(cols, orders) ]
|
||||||
|
|
||||||
users = self.db.query(orm.User).join(orm.Spawner).order_by(*ordered)
|
users = self.db.query(orm.User).outerjoin(orm.Spawner).order_by(*ordered)
|
||||||
users = [ self._user_from_orm(u) for u in users ]
|
users = [ self._user_from_orm(u) for u in users ]
|
||||||
running = [ u for u in users if u.running ]
|
running = [ u for u in users if u.running ]
|
||||||
|
|
||||||
|
@@ -24,7 +24,6 @@ from sqlalchemy.pool import StaticPool
|
|||||||
from sqlalchemy.sql.expression import bindparam
|
from sqlalchemy.sql.expression import bindparam
|
||||||
from sqlalchemy import create_engine, Table
|
from sqlalchemy import create_engine, Table
|
||||||
|
|
||||||
from .dbutil import _temp_alembic_ini
|
|
||||||
from .utils import (
|
from .utils import (
|
||||||
random_port,
|
random_port,
|
||||||
new_token, hash_token, compare_token,
|
new_token, hash_token, compare_token,
|
||||||
@@ -463,6 +462,8 @@ def check_db_revision(engine):
|
|||||||
current_table_names = set(engine.table_names())
|
current_table_names = set(engine.table_names())
|
||||||
my_table_names = set(Base.metadata.tables.keys())
|
my_table_names = set(Base.metadata.tables.keys())
|
||||||
|
|
||||||
|
from .dbutil import _temp_alembic_ini
|
||||||
|
|
||||||
with _temp_alembic_ini(engine.url) as ini:
|
with _temp_alembic_ini(engine.url) as ini:
|
||||||
cfg = alembic.config.Config(ini)
|
cfg = alembic.config.Config(ini)
|
||||||
scripts = ScriptDirectory.from_config(cfg)
|
scripts = ScriptDirectory.from_config(cfg)
|
||||||
|
12
package.json
12
package.json
@@ -8,10 +8,20 @@
|
|||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/jupyter/jupyterhub.git"
|
"url": "https://github.com/jupyter/jupyterhub.git"
|
||||||
},
|
},
|
||||||
|
"scripts": {
|
||||||
|
"postinstall": "./bower-lite",
|
||||||
|
"lessc": "lessc"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"bower": "*",
|
|
||||||
"less": "^2.7.1",
|
"less": "^2.7.1",
|
||||||
"less-plugin-clean-css": "^1.5.1",
|
"less-plugin-clean-css": "^1.5.1",
|
||||||
"clean-css": "^3.4.13"
|
"clean-css": "^3.4.13"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"bootstrap": "^3.3.7",
|
||||||
|
"font-awesome": "^4.7.0",
|
||||||
|
"jquery": "^3.2.1",
|
||||||
|
"moment": "^2.18.1",
|
||||||
|
"requirejs": "^2.3.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
36
setup.py
36
setup.py
@@ -149,45 +149,34 @@ class BaseCommand(Command):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
class Bower(BaseCommand):
|
class NPM(BaseCommand):
|
||||||
description = "fetch static client-side components with bower"
|
description = "fetch static client-side components with bower"
|
||||||
|
|
||||||
user_options = []
|
user_options = []
|
||||||
bower_dir = pjoin(static, 'components')
|
|
||||||
node_modules = pjoin(here, 'node_modules')
|
node_modules = pjoin(here, 'node_modules')
|
||||||
|
bower_dir = pjoin(static, 'components')
|
||||||
|
|
||||||
def should_run(self):
|
def should_run(self):
|
||||||
if not os.path.exists(self.bower_dir):
|
|
||||||
return True
|
|
||||||
return mtime(self.bower_dir) < mtime(pjoin(here, 'bower.json'))
|
|
||||||
|
|
||||||
def should_run_npm(self):
|
|
||||||
if not shutil.which('npm'):
|
if not shutil.which('npm'):
|
||||||
print("npm unavailable", file=sys.stderr)
|
print("npm unavailable", file=sys.stderr)
|
||||||
return False
|
return False
|
||||||
|
if not os.path.exists(self.bower_dir):
|
||||||
|
return True
|
||||||
if not os.path.exists(self.node_modules):
|
if not os.path.exists(self.node_modules):
|
||||||
return True
|
return True
|
||||||
|
if mtime(self.bower_dir) < mtime(self.node_modules):
|
||||||
|
return True
|
||||||
return mtime(self.node_modules) < mtime(pjoin(here, 'package.json'))
|
return mtime(self.node_modules) < mtime(pjoin(here, 'package.json'))
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
if not self.should_run():
|
if not self.should_run():
|
||||||
print("bower dependencies up to date")
|
print("npm dependencies up to date")
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.should_run_npm():
|
print("installing js dependencies with npm")
|
||||||
print("installing build dependencies with npm")
|
|
||||||
check_call(['npm', 'install', '--progress=false'], cwd=here, shell=shell)
|
check_call(['npm', 'install', '--progress=false'], cwd=here, shell=shell)
|
||||||
os.utime(self.node_modules)
|
os.utime(self.node_modules)
|
||||||
|
|
||||||
env = os.environ.copy()
|
|
||||||
env['PATH'] = npm_path
|
|
||||||
args = ['bower', 'install', '--allow-root', '--config.interactive=false']
|
|
||||||
try:
|
|
||||||
check_call(args, cwd=here, env=env, shell=shell)
|
|
||||||
except OSError as e:
|
|
||||||
print("Failed to run bower: %s" % e, file=sys.stderr)
|
|
||||||
print("You can install js dependencies with `npm install`", file=sys.stderr)
|
|
||||||
raise
|
|
||||||
os.utime(self.bower_dir)
|
os.utime(self.bower_dir)
|
||||||
# update data-files in case this created new files
|
# update data-files in case this created new files
|
||||||
self.distribution.data_files = get_data_files()
|
self.distribution.data_files = get_data_files()
|
||||||
@@ -225,22 +214,21 @@ class CSS(BaseCommand):
|
|||||||
return
|
return
|
||||||
|
|
||||||
self.run_command('js')
|
self.run_command('js')
|
||||||
|
print("Building css with less")
|
||||||
|
|
||||||
style_less = pjoin(static, 'less', 'style.less')
|
style_less = pjoin(static, 'less', 'style.less')
|
||||||
style_css = pjoin(static, 'css', 'style.min.css')
|
style_css = pjoin(static, 'css', 'style.min.css')
|
||||||
sourcemap = style_css + '.map'
|
sourcemap = style_css + '.map'
|
||||||
|
|
||||||
env = os.environ.copy()
|
|
||||||
env['PATH'] = npm_path
|
|
||||||
args = [
|
args = [
|
||||||
'lessc', '--clean-css',
|
'npm', 'run', 'lessc', '--', '--clean-css',
|
||||||
'--source-map-basepath={}'.format(static),
|
'--source-map-basepath={}'.format(static),
|
||||||
'--source-map={}'.format(sourcemap),
|
'--source-map={}'.format(sourcemap),
|
||||||
'--source-map-rootpath=../',
|
'--source-map-rootpath=../',
|
||||||
style_less, style_css,
|
style_less, style_css,
|
||||||
]
|
]
|
||||||
try:
|
try:
|
||||||
check_call(args, cwd=here, env=env, shell=shell)
|
check_call(args, cwd=here, shell=shell)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
print("Failed to run lessc: %s" % e, file=sys.stderr)
|
print("Failed to run lessc: %s" % 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)
|
||||||
@@ -275,7 +263,7 @@ class bdist_egg_disabled(bdist_egg):
|
|||||||
|
|
||||||
|
|
||||||
setup_args['cmdclass'] = {
|
setup_args['cmdclass'] = {
|
||||||
'js': Bower,
|
'js': NPM,
|
||||||
'css': CSS,
|
'css': CSS,
|
||||||
'build_py': js_css_first(build_py, strict=is_repo),
|
'build_py': js_css_first(build_py, strict=is_repo),
|
||||||
'sdist': js_css_first(sdist, strict=True),
|
'sdist': js_css_first(sdist, strict=True),
|
||||||
|
@@ -35,7 +35,7 @@
|
|||||||
<label for="username_input">Username:</label>
|
<label for="username_input">Username:</label>
|
||||||
<input
|
<input
|
||||||
id="username_input"
|
id="username_input"
|
||||||
type="username"
|
type="text"
|
||||||
autocapitalize="off"
|
autocapitalize="off"
|
||||||
autocorrect="off"
|
autocorrect="off"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
|
@@ -35,8 +35,8 @@
|
|||||||
<link rel="stylesheet" href="{{ static_url("css/style.min.css") }}" type="text/css"/>
|
<link rel="stylesheet" href="{{ static_url("css/style.min.css") }}" type="text/css"/>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
<script src="{{static_url("components/requirejs/require.js") }}" type="text/javascript" charset="utf-8"></script>
|
<script src="{{static_url("components/requirejs/require.js") }}" type="text/javascript" charset="utf-8"></script>
|
||||||
<script src="{{static_url("components/jquery/jquery.min.js") }}" type="text/javascript" charset="utf-8"></script>
|
<script src="{{static_url("components/jquery/dist/jquery.min.js") }}" type="text/javascript" charset="utf-8"></script>
|
||||||
<script src="{{static_url("components/bootstrap/js/bootstrap.min.js") }}" type="text/javascript" charset="utf-8"></script>
|
<script src="{{static_url("components/bootstrap/dist/js/bootstrap.min.js") }}" type="text/javascript" charset="utf-8"></script>
|
||||||
<script>
|
<script>
|
||||||
require.config({
|
require.config({
|
||||||
{% if version_hash %}
|
{% if version_hash %}
|
||||||
@@ -45,8 +45,8 @@
|
|||||||
baseUrl: '{{static_url("js", include_version=False)}}',
|
baseUrl: '{{static_url("js", include_version=False)}}',
|
||||||
paths: {
|
paths: {
|
||||||
components: '../components',
|
components: '../components',
|
||||||
jquery: '../components/jquery/jquery.min',
|
jquery: '../components/jquery/dist/jquery.min',
|
||||||
bootstrap: '../components/bootstrap/js/bootstrap.min',
|
bootstrap: '../components/bootstrap/dist/js/bootstrap.min',
|
||||||
moment: "../components/moment/moment",
|
moment: "../components/moment/moment",
|
||||||
},
|
},
|
||||||
shim: {
|
shim: {
|
||||||
|
Reference in New Issue
Block a user