Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a gallery JupyterHub service #17

Merged
merged 8 commits into from
Jun 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion tljh-voila-gallery/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,12 @@
"console_scripts": ["build-gallery-images = tljh_voila_gallery.build_images:main"]
},
packages=find_packages(),
include_package_data=True
include_package_data=True,
install_requires=[
# These get installed into the hub environment
'dockerspawner',
yuvipanda marked this conversation as resolved.
Show resolved Hide resolved
'jupyter-repo2docker',
'binderhub',
'nullauthenticator'
]
)
66 changes: 37 additions & 29 deletions tljh-voila-gallery/tljh_voila_gallery/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import socket
import sys
import os
import jinja2
from pkg_resources import resource_stream, resource_filename
Expand Down Expand Up @@ -29,26 +30,41 @@ def options_form(spawner):

@hookimpl
def tljh_custom_jupyterhub_config(c):
# Since dockerspawner isn't available at import time
from dockerspawner import DockerSpawner
from nullauthenticator import NullAuthenticator
class GallerySpawner(DockerSpawner):
def options_from_form(self, formdata):
options = {}
if 'example' in formdata:
options['example'] = formdata['example'][0]
return options
# FIXME: What to do about idle culling?!
cmd = 'jupyter-notebook'

events = False

def get_args(self):
args = [
'--ip=0.0.0.0',
'--port=%i' % self.port,
'--NotebookApp.base_url=%s' % self.server.base_url,
'--NotebookApp.token=%s' % self.user_options['token'],
'--NotebookApp.trust_xheaders=True',
]
return args + self.args

def start(self):
gallery = get_gallery()
examples = gallery['examples']
chosen_example = self.user_options['example']
assert chosen_example in examples
self.default_url = examples[chosen_example]['url']
self.image = examples[chosen_example]['image']
if 'token' not in self.user_options:
raise web.HTTPError(400, "token required")
if 'image' not in self.user_options:
raise web.HTTPError(400, "image required")
self.image = self.user_options['image']
return super().start()


class GalleryAuthenticator(NullAuthenticator):
auto_login = True

def login_url(self, base_url):
return '/services/gallery'

c.JupyterHub.spawner_class = GallerySpawner
c.JupyterHub.authenticator_class = 'tmpauthenticator.TmpAuthenticator'
c.JupyterHub.authenticator_class = GalleryAuthenticator

c.JupyterHub.hub_connect_ip = socket.gethostname()

Expand All @@ -57,23 +73,15 @@ def start(self):

# rm containers when they stop
c.DockerSpawner.remove = True
# Disabled until we fix it in TmpAuthenticator
# c.TmpAuthenticator.force_new_server = True

c.DockerSpawner.cmd = ['jupyterhub-singleuser']

# Override JupyterHub template
c.JupyterHub.template_paths = [TEMPLATES_PATH]

c.Spawner.options_form = options_form


@hookimpl
def tljh_extra_hub_pip_packages():
return [
'dockerspawner',
'git+https://github.com/jupyter/repo2docker.git@f19e159dfe1006dbd82c7728e15cdd19751e8aec'
]
c.JupyterHub.services = [{
'name': 'gallery',
'admin': True,
'url': 'http://127.0.0.1:9888',
'command': [
sys.executable, '-m', 'tljh_voila_gallery.gallery'
]
}]

@hookimpl
def tljh_extra_apt_packages():
Expand Down
62 changes: 62 additions & 0 deletions tljh-voila-gallery/tljh_voila_gallery/gallery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import os
from tornado import ioloop, web
from pkg_resources import resource_stream, resource_filename
from ruamel.yaml import YAML
from jinja2 import PackageLoader, Environment
import json
from binderhub.launcher import Launcher
from urllib.parse import urljoin, urlencode

yaml = YAML()

# Read gallery.yaml on each spawn. If this gets too expensive, cache it here
def get_gallery():
with resource_stream(__name__, 'gallery.yaml') as f:
return yaml.load(f)

templates = Environment(loader=PackageLoader('tljh_voila_gallery', 'templates'))

class GalleryHandler(web.RequestHandler):
def get(self):
gallery_template = templates.get_template('gallery-examples.html')
gallery = get_gallery()

self.write(gallery_template.render(
url=self.request.full_url(),
examples=gallery.get('examples', [])
))

async def post(self):
gallery = get_gallery()

example_name = self.get_body_argument('example')

example = gallery['examples'][example_name]


launcher = Launcher(
hub_api_token=os.environ['JUPYTERHUB_API_TOKEN'],
hub_url=os.environ['JUPYTERHUB_BASE_URL']
)
response = await launcher.launch(
example['image'],
launcher.unique_name_from_repo(example['repo_url'])
)
redirect_url = urljoin(
response['url'],
example['url']
) + '?' + urlencode({'token': response['token']})
self.redirect(redirect_url)


def make_app():
return web.Application([
(os.environ['JUPYTERHUB_SERVICE_PREFIX'] + '/?', GalleryHandler),
])

if __name__ == "__main__":
if not os.environ['JUPYTERHUB_API_URL'].endswith('/'):
os.environ['JUPYTERHUB_API_URL'] = os.environ['JUPYTERHUB_API_URL'] + '/'
app = make_app()
app.listen(9888)
ioloop.IOLoop.current().start()
6 changes: 3 additions & 3 deletions tljh-voila-gallery/tljh_voila_gallery/gallery.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@ examples:
title: country-indicators
description: Explore the correlations between indicators of development using matplotlib and ipywidgets
image: country-indicators:latest
url: /voila/render/index.ipynb
url: voila/render/index.ipynb
repo_url: https://github.com/voila-gallery/voila-gallery-country-indicators
image_url: https://i.imgur.com/IeG75O3.png
gaussian-density:
title: gaussian-density
description: Explore the normal distribution interactively with bqplot
image: gaussian-density:latest
url: /voila/render/index.ipynb
url: voila/render/index.ipynb
repo_url: https://github.com/voila-gallery/gaussian-density
image_url: https://i.imgur.com/J1Mj6rc.png
render-stl:
title: render-stl
description: Explore STL files with ipyvolume
image: render-stl:latest
url: /voila/render/index.ipynb
url: voila/render/index.ipynb
repo_url: https://github.com/voila-gallery/render-stl
image_url: https://github.com/voila-gallery/render-stl/raw/master/thumbnail.png
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
{% extends "page.html" %}

{% block body %}
<form enctype="multipart/form-data" id="spawn_form" action="{{url}}" method="post" role="form">
<div class="row">
{% for example_name, info in examples.items() %}
<div class="col-md-4">
Expand All @@ -18,3 +22,7 @@ <h4>{{ info.title }}</h4>
</div>
{% endfor %}
</div>

</form>

{% endblock %}
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,8 @@ <h1 class="jumbotron-heading">Voila Dashboards Gallery</h1>

<div class="album py-5 bg-light">
<div class="container">

<form enctype="multipart/form-data" id="spawn_form" action="{{url}}" method="post" role="form">
{{spawner_options_form | safe}}
</form>
{% block body %}
{% endblock %}
</div>
</div>

Expand Down