├── .gitattributes ├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── binder └── Dockerfile ├── hooks └── post_push ├── requirements-test.txt └── tests ├── conftest.py └── test_job.py /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | bin/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | doc/_build 25 | 26 | # Installer logs 27 | pip-log.txt 28 | pip-delete-this-directory.txt 29 | 30 | # Unit test / coverage reports 31 | htmlcov/ 32 | .tox/ 33 | cover/ 34 | .coverage 35 | .cache 36 | nosetests.xml 37 | coverage.xml 38 | cover/ 39 | 40 | # Translations 41 | *.mo 42 | 43 | # Mr Developer 44 | .mr.developer.cfg 45 | .project 46 | .pydevproject 47 | 48 | # Rope 49 | .ropeproject 50 | 51 | # Django stuff: 52 | *.log 53 | *.pot 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | #mac 59 | .DS_Store 60 | *~ 61 | 62 | #pycharm 63 | .idea/* 64 | 65 | #Dolphin browser files 66 | .directory/ 67 | .directory 68 | 69 | #Binary data files 70 | *.volume 71 | *.am 72 | *.tiff 73 | *.tif 74 | *.dat 75 | *.DAT 76 | 77 | #generated documntation files 78 | generated/ 79 | 80 | #ipython notebook 81 | .ipynb_checkpoints/ 82 | 83 | #vim 84 | *.swp 85 | 86 | #data files 87 | *.zip 88 | *.jpg 89 | 90 | # ctags 91 | .tags* 92 | 93 | # Emacs temp and backup files 94 | *~ 95 | \#*\# -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 3.6 4 | sudo: required 5 | services: 6 | - docker 7 | install: 8 | - make test-env 9 | script: 10 | - make build 11 | - make test 12 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM jupyter/datascience-notebook:87210526f381 2 | 3 | # Add RUN statements to install packages as the $NB_USER defined in the base images. 4 | 5 | # Add a "USER root" statement followed by RUN statements to install system packages using apt-get, 6 | # change file permissions, etc. 7 | 8 | # If you do switch to root, always be sure to add a "USER $NB_USER" command at the end of the 9 | # file to ensure the image runs as a unprivileged user by default. 10 | 11 | # The conda-forge channel is already present in the system .condarc file, so there is no need to 12 | # add a channel invocation in any of the next commands. 13 | 14 | # Add nbgrader 0.5.5 to the image 15 | # More info at https://nbgrader.readthedocs.io/en/stable/ 16 | 17 | # First install some missing dependencies 18 | # Note: there is no need to install the "jupyter" metapackage because all the needed 19 | # pieces for nbgrader funtionality are already installed by the bootstraped image. 20 | RUN conda install fuzzywuzzy --yes 21 | 22 | # Then install nbgrader with --no-deps because all the neeeded deps are already present. 23 | # Additionally, latest nbgrader release is pinging an old ipython version breaking stuff. 24 | # Note: Eventually, when things get fixed upstream we can remove the previous installation 25 | # of "fuzzywuzzy" and remove the --no-deps flag. 26 | RUN conda install nbgrader --no-deps --yes 27 | 28 | # Add RISE 5.4.1 to the mix as well so user can show live slideshows from their notebooks 29 | # More info at https://rise.readthedocs.io 30 | # Note: Installing RISE with --no-deps because all the neeeded deps are already present. 31 | RUN conda install rise --no-deps --yes -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, umsi-mads 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: help build dev test test-env 2 | 3 | # Docker image name and tag 4 | IMAGE:=umsimads/education-notebook 5 | TAG?=latest 6 | # Shell that make should use 7 | SHELL:=bash 8 | 9 | help: 10 | # http://marmelab.com/blog/2016/02/29/auto-documented-makefile.html 11 | @grep -E '^[a-zA-Z0-9_%/-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 12 | 13 | build: DARGS?= 14 | build: ## Make the latest build of the image 15 | docker build $(DARGS) --rm --force-rm -t $(IMAGE):$(TAG) . 16 | 17 | dev: ARGS?= 18 | dev: DARGS?= 19 | dev: PORT?=8888 20 | dev: ## Make a container from a tagged image image 21 | docker run -it --rm -p $(PORT):8888 $(DARGS) $(REPO) $(ARGS) 22 | 23 | test: ## Make a test run against the latest image 24 | pytest tests 25 | 26 | test-env: ## Make a test environment by installing test dependencies with pip 27 | pip install -r requirements-test.txt -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # education-notebook 2 | 3 | **education-notebook** is an education-focused Jupyter Docker Stack image 4 | maintained by the community. 5 | 6 | ## What is education-notebook 7 | 8 | The `education-notebook` image adds RISE and nbgrader packages to the official 9 | Jupyter docker stacks' 10 | [`datascience-notebook`](https://jupyter-docker-stacks.readthedocs.io/en/latest/using/selecting.html#jupyter-datascience-notebook) image. 11 | 12 | [RISE](https://rise.readthedocs.io) gives educators a great tool for presenting 13 | notebooks and allows the instructor to interact with the notebook while teaching. 14 | 15 | [nbgrader](https://nbgrader.readthedocs.io) offers grading capabilities as well 16 | as notebook assignment distribution and collection. 17 | 18 | The `education-notebook` contains Python, R, and Julia as well as frequently 19 | used data science libraries. 20 | 21 | ## Try it on Binder 22 | 23 | Click on the following badge to launch a notebook on the `https://mybinder.org` service. 24 | 25 | [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/umsi-mads/education-notebook/master) 26 | -------------------------------------------------------------------------------- /binder/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM umsimads/education-notebook:1017284261a0 2 | -------------------------------------------------------------------------------- /hooks/post_push: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Tag the latest build with the short git sha. Push the tag in addition 4 | # to the "latest" tag already pushed. 5 | GIT_SHA_TAG=${SOURCE_COMMIT:0:12} 6 | docker tag $IMAGE_NAME $DOCKER_REPO:$GIT_SHA_TAG 7 | docker push $DOCKER_REPO:$GIT_SHA_TAG -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | docker 2 | pytest 3 | requests -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import docker 4 | import pytest 5 | import requests 6 | 7 | from requests.packages.urllib3.util.retry import Retry 8 | from requests.adapters import HTTPAdapter 9 | 10 | 11 | @pytest.fixture(scope='session') 12 | def http_client(): 13 | """Requests session with retries and backoff.""" 14 | s = requests.Session() 15 | retries = Retry(total=5, backoff_factor=1) 16 | s.mount('http://', HTTPAdapter(max_retries=retries)) 17 | s.mount('https://', HTTPAdapter(max_retries=retries)) 18 | return s 19 | 20 | 21 | @pytest.fixture(scope='session') 22 | def docker_client(): 23 | """Docker client configured based on the host environment""" 24 | return docker.from_env() 25 | 26 | 27 | @pytest.fixture(scope='session') 28 | def image_name(): 29 | """Image name to test""" 30 | return 'umsimads/education-notebook' 31 | 32 | 33 | class TrackedContainer(object): 34 | """Wrapper that collects docker container configuration and delays 35 | container creation/execution. 36 | Parameters 37 | ---------- 38 | docker_client: docker.DockerClient 39 | Docker client instance 40 | image_name: str 41 | Name of the docker image to launch 42 | **kwargs: dict, optional 43 | Default keyword arguments to pass to docker.DockerClient.containers.run 44 | """ 45 | def __init__(self, docker_client, image_name, **kwargs): 46 | self.container = None 47 | self.docker_client = docker_client 48 | self.image_name = image_name 49 | self.kwargs = kwargs 50 | 51 | def run(self, **kwargs): 52 | """Runs a docker container using the preconfigured image name 53 | and a mix of the preconfigured container options and those passed 54 | to this method. 55 | Keeps track of the docker.Container instance spawned to kill it 56 | later. 57 | Parameters 58 | ---------- 59 | **kwargs: dict, optional 60 | Keyword arguments to pass to docker.DockerClient.containers.run 61 | extending and/or overriding key/value pairs passed to the constructor 62 | Returns 63 | ------- 64 | docker.Container 65 | """ 66 | all_kwargs = {} 67 | all_kwargs.update(self.kwargs) 68 | all_kwargs.update(kwargs) 69 | self.container = self.docker_client.containers.run(self.image_name, **all_kwargs) 70 | return self.container 71 | 72 | def remove(self): 73 | """Kills and removes the tracked docker container.""" 74 | if self.container: 75 | self.container.remove(force=True) 76 | 77 | 78 | @pytest.fixture(scope='function') 79 | def container(docker_client, image_name): 80 | """Notebook container with initial configuration appropriate for testing 81 | (e.g., HTTP port exposed to the host for HTTP calls). 82 | Yields the container instance and kills it when the caller is done with it. 83 | """ 84 | container = TrackedContainer( 85 | docker_client, 86 | image_name, 87 | detach=True, 88 | ports={ 89 | '8888/tcp': 8888 90 | } 91 | ) 92 | yield container 93 | container.remove() -------------------------------------------------------------------------------- /tests/test_job.py: -------------------------------------------------------------------------------- 1 | def test_secured_server(container, http_client): 2 | """Notebook server should eventually request user login.""" 3 | container.run() 4 | resp = http_client.get('http://localhost:8888') 5 | resp.raise_for_status() 6 | assert 'login_submit' in resp.text --------------------------------------------------------------------------------