mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-07 10:04:07 +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
|
||||
WORKDIR /src/jupyterhub
|
||||
|
||||
RUN python setup.py js && pip install . && \
|
||||
RUN npm install --unsafe-perm && \
|
||||
pip install . && \
|
||||
rm -rf $PWD ~/.cache ~/.npm
|
||||
|
||||
RUN mkdir -p /srv/jupyterhub/
|
||||
|
13
MANIFEST.in
13
MANIFEST.in
@@ -1,7 +1,7 @@
|
||||
include README.md
|
||||
include COPYING.md
|
||||
include setupegg.py
|
||||
include bower.json
|
||||
include bower-lite
|
||||
include package.json
|
||||
include *requirements.txt
|
||||
include Dockerfile
|
||||
@@ -18,12 +18,13 @@ graft docs
|
||||
prune docs/node_modules
|
||||
|
||||
# prune some large unused files from components
|
||||
prune share/jupyter/hub/static/components/bootstrap/css
|
||||
exclude share/jupyter/hub/static/components/components/fonts/*.svg
|
||||
exclude share/jupyter/hub/static/components/bootstrap/less/*.js
|
||||
exclude share/jupyter/hub/static/components/font-awesome/css
|
||||
prune share/jupyter/hub/static/components/bootstrap/dist/css
|
||||
exclude share/jupyter/hub/static/components/bootstrap/dist/fonts/*.svg
|
||||
prune 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/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/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]
|
||||
|
||||
## [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!
|
||||
|
||||
@@ -235,7 +260,8 @@ Fix removal of `/login` page in 0.4.0, breaking some OAuth providers.
|
||||
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.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
|
||||
|
@@ -190,7 +190,7 @@ class MyAuthenticator(Authenticator):
|
||||
username = yield identify_user(handler, data)
|
||||
upstream_token = yield token_for_user(username)
|
||||
return {
|
||||
'username': username,
|
||||
'name': username,
|
||||
'auth_state': {
|
||||
'upstream_token': upstream_token,
|
||||
},
|
||||
|
@@ -114,10 +114,11 @@ r.raise_for_status()
|
||||
r.json()
|
||||
```
|
||||
|
||||
Note that the API token authorizes **JupyterHub** REST API requests. The same
|
||||
token does **not** authorize access to the [Jupyter Notebook REST API][]
|
||||
provided by notebook servers managed by JupyterHub. A different token is used
|
||||
to access the **Jupyter Notebook** API.
|
||||
The same API token can also authorize access to the [Jupyter Notebook REST API][]
|
||||
provided by notebook servers managed by JupyterHub if one of the following is true:
|
||||
|
||||
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
|
||||
|
||||
|
@@ -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
|
||||
`JUPYTERHUB_SERVICE_PREFIX` into account when routing requests. For example, a
|
||||
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
|
||||
|
||||
@@ -269,7 +275,7 @@ def authenticated(f):
|
||||
return decorated
|
||||
|
||||
|
||||
@app.route(prefix + '/')
|
||||
@app.route(prefix)
|
||||
@authenticated
|
||||
def whoami(user):
|
||||
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
|
||||
|
||||
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:
|
||||
|
||||
|
@@ -43,7 +43,7 @@ def authenticated(f):
|
||||
return decorated
|
||||
|
||||
|
||||
@app.route(prefix + '/')
|
||||
@app.route(prefix)
|
||||
@authenticated
|
||||
def whoami(user):
|
||||
return Response(
|
||||
|
@@ -13,7 +13,8 @@ def get_data_files():
|
||||
# walk up, looking for prefix/share/jupyter
|
||||
while path != '/':
|
||||
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
|
||||
path, _ = split(path)
|
||||
# didn't find it, give up
|
||||
|
@@ -6,7 +6,8 @@
|
||||
version_info = (
|
||||
0,
|
||||
8,
|
||||
0,
|
||||
1,
|
||||
# 'dev',
|
||||
)
|
||||
|
||||
__version__ = '.'.join(map(str, version_info))
|
||||
|
@@ -12,7 +12,6 @@ import logging
|
||||
from operator import itemgetter
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import signal
|
||||
import sys
|
||||
from textwrap import dedent
|
||||
@@ -23,7 +22,6 @@ if sys.version_info[:2] < (3, 3):
|
||||
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.exc import OperationalError
|
||||
|
||||
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
|
||||
import tornado.options
|
||||
from tornado import gen, web
|
||||
from tornado.platform.asyncio import AsyncIOMainLoop
|
||||
AsyncIOMainLoop().install()
|
||||
|
||||
from traitlets import (
|
||||
Unicode, Integer, Dict, TraitError, List, Bool, Any,
|
||||
@@ -99,6 +99,13 @@ flags = {
|
||||
'no-db': ({'JupyterHub': {'db_url': 'sqlite:///:memory:'}},
|
||||
"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}},
|
||||
"[DEPRECATED in 0.7: does nothing]"
|
||||
),
|
||||
@@ -165,39 +172,11 @@ class UpgradeDB(Application):
|
||||
aliases = common_aliases
|
||||
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):
|
||||
hub = JupyterHub(parent=self)
|
||||
hub.load_config_file(hub.config_file)
|
||||
self.log = hub.log
|
||||
if (hub.db_url.startswith('sqlite:///')):
|
||||
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)
|
||||
dbutil.upgrade_if_needed(hub.db_url, log=self.log)
|
||||
|
||||
|
||||
class JupyterHub(Application):
|
||||
@@ -634,6 +613,12 @@ class JupyterHub(Application):
|
||||
"""
|
||||
).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,
|
||||
help="Purge and reset the database."
|
||||
).tag(config=True)
|
||||
@@ -897,7 +882,11 @@ class JupyterHub(Application):
|
||||
|
||||
def init_db(self):
|
||||
"""Create the database connection"""
|
||||
|
||||
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:
|
||||
self.session_factory = orm.new_session_factory(
|
||||
self.db_url,
|
||||
|
@@ -144,6 +144,12 @@ class Authenticator(LoggingConfigurable):
|
||||
|
||||
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:
|
||||
return True
|
||||
return bool(self.username_regex.match(username))
|
||||
|
@@ -5,11 +5,17 @@
|
||||
# Based on pgcontents.utils.migrate, used under the Apache license.
|
||||
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime
|
||||
import os
|
||||
import shutil
|
||||
from subprocess import check_call
|
||||
import sys
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
|
||||
from . import orm
|
||||
|
||||
_here = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
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):
|
||||
"""Run an alembic command with a temporary alembic.ini"""
|
||||
with _temp_alembic_ini('sqlite:///jupyterhub.sqlite') as alembic_ini:
|
||||
|
@@ -3,6 +3,7 @@
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
import copy
|
||||
import re
|
||||
from datetime import timedelta
|
||||
from http.client import responses
|
||||
@@ -705,9 +706,11 @@ class UserSpawnHandler(BaseHandler):
|
||||
# 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.
|
||||
# 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",
|
||||
spawner._log_name, spawner._spawn_future.exception())
|
||||
raise spawner._spawn_future.exception()
|
||||
spawner._log_name, exc)
|
||||
# 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
|
||||
if spawner.pending and spawner._spawn_future:
|
||||
|
@@ -20,7 +20,8 @@ class LogoutHandler(BaseHandler):
|
||||
self.clear_login_cookie()
|
||||
self.statsd.incr('logout')
|
||||
if self.authenticator.auto_login:
|
||||
self.render_template('logout.html')
|
||||
html = self.render_template('logout.html')
|
||||
self.finish(html)
|
||||
else:
|
||||
self.redirect(self.settings['login_url'], permanent=False)
|
||||
|
||||
|
@@ -201,7 +201,7 @@ class AdminHandler(BaseHandler):
|
||||
# get User.col.desc() order objects
|
||||
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 ]
|
||||
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 import create_engine, Table
|
||||
|
||||
from .dbutil import _temp_alembic_ini
|
||||
from .utils import (
|
||||
random_port,
|
||||
new_token, hash_token, compare_token,
|
||||
@@ -463,6 +462,8 @@ def check_db_revision(engine):
|
||||
current_table_names = set(engine.table_names())
|
||||
my_table_names = set(Base.metadata.tables.keys())
|
||||
|
||||
from .dbutil import _temp_alembic_ini
|
||||
|
||||
with _temp_alembic_ini(engine.url) as ini:
|
||||
cfg = alembic.config.Config(ini)
|
||||
scripts = ScriptDirectory.from_config(cfg)
|
||||
|
40
package.json
40
package.json
@@ -1,17 +1,27 @@
|
||||
{
|
||||
"name": "jupyterhub-deps",
|
||||
"version": "0.0.0",
|
||||
"description": "JupyterHub nodejs dependencies",
|
||||
"author": "Jupyter Developers",
|
||||
"license": "BSD",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/jupyter/jupyterhub.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"bower": "*",
|
||||
"less": "^2.7.1",
|
||||
"less-plugin-clean-css": "^1.5.1",
|
||||
"clean-css": "^3.4.13"
|
||||
}
|
||||
"name": "jupyterhub-deps",
|
||||
"version": "0.0.0",
|
||||
"description": "JupyterHub nodejs dependencies",
|
||||
"author": "Jupyter Developers",
|
||||
"license": "BSD",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/jupyter/jupyterhub.git"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "./bower-lite",
|
||||
"lessc": "lessc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"less": "^2.7.1",
|
||||
"less-plugin-clean-css": "^1.5.1",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
40
setup.py
40
setup.py
@@ -149,45 +149,34 @@ class BaseCommand(Command):
|
||||
return []
|
||||
|
||||
|
||||
class Bower(BaseCommand):
|
||||
class NPM(BaseCommand):
|
||||
description = "fetch static client-side components with bower"
|
||||
|
||||
user_options = []
|
||||
bower_dir = pjoin(static, 'components')
|
||||
node_modules = pjoin(here, 'node_modules')
|
||||
bower_dir = pjoin(static, 'components')
|
||||
|
||||
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'):
|
||||
print("npm unavailable", file=sys.stderr)
|
||||
return False
|
||||
if not os.path.exists(self.bower_dir):
|
||||
return True
|
||||
if not os.path.exists(self.node_modules):
|
||||
return True
|
||||
if mtime(self.bower_dir) < mtime(self.node_modules):
|
||||
return True
|
||||
return mtime(self.node_modules) < mtime(pjoin(here, 'package.json'))
|
||||
|
||||
def run(self):
|
||||
if not self.should_run():
|
||||
print("bower dependencies up to date")
|
||||
print("npm dependencies up to date")
|
||||
return
|
||||
|
||||
if self.should_run_npm():
|
||||
print("installing build dependencies with npm")
|
||||
check_call(['npm', 'install', '--progress=false'], cwd=here, shell=shell)
|
||||
os.utime(self.node_modules)
|
||||
print("installing js dependencies with npm")
|
||||
check_call(['npm', 'install', '--progress=false'], cwd=here, shell=shell)
|
||||
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)
|
||||
# update data-files in case this created new files
|
||||
self.distribution.data_files = get_data_files()
|
||||
@@ -225,22 +214,21 @@ class CSS(BaseCommand):
|
||||
return
|
||||
|
||||
self.run_command('js')
|
||||
print("Building css with less")
|
||||
|
||||
style_less = pjoin(static, 'less', 'style.less')
|
||||
style_css = pjoin(static, 'css', 'style.min.css')
|
||||
sourcemap = style_css + '.map'
|
||||
|
||||
env = os.environ.copy()
|
||||
env['PATH'] = npm_path
|
||||
args = [
|
||||
'lessc', '--clean-css',
|
||||
'npm', 'run', 'lessc', '--', '--clean-css',
|
||||
'--source-map-basepath={}'.format(static),
|
||||
'--source-map={}'.format(sourcemap),
|
||||
'--source-map-rootpath=../',
|
||||
style_less, style_css,
|
||||
]
|
||||
try:
|
||||
check_call(args, cwd=here, env=env, shell=shell)
|
||||
check_call(args, cwd=here, shell=shell)
|
||||
except OSError as e:
|
||||
print("Failed to run lessc: %s" % e, 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'] = {
|
||||
'js': Bower,
|
||||
'js': NPM,
|
||||
'css': CSS,
|
||||
'build_py': js_css_first(build_py, strict=is_repo),
|
||||
'sdist': js_css_first(sdist, strict=True),
|
||||
|
@@ -35,7 +35,7 @@
|
||||
<label for="username_input">Username:</label>
|
||||
<input
|
||||
id="username_input"
|
||||
type="username"
|
||||
type="text"
|
||||
autocapitalize="off"
|
||||
autocorrect="off"
|
||||
class="form-control"
|
||||
|
@@ -35,8 +35,8 @@
|
||||
<link rel="stylesheet" href="{{ static_url("css/style.min.css") }}" type="text/css"/>
|
||||
{% endblock %}
|
||||
<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/bootstrap/js/bootstrap.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/dist/js/bootstrap.min.js") }}" type="text/javascript" charset="utf-8"></script>
|
||||
<script>
|
||||
require.config({
|
||||
{% if version_hash %}
|
||||
@@ -45,8 +45,8 @@
|
||||
baseUrl: '{{static_url("js", include_version=False)}}',
|
||||
paths: {
|
||||
components: '../components',
|
||||
jquery: '../components/jquery/jquery.min',
|
||||
bootstrap: '../components/bootstrap/js/bootstrap.min',
|
||||
jquery: '../components/jquery/dist/jquery.min',
|
||||
bootstrap: '../components/bootstrap/dist/js/bootstrap.min',
|
||||
moment: "../components/moment/moment",
|
||||
},
|
||||
shim: {
|
||||
|
Reference in New Issue
Block a user