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 basic functionality (WIP) #1

Merged
merged 11 commits into from
Nov 13, 2017
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,7 @@ ENV/

# mypy
.mypy_cache/

# osx
*.DS_Store
notebook.zip
6 changes: 6 additions & 0 deletions .travis.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
sudo: false
language: python
python:
- "3.5"
install: pip install tox-travis
script: tox
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,21 @@
# nbzip
Zips and downloads all the contents of a jupyter notebook

![nbzip demo](doc/demo.gif)

# Installation

There is no package on PyPI available right now. You can install directly from master:

pip install git+https://github.com/data-8/nbzip


You can then enable the serverextension

jupyter serverextension enable --py nbzip --sys-prefix
jupyter nbextension install --py nbzip
jupyter nbextension enable --py nbzip

# What is it?

nbzip allows you to download all the contents of your jupyter notebook into a zipped file called 'notebook.zip'.
Binary file added doc/demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions manual-test.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pip install --upgrade -e .
jupyter serverextension enable --py nbzip
jupyter nbextension install --py nbzip
jupyter nbextension enable --py nbzip
jupyter-notebook
33 changes: 33 additions & 0 deletions nbzip/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from notebook.utils import url_path_join
from .handlers import ZipHandler, UIHandler
from tornado.web import StaticFileHandler

import os

# Jupyter Extension points
def _jupyter_server_extension_paths():
return [{
'module': 'nbzip',
}]

def _jupyter_nbextension_paths():
return [{
"section":"tree",
"dest":"nbzip",
"src":"static",
"require":"nbzip/tree"
}]

def load_jupyter_server_extension(nbapp):
web_app = nbapp.web_app
base_url = url_path_join(web_app.settings['base_url'], 'zip-download')
handlers = [
(url_path_join(base_url, 'api'), ZipHandler),
(base_url, UIHandler),
(
url_path_join(base_url, 'static', '(.*)'),
StaticFileHandler,
{'path': os.path.join(os.path.dirname(__file__), 'static')}
)
]
web_app.add_handlers('.*', handlers)
122 changes: 122 additions & 0 deletions nbzip/handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
from tornado import gen, web, locks
from notebook.utils import url_path_join
from notebook.base.handlers import IPythonHandler
from queue import Queue, Empty

import traceback
import urllib.parse
import threading
import json
import os
import jinja2
import zipfile

TEMP_ZIP_NAME = 'notebook.zip'

class ZipHandler(IPythonHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

def _emit_progress(self, progress):
if isinstance(progress, Exception):
self.emit({
'phase': 'error',
'message': str(progress),
'output': '\n'.join([
l.strip()
for l in traceback.format_exception(
type(progress), progress, progress.__traceback__
)
])
})
else:
self.emit({'output': progress, 'phase': 'zipping'})

@gen.coroutine
def emit(self, data):
if type(data) is not str:
serialized_data = json.dumps(data)
if 'output' in data:
self.log.info(data['output'].rstrip())
else:
serialized_data = data
self.log.info(data)
self.write('data: {}\n\n'.format(serialized_data))
yield self.flush()

@gen.coroutine
def get(self):
try:
base_url = self.get_argument('baseUrl')

# We gonna send out event streams!
self.set_header('content-type', 'text/event-stream')
self.set_header('cache-control', 'no-cache')

self.emit({'output': 'Removing old {}...\n'.format(TEMP_ZIP_NAME), 'phase': 'zipping'})

if os.path.isfile(TEMP_ZIP_NAME):
os.remove(TEMP_ZIP_NAME)
self.emit({'output': 'Removed old {}!\n'.format(TEMP_ZIP_NAME), 'phase': 'zipping'})
else:
self.emit({'output': '{} does not exist!\n'.format(TEMP_ZIP_NAME), 'phase': 'zipping'})

self.emit({'output': 'Zipping files:\n', 'phase': 'zipping'})

q = Queue()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we have no intermediate page, what is this queue being used for? Am a bit confused :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i left it for logging, but I guess we don't really need it. I'll remove it.

def zip():
try:
file_name = None
zipf = zipfile.ZipFile(TEMP_ZIP_NAME, 'w', zipfile.ZIP_DEFLATED)
for root, dirs, files in os.walk('./'):
for file in files:
file_name = os.path.join(root, file)
q.put_nowait("{}\n".format(file_name))
zipf.write(file_name)
zipf.close()

# Sentinel when we're done
q.put_nowait(None)
except Exception as e:
q.put_nowait(e)
raise e

self.gp_thread = threading.Thread(target=zip)
self.gp_thread.start()

while True:
try:
progress = q.get_nowait()
except Empty:
yield gen.sleep(0.5)
continue
if progress is None:
break
self._emit_progress(progress)

self.emit({'phase': 'finished', 'redirect': url_path_join(base_url, 'tree')})
except Exception as e:
self._emit_progress(e)


class UIHandler(IPythonHandler):
def initialize(self):
super().initialize()
# FIXME: Is this really the best way to use jinja2 here?
# I can't seem to get the jinja2 env in the base handler to
# actually load templates from arbitrary paths ugh.
jinja2_env = self.settings['jinja2_env']
jinja2_env.loader = jinja2.ChoiceLoader([
jinja2_env.loader,
jinja2.FileSystemLoader(
os.path.join(os.path.dirname(__file__), 'templates')
)
])

@gen.coroutine
def get(self):
self.write(
self.render_template(
'status.html'
))
self.flush()
Loading