Skip to content

Commit

Permalink
JupyterLab Polus Render 0.1.0 (#39)
Browse files Browse the repository at this point in the history
  • Loading branch information
jcaxle authored Nov 30, 2023
1 parent c43c850 commit b70594b
Show file tree
Hide file tree
Showing 42 changed files with 3,289 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1 @@

.DS_Store
21 changes: 21 additions & 0 deletions jupyterlab_polus_render/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2023 LabShare

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Empty file.
117 changes: 117 additions & 0 deletions jupyterlab_polus_render/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# JupyterLab Polus Render
JupyterLab Polus Render makes Polus Render available as a JupyterLab extension.

Polus Render allows visualizing tiled raster datasets in Zarr and TIFF formats, as well as vector overlays in MicroJSON format. It uses lookup tables to map intensity values in these datasets to colors.

The are three ways to load the data:
1. Specifying a URL to a server serving the data.
2. Specifying a local path to a file from JupyterLab.
3. Dragging-and-dropping the dataset does not use a server.
</br>

Please note that usage differs significantly from https://pypi.org/project/polus-render/0.0.4.0.1.5/

<img src="images/home.png"/>

# Requirements
* Python 3.9+

# Installation
```
pip install "git+https://github.com/PolusAI/jupyterlab-extensions.git#egg=jupyterlab_polus_render&subdirectory=jupyterlab_polus_render"
```
You will need to restart Jupyter Server for `render-server-ext` endpoints to take effect.

# Project File Structure
```
jupyterlab_polus_render
| LICENSE
| MANIFEST.in // Packaging entries
| pyproject.toml // Pypi config
| README
└───render-server-ext // Server extension used by jupyterlab_polus_render
└───polus
| polus_render.py // Main file, contains render function used by user
```

# Build Instructions
- cd to `jupyterlab_polus_render` root directory.
- `py -m build`
- `py -m twine upload dist/*`
- Enter `__token__` as user and reference API keys for password

## NOTE:
- For each upload, version number must be changed in `pyproject.toml`
- Add additional files to `MANIFEST.in` to bundle them with Pypi package

# Render: Static build functionality
JupyterLab Polus Render is bundled with a build of Polus Render which supporting the following functionality
| Version | Zarr from URL/Path | TIF from URL/Path | Micro-JSON Support | Zarr/TIF Drag & Drop | Micro-JSON Drag & Drop |
|----------------|---------------|---------------|----------------|-----------|-----|
| Static | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark:

# Drag & Drop Demo
<img src="images/drag-drop.gif"/>

# Sample usage
``` Python
from polus.polus_render import render

# pathlib and urllib are built-ins
from urllib.parse import urlparse
from pathlib import Path

# Make sure to keep track of your JupyterLab url and file root if your root is not at "/home/joyvan".
JL_URL = urlparse("https://<JUPYTERHUB_URL>/user/<USERNAME>/user-namespaces/lab?")

# Embeds an IFrame of a static build of Polus Render into Jupyter Lab, this is sufficient if your file root is "/home/joyvan/"
render(nbhub_url=JL_URL)

# Same as above; however, if your file root is "/Users/jeff.chen/", your invocation will require nb_root argument
render(nbhub_url=JL_URL, \
nb_root=Path("/Users/jeff.chen/"))

# Embeds an IFrame of a static build of Polus Render with an image file hosted at "https://viv-demo.storage.googleapis.com/LuCa-7color_Scan1/"
render(nbhub_url=JL_URL, \
image_location=urlparse("https://viv-demo.storage.googleapis.com/LuCa-7color_Scan1/"))

# Embeds an IFrame of a static build of Polus Render with an image hosted at "/home/joyvan/zarr files/pyramid.zarr"
render(nbhub_url=JL_URL, \
image_location=Path(r"zarr files/pyramid.zarr"))

# Embeds an IFrame of a static build of Polus Render with an image and overlay file
render(nbhub_url=JL_URL, \
image_location=Path("zarr files/pyramid.zarr"), \
microjson_overlay_location=Path("overlay files/x00_y01_c1_segmentations.json"))

# Embeds an IFrame of a static build of Polus Render with an image and overlay file served online
render(nbhub_url=JL_URL, \
image_location=urlparse("https://files.scb-ncats.io/pyramids/segmentations/x00_y01_c1.ome.tif"), \
microjson_overlay_location=urlparse("https://files.scb-ncats.io/pyramids/segmentations/x00_y03_c1_segmentations.json"))
```

# Functions
``` Python
def render(nbhub_url:ParseResult, nb_root:PurePath = Path("/home/jovyan/"), image_location:Union[ParseResult, PurePath] = "", microjson_overlay_location:Union[ParseResult, PurePath] = "", width:int=960, height:int=500)->str:
"""
Embeds a static build of render into a JupyterLabs notebook with the help of `render-server-ext`
Param:
nbhub_url (ParseResult): URL used used for jupyterhub. Contains '/lab/' in its uri
nb_root (ParseResult): Root path used to search files in. Default is '/home/jovyan/' which works for notebooks hub. Can be set to empty path
if absolute paths will be used for images and json files.
image_location(ParseResult|Purepath): Acquired from urllib.parse.ParseResult or Path, renders url in render.
If not specified, renders default render url.
microjson_overlay_location(ParseResult|Purepath): Acquired from urllib.parse.ParseResult or Path, renders url in render.
If not specified, renders default render url
width (int): width of render to be displayed, default is 960
height (int): height of render to be displayed, default is 500
Returns: Render URL
"""
```

# Implementation Details
- Render application is loaded in an IFrame.
- render() builds up URL scheme fragments for render url, image url, and microjson url. It then combines url fragments into a single url which is displayed through an embedded IFrame.
- Static build of Polus Render as well as files to be displayed are served by Jupyter Server extension
- Dragging-and-dropping the dataset does not use a server. It calls an API from the front end (It should the this under the hood https://developer.mozilla.org/en-US/docs/Web/API/File_API).
Binary file added jupyterlab_polus_render/images/drag-drop.gif
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 jupyterlab_polus_render/images/home.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file.
50 changes: 50 additions & 0 deletions jupyterlab_polus_render/polus/polus_render.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import os
from IPython.display import display, IFrame
from urllib.parse import ParseResult
from pathlib import PurePath, Path
from typing import Union



def render(nbhub_url:ParseResult, nb_root:PurePath = Path("/home/jovyan/"), image_location:Union[ParseResult, PurePath] = "", microjson_overlay_location:Union[ParseResult, PurePath] = "", width:int=960, height:int=500)->str:
"""
Embeds a static build of render into a JupyterLabs notebook with the help of `render-server-ext`
Param:
nbhub_url (ParseResult): URL used used for jupyterhub. Contains '/lab/' in its uri
nb_root (ParseResult): Root path used to search files in. Default is '/home/jovyan/' which works for notebooks hub. Can be set to empty path
if absolute paths will be used for images and json files.
image_location(ParseResult|Purepath): Acquired from urllib.parse.ParseResult or Path, renders url in render.
If not specified, renders default render url.
microjson_overlay_location(ParseResult|Purepath): Acquired from urllib.parse.ParseResult or Path, renders url in render.
If not specified, renders default render url
width (int): width of render to be displayed, default is 960
height (int): height of render to be displayed, default is 500
Returns: Render URL
"""
assert(nbhub_url)
base_nbhub = nbhub_url.geturl().rpartition("lab")[0]
# Extract url from file path if provided. ?imageUrl is required scheme for render
if isinstance(image_location, PurePath):
image_location = "?imageUrl=" + base_nbhub + "render/file/" + os.path.join(str(nb_root), str(image_location))

# Otherwise, extract url from user provided url if provided
elif isinstance(image_location, ParseResult):
image_location = "?imageUrl=" + image_location.geturl() # Can be manually rebuilt to check if a valid format url is sent

# Do the same but for JSON
if isinstance(microjson_overlay_location, PurePath):
microjson_overlay_location = "&overlayUrl=" + base_nbhub + "render/file/" + os.path.join(str(nb_root), str(microjson_overlay_location))

elif isinstance(microjson_overlay_location, ParseResult):
microjson_overlay_location = "&overlayUrl=" + microjson_overlay_location.geturl()

# static render
render_url = f"{base_nbhub}static/render/render-ui/index.html"

# Display render
display(IFrame(src=(f"{render_url}{image_location}{microjson_overlay_location}")
, width=width, height=height))

return f"{render_url}{image_location}{microjson_overlay_location}"

18 changes: 18 additions & 0 deletions jupyterlab_polus_render/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[tool.poetry]
name = "jupyterlab_polus_render"
version = "0.1.0"
description = "Displays Polus Render dashboard in Jupyter Notebooks."
authors = ["Jeff Chen <[email protected]>"]
packages = [{ include = "polus"}, {include = "render-server-ext"}]
License = "MIT"
readme = "README.md"
repository = "https://github.com/PolusAI/jupyterlab-extensions"

[tool.poetry.dependencies]
python = ">=3.9"
render-server-ext = { path = "./render-server-ext/", develop = false }


[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
21 changes: 21 additions & 0 deletions jupyterlab_polus_render/render-server-ext/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2023 LabShare

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
43 changes: 43 additions & 0 deletions jupyterlab_polus_render/render-server-ext/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Render Server Ext
Jupyter Server Extension serves files and provides a static build of Polus Render.

<img src="images/home.png"/>

# Requirements
- Jupyter Server 2.7.0

# Adding a static build of Polus Render
- Remove all existing files in `~/render-server-ext/static/render-ui/`.
- Run `npx nx build render-ui` in the root of your Polus Render folder
- Transfer generated files from `~/Polus Render/dist/apps/render-ui/` into `~/render-server-ext/static/render-ui/`.

# API Endpoints
- `/render/default/(.*)`: Help on usage of extension
- `/static/render/render-ui/index.html`: Serves static build of Polus Render.
- `/render/file/(.+)`: Serves files at a specfied path. Does not serve directories.

# Examples
For the following examples, JupyterLab is ran locally at `https://localhost:7832/lab`


## Render-UI
URL: `http://localhost:7832/static/render/render-ui/index.html?imageUrl=http://localhost:7832/render/file//home/jovyan/work/images/4by4large_intensity_image.ome.tif`

<img src="images/renderui-1.png"/>


URL: `http://localhost:7832/static/render/render-ui/index.html?imageUrl=http://localhost:7832/render/file//home/jovyan/work/images/x00_y01_p02_c1.ome.tif&overlayUrl=http://localhost:7832/render/file//home/jovyan/work/overlays/combined.json`

<img src="images/renderui-2.png"/>


## Image
URL: `http://localhost:7832/render/file/home/jovyan/work/samples/butterfly.jpeg`

<img src="images/image.png"/>

## Markdown
URL: `http://localhost:7832/render/file/home/jovyan/work/samples/README.md`

<img src="images/markdown.png"/>

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"ServerApp": {
"jpserver_extensions": {
"serve": true
}
}
}

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""Configuration file for jupyter-server extensions."""
# ------------------------------------------------------------------------------
# Application(SingletonConfigurable) configuration
# ------------------------------------------------------------------------------
# The date format used by logging formatters for %(asctime)s
c.Application.log_datefmt = ( # type:ignore[name-defined]
"%Y-%m-%d %H:%M:%S Render-Server-Ext"
)
32 changes: 32 additions & 0 deletions jupyterlab_polus_render/render-server-ext/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "render-server-ext"
description = "Serves files and serves a local build of Polus Render"
readme = "README.md"
license = "MIT"
requires-python = ">=3.9"
dependencies = [
"jinja2",
]
authors = [{name = "Jeff Chen", email="[email protected]"}]
version = "0.1.0"

[project.urls]
Homepage = "https://github.com/PolusAI/jupyterlab-extensions"

[project.scripts]
jupyter-server = "serve.application:main"

[tool.hatch.build.targets.wheel.shared-data]
"etc/jupyter/jupyter_server_config.d" = "etc/jupyter/jupyter_server_config.d"

[tool.hatch.build]
include = [
"serve/",
"etc/",
"setup.py",
"jupyter_server_config.py"
]
6 changes: 6 additions & 0 deletions jupyterlab_polus_render/render-server-ext/serve/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from .application import Application


# Sets server activation ID and the app itself, called from jupyter server --ServerApp.jpserver_extensions="{'simple_ext1': True}"
def _jupyter_server_extension_points():
return [{"module": "serve.application", "app": Application}]
6 changes: 6 additions & 0 deletions jupyterlab_polus_render/render-server-ext/serve/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Application cli main."""
from .application import main

# Main entry point into program
if __name__ == "__main__":
main()
51 changes: 51 additions & 0 deletions jupyterlab_polus_render/render-server-ext/serve/application.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""Jupyter server example application."""
import os

from jupyter_server.extension.application import ExtensionApp, ExtensionAppJinjaMixin
# Handles incoming requests and adds endpoints
from .handlers import (
DefaultHandler,
ErrorHandler,
AuthFileHandler,
)


DEFAULT_STATIC_FILES_PATH = os.path.join(os.path.dirname(__file__), "static")
DEFAULT_TEMPLATE_FILES_PATH = os.path.join(os.path.dirname(__file__), "templates")


class Application(ExtensionAppJinjaMixin, ExtensionApp):
"""A simple jupyter server application."""
# The name of the extension.
name = "render"

# The url that your extension will serve its homepage.
default_url = "/render/default/"

# Should your extension expose other server extensions when launched directly?
load_other_extensions = True

# Local path to static files directory.
static_paths = [DEFAULT_STATIC_FILES_PATH] # type:ignore[assignment]

# Local path to templates directory.
template_paths = [DEFAULT_TEMPLATE_FILES_PATH] # type:ignore[assignment]

# Init handlers and link endpoints to them
def initialize_handlers(self):
"""Initialize handlers."""
self.handlers.extend(
[
(rf"/{self.name}/default/(.*)", DefaultHandler),
(rf"/{self.name}/file/(.+)", AuthFileHandler, {"path":"/"}),
(rf"/{self.name}/(.*)", ErrorHandler),
]
)


def initialize_settings(self):
"""Initialize settings."""
self.log.info(f"Config {self.config}")

main = launch_new_instance = Application.launch_instance

Loading

0 comments on commit b70594b

Please sign in to comment.