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

Compatibility with ServerApp backend #65

Merged
merged 5 commits into from
Mar 9, 2023
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
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,20 @@ Appmode adds the following [configuration options](https://jupyter-notebook.read
- `Appmode.trusted_path` Run only notebooks below this path in Appmode. Default: No restrictions.
- `Appmode.show_edit_button` Show _Edit App_ button during Appmode. Default: True.
- `Appmode.show_other_buttons` Show other buttons, e.g. Logout, during Appmode. Default: True.
- `Appmode.temp_dir` Create temp notebooks under this directory. Default: Same directory as current notebook
- `Appmode.hidden_temp_files` Create temp notebooks as hidden files. Default: True

Writing to hidden files is disabled in newer versions of notebook. You will need to enable one of the following
configuration options to run appmode:
```
ContentsManager.allow_hidden=True
or
Appmode.hidden_temp_files=False
```
Example:
```
jupyter notebook --Appmode.hidden_temp_files=False
```

## Client Side Customization

Expand Down
76 changes: 51 additions & 25 deletions appmode/server_extension.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,38 @@
# -*- coding: utf-8 -*-

import os
import inspect
import itertools
from notebook.utils import url_path_join
from notebook.base.handlers import IPythonHandler, FilesRedirectHandler, path_regex
import notebook.notebook.handlers as orig_handler
from tornado import web, gen
import notebook
from tornado import web
from traitlets.config import LoggingConfigurable
from traitlets import Bool, Unicode


async def await_if_awaitable(obj):
"""Convert a non-awaitable object to a coroutine if needed,
and await if it was a coroutine.

Designed to be called on the result of calling a function,
when that function could be asynchronous or not.
"""
if inspect.isawaitable(obj):
obj = await obj
return obj


class Appmode(LoggingConfigurable):
"""Object containing server-side configuration settings for Appmode.
Defined separately from the AppmodeHandler to avoid multiple inheritance.
"""
trusted_path = Unicode('', help="Run only notebooks below this path in Appmode.", config=True)
show_edit_button = Bool(True, help="Show Edit App button during Appmode.", config=True)
show_other_buttons = Bool(True, help="Show other buttons, e.g. Logout, during Appmode.", config=True)
temp_dir = Unicode('', help="Create temporary Appmode notebooks in this directory.", config=True)
hidden_temp_files = Bool(True, help="Temporary Appmode notebooks are hidden files.", config=True)

#===============================================================================
class AppmodeHandler(IPythonHandler):
Expand All @@ -32,12 +48,19 @@ def show_edit_button(self):
def show_other_buttons(self):
return self.settings['appmode'].show_other_buttons

@property
def temp_dir(self):
return self.settings['appmode'].temp_dir

@property
def hidden_temp_files(self):
return self.settings['appmode'].hidden_temp_files

#===========================================================================
@web.authenticated
def get(self, path):
async def get(self, path):
"""get renders the notebook template if a name is given, or
redirects to the '/files/' handler if the name is not given."""

path = path.strip('/')
self.log.info('Appmode get: %s', path)

Expand All @@ -47,10 +70,9 @@ def get(self, path):
raise web.HTTPError(401, 'Notebook is not within trusted Appmode path.')

cm = self.contents_manager

# will raise 404 on not found
try:
model = cm.get(path, content=False)
model = await await_if_awaitable(cm.get(path, content=False))
except web.HTTPError as e:
if e.status_code == 404 and 'files' in path.split('/'):
# 404, but '/files/' in URL, let FilesRedirect take care of it
Expand All @@ -65,7 +87,7 @@ def get(self, path):
self.add_header("Cache-Control", "cache-control: private, max-age=0, no-cache, no-store")

# gather template parameters
tmp_path = self.mk_tmp_copy(path)
tmp_path = await self.mk_tmp_copy(path)
tmp_name = tmp_path.rsplit('/', 1)[-1]
render_kwargs = {
'notebook_path': tmp_path,
Expand All @@ -87,52 +109,55 @@ def get(self, path):

#===========================================================================
@web.authenticated
@gen.coroutine
def post(self, path):
async def post(self, path):
assert self.get_body_arguments("appmode_action")[0] == "delete"
path = path.strip('/')
self.log.info('Appmode deleting: %s', path)

# delete session, including the kernel
sm = self.session_manager
if gen.is_coroutine_function(sm.get_session):
s = yield sm.get_session(path=path)
else:
s = sm.get_session(path=path)
if gen.is_coroutine_function(sm.delete_session):
yield sm.delete_session(session_id=s['id'])
else:
sm.delete_session(session_id=s['id'])

s = await await_if_awaitable(sm.get_session(path=path))
await await_if_awaitable(sm.delete_session(session_id=s['id']))

# delete tmp copy
cm = self.contents_manager
cm.delete(path)
self.finish()
await await_if_awaitable(cm.delete(path))
await self.finish()

#===========================================================================
def mk_tmp_copy(self, path):
async def mk_tmp_copy(self, path):
cm = self.contents_manager

# find tmp_path
dirname = os.path.dirname(path)
dirname = self.temp_dir or os.path.dirname(path)
if dirname and not os.path.exists(dirname):
os.makedirs(dirname)
fullbasename = os.path.basename(path)
basename, ext = os.path.splitext(fullbasename)
if self.hidden_temp_files:
basename = '.' + basename
for i in itertools.count():
tmp_path = "%s/.%s-%i%s"%(dirname, basename, i, ext)
tmp_path = "%s/%s-%i%s"%(dirname, basename, i, ext)
if not cm.exists(tmp_path):
break

# create tmp copy - allows opening same notebook multiple times
self.log.info("Appmode creating tmp copy: "+tmp_path)
cm.copy(path, tmp_path)
await await_if_awaitable(cm.copy(path, tmp_path))
return tmp_path

return(tmp_path)

#===============================================================================
def load_jupyter_server_extension(nbapp):
tmpl_dir = os.path.dirname(__file__)
notebook_tmpl_dir = os.path.join(os.path.dirname(notebook.__file__), 'templates')
# does not work, because init_webapp() happens before init_server_extensions()
#nbapp.extra_template_paths.append(tmpl_dir) # dows
# nbapp.extra_template_paths.append(tmpl_dir) # dows

# For jupyter server, the notebook templates are not available in the default search paths. This can be addressed
# by using --ServerApp.extra_template_paths='***site-packages***\notebook\templates', but this is messy.
# To emulate this instead insert the notebook template directory at the start of the searchpath
# These will be used last, so the notebook.html resolves, but the page.html is still from jupyter server templates

# For configuration values that can be set server side
appmode = Appmode(parent=nbapp)
Expand All @@ -143,6 +168,7 @@ def load_jupyter_server_extension(nbapp):
for loader in getattr(rootloader, 'loaders', [rootloader]):
if hasattr(loader, 'searchpath') and tmpl_dir not in loader.searchpath:
loader.searchpath.append(tmpl_dir)
loader.searchpath.insert(0, notebook_tmpl_dir)

web_app = nbapp.web_app
host_pattern = '.*$'
Expand Down
3 changes: 1 addition & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,13 @@ def find_version():
packages=["appmode"],
include_package_data = True,
install_requires=['notebook>=5'],
python_requires='>=3.5',
data_files=[('share/jupyter/nbextensions/appmode', [
'appmode/static/main.js',
'appmode/static/gears.svg'
])],

classifiers=[
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.5",
"License :: OSI Approved :: MIT License",
Expand Down