Skip to content

Commit 91e2a44

Browse files
committed
Merge branch 'main' into giovp/pre-commit
2 parents d117b36 + b2e3120 commit 91e2a44

File tree

5 files changed

+131
-19
lines changed

5 files changed

+131
-19
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ jobs:
77
runs-on: ubuntu-latest
88
strategy:
99
matrix:
10-
version: ['3.7', '3.10']
10+
version: ['3.8', '3.12']
1111
steps:
1212
- uses: actions/checkout@v2
1313
- uses: actions/setup-python@v2

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "vitessce"
7-
version = "3.2.3"
7+
version = "3.2.6"
88
authors = [
99
{ name="Mark Keller", email="mark_keller@hms.harvard.edu" },
1010
]
@@ -73,7 +73,7 @@ docs = [
7373
]
7474
all = [
7575
'jupyter-server-proxy>=1.5.2',
76-
'anywidget>=0.9.3',
76+
'anywidget>=0.9.10',
7777
'uvicorn>=0.17.0',
7878
'ujson>=4.0.1',
7979
'starlette==0.14.0',

vitessce/config.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,13 @@ def get_routes(self):
219219

220220
return routes
221221

222+
def get_stores(self, base_url=None):
223+
stores = {}
224+
for obj in self.objs:
225+
stores = {**stores, **obj.get_stores(base_url)}
226+
227+
return stores
228+
222229

223230
class VitessceConfigViewHConcat:
224231
"""
@@ -1506,6 +1513,18 @@ def get_routes(self):
15061513
routes += d.get_routes()
15071514
return routes
15081515

1516+
def get_stores(self, base_url=None):
1517+
"""
1518+
Convert the routes for this view config from the datasets.
1519+
1520+
:returns: A list of server routes.
1521+
:rtype: list[starlette.routing.Route]
1522+
"""
1523+
stores = {}
1524+
for d in self.config["datasets"]:
1525+
stores = {**stores, **d.get_stores(base_url)}
1526+
return stores
1527+
15091528
def to_python(self):
15101529
"""
15111530
Convert the VitessceConfig instance to a one-line Python code snippet that can be used to generate it.

vitessce/widget.py

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@
1111
from starlette.applications import Starlette
1212
from starlette.middleware import Middleware
1313
from starlette.middleware.cors import CORSMiddleware
14-
from traitlets import Bool, Dict, Int, Unicode
14+
from traitlets import Bool, Dict, Int, List, Unicode
1515

16+
# Server dependencies
1617
# Server dependencies
1718
from uvicorn import Config, Server
1819

@@ -194,6 +195,7 @@ def get_uid_str(uid):
194195
const customJsUrl = view.model.get('custom_js_url');
195196
const pluginEsm = view.model.get('plugin_esm');
196197
const remountOnUidChange = view.model.get('remount_on_uid_change');
198+
const storeUrls = view.model.get('store_urls');
197199
198200
const pkgName = (jsDevMode ? "@vitessce/dev" : "vitessce");
199201
@@ -217,6 +219,19 @@ def get_uid_str(uid):
217219
let pluginFileTypes;
218220
let pluginJointFileTypes;
219221
222+
const stores = Object.fromEntries(
223+
storeUrls.map(storeUrl => ([
224+
storeUrl,
225+
{
226+
async get(key) {
227+
const [data, buffers] = await view.experimental.invoke("_zarr_get", [storeUrl, key]);
228+
if (!data.success) return undefined;
229+
return buffers[0].buffer;
230+
},
231+
}
232+
])),
233+
);
234+
220235
try {
221236
const pluginEsmUrl = URL.createObjectURL(new Blob([pluginEsm], { type: "text/javascript" }));
222237
const pluginModule = (await import(pluginEsmUrl)).default;
@@ -303,7 +318,7 @@ def get_uid_str(uid):
303318
const vitessceProps = {
304319
height, theme, config, onConfigChange, validateConfig,
305320
pluginViewTypes, pluginCoordinationTypes, pluginFileTypes, pluginJointFileTypes,
306-
remountOnUidChange,
321+
remountOnUidChange, stores,
307322
};
308323
309324
return e('div', { ref: divRef, style: { height: height + 'px' } },
@@ -377,12 +392,14 @@ class VitessceWidget(anywidget.AnyWidget):
377392

378393
next_port = DEFAULT_PORT
379394

380-
js_package_version = Unicode("3.3.7").tag(sync=True)
395+
js_package_version = Unicode("3.3.12").tag(sync=True)
381396
js_dev_mode = Bool(False).tag(sync=True)
382397
custom_js_url = Unicode("").tag(sync=True)
383398
plugin_esm = Unicode(DEFAULT_PLUGIN_ESM).tag(sync=True)
384399
remount_on_uid_change = Bool(True).tag(sync=True)
385400

401+
store_urls = List(trait=Unicode(""), default_value=[]).tag(sync=True)
402+
386403
def __init__(
387404
self,
388405
config,
@@ -391,7 +408,7 @@ def __init__(
391408
uid=None,
392409
port=None,
393410
proxy=False,
394-
js_package_version="3.3.7",
411+
js_package_version="3.3.12",
395412
js_dev_mode=False,
396413
custom_js_url="",
397414
plugin_esm=DEFAULT_PLUGIN_ESM,
@@ -429,9 +446,11 @@ def __init__(
429446
config_dict = config.to_dict(base_url=base_url)
430447
routes = config.get_routes()
431448

449+
self._stores = config.get_stores(base_url=base_url)
450+
432451
uid_str = get_uid_str(uid)
433452

434-
super(VitessceWidget, self).__init__( # noqa: UP008
453+
super().__init__(
435454
config=config_dict,
436455
height=height,
437456
theme=theme,
@@ -442,6 +461,7 @@ def __init__(
442461
plugin_esm=plugin_esm,
443462
remount_on_uid_change=remount_on_uid_change,
444463
uid=uid_str,
464+
store_urls=list(self._stores.keys()),
445465
)
446466

447467
serve_routes(config, routes, use_port)
@@ -474,6 +494,16 @@ def close(self):
474494
self.config_obj.stop_server(self.port)
475495
super().close()
476496

497+
@anywidget.experimental.command
498+
def _zarr_get(self, params, buffers):
499+
[store_url, key] = params
500+
store = self._stores[store_url]
501+
try:
502+
buffers = [store[key.lstrip("/")]]
503+
except KeyError:
504+
buffers = []
505+
return {"success": len(buffers) == 1}, buffers
506+
477507

478508
# Launch Vitessce using plain HTML representation (no ipywidgets)
479509

@@ -487,7 +517,7 @@ def ipython_display(
487517
uid=None,
488518
port=None,
489519
proxy=False,
490-
js_package_version="3.3.7",
520+
js_package_version="3.3.12",
491521
js_dev_mode=False,
492522
custom_js_url="",
493523
plugin_esm=DEFAULT_PLUGIN_ESM,
@@ -516,6 +546,7 @@ def ipython_display(
516546
"height": height,
517547
"theme": theme,
518548
"config": config_dict,
549+
"store_urls": [],
519550
}
520551

521552
# We need to clean up the React and DOM state in any case in which

vitessce/wrappers.py

Lines changed: 72 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from pathlib import PurePath, PurePosixPath
55
from uuid import uuid4
66

7+
import zarr
8+
79
from .constants import (
810
DataType as dt,
911
)
@@ -42,12 +44,16 @@ def __init__(self, **kwargs):
4244
Abstract constructor to be inherited by dataset wrapper classes.
4345
4446
:param str out_dir: The path to a local directory used for data processing outputs. By default, uses a temp. directory.
47+
:param dict request_init: options to be passed along with every fetch request from the browser, like `{ "header": { "Authorization": "Bearer dsfjalsdfa1431" } }`
4548
"""
4649
self.out_dir = kwargs["out_dir"] if "out_dir" in kwargs else tempfile.mkdtemp()
4750
self.routes = []
48-
self.is_remote = False
51+
self.is_remote = False # TODO: change to needs_localhost_serving for clarity
52+
self.is_store = False # TODO: change to needs_store_registration for clarity
4953
self.file_def_creators = []
5054
self.base_dir = None
55+
self.stores = {}
56+
self._request_init = kwargs["request_init"] if "request_init" in kwargs else None
5157

5258
def __repr__(self):
5359
return self._repr
@@ -74,6 +80,20 @@ def get_routes(self):
7480
"""
7581
return self.routes
7682

83+
def get_stores(self, base_url):
84+
"""
85+
Obtain the stores that have been created for this wrapper class.
86+
87+
:returns: A dictionary that maps file URLs to Zarr Store objects.
88+
:rtype: dict[str, zarr.Store]
89+
"""
90+
relative_stores = self.stores
91+
absolute_stores = {}
92+
for relative_url, store in relative_stores.items():
93+
absolute_url = base_url + relative_url
94+
absolute_stores[absolute_url] = store
95+
return absolute_stores
96+
7797
def get_file_defs(self, base_url):
7898
"""
7999
Obtain the file definitions for this wrapper class.
@@ -114,6 +134,30 @@ def get_local_dir_url(self, base_url, dataset_uid, obj_i, local_dir_path, local_
114134
return self._get_url_simple(base_url, file_path_to_url_path(local_dir_path, prepend_slash=False))
115135
return self._get_url(base_url, dataset_uid, obj_i, local_dir_uid)
116136

137+
def register_zarr_store(self, dataset_uid, obj_i, store_or_local_dir_path, local_dir_uid):
138+
if not self.is_remote and self.is_store:
139+
# Set up `store` and `local_dir_path` variables.
140+
if isinstance(store_or_local_dir_path, str):
141+
# TODO: use zarr.FSStore if fsspec is installed?
142+
store = zarr.DirectoryStore(store_or_local_dir_path)
143+
local_dir_path = store_or_local_dir_path
144+
else:
145+
# TODO: check that store_or_local_dir_path is a zarr.Store or StoreLike?
146+
store = store_or_local_dir_path
147+
# A store instance was passed directly, so there is no local directory path.
148+
# Instead we just make one up using _get_route_str but it could be any string.
149+
local_dir_path = self._get_route_str(dataset_uid, obj_i, local_dir_uid)
150+
151+
# Register the store on the same route path
152+
# that will be used for the "url" field in the file definition.
153+
if self.base_dir is None:
154+
route_path = self._get_route_str(dataset_uid, obj_i, local_dir_uid)
155+
else:
156+
route_path = file_path_to_url_path(local_dir_path)
157+
local_dir_path = join(self.base_dir, local_dir_path)
158+
159+
self.stores[route_path] = store
160+
117161
def get_local_dir_route(self, dataset_uid, obj_i, local_dir_path, local_dir_uid):
118162
"""
119163
Obtain the Mount for some local directory
@@ -903,6 +947,7 @@ def __init__(
903947
self,
904948
adata_path=None,
905949
adata_url=None,
950+
adata_store=None,
906951
obs_feature_matrix_path=None,
907952
feature_filter_path=None,
908953
initial_feature_filter_path=None,
@@ -915,7 +960,6 @@ def __init__(
915960
obs_embedding_dims=None,
916961
obs_spots_path=None,
917962
obs_points_path=None,
918-
request_init=None,
919963
feature_labels_path=None,
920964
obs_labels_path=None,
921965
convert_to_dense=True,
@@ -929,6 +973,8 @@ def __init__(
929973
930974
:param str adata_path: A path to an AnnData object written to a Zarr store containing single-cell experiment data.
931975
:param str adata_url: A remote url pointing to a zarr-backed AnnData store.
976+
:param adata_store: A path to pass to zarr.FSStore, or an existing store instance.
977+
:type adata_store: str or zarr.Storage
932978
:param str obs_feature_matrix_path: Location of the expression (cell x gene) matrix, like `X` or `obsm/highly_variable_genes_subset`
933979
:param str feature_filter_path: A string like `var/highly_variable` used in conjunction with `obs_feature_matrix_path` if obs_feature_matrix_path points to a subset of `X` of the full `var` list.
934980
:param str initial_feature_filter_path: A string like `var/highly_variable` used in conjunction with `obs_feature_matrix_path` if obs_feature_matrix_path points to a subset of `X` of the full `var` list.
@@ -941,7 +987,6 @@ def __init__(
941987
:param list[str] obs_embedding_dims: Dimensions along which to get data for the scatterplot, like `[[0, 1], [4, 5]]` where `[0, 1]` is just the normal x and y but `[4, 5]` could be comparing the third and fourth principal components, for example.
942988
:param str obs_spots_path: Column name in `obsm` that contains centroid coordinates for displaying spots in the spatial viewer
943989
:param str obs_points_path: Column name in `obsm` that contains centroid coordinates for displaying points in the spatial viewer
944-
:param dict request_init: options to be passed along with every fetch request from the browser, like `{ "header": { "Authorization": "Bearer dsfjalsdfa1431" } }`
945990
:param str feature_labels_path: The name of a column containing feature labels (e.g., alternate gene symbols), instead of the default index in `var` of the AnnData store.
946991
:param str obs_labels_path: (DEPRECATED) The name of a column containing observation labels (e.g., alternate cell IDs), instead of the default index in `obs` of the AnnData store. Use `obs_labels_paths` and `obs_labels_names` instead. This arg will be removed in a future release.
947992
:param list[str] obs_labels_paths: The names of columns containing observation labels (e.g., alternate cell IDs), instead of the default index in `obs` of the AnnData store.
@@ -955,16 +1000,28 @@ def __init__(
9551000
self._repr = make_repr(locals())
9561001
self._adata_path = adata_path
9571002
self._adata_url = adata_url
958-
if adata_url is not None and (adata_path is not None):
959-
raise ValueError("Did not expect adata_url to be provided with adata_path")
960-
if adata_url is None and (adata_path is None):
961-
raise ValueError("Expected either adata_url or adata_path to be provided")
1003+
self._adata_store = adata_store
1004+
1005+
num_inputs = sum([1 for x in [adata_path, adata_url, adata_store] if x is not None])
1006+
if num_inputs > 1:
1007+
raise ValueError("Expected only one of adata_path, adata_url, or adata_store to be provided")
1008+
if num_inputs == 0:
1009+
raise ValueError("Expected one of adata_path, adata_url, or adata_store to be provided")
1010+
9621011
if adata_path is not None:
9631012
self.is_remote = False
1013+
self.is_store = False
9641014
self.zarr_folder = "anndata.zarr"
965-
else:
1015+
elif adata_url is not None:
9661016
self.is_remote = True
1017+
self.is_store = False
9671018
self.zarr_folder = None
1019+
else:
1020+
# Store case
1021+
self.is_remote = False
1022+
self.is_store = True
1023+
self.zarr_folder = None
1024+
9681025
self.local_dir_uid = make_unique_filename(".adata.zarr")
9691026
self._expression_matrix = obs_feature_matrix_path
9701027
self._cell_set_obs_names = obs_set_names
@@ -978,7 +1035,6 @@ def __init__(
9781035
self._mappings_obsm_dims = obs_embedding_dims
9791036
self._spatial_spots_obsm = obs_spots_path
9801037
self._spatial_points_obsm = obs_points_path
981-
self._request_init = request_init
9821038
self._gene_alias = feature_labels_path
9831039
# Support legacy provision of single obs labels path
9841040
if obs_labels_path is not None:
@@ -1004,6 +1060,9 @@ def convert_and_save(self, dataset_uid, obj_i, base_dir=None):
10041060
def make_anndata_routes(self, dataset_uid, obj_i):
10051061
if self.is_remote:
10061062
return []
1063+
elif self.is_store:
1064+
self.register_zarr_store(dataset_uid, obj_i, self._adata_store, self.local_dir_uid)
1065+
return []
10071066
else:
10081067
return self.get_local_dir_route(dataset_uid, obj_i, self._adata_path, self.local_dir_uid)
10091068

@@ -1138,6 +1197,9 @@ def get_zarr_url(self, base_url="", dataset_uid="", obj_i=""):
11381197

11391198
def make_genomic_profiles_file_def_creator(self, dataset_uid, obj_i):
11401199
def genomic_profiles_file_def_creator(base_url):
1141-
return {"fileType": "genomic-profiles.zarr", "url": self.get_zarr_url(base_url, dataset_uid, obj_i)}
1200+
obj_file_def = {"fileType": "genomic-profiles.zarr", "url": self.get_zarr_url(base_url, dataset_uid, obj_i)}
1201+
if self._request_init is not None:
1202+
obj_file_def["requestInit"] = self._request_init
1203+
return obj_file_def
11421204

11431205
return genomic_profiles_file_def_creator

0 commit comments

Comments
 (0)