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

Mccalluc/serve notebooks plus introspection #1877

Closed
wants to merge 10 commits into from
Closed
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
1 change: 1 addition & 0 deletions CHANGELOG-notebook.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Add an endpoint which serves a Jupyter notebook with the viewconf for a dataset.
9 changes: 4 additions & 5 deletions context/app/api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ def get_entity(self, uuid=None, hbm_id=None):
entity = hits[0]['_source']
return entity

def get_vitessce_conf(self, entity):
def get_vitessce_conf_cells(self, entity):
# First, try "vis-lifting": Display image pyramids on their parent entity pages.
image_pyramid_descendants = _get_image_pyramid_descendants(entity)
if image_pyramid_descendants:
Expand All @@ -116,20 +116,19 @@ def get_vitessce_conf(self, entity):
# about "files". Bill confirms that when the new structure comes in
# there will be a period of backward compatibility to allow us to migrate.
derived_entity['files'] = derived_entity['metadata']['files']
return self.get_vitessce_conf(derived_entity)
return self.get_vitessce_conf_cells(derived_entity)

if 'files' not in entity or 'data_types' not in entity:
return None
if self.is_mock:
return self._get_mock_vitessce_conf()
return self._get_mock_vitessce_conf_cells()

# Otherwise, just try to visualize the data for the entity itself:
try:
vc = get_view_config_class_for_data_types(
entity=entity, nexus_token=self.nexus_token
)
conf = vc.build_vitessce_conf()
return conf
return vc.build_vitessce_conf_cells()
except Exception:
message = f'Building vitessce conf threw error: {traceback.format_exc()}'
current_app.logger.error(message)
Expand Down
33 changes: 31 additions & 2 deletions context/app/api/vitessce_confs/base_confs.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from pathlib import Path
import re

