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

Support for Tmod and Lmod #62

Merged
merged 4 commits into from
May 16, 2024
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
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ jupyterlmod.egg-info/
.ipynb_checkpoints/
*.ipynb
.vscode/
*.lock
*env*/
package-lock.json
jupyterlmod/labextension

*.bundle.*
lib/
node_modules/
*.egg-info/
.ipynb_checkpoints
19 changes: 14 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
# Jupyter Lmod
# Jupyter Lmod/Tmod

Jupyter interactive notebook server extension that allows user
to interact with environment modules before launching kernels.
The extension use Lmod's Python interface to accomplish module
to interact with environment modules (Lmod or Tmod) before launching kernels.
The extension use environment module's Python interface to accomplish module
related task like loading, unloading, saving collection, etc.

## requirements

- [jupyter notebook](https://github.com/jupyter/notebook) >= 6.0
- [Lmod](https://github.com/TACC/Lmod) >= 6.0
- [Lmod](https://github.com/TACC/Lmod) >= 6.0 or [Tmod](https://modules.readthedocs.io/en/latest/) >= 5.0
- optional: [jupyterlab](https://github.com/jupyter/notebook) >= 3.0
- optional: [jupyter-server-proxy](https://github.com/jupyterhub/jupyter-server-proxy) >= 3.2.0
- optional: [jupyterlab-server-proxy](https://github.com/jupyterhub/jupyter-server-proxy) >= 3.2.0

**Note** that the extension supports Tmod < 5.0 too. However, if `MODULES_RUN_QUARANTINE` is not empty on the platform, module's Python API does
not have correct behaviour. On default installations, `MODULES_RUN_QUARANTINE=LD_LIBRARY_PATH` is used. If `LD_LIBRARY_PATH` is not
empty before loading a module, the existant paths in `LD_LIBRARY_PATH` is lost
after loading the module. More discussion can be found [here](https://sourceforge.net/p/modules/mailman/message/36113970/).

If jupyter-server-proxy and jupyterlab-server-proxy are detected, jupyter-lmod will add the
proxy server launchers to JupyterLab UI when modules with matching names are loaded.

Expand Down Expand Up @@ -51,6 +56,11 @@ the Jupyter notebook configuration file, like this:
```
c.Lmod.launcher_pins = ['Desktop', 'RStudio']
```
or
```
c.Tmod.launcher_pins = ['Desktop', 'RStudio']
```
based on your module system.

## demo

Expand All @@ -75,7 +85,6 @@ c.Lmod.launcher_pins = ['Desktop', 'RStudio']
```
- labextension
```shell
cd jupyterlab
npm install
npm run build
# To install extension in jupyterlab in develop mode:
Expand Down
5 changes: 5 additions & 0 deletions install.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"packageManager": "python",
"packageName": "jupyterlmod",
"uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package jupyterlmod"
}
41 changes: 35 additions & 6 deletions jupyterlmod/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,24 @@
import os
import sys
import json
from jupyter_server.utils import url_path_join as ujoin
from pathlib import Path

from .config import Lmod as LmodConfig
from .handler import default_handlers, PinsHandler
from .config import Module as ModuleConfig
from .handler import default_handlers, PinsHandler, ModuleSystemLogoHandler

from module import MODULE_SYSTEM

HERE = Path(__file__).parent.resolve()

with (HERE / "labextension" / "package.json").open() as fid:
data = json.load(fid)

def _jupyter_labextension_paths():
return [{
"src": "labextension",
"dest": data["name"]
}]


def _jupyter_server_extension_points():
Expand All @@ -24,17 +41,29 @@ def _load_jupyter_server_extension(nbapp):
Args:
nbapp : handle to the Notebook webserver instance.
"""
nbapp.log.info("Loading lmod extension")
lmod_config = LmodConfig(parent=nbapp)
launcher_pins = lmod_config.launcher_pins
nbapp.log.info("Loading lmod/tmod extension")
module_config = ModuleConfig(parent=nbapp)
launcher_pins = module_config.launcher_pins

# As of now (31/march/2023) the extension is not working on jupyter
# notebook using jupyter_server>2. See https://github.com/jupyter-server/jupyter_server/pull/1221
# The extension has been tested on jupyter_server<2 and it is working
# as expected
web_app = nbapp.web_app
base_url = web_app.settings["base_url"]
for path, class_ in default_handlers:
web_app.add_handlers(".*$", [(ujoin(base_url, path), class_)])

web_app.add_handlers(".*$", [
(ujoin(base_url, 'lmod/launcher-pins'), PinsHandler, {'launcher_pins': launcher_pins}),
(ujoin(base_url, 'module/launcher-pins'), PinsHandler, {'launcher_pins': launcher_pins}),
])

logo_path = os.path.join(
sys.prefix, 'share', 'jupyter', 'nbextensions', 'jupyterlmod', 'logos',
f'{MODULE_SYSTEM}.png'
)
web_app.add_handlers(".*$", [
(ujoin(base_url, 'module/logo'), ModuleSystemLogoHandler, {'path': logo_path}),
])

# For backward compatibility
Expand Down
26 changes: 24 additions & 2 deletions jupyterlmod/config.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
from traitlets.config import Configurable
from traitlets import List
from traitlets import List, Unicode


class Lmod(Configurable):
launcher_pins = List().tag(config=True)
"""Configurable for Lmod"""
launcher_pins = List(
trait=Unicode(),
help="""
Launcher items to be displayed regardless of the loaded modules
"""
).tag(config=True)


class Tmod(Configurable):
"""Configurable for Tmod"""
launcher_pins = List(
trait=Unicode(),
help="""
Launcher items to be displayed regardless of the loaded modules
"""
).tag(config=True)


class Module(Lmod, Tmod):
"""Derived class which will be used during configuration"""
pass
88 changes: 64 additions & 24 deletions jupyterlmod/handler.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
"""List of handlers to register"""

import json
import os

import lmod
import module

from functools import wraps
from glob import glob
Expand All @@ -11,6 +13,7 @@

from jupyter_server.base.handlers import JupyterHandler


def jupyter_path_decorator(func):
@wraps(func)
async def wrapper(self, *args, **kwargs):
Expand All @@ -20,18 +23,41 @@ async def wrapper(self, *args, **kwargs):
self.kernel_spec_manager.kernel_dirs = jupyter_path("kernels")
return wrapper

LMOD = lmod.API()

class Lmod(JupyterHandler):
MODULE = module.ModuleAPI()


class ModuleSystemLogoHandler(JupyterHandler, web.StaticFileHandler):
"""Handler to get current module system logo"""
@web.authenticated
def get(self):
return super().get('')

@classmethod
def get_absolute_path(cls, root, path):
"""We only serve one file, ignore relative path"""
return os.path.abspath(root)


class ModuleSystem(JupyterHandler):
"""Handler to get current module system name"""
@web.authenticated
async def get(self):
result = await MODULE.system()
self.finish(json.dumps(result))


class Module(JupyterHandler):
"""Handler to load, unload and list modules"""
@web.authenticated
async def get(self):
lang = self.get_query_argument(name="lang", default=None)
all = self.get_query_argument(name="all", default=None)
if lang is None:
all = all is not None and all == "true"
result = await LMOD.list(include_hidden=all)
result = await MODULE.list(include_hidden=all)
elif lang == "python":
result = await LMOD.freeze()
result = await MODULE.freeze()
else:
raise web.HTTPError(400, u'Unknown value for lang argument')
self.finish(json.dumps(result))
Expand All @@ -44,7 +70,7 @@ async def post(self):
raise web.HTTPError(400, u'modules missing from body')
elif not isinstance(modules, list):
raise web.HTTPError(400, u'modules argument needs to be a list')
result = await LMOD.load(*modules)
result = await MODULE.load(*modules)
self.finish(json.dumps(result))

@web.authenticated
Expand All @@ -55,33 +81,39 @@ async def delete(self):
raise web.HTTPError(400, u'modules missing from body')
elif not isinstance(modules, list):
raise web.HTTPError(400, u'modules argument needs to be a list')
result = await LMOD.unload(*modules)
result = await MODULE.unload(*modules)
self.finish(json.dumps(result))

class LmodModules(JupyterHandler):

class AvailModules(JupyterHandler):
"""Handler to get available modules"""
@web.authenticated
async def get(self):
result = await LMOD.avail()
result = await MODULE.avail()
self.finish(json.dumps(result))

class LmodModule(JupyterHandler):

class ShowModule(JupyterHandler):
"""Handler to show module"""
@web.authenticated
async def get(self, module=None):
result = await LMOD.show(module)
result = await MODULE.show(module)
self.finish(json.dumps(result))

class LmodCollections(JupyterHandler):

class ModuleCollections(JupyterHandler):
"""Handler to get, create and update module collections"""
@web.authenticated
async def get(self):
result = await LMOD.savelist()
result = await MODULE.savelist()
self.finish(json.dumps(result))

@web.authenticated
async def post(self):
name = self.get_json_body().get('name')
if not name:
raise web.HTTPError(400, u'name argument missing')
result = await LMOD.save(name)
result = await MODULE.save(name)
self.finish(json.dumps(result))

@web.authenticated
Expand All @@ -90,10 +122,12 @@ async def patch(self):
name = self.get_json_body().get('name')
if not name:
raise web.HTTPError(400, u'name argument missing')
result = await LMOD.restore(name)
result = await MODULE.restore(name)
self.finish(json.dumps(result))

class LmodPaths(JupyterHandler):

class ModulePaths(JupyterHandler):
"""Handler to get, set and delete module paths"""
@web.authenticated
async def get(self):
result = os.environ.get("MODULEPATH")
Expand All @@ -110,7 +144,7 @@ async def post(self):
append = self.get_json_body().get('append', False)
if not paths:
raise web.HTTPError(400, u'paths argument missing')
result = await LMOD.use(*paths, append=append)
result = await MODULE.use(*paths, append=append)
self.finish(json.dumps(result))

@web.authenticated
Expand All @@ -119,29 +153,35 @@ async def delete(self):
paths = self.get_json_body().get('paths')
if not paths:
raise web.HTTPError(400, u'paths argument missing')
result = await LMOD.unuse(*paths)
result = await MODULE.unuse(*paths)
self.finish(json.dumps(result))


class FoldersHandler(JupyterHandler):
"""Handler to get folders"""
@web.authenticated
async def get(self, path):
result = glob(path + "*/")
result = [path[:-1] for path in result]
self.finish(json.dumps(result))


class PinsHandler(JupyterHandler):
"""Handler to get list of pinned apps"""
def initialize(self, launcher_pins):
self.launcher_pins = launcher_pins

@web.authenticated
async def get(self):
self.write({'launcher_pins': self.launcher_pins})


default_handlers = [
(r"/lmod", Lmod),
(r"/lmod/modules", LmodModules),
(r"/lmod/modules/(.*)", LmodModule),
(r"/lmod/collections", LmodCollections),
(r"/lmod/paths", LmodPaths),
(r"/lmod/folders/(.*)", FoldersHandler)
(r"/module/system", ModuleSystem),
(r"/module", Module),
(r"/module/modules", AvailModules),
(r"/module/modules/(.*)", ShowModule),
(r"/module/collections", ModuleCollections),
(r"/module/paths", ModulePaths),
(r"/module/folders/(.*)", FoldersHandler)
]
Binary file added jupyterlmod/static/logos/lmod.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added jupyterlmod/static/logos/tmod.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading