Skip to content

Commit

Permalink
Create initial API for Jupyter Server containers
Browse files Browse the repository at this point in the history
  • Loading branch information
yannickperrenet committed Dec 11, 2019
1 parent ed9c767 commit d7acec3
Show file tree
Hide file tree
Showing 11 changed files with 374 additions and 0 deletions.
137 changes: 137 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

.idea/

# Flask stuff:
instance/
.webassets-cache

# NPM node_modules for web
node_modules/

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
.python-version

# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock

# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# Mac files
.DS_Store
16 changes: 16 additions & 0 deletions jupyter_server_api/apis/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from flask import Blueprint
from flask_restplus import Api

from .namespace_servers import api as ns_servers


blueprint = Blueprint('api', __name__)

api = Api(
blueprint,
title='Databoost - Jupyter API',
version='1.0',
description='Start and shutdown (a single) Jupyter server'
)

api.add_namespace(ns_servers)
88 changes: 88 additions & 0 deletions jupyter_server_api/apis/namespace_servers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import asyncio
import json
import requests
import subprocess
import time

from flask_restplus import Namespace, Resource, fields


api = Namespace('servers', description='Start and stop Jupyter servers')

server = api.model('Launch', {
'url': fields.String(required=True, description='URL of the server'),
'hostname': fields.String(required=True, default='localhost', description='Hostname'),
'port': fields.Integer(required=True, description='Port to access the server'),
'secure': fields.Boolean(required=True, description='Any extra security measures'),
'base_url': fields.String(required=True, default='/', description='Base URL'),
'token': fields.String(required=True, description='Token for authentication'),
'notebook_dir': fields.String(required=True, description='Directory of the server'),
'password': fields.Boolean(required=True, description='Password if one is set'),
'pid': fields.Integer(required=True, description='PID'),
})

SERVER = None


@api.route('/')
@api.response(404, 'Launch not found')
class Server(Resource):
@api.doc('get_launch')
@api.marshal_with(server)
def get(self):
"""Fetch the server information if it is running."""
global SERVER
if SERVER is not None:
return SERVER

return {'message': 'No currently running server'}, 404

# TODO: add arguments to the post for notebook-dir etc.
@api.doc('start_server')
def post(self):
"""Start a Jupyter server."""
# Need to start a new event loop to start a subprocess.
asyncio.set_event_loop(asyncio.new_event_loop())

# Start a Jupyter server within a subprocess. The "-u" option
# is to avoid buffering. Since it will be a long running
# process, we want output whilst the program is running such
# that we know when and if the server did successfully start.
proc = subprocess.Popen(args=['python', '-u', 'core/start_server.py'],
stdout=subprocess.PIPE)

# Wait for the server to be booted, it will write a message to
# stdout once successful.
_ = proc.stdout.readline()

# Get the information to connect to the server.
with open('tmp/server_info.json', 'r') as f:
info = json.load(f)

global SERVER
SERVER = info

return {'message': 'successful launch', 'info': info}

@api.doc('shutdown_server')
def delete(self):
"""Shutdown Jupyter server."""
global SERVER

# Send an authenticated POST request to the <server_url>/api/shutdown
# Authentication is done via the token of the server.
token = SERVER['token']
headers = {'Authorization': f'Token {token}'}

# The server's url already contains a trailing slash.
server_url = SERVER['url']
url = f'{server_url}api/shutdown'

# Shutdown the server, such that it also shuts down all related
# kernels.
requests.post(url, headers=headers)

# There no longer is a running server.
SERVER = None

return {'message': 'Server shutdown was successful'}
1 change: 1 addition & 0 deletions jupyter_server_api/apis/server_info.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"url": "http://localhost:8890/", "hostname": "localhost", "port": 8890, "secure": false, "base_url": "/", "token": "5af72b15a07c2cef3b48cd016da024a32311b19f788f8b23", "notebook_dir": "/Users/yannick/Documents/projects/databoost", "password": false, "pid": 2348}
17 changes: 17 additions & 0 deletions jupyter_server_api/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from flask import Flask
from flask_socketio import SocketIO

from apis import blueprint as api
from settings import app_config


app = Flask(__name__)
app.config.update(app_config)
app.register_blueprint(api, url_prefix='/api')

# Use SocketIO to be able to start jupyter server in a subprocess.
socketio = SocketIO(app)


if __name__ == '__main__':
socketio.run(app)
Empty file.
30 changes: 30 additions & 0 deletions jupyter_server_api/core/start_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import json
import sys

from jupyterlab.labapp import LabApp


def main():
# --no-browser
# --ip
# --port
# --gateway-url
# --notebook-dir

sys.argv.append('--notebook-dir=/Users/yannick/Documents/projects/databoost')
sys.argv.append('--no-browser')

la = LabApp()
la.initialize()

fname = 'tmp/server_info.json'
with open(fname, 'w') as f:
json.dump(la.server_info(), f)

print('Started Jupyter Notebook server')

la.start()


if __name__ == '__main__':
main()
64 changes: 64 additions & 0 deletions jupyter_server_api/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
aniso8601==8.0.0
appnope==0.1.0
attrs==19.3.0
backcall==0.1.0
bleach==3.1.0
certifi==2019.11.28
chardet==3.0.4
Click==7.0
decorator==4.4.1
defusedxml==0.6.0
dnspython==1.16.0
entrypoints==0.3
eventlet==0.25.1
Flask==1.1.1
flask-restplus==0.13.0
Flask-SocketIO==4.2.1
greenlet==0.4.15
idna==2.8
importlib-metadata==1.3.0
ipykernel==5.1.3
ipython==7.10.1
ipython-genutils==0.2.0
itsdangerous==1.1.0
jedi==0.15.1
Jinja2==2.10.3
json5==0.8.5
jsonschema==3.2.0
jupyter-client==5.3.4
jupyter-core==4.6.1
jupyterlab==1.2.4
jupyterlab-server==1.0.6
MarkupSafe==1.1.1
mistune==0.8.4
monotonic==1.5
more-itertools==8.0.2
nbconvert==5.6.1
nbformat==4.4.0
notebook==6.0.2
pandocfilters==1.4.2
parso==0.5.1
pexpect==4.7.0
pickleshare==0.7.5
prometheus-client==0.7.1
prompt-toolkit==3.0.2
ptyprocess==0.6.0
Pygments==2.5.2
pyrsistent==0.15.6
python-dateutil==2.8.1
python-engineio==3.11.1
python-socketio==4.4.0
pytz==2019.3
pyzmq==18.1.1
requests==2.22.0
Send2Trash==1.5.0
six==1.13.0
terminado==0.8.3
testpath==0.4.4
tornado==6.0.3
traitlets==4.3.3
urllib3==1.25.7
wcwidth==0.1.7
webencodings==0.5.1
Werkzeug==0.16.0
zipp==0.6.0
15 changes: 15 additions & 0 deletions jupyter_server_api/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
flask_settings = {
'FLASK_DEBUG': False,
'FLASK_SERVER_NAME': 'localhost:5000'
}

flask_restplus_settings = {
'RESTPLUS_SWAGGER_UI_DOC_EXPANSION': 'list',
'RESTPLUS_VALIDATE': True,
'RESTPLUS_MASK_SWAGGER': False,
'RESTPLUS_ERROR_404_HELP': False
}

app_config = {}
app_config.update(flask_settings)
app_config.update(flask_restplus_settings)
6 changes: 6 additions & 0 deletions jupyter_server_api/tmp/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Ignore everything
*

# Except
!.gitignore
!__init__.py
Empty file.

0 comments on commit d7acec3

Please sign in to comment.