import nbformat
from flask import current_app
from vitessce import (
VitessceConfig,
Expand Down Expand Up @@ -119,7 +120,8 @@ def __init__(self, entity, nexus_token, is_mock=False):
self.image_pyramid_regex = IMAGE_PYRAMID_DIR
super().__init__(entity, nexus_token, is_mock)

def build_vitessce_conf(self):
def build_vitessce_conf_cells(self):
cells = []
file_paths_found = self._get_file_paths()
found_images = get_matches(
file_paths_found, self.image_pyramid_regex + r".*\.ome\.tiff?$",
Expand All @@ -131,8 +133,18 @@ def build_vitessce_conf(self):
raise FileNotFoundError(message)

vc = VitessceConfig(name="HuBMAP Data Portal")
cells.append(nbformat.v4.new_code_cell(
'vc = VitessceConfig(name="HuBMAP Data Portal")'
))

dataset = vc.add_dataset(name="Visualization Files")
cells.append(nbformat.v4.new_code_cell(
'vc.add_dataset(name="Visualization Files")'
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Suggested change
'vc.add_dataset(name="Visualization Files")'
'dataset = vc.add_dataset(name="Visualization Files")'

))

images = []
cells.append(nbformat.v4.new_code_cell(f'images = []'))

for img_path in found_images:
img_url, offsets_url = self._get_img_and_offset_url(
img_path, self.image_pyramid_regex
Expand All @@ -142,12 +154,29 @@ def build_vitessce_conf(self):
img_url=img_url, offsets_url=offsets_url, name=Path(img_path).name
)
)
cells.append(nbformat.v4.new_code_cell(
f"images.append(OmeTiffWrapper(img_url='{img_url}', offsets_url='{offsets_url}', name=Path('{img_path}').name))"
))
# NOTE: If OmeTiffWrapper had a repr which reproduced the object, we could just say:
# f"images = {images}"
Comment on lines +160 to +161
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Or, better, do it at the very end, with MultiImageWrapper.


dataset = dataset.add_object(MultiImageWrapper(images))
cells.append(nbformat.v4.new_code_cell(
'dataset = dataset.add_object(MultiImageWrapper(images))'
))

vc = self._setup_view_config_raster(vc, dataset)
cells.append(nbformat.v4.new_code_cell(
f'vc = self._setup_view_config_raster(vc, dataset)'
))

# NOTE: We could skip the serialization to JSON and then back to an object.
# ... but not sure how to handle the del below... does a method need to be added to the viewconf?

conf = vc.to_dict()
# Don't want to render all layers
del conf["datasets"][0]["files"][0]["options"]["renderLayers"]
return conf
return (conf, cells)


class ScatterplotViewConf(ViewConf):
Expand Down
47 changes: 38 additions & 9 deletions context/app/routes_main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from os.path import dirname
from urllib.parse import urlparse
import json
import nbformat

from flask import (Blueprint, render_template, abort, current_app,
session, request, redirect, url_for, Response)
Expand Down Expand Up @@ -95,7 +96,7 @@ def details(type, uuid):
flask_data = {
'endpoints': _get_endpoints(),
'entity': entity,
'vitessce_conf': client.get_vitessce_conf(entity)
'vitessce_conf': client.get_vitessce_conf_cells(entity)[0]
}
return render_template(
template,
Expand All @@ -106,25 +107,53 @@ def details(type, uuid):
)


@blueprint.route('/browse/<type>/<uuid>.<ext>')
def details_ext(type, uuid, ext):
@blueprint.route('/browse/<type>/<uuid>.json')
def details_json(type, uuid):
if type not in entity_types:
abort(404)
if ext != 'json':
abort(404)
client = _get_client()
entity = client.get_entity(uuid)
return entity


@blueprint.route('/browse/<type>/<uuid>.rui.<ext>')
def details_rui_ext(type, uuid, ext):
@blueprint.route('/browse/<type>/<uuid>.ipynb')
def details_notebook(type, uuid):
if type not in entity_types:
abort(404)
client = _get_client()
entity = client.get_entity(uuid)
vitessce_cells = client.get_vitessce_conf_cells(entity)[1]
if vitessce_cells is None:
abort(404)
nb = nbformat.v4.new_notebook()
nb['cells'] = [
nbformat.v4.new_markdown_cell(f"""
Visualization for [{entity['display_doi']}]({request.base_url.replace('.ipynb','')})
""".strip()),
nbformat.v4.new_code_cell('\n'.join([
'!pip install vitessce==0.1.0a9',
'!jupyter nbextension install --py --sys-prefix vitessce',
'!jupyter nbextension enable --py --sys-prefix vitessce'
])),
nbformat.v4.new_code_cell('from vitessce import VitessceConfig')
] + vitessce_cells + [
nbformat.v4.new_code_cell('vc = VitessceConfig.from_dict(vitessce_conf)'),
nbformat.v4.new_code_cell('vw = vc.widget()'),
nbformat.v4.new_code_cell('vw')
]
return Response(
response=nbformat.writes(nb),
headers={'Content-Disposition': f"attachment; filename={entity['display_doi']}.ipynb"},
mimetype='application/x-ipynb+json'
)


@blueprint.route('/browse/<type>/<uuid>.rui.json')
def details_rui_json(type, uuid):
# Note that the API returns a blob of JSON as a string,
# so, to return a JSON object, and not just a string, we need to decode.
if type not in entity_types:
abort(404)
if ext != 'json':
abort(404)
client = _get_client()
entity = client.get_entity(uuid)
# For Samples...
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ function ErrorBody({ errorCode, urlPath, isAuthenticated, isGlobus401, isMainten

if (errorCode === 404) {
if (urlPath && urlPath.startsWith('/browse/')) {
const uuid = urlPath.split('/').pop();
const uuid = urlPath.split('/').pop().split('.')[0];
if (uuid.length !== 32) {
return (
<>
Expand Down
1 change: 1 addition & 0 deletions context/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
nbformat==5.1.2
Flask==1.1.2
globus-sdk==2.0.1
requests==2.22.0
Expand Down