From f99deb62fff560ff2b72b974dd5b5c8bbcd0dfb0 Mon Sep 17 00:00:00 2001 From: pdstack <125979306+pdstack@users.noreply.github.com> Date: Wed, 22 Feb 2023 00:01:01 +1100 Subject: [PATCH 1/5] Compatibility with newer versions of jupyter_client, added temp_dir option --- README.md | 1 + appmode/notebook.html | 377 ++++++++++++++++++++++++++++++++++++ appmode/page_appmode.html | 240 +++++++++++++++++++++++ appmode/server_extension.py | 56 +++--- setup.py | 3 +- 5 files changed, 651 insertions(+), 26 deletions(-) create mode 100644 appmode/notebook.html create mode 100644 appmode/page_appmode.html diff --git a/README.md b/README.md index 2e926ad..2e010c5 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ 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 ## Client Side Customization diff --git a/appmode/notebook.html b/appmode/notebook.html new file mode 100644 index 0000000..8551425 --- /dev/null +++ b/appmode/notebook.html @@ -0,0 +1,377 @@ +{% extends "page_appmode.html" %} + +{% block favicon %}{% endblock %} + +{% block stylesheet %} + +{% if mathjax_url %} + +{% endif %} + + + + + +{{super()}} + + + + +{% endblock %} + +{% block bodyclasses %}notebook_app {{super()}}{% endblock %} + +{% block params %} + +{{super()}} +data-base-url="{{base_url | urlencode}}" +data-ws-url="{{ws_url | urlencode}}" +data-notebook-name="{{notebook_name | urlencode}}" +data-notebook-path="{{notebook_path | urlencode}}" +{% endblock %} + + +{% block headercontainer %} + + + + + + + + +{{super()}} + + + {% block kernel_logo_widget %} + + {% endblock %} + + +{% endblock headercontainer %} + +{% block header %} + + +
+{% endblock header %} + +{% block site %} + +
+
+
+ +
+
+ + +{% endblock %} + +{% block after_site %} + +
+
+
+
+
+
+ +{% endblock %} + +{% block script %} +{{super()}} + + + + + + +{% endblock %} diff --git a/appmode/page_appmode.html b/appmode/page_appmode.html new file mode 100644 index 0000000..218fcbd --- /dev/null +++ b/appmode/page_appmode.html @@ -0,0 +1,240 @@ + + + + + + + {% block title %}Jupyter Notebook{% endblock %} + {% block favicon %}{% endblock %} + + + + + + {% block stylesheet %} + + {% endblock %} + + + + + + + + + {% block meta %} + {% endblock %} + + + + + + + + + +
+{% block site %} +{% endblock %} +
+ +{% block after_site %} +{% endblock %} + +{% block script %} +{% endblock %} + + + + + diff --git a/appmode/server_extension.py b/appmode/server_extension.py index a68129a..7985c74 100644 --- a/appmode/server_extension.py +++ b/appmode/server_extension.py @@ -5,7 +5,8 @@ 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 collections.abc +from tornado import web from traitlets.config import LoggingConfigurable from traitlets import Bool, Unicode @@ -17,6 +18,7 @@ class Appmode(LoggingConfigurable): 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) #=============================================================================== class AppmodeHandler(IPythonHandler): @@ -32,12 +34,15 @@ 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 + #=========================================================================== @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) @@ -47,10 +52,11 @@ 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) + if isinstance(model, collections.abc.Awaitable): + model = await model 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 @@ -65,7 +71,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, @@ -87,46 +93,48 @@ 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 = sm.get_session(path=path) + if isinstance(s, collections.abc.Awaitable): + s = await s + sd = sm.delete_session(session_id=s['id']) + if isinstance(sd, collections.abc.Awaitable): + await sd # delete tmp copy cm = self.contents_manager - cm.delete(path) - self.finish() + pd = cm.delete(path) + if isinstance(pd, collections.abc.Awaitable): + await pd + 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) 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) - - return(tmp_path) + pc = cm.copy(path, tmp_path) + if isinstance(pc, collections.abc.Awaitable): + await pc + return tmp_path #=============================================================================== def load_jupyter_server_extension(nbapp): diff --git a/setup.py b/setup.py index 45abc63..4624145 100644 --- a/setup.py +++ b/setup.py @@ -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", From 71d289e56c0b4f30ecf24b646a07154967061257 Mon Sep 17 00:00:00 2001 From: pdstack <125979306+pdstack@users.noreply.github.com> Date: Sat, 4 Mar 2023 23:46:29 +1100 Subject: [PATCH 2/5] Removed html templates, added notebook templates to jinja2_env loader path on extension load, added hidden_temp_files configuration option --- README.md | 13 ++ appmode/notebook.html | 377 ------------------------------------ appmode/page_appmode.html | 240 ----------------------- appmode/server_extension.py | 17 +- 4 files changed, 29 insertions(+), 618 deletions(-) delete mode 100644 appmode/notebook.html delete mode 100644 appmode/page_appmode.html diff --git a/README.md b/README.md index 2e010c5..f64a675 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,19 @@ Appmode adds the following [configuration options](https://jupyter-notebook.read - `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 diff --git a/appmode/notebook.html b/appmode/notebook.html deleted file mode 100644 index 8551425..0000000 --- a/appmode/notebook.html +++ /dev/null @@ -1,377 +0,0 @@ -{% extends "page_appmode.html" %} - -{% block favicon %}{% endblock %} - -{% block stylesheet %} - -{% if mathjax_url %} - -{% endif %} - - - - - -{{super()}} - - - - -{% endblock %} - -{% block bodyclasses %}notebook_app {{super()}}{% endblock %} - -{% block params %} - -{{super()}} -data-base-url="{{base_url | urlencode}}" -data-ws-url="{{ws_url | urlencode}}" -data-notebook-name="{{notebook_name | urlencode}}" -data-notebook-path="{{notebook_path | urlencode}}" -{% endblock %} - - -{% block headercontainer %} - - - - - - - - -{{super()}} - - - {% block kernel_logo_widget %} - - {% endblock %} - - -{% endblock headercontainer %} - -{% block header %} - - -
-{% endblock header %} - -{% block site %} - -
-
-
- -
-
- - -{% endblock %} - -{% block after_site %} - -
-
-
-
-
-
- -{% endblock %} - -{% block script %} -{{super()}} - - - - - - -{% endblock %} diff --git a/appmode/page_appmode.html b/appmode/page_appmode.html deleted file mode 100644 index 218fcbd..0000000 --- a/appmode/page_appmode.html +++ /dev/null @@ -1,240 +0,0 @@ - - - - - - - {% block title %}Jupyter Notebook{% endblock %} - {% block favicon %}{% endblock %} - - - - - - {% block stylesheet %} - - {% endblock %} - - - - - - - - - {% block meta %} - {% endblock %} - - - - - - - - - -
-{% block site %} -{% endblock %} -
- -{% block after_site %} -{% endblock %} - -{% block script %} -{% endblock %} - - - - - diff --git a/appmode/server_extension.py b/appmode/server_extension.py index 7985c74..b590356 100644 --- a/appmode/server_extension.py +++ b/appmode/server_extension.py @@ -5,6 +5,7 @@ from notebook.utils import url_path_join from notebook.base.handlers import IPythonHandler, FilesRedirectHandler, path_regex import notebook.notebook.handlers as orig_handler +import notebook import collections.abc from tornado import web from traitlets.config import LoggingConfigurable @@ -19,6 +20,7 @@ class Appmode(LoggingConfigurable): 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): @@ -38,6 +40,10 @@ def show_other_buttons(self): 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 async def get(self, path): @@ -124,6 +130,8 @@ async def mk_tmp_copy(self, path): 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) if not cm.exists(tmp_path): @@ -139,8 +147,14 @@ async def mk_tmp_copy(self, path): #=============================================================================== def load_jupyter_server_extension(nbapp): tmpl_dir = os.path.dirname(__file__) + notebook_tmpl_dir = os.path.join(notebook.__path__[0], '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) @@ -151,6 +165,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 = '.*$' From 75c25cd9c7e2ed7bd7ed2a70646b6d2a41740759 Mon Sep 17 00:00:00 2001 From: pdstack <125979306+pdstack@users.noreply.github.com> Date: Sun, 5 Mar 2023 17:52:04 +1100 Subject: [PATCH 3/5] Swap over path reference for notebook templates to use __file__ instead of __path__ for consistency --- appmode/server_extension.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appmode/server_extension.py b/appmode/server_extension.py index b590356..24a2dc9 100644 --- a/appmode/server_extension.py +++ b/appmode/server_extension.py @@ -147,7 +147,7 @@ async def mk_tmp_copy(self, path): #=============================================================================== def load_jupyter_server_extension(nbapp): tmpl_dir = os.path.dirname(__file__) - notebook_tmpl_dir = os.path.join(notebook.__path__[0], 'templates') + 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 From 46090e38b72fb97822b78e25e909f474d346e0fb Mon Sep 17 00:00:00 2001 From: pdstack <125979306+pdstack@users.noreply.github.com> Date: Fri, 10 Mar 2023 00:40:24 +1100 Subject: [PATCH 4/5] Added ensure_async wrapper for session manager and content manager calls --- appmode/server_extension.py | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/appmode/server_extension.py b/appmode/server_extension.py index 24a2dc9..5fce8d5 100644 --- a/appmode/server_extension.py +++ b/appmode/server_extension.py @@ -1,17 +1,29 @@ # -*- 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 import notebook -import collections.abc from tornado import web from traitlets.config import LoggingConfigurable from traitlets import Bool, Unicode +async def ensure_async(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. @@ -60,9 +72,7 @@ async def get(self, path): cm = self.contents_manager # will raise 404 on not found try: - model = cm.get(path, content=False) - if isinstance(model, collections.abc.Awaitable): - model = await model + model = await ensure_async(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 @@ -107,18 +117,12 @@ async def post(self, path): # delete session, including the kernel sm = self.session_manager - s = sm.get_session(path=path) - if isinstance(s, collections.abc.Awaitable): - s = await s - sd = sm.delete_session(session_id=s['id']) - if isinstance(sd, collections.abc.Awaitable): - await sd + s = await ensure_async(sm.get_session(path=path)) + await ensure_async(sm.delete_session(session_id=s['id'])) # delete tmp copy cm = self.contents_manager - pd = cm.delete(path) - if isinstance(pd, collections.abc.Awaitable): - await pd + await ensure_async(cm.delete(path)) await self.finish() #=========================================================================== @@ -139,11 +143,10 @@ async def mk_tmp_copy(self, path): # create tmp copy - allows opening same notebook multiple times self.log.info("Appmode creating tmp copy: "+tmp_path) - pc = cm.copy(path, tmp_path) - if isinstance(pc, collections.abc.Awaitable): - await pc + await ensure_async(cm.copy(path, tmp_path)) return tmp_path + #=============================================================================== def load_jupyter_server_extension(nbapp): tmpl_dir = os.path.dirname(__file__) From 16740bec0a02b528d4f0c208ce360abdc59cf7d8 Mon Sep 17 00:00:00 2001 From: pdstack <125979306+pdstack@users.noreply.github.com> Date: Fri, 10 Mar 2023 02:21:41 +1100 Subject: [PATCH 5/5] Rename ensure_async to await_if_awaitable --- appmode/server_extension.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/appmode/server_extension.py b/appmode/server_extension.py index 5fce8d5..717e149 100644 --- a/appmode/server_extension.py +++ b/appmode/server_extension.py @@ -12,7 +12,7 @@ from traitlets import Bool, Unicode -async def ensure_async(obj): +async def await_if_awaitable(obj): """Convert a non-awaitable object to a coroutine if needed, and await if it was a coroutine. @@ -72,7 +72,7 @@ async def get(self, path): cm = self.contents_manager # will raise 404 on not found try: - model = await ensure_async(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 @@ -117,12 +117,12 @@ async def post(self, path): # delete session, including the kernel sm = self.session_manager - s = await ensure_async(sm.get_session(path=path)) - await ensure_async(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 - await ensure_async(cm.delete(path)) + await await_if_awaitable(cm.delete(path)) await self.finish() #=========================================================================== @@ -143,7 +143,7 @@ async def mk_tmp_copy(self, path): # create tmp copy - allows opening same notebook multiple times self.log.info("Appmode creating tmp copy: "+tmp_path) - await ensure_async(cm.copy(path, tmp_path)) + await await_if_awaitable(cm.copy(path, tmp_path)) return tmp_path