diff --git a/setup.cfg b/setup.cfg index 6b2a432..64ab1b8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,12 +31,13 @@ project_urls = [options] packages = find: install_requires = + cryoet_data_portal + fsspec[http,s3] npe2 numpy napari_ome_zarr ndjson qtpy - s3fs superqt python_requires = >=3.8 diff --git a/src/napari_cryoet_data_portal/__init__.py b/src/napari_cryoet_data_portal/__init__.py index 59080ac..9a1fca1 100644 --- a/src/napari_cryoet_data_portal/__init__.py +++ b/src/napari_cryoet_data_portal/__init__.py @@ -2,10 +2,11 @@ from ._version import version as __version__ except ImportError: __version__ = "unknown" -from ._logging import logger from ._reader import ( points_annotations_reader, - read_points_annotations_json, + read_annotation, + read_points_annotations_ndjson, + read_tomogram, read_tomogram_ome_zarr, tomogram_ome_zarr_reader, ) @@ -14,7 +15,9 @@ __all__ = ( "DataPortalWidget", "points_annotations_reader", + "read_annotation", + "read_tomogram", "read_tomogram_ome_zarr", - "read_points_annotations_json", + "read_points_annotations_ndjson", "tomogram_ome_zarr_reader", ) diff --git a/src/napari_cryoet_data_portal/_io.py b/src/napari_cryoet_data_portal/_io.py deleted file mode 100644 index c119c9b..0000000 --- a/src/napari_cryoet_data_portal/_io.py +++ /dev/null @@ -1,56 +0,0 @@ -"""IO utilities for abstracting paths across S3, HTTPS, and a local filesystem.""" - -import json -import os -from typing import Any, Callable, Dict, List, Tuple - -from s3fs import S3FileSystem - - -def list_dir(path: str) -> Tuple[str, ...]: - """Returns the names of the contents of a directory-like path.""" - names = _list_dir_s3(path) if _is_s3(path) else os.listdir(path) - return tuple(sorted(names)) - - -def path_exists(path: str) -> bool: - """Returns true if the given path exists, false otherwise.""" - return _path_exists_s3(path) if _is_s3(path) else os.path.exists(path) - - -def get_open(path: str) -> Callable[[str], Any]: - """Returns a function to open a file at the given path.""" - if _is_s3(path): - s3 = S3FileSystem(anon=True) - return s3.open - return open - - -def s3_to_https(path: str) -> str: - """Converts an S3 URI to the equivalent CloudFront HTTPS URI.""" - return path.replace( - "s3://cryoet-data-portal-public", - "https://files.cryoetdataportal.cziscience.com", - ) - - -def read_json(path: str) -> Dict[str, Any]: - """Reads JSON from the given path.""" - open_ = get_open(path) - with open_(path) as f: - return json.load(f) - - -def _is_s3(path: str) -> bool: - return path.startswith("s3://") - - -def _path_exists_s3(path: str) -> bool: - s3 = S3FileSystem(anon=True) - return s3.exists(path) - - -def _list_dir_s3(path: str) -> List[str]: - s3 = S3FileSystem(anon=True) - bucket_path = path.replace("s3://", "") - return [p.split("/")[-1] for p in s3.ls(bucket_path)] diff --git a/src/napari_cryoet_data_portal/_listing_widget.py b/src/napari_cryoet_data_portal/_listing_widget.py index 4e2892c..64a2e1c 100644 --- a/src/napari_cryoet_data_portal/_listing_widget.py +++ b/src/napari_cryoet_data_portal/_listing_widget.py @@ -1,4 +1,4 @@ -from typing import Generator, Optional +from typing import Generator, List, Optional, Tuple from qtpy.QtCore import Qt from qtpy.QtWidgets import ( @@ -8,11 +8,10 @@ QVBoxLayout, QWidget, ) +from cryoet_data_portal import Client, Dataset, Tomogram -from napari_cryoet_data_portal._io import list_dir from napari_cryoet_data_portal._listing_tree_widget import ListingTreeWidget from napari_cryoet_data_portal._logging import logger -from napari_cryoet_data_portal._model import Dataset from napari_cryoet_data_portal._progress_widget import ProgressWidget from napari_cryoet_data_portal._vendored.superqt._searchable_tree_widget import ( _update_visible_items, @@ -44,37 +43,37 @@ def __init__(self, parent: Optional[QWidget] = None) -> None: layout.addStretch(0) self.setLayout(layout) - def load(self, path: str) -> None: - """Lists the datasets and tomograms at the given data portal path.""" - logger.debug("ListingWidget.load: %s", path) + def load(self, uri: str) -> None: + """Lists the datasets and tomograms using the given portal URI.""" + logger.debug("ListingWidget.load: %s", uri) self.tree.clear() self.show() - self._progress.submit(path) + self._progress.submit(uri) def cancel(self) -> None: """Cancels the last listing.""" logger.debug("ListingWidget.cancel") self._progress.cancel() - def _loadDatasets(self, path: str) -> Generator[Dataset, None, None]: - logger.debug("ListingWidget._loadDatasets: %s", path) - # TODO: only list non-hidden directories. - dataset_names = tuple( - p for p in list_dir(path) if not p.startswith(".") - ) - if len(dataset_names) == 0: - logger.debug("No datasets found") - for name in dataset_names: - yield Dataset.from_data_path_and_name(path, name) + def _loadDatasets(self, uri: str) -> Generator[Tuple[Dataset, List[Tomogram]], None, None]: + logger.debug("ListingWidget._loadDatasets: %s", uri) + client = Client(uri) + for dataset in Dataset.find(client): + tomograms: List[Tomogram] = [] + for run in dataset.runs: + for spacing in run.tomogram_voxel_spacings: + tomograms.extend(spacing.tomograms) + yield dataset, tomograms - def _onDatasetLoaded(self, dataset: Dataset) -> None: - logger.debug("ListingWidget._onDatasetLoaded: %s", dataset.name) - text = f"{dataset.name} ({len(dataset.tomograms)})" + def _onDatasetLoaded(self, result: Tuple[Dataset, List[Tomogram]]) -> None: + dataset, tomograms = result + logger.debug("ListingWidget._onDatasetLoaded: %s", dataset.id) + text = f"{dataset.id} ({len(tomograms)})" item = QTreeWidgetItem((text,)) item.setData(0, Qt.ItemDataRole.UserRole, dataset) - for s in dataset.tomograms: - tomogram_item = QTreeWidgetItem((s.name,)) - tomogram_item.setData(0, Qt.ItemDataRole.UserRole, s) + for tomogram in tomograms: + tomogram_item = QTreeWidgetItem((tomogram.name,)) + tomogram_item.setData(0, Qt.ItemDataRole.UserRole, tomogram) item.addChild(tomogram_item) _update_visible_items(item, self.tree.last_filter) self.tree.addTopLevelItem(item) diff --git a/src/napari_cryoet_data_portal/_metadata_widget.py b/src/napari_cryoet_data_portal/_metadata_widget.py index 9634a6f..94bae45 100644 --- a/src/napari_cryoet_data_portal/_metadata_widget.py +++ b/src/napari_cryoet_data_portal/_metadata_widget.py @@ -5,10 +5,9 @@ QVBoxLayout, QWidget, ) +from cryoet_data_portal import Dataset, Tomogram -from napari_cryoet_data_portal._io import read_json from napari_cryoet_data_portal._logging import logger -from napari_cryoet_data_portal._model import Dataset, Tomogram from napari_cryoet_data_portal._progress_widget import ProgressWidget from napari_cryoet_data_portal._vendored.superqt._searchable_tree_widget import ( QSearchableTreeWidget, @@ -39,7 +38,8 @@ def load(self, data: Union[Dataset, Tomogram]) -> None: """Loads the JSON metadata of the given dataset or tomogram.""" logger.debug("MetadataWidget.load: %s", data) self._main.tree.clear() - self.setTitle(f"Metadata: {data.name}") + name = data.id if isinstance(data, Dataset) else data.name + self.setTitle(f"Metadata: {name}") self.show() self._progress.submit(data) @@ -50,14 +50,7 @@ def cancel(self) -> None: def _loadMetadata(self, data: Union[Dataset, Tomogram]) -> Dict[str, Any]: logger.debug("MetadataWidget._loadMetadata: %s", data) - path = "" - if isinstance(data, Dataset): - path = data.metadata_path - elif isinstance(data, Tomogram): - path = data.tomogram_metadata_path - else: - raise AssertionError("Expected Dataset or Tomogram data") - return read_json(path) + return data.to_dict() def _onMetadataLoaded(self, metadata: Dict[str, Any]) -> None: logger.debug("MetadataWidget._onMetadataLoaded: %s", metadata) diff --git a/src/napari_cryoet_data_portal/_model.py b/src/napari_cryoet_data_portal/_model.py deleted file mode 100644 index 3fa878c..0000000 --- a/src/napari_cryoet_data_portal/_model.py +++ /dev/null @@ -1,113 +0,0 @@ -"""Data model used to abstract the contents and structure of the portal.""" - -from dataclasses import dataclass, field -from functools import cached_property -from typing import Tuple - -from napari_cryoet_data_portal._io import list_dir - -PORTAL_S3_URI = "s3://cryoet-data-portal-public" - -# TODO: hardcoding these correspondences as a short term fix -# for the recent bucket layout change which avoids an extra listing -# call to s3. Need a long term fix here, which likely uses the main -# Python client. -DATASET_TO_SPACING = { - '10000': 'VoxelSpacing13.48', - '10001': 'VoxelSpacing13.48', - '10004': 'VoxelSpacing7.56', -} -DEFAULT_SPACING = 'VoxelSpacing13.48' - - -@dataclass(frozen=True) -class Tomogram: - """Represents a tomogram within a dataset. - - Attributes - ---------- - name : str - The name of the tomogram (e.g. 'TS_026'). - path : str - The full directory-like path associated with the tomogram - (e.g. 's3://cryoet-data-portal-public/10000/TS_026'). - tomogram_path : str - The full directory-like path to the tomogram directory that contains the volumes and annotations - (e.g. 's3://cryoet-data-portal-public/10000/TS_026/Tomograms/VoxelSpacing13.48'). - image_path : str - The full directory-like path to the tomogram as an OME-Zarr multi-scale image - (e.g. 's3://cryoet-data-portal-public/10000/TS_026/Tomograms/VoxelSpacing13.48/CanonicalTomogram/TS_026.zarr'). - annotation_paths : tuple of str - The full file-like paths to the annotation JSON files - (e.g. ['s3://cryoet-data-portal-public/10000/TS_026/Tomograms/VoxelSpacing13.48/Annotations/sara_goetz-ribosome-1.0.json', ...]). - """ - - name: str - path: str - tomogram_path: str - image_path: str - annotation_paths: Tuple[str, ...] - - @cached_property - def tomogram_metadata_path(self) -> str: - return ( - f"{self.tomogram_path}/CanonicalTomogram/tomogram_metadata.json" - ) - - @classmethod - def from_dataset_path_and_name( - cls, dataset_path: str, name: str - ) -> "Tomogram": - path = f"{dataset_path}/{name}" - dataset_name = dataset_path.split('/')[-1] - spacing = DATASET_TO_SPACING.get(dataset_name, DEFAULT_SPACING) - tomogram_path = f"{path}/Tomograms/{spacing}" - image_path = f"{tomogram_path}/CanonicalTomogram/{name}.zarr" - annotation_dir = f"{tomogram_path}/Annotations" - annotation_paths = tuple( - f"{annotation_dir}/{p}" - for p in list_dir(annotation_dir) - if p.endswith(".json") - ) - return cls( - name=name, - path=path, - tomogram_path=tomogram_path, - image_path=image_path, - annotation_paths=annotation_paths, - ) - - -@dataclass(frozen=True) -class Dataset: - """Represents an entire dataset of many tomograms and their annotations. - - Attributes - ---------- - name : str - The name of the dataset (e.g. '10000'). - path : str - The full directory-like path associated with the dataset - (e.g. 's3://cryoet-data-portal-public/10000'). - tomograms : tuple of tomograms - The tomograms within the dataset. - """ - - name: str - path: str - tomograms: Tuple[Tomogram, ...] = field(repr=False) - - @cached_property - def metadata_path(self) -> str: - return f"{self.path}/dataset_metadata.json" - - @classmethod - def from_data_path_and_name(cls, data_path: str, name: str) -> "Dataset": - path = f"{data_path}/{name}" - tomograms = tuple( - Tomogram.from_dataset_path_and_name(path, p) - for p in list_dir(path) - # TODO: better way to select non-hidden directories. - if "." not in p - ) - return Dataset(name=name, path=path, tomograms=tomograms) diff --git a/src/napari_cryoet_data_portal/_open_widget.py b/src/napari_cryoet_data_portal/_open_widget.py index c8a3d4d..1508569 100644 --- a/src/napari_cryoet_data_portal/_open_widget.py +++ b/src/napari_cryoet_data_portal/_open_widget.py @@ -13,13 +13,13 @@ QVBoxLayout, QWidget, ) +from cryoet_data_portal import Annotation, Client, Tomogram from napari_cryoet_data_portal._logging import logger -from napari_cryoet_data_portal._model import Tomogram from napari_cryoet_data_portal._progress_widget import ProgressWidget from napari_cryoet_data_portal._reader import ( - read_points_annotations_json, - read_tomogram_ome_zarr, + read_annotation, + read_tomogram, ) if TYPE_CHECKING: @@ -39,7 +39,7 @@ class Resolution: MID_RESOLUTION = Resolution(name="Mid", indices=(1,), scale=2) LOW_RESOLUTION = Resolution(name="Low", indices=(2,), scale=4) -RESOLUTIONS: Tuple[Resolution] = ( +RESOLUTIONS: Tuple[Resolution, ...] = ( MULTI_RESOLUTION, HIGH_RESOLUTION, MID_RESOLUTION, @@ -56,7 +56,8 @@ def __init__( super().__init__(parent) self._viewer = viewer - self._tomogram : Optional[Tomogram] = None + self._uri: Optional[str] = None + self._tomogram: Optional[Tomogram] = None self.setTitle("Tomogram") @@ -88,6 +89,10 @@ def __init__( layout.addWidget(self._progress) self.setLayout(layout) + def setUri(self, uri: str) -> None: + """Sets the URI of the portal that should be used to open data.""" + self._uri = uri + def setTomogram(self, tomogram: Tomogram) -> None: """Sets the current tomogram that should be opened.""" self.cancel() @@ -117,9 +122,7 @@ def _loadTomogram( resolution: Resolution, ) -> Generator[FullLayerData, None, None]: logger.debug("OpenWidget._loadTomogram: %s", tomogram.name) - image_data, image_attrs, _ = read_tomogram_ome_zarr(tomogram.image_path) - # TODO: read JSON metadata in reader to get name from there. - image_attrs["name"] = f"{tomogram.name}-tomogram" + image_data, image_attrs, _ = read_tomogram(tomogram) # Skip indexing for multi-resolution to avoid adding any # unnecessary nodes to the dask compute graph. if resolution is not MULTI_RESOLUTION: @@ -132,11 +135,17 @@ def _loadTomogram( ) yield image_data, image_attrs, "image" - for p in tomogram.annotation_paths: - points_data, points_attrs, _ = read_points_annotations_json(p) - annotation_name = points_attrs["name"] - points_attrs["name"] = f"{tomogram.name}-{annotation_name}" - yield points_data, points_attrs, "points" + # Looking up tomogram.tomogram_voxel_spacing.annotations triggers a query + # using the client from where the tomogram was found. + # A single client is not thread safe, so we need a new instance for each query. + client = Client(self._uri) + annotations = Annotation.find( + client, + [Annotation.tomogram_voxel_spacing_id == tomogram.tomogram_voxel_spacing_id], + ) + + for annotation in annotations: + yield read_annotation(annotation, tomogram=tomogram) def _onLayerLoaded(self, layer_data: FullLayerData) -> None: logger.debug("OpenWidget._onLayerLoaded") diff --git a/src/napari_cryoet_data_portal/_reader.py b/src/napari_cryoet_data_portal/_reader.py index ee516d5..a5fb55a 100644 --- a/src/napari_cryoet_data_portal/_reader.py +++ b/src/napari_cryoet_data_portal/_reader.py @@ -1,22 +1,20 @@ """Functions to read data from the portal into napari types.""" -import json -import os from typing import Any, Dict, List, Optional, Tuple +import fsspec import ndjson from napari_ome_zarr import napari_get_reader from npe2.types import FullLayerData, PathOrPaths, ReaderFunction - -from napari_cryoet_data_portal._io import get_open, s3_to_https +from cryoet_data_portal import Annotation, Tomogram OBJECT_COLOR = { - 'ribosome': 'red', - 'ribosome, 80 s': 'red', - 'fatty acid synthase': 'darkblue', + "ribosome": "red", + "ribosome, 80 s": "red", + "fatty acid synthase": "darkblue", } -DEFAULT_OBJECT_COLOR = 'red' +DEFAULT_OBJECT_COLOR = "red" def tomogram_ome_zarr_reader(path: PathOrPaths) -> Optional[ReaderFunction]: @@ -71,24 +69,49 @@ def read_tomogram_ome_zarr(path: str) -> FullLayerData: Examples -------- - >>> from napari.layers import Image >>> path = 's3://cryoet-data-portal-public/10000/TS_026/Tomograms/VoxelSpacing13.48/CanonicalTomogram/TS_026.zarr' >>> data, attrs, _ = read_tomogram_ome_zarr(path) >>> image = Image(data, **attrs) """ - path = s3_to_https(path) reader = napari_get_reader(path) layers = reader(path) return layers[0] +def read_tomogram(tomogram: Tomogram) -> FullLayerData: + """Reads a napari image layer from a tomogram. + + Parameters + ---------- + tomogram : Tomogram + The tomogram to read. + + Returns + ------- + napari layer data tuple + The data, attributes, and type name of the image layer that would be + returned by `Image.as_layer_data_tuple`. + + Examples + -------- + >>> client = Client() + >>> tomogram = client.find_one(Tomogram) + >>> data, attrs, _ = read_tomogram(tomogram) + >>> image = Image(data, **attrs) + """ + data, attributes, layer_type = read_tomogram_ome_zarr(tomogram.https_omezarr_dir) + attributes["name"] = tomogram.name + attributes["metadata"] = tomogram.to_dict() + return data, attributes, layer_type + + def points_annotations_reader(path: PathOrPaths) -> Optional[ReaderFunction]: """napari plugin entry point for reading points annotations. Parameters ---------- path : str or sequence of str - The path or paths of the annotation JSON file(s) to read. + The path or paths of the annotation NDJSON file(s) to read. Returns ------- @@ -109,22 +132,22 @@ def points_annotations_reader(path: PathOrPaths) -> Optional[ReaderFunction]: >>> reader = points_annotations_reader(path) >>> layers = reader(path) """ - return _read_many_points_annotations + return _read_many_points_annotations_ndjson -def _read_many_points_annotations(paths: PathOrPaths) -> List[FullLayerData]: +def _read_many_points_annotations_ndjson(paths: PathOrPaths) -> List[FullLayerData]: if isinstance(paths, str): paths = [paths] - return [read_points_annotations_json(p) for p in paths] + return [read_points_annotations_ndjson(p) for p in paths] -def read_points_annotations_json(path: str) -> FullLayerData: - """Reads a napari points layer from one annotation JSON File. +def read_points_annotations_ndjson(path: str) -> FullLayerData: + """Reads a napari points layer from an NDJSON annotation file. Parameters ---------- path : str - The path of the annotation JSON file to read. + The path to the NDJSON annotations file. Returns ------- @@ -134,41 +157,60 @@ def read_points_annotations_json(path: str) -> FullLayerData: Examples -------- - >>> from napari.layers import Points >>> path = 's3://cryoet-data-portal-public/10000/TS_026/Tomograms/VoxelSpacing13.48/Annotations/sara_goetz-ribosome-1.0.json' - >>> data, attrs, _ = read_points_annotations_json(path) + >>> data, attrs, _ = read_points_annotations_ndjson(path) >>> points = Points(data, **attrs) """ - data: List[Tuple[float, float, float]] = [] - open_ = get_open(path) - with open_(path) as f: - metadata = json.load(f) - data_dir = os.path.dirname(path) - for sub_file in metadata["files"]: - sub_name = sub_file["file"] - sub_path = f"{data_dir}/{sub_name}" - sub_data = _read_points_annotations_ndjson(sub_path) - data.extend(sub_data) - anno_object = metadata["annotation_object"] - name = anno_object["name"] - face_color = OBJECT_COLOR.get(name.lower(), DEFAULT_OBJECT_COLOR) + data = _read_points_data(path) attributes = { - "name": name, - "metadata": metadata, + "name": "annotations", "size": 14, - "face_color": face_color, + "face_color": "red", "opacity": 0.5, "out_of_slice_display": True, } return data, attributes, "points" -def _read_points_annotations_ndjson( +def read_annotation(annotation: Annotation, *, tomogram: Optional[Tomogram] = None) -> FullLayerData: + """Reads a napari points layer from an annotation. + + Parameters + ---------- + annotation : Annotation + The tomogram annotation. + tomogram : Tomogram, optional + The associated tomogram, which may be used for other metadata. + + Returns + ------- + napari layer data tuple + The data, attributes, and type name of the points layer that would be + returned by `Points.as_layer_data_tuple`. + + Examples + -------- + >>> client = Client() + >>> annotation = client.find_one(Annotation) + >>> data, attrs, _ = read_annotation(annotation) + >>> points = Points(data, **attrs) + """ + data, attributes, layer_type = read_points_annotations_ndjson(annotation.https_annotations_path) + name = annotation.object_name + if tomogram is None: + attributes["name"] = name + else: + attributes["name"] = f"{tomogram.name}-{name}" + attributes["metadata"] = annotation.to_dict() + attributes["face_color"] = OBJECT_COLOR.get(name.lower(), DEFAULT_OBJECT_COLOR) + return data, attributes, layer_type + + +def _read_points_data( path: str, ) -> List[Tuple[float, float, float]]: data: List[Tuple[float, float, float]] = [] - open_ = get_open(path) - with open_(path) as f: + with fsspec.open(path) as f: sub_data = [ _annotation_to_point(annotation) for annotation in ndjson.load(f) diff --git a/src/napari_cryoet_data_portal/_sample_data.py b/src/napari_cryoet_data_portal/_sample_data.py index 58d2968..9ab4dae 100644 --- a/src/napari_cryoet_data_portal/_sample_data.py +++ b/src/napari_cryoet_data_portal/_sample_data.py @@ -2,10 +2,11 @@ import numpy as np from npe2.types import FullLayerData +from cryoet_data_portal import Client, Tomogram, TomogramVoxelSpacing from napari_cryoet_data_portal import ( - read_points_annotations_json, - read_tomogram_ome_zarr, + read_annotation, + read_tomogram, ) @@ -20,26 +21,21 @@ def tomogram_10000_ts_027() -> List[FullLayerData]: def _read_tomogram_from_10000(name: str) -> List[FullLayerData]: - base_uri = f"s3://cryoet-data-portal-public/10000/{name}/Tomograms/VoxelSpacing13.48" - tomogram_uri = f"{base_uri}/CanonicalTomogram/{name}.zarr" - annotations_uri = f"{base_uri}/Annotations" - ribosome_uri = f"{annotations_uri}/sara_goetz-ribosome-1.0.json" - fatty_acid_uri = ( - f"{annotations_uri}/sara_goetz-fatty_acid_synthase-1.0.json" - ) - - tomogram_image = read_tomogram_ome_zarr(tomogram_uri) + client = Client() + + tomogram_spacing_url = f"https://files.cryoetdataportal.cziscience.com/10000/{name}/Tomograms/VoxelSpacing13.48/" + tomogram_spacing = next(TomogramVoxelSpacing.find(client, [TomogramVoxelSpacing.https_prefix == tomogram_spacing_url])) + + tomogram: Tomogram = next(tomogram_spacing.tomograms) + + tomogram_image = read_tomogram(tomogram) # Materialize lowest resolution for speed. tomogram_image = (np.asarray(tomogram_image[0][-1]), *tomogram_image[1:]) tomogram_image[1]["scale"] = (4, 4, 4) - # TODO: fix this in reader or data. - tomogram_image[1]["name"] = "Tomogram" - - ribosome_points = read_points_annotations_json(ribosome_uri) - fatty_acid_points = read_points_annotations_json(fatty_acid_uri) - # Make different annotations distinctive. - fatty_acid_points[1]["face_color"] = "blue" + annotations = tuple(tomogram_spacing.annotations) + ribosome_points = read_annotation(annotations[0], tomogram=tomogram) + fatty_acid_points = read_annotation(annotations[1], tomogram=tomogram) return [ tomogram_image, diff --git a/src/napari_cryoet_data_portal/_tests/_mocks.py b/src/napari_cryoet_data_portal/_tests/_mocks.py deleted file mode 100644 index 53e74c8..0000000 --- a/src/napari_cryoet_data_portal/_tests/_mocks.py +++ /dev/null @@ -1,168 +0,0 @@ -from typing import Any, Dict, Tuple - -import numpy as np - -from napari_cryoet_data_portal._model import Dataset, Tomogram - - -MOCK_S3_URI = 's3://mock-portal' - - -def mock_dataset( - *, - name: str, - tomograms: Tuple[Tomogram, ...], -) -> Dataset: - path = f"{MOCK_S3_URI}/{name}" - return Dataset( - name=name, - path=path, - tomograms=tomograms, - ) - - -def mock_tomogram( - *, - dataset_name: str, - tomogram_name: str, - voxel_spacing: str, - annotation_names: Tuple[str, ...] -) -> Tomogram: - path = f"{MOCK_S3_URI}/{dataset_name}/{tomogram_name}" - tomogram_path = f"{path}/Tomograms/VoxelSpacing{voxel_spacing}" - image_path = f"{tomogram_path}/CanonicalTomogram/{tomogram_name}.zarr" - annotations_path = f"{tomogram_path}/Annotations" - annotations_paths = tuple( - f"{annotations_path}/{name}.json" - for name in annotation_names - ) - return Tomogram( - name=tomogram_name, - path=path, - tomogram_path=tomogram_path, - image_path=image_path, - annotation_paths=annotations_paths, - ) - - -MOCK_TOMOGRAM_TS_026 = mock_tomogram( - dataset_name="10000", - tomogram_name="TS_026", - voxel_spacing="13.48", - annotation_names=("ribosome", "fatty-acid-synthase"), -) - -MOCK_TOMOGRAM_TS_026_METADATA = { - 'run_name': 'TS_026', - 'voxel_spacing': 13.48, - 'size': { - 'z': 8, - 'y': 8, - 'x': 8, - } -} - -MOCK_TOMOGRAM_TS_026_IMAGE_DATA = [ - np.zeros((8, 8, 8)), - np.zeros((4, 4, 4)), - np.zeros((2, 2, 2)), -] - -MOCK_TOMOGRAM_TS_026_IMAGE_ATTRS = { - 'name': 'TS_026', - 'scale': (1, 1, 1), -} - -MOCK_TOMOGRAM_TS_026_RIBOSOME_DATA = [[0, 0, 0], [2, 2, 2]] -MOCK_TOMOGRAM_TS_026_RIBOSOME_ATTRS = {'name': 'ribosome'} - -MOCK_TOMOGRAM_TS_026_FAS_DATA = [[1, 1, 1], [3, 3, 3]] -MOCK_TOMOGRAM_TS_026_FAS_ATTRS = {'name': 'fatty acid synthase'} - -MOCK_TOMOGRAM_TS_027 = mock_tomogram( - dataset_name="10000", - tomogram_name="TS_027", - voxel_spacing="13.48", - annotation_names=("ribosome", "fatty-acid-synthase"), -) - -MOCK_DATASET_10000 = mock_dataset( - name="10000", - tomograms=(MOCK_TOMOGRAM_TS_026, MOCK_TOMOGRAM_TS_027), -) - -MOCK_DATASET_10000_METADATA = { - 'dataset_title': 'mock dataset', - 'authors': [ - { - 'name': 'mock author', - 'ORCID': "0000-1111-2222-3333", - } - ], - 'organism': { - 'name': 'mock organism', - } -} - -MOCK_TOMOGRAM_POS_128 = mock_tomogram( - dataset_name="10004", - tomogram_name="Position_128_2", - voxel_spacing="7.56", - annotation_names=("ribosome"), -) - -MOCK_TOMOGRAM_POS_129 = mock_tomogram( - dataset_name="10004", - tomogram_name="Position_129_2", - voxel_spacing="7.56", - annotation_names=("ribosome"), -) - -MOCK_DATASET_10004 = mock_dataset( - name="10004", - tomograms=(MOCK_TOMOGRAM_POS_128, MOCK_TOMOGRAM_POS_129), -) - -MOCK_DATASETS = (MOCK_DATASET_10000, MOCK_DATASET_10004) - - -def mock_path_exists(path: str) -> bool: - return path == MOCK_S3_URI - - -def mock_list_dir(path: str) -> Tuple[str, ...]: - if path == f'{MOCK_S3_URI}': - return tuple(ds.name for ds in MOCK_DATASETS) - for ds in MOCK_DATASETS: - if ds.path == path: - return tuple(tomo.name for tomo in ds.tomograms) - for tomo in ds.tomograms: - anno_path = f"{tomo.tomogram_path}/Annotations" - if path == anno_path: - return tuple( - p.split('/')[-1] - for p in tomo.annotation_paths - ) - raise ValueError(f"Mock path not supported: {path}") - - -def mock_read_json(path: str) -> Dict[str, Any]: - if path == MOCK_DATASET_10000.metadata_path: - return MOCK_DATASET_10000_METADATA - if path == MOCK_TOMOGRAM_TS_026.tomogram_metadata_path: - return MOCK_TOMOGRAM_TS_026_METADATA - raise ValueError(f'Mock path not supported: {path}') - - -def mock_read_tomogram_ome_zarr(path: str) -> Dict[str, Any]: - if path == MOCK_TOMOGRAM_TS_026.image_path: - return MOCK_TOMOGRAM_TS_026_IMAGE_DATA, MOCK_TOMOGRAM_TS_026_IMAGE_ATTRS, "image" - raise ValueError(f'Mock path not supported: {path}') - - -def mock_read_points_annotations_json(path: str) -> Dict[str, Any]: - if path == MOCK_TOMOGRAM_TS_026.annotation_paths[0]: - return MOCK_TOMOGRAM_TS_026_RIBOSOME_DATA, MOCK_TOMOGRAM_TS_026_RIBOSOME_ATTRS, "points" - if path == MOCK_TOMOGRAM_TS_026.annotation_paths[1]: - return MOCK_TOMOGRAM_TS_026_FAS_DATA, MOCK_TOMOGRAM_TS_026_FAS_ATTRS, "points" - raise ValueError(f'Mock path not supported: {path}') diff --git a/src/napari_cryoet_data_portal/_tests/conftest.py b/src/napari_cryoet_data_portal/_tests/conftest.py new file mode 100644 index 0000000..d979be1 --- /dev/null +++ b/src/napari_cryoet_data_portal/_tests/conftest.py @@ -0,0 +1,18 @@ +import pytest + +from cryoet_data_portal import Client, Dataset, Tomogram + + +@pytest.fixture() +def client() -> Client: + return Client() + + +@pytest.fixture() +def dataset(client: Client) -> Dataset: + return next(Dataset.find(client, [Dataset.id == 10000])) + + +@pytest.fixture() +def tomogram(client: Client) -> Tomogram: + return next(Tomogram.find(client, [Tomogram.name == 'TS_026'])) \ No newline at end of file diff --git a/src/napari_cryoet_data_portal/_tests/test_listing_widget.py b/src/napari_cryoet_data_portal/_tests/test_listing_widget.py index 3badfe5..c28743e 100644 --- a/src/napari_cryoet_data_portal/_tests/test_listing_widget.py +++ b/src/napari_cryoet_data_portal/_tests/test_listing_widget.py @@ -1,17 +1,12 @@ import pytest -from pytest_mock import MockerFixture from pytestqt.qtbot import QtBot from napari_cryoet_data_portal._listing_widget import ListingWidget -from napari_cryoet_data_portal._tests._mocks import ( - MOCK_S3_URI, - mock_list_dir, -) from napari_cryoet_data_portal._tests._utils import ( tree_item_children, - tree_items_names, tree_top_items, ) +from napari_cryoet_data_portal._uri_widget import GRAPHQL_URI @pytest.fixture() @@ -30,23 +25,12 @@ def test_init(qtbot: QtBot): assert not widget._progress.isVisibleTo(widget) -def test_load_lists_data(widget: ListingWidget, mocker: MockerFixture, qtbot: QtBot): - mocker.patch( - 'napari_cryoet_data_portal._listing_widget.list_dir', - mock_list_dir, - ) - mocker.patch( - 'napari_cryoet_data_portal._model.list_dir', - mock_list_dir, - ) - - with qtbot.waitSignal(widget._progress.finished): - widget.load(MOCK_S3_URI) +def test_load_lists_data(widget: ListingWidget, qtbot: QtBot): + with qtbot.waitSignal(widget._progress.finished, timeout=60000): + widget.load(GRAPHQL_URI) dataset_items = tree_top_items(widget.tree) - assert tree_items_names(dataset_items) == ('10000 (2)', '10004 (2)') - tomogram_items_10000 = tree_item_children(dataset_items[0]) - assert tree_items_names(tomogram_items_10000) == ('TS_026', 'TS_027') - tomogram_items_10001 = tree_item_children(dataset_items[1]) - assert tree_items_names(tomogram_items_10001) == ('Position_128_2', 'Position_129_2') + assert len(dataset_items) > 0 + tomogram_items = tree_item_children(dataset_items[0]) + assert len(tomogram_items) > 0 \ No newline at end of file diff --git a/src/napari_cryoet_data_portal/_tests/test_metadata_widget.py b/src/napari_cryoet_data_portal/_tests/test_metadata_widget.py index e73d220..91eabb9 100644 --- a/src/napari_cryoet_data_portal/_tests/test_metadata_widget.py +++ b/src/napari_cryoet_data_portal/_tests/test_metadata_widget.py @@ -1,13 +1,9 @@ import pytest -from pytest_mock import MockerFixture from pytestqt.qtbot import QtBot +from cryoet_data_portal import Dataset, Tomogram + from napari_cryoet_data_portal._metadata_widget import MetadataWidget -from napari_cryoet_data_portal._tests._mocks import ( - MOCK_DATASET_10000, - MOCK_TOMOGRAM_TS_026, - mock_read_json, -) from napari_cryoet_data_portal._tests._utils import tree_top_items @@ -26,39 +22,17 @@ def test_init(qtbot: QtBot): assert not widget._progress.isVisibleTo(widget) -def test_load_dataset_lists_metadata(widget: MetadataWidget, mocker: MockerFixture, qtbot: QtBot): - mocker.patch( - 'napari_cryoet_data_portal._metadata_widget.read_json', - mock_read_json, - ) - +def test_load_dataset_lists_metadata(widget: MetadataWidget, dataset: Dataset, qtbot: QtBot): with qtbot.waitSignal(widget._progress.finished): - widget.load(MOCK_DATASET_10000) + widget.load(dataset) items = tree_top_items(widget._main.tree) - - assert len(items) == 3 - assert items[0].text(0) == 'dataset_title' - assert items[0].text(1) == 'mock dataset' - assert items[1].text(0) == 'authors' - assert items[2].text(0) == 'organism' + assert len(items) > 0 -def test_load_tomogram_lists_metadata(widget: MetadataWidget, mocker: MockerFixture, qtbot: QtBot): - mocker.patch( - 'napari_cryoet_data_portal._metadata_widget.read_json', - mock_read_json, - ) - +def test_load_tomogram_lists_metadata(widget: MetadataWidget, tomogram: Tomogram, qtbot: QtBot): with qtbot.waitSignal(widget._progress.finished): - widget.load(MOCK_TOMOGRAM_TS_026) + widget.load(tomogram) items = tree_top_items(widget._main.tree) - - assert len(items) == 3 - assert items[0].text(0) == 'run_name' - assert items[0].text(1) == 'TS_026' - assert items[1].text(0) == 'voxel_spacing' - assert items[1].text(1) == '13.48' - assert items[2].text(0) == 'size' - \ No newline at end of file + assert len(items) > 0 \ No newline at end of file diff --git a/src/napari_cryoet_data_portal/_tests/test_model.py b/src/napari_cryoet_data_portal/_tests/test_model.py deleted file mode 100644 index 559e61c..0000000 --- a/src/napari_cryoet_data_portal/_tests/test_model.py +++ /dev/null @@ -1,25 +0,0 @@ -from napari_cryoet_data_portal._model import PORTAL_S3_URI, Dataset, Tomogram - - -def test_dataset_from_data_path_and_name(): - dataset = Dataset.from_data_path_and_name(PORTAL_S3_URI, "10000") - - assert dataset.name == "10000" - assert dataset.path == f"{PORTAL_S3_URI}/10000" - assert len(dataset.tomograms) > 0 - tomogram_names = tuple(s.name for s in dataset.tomograms) - assert "TS_026" in tomogram_names - - -def test_tomogram_from_dataset_path_and_name(): - dataset_dir = f"{PORTAL_S3_URI}/10000" - - tomogram = Tomogram.from_dataset_path_and_name(dataset_dir, "TS_026") - - assert tomogram.name == "TS_026" - assert tomogram.path == f"{dataset_dir}/TS_026" - assert ( - tomogram.image_path - == f"{dataset_dir}/TS_026/Tomograms/VoxelSpacing13.48/CanonicalTomogram/TS_026.zarr" - ) - assert len(tomogram.annotation_paths) > 0 diff --git a/src/napari_cryoet_data_portal/_tests/test_open_widget.py b/src/napari_cryoet_data_portal/_tests/test_open_widget.py index 4d2e205..8aa1b8f 100644 --- a/src/napari_cryoet_data_portal/_tests/test_open_widget.py +++ b/src/napari_cryoet_data_portal/_tests/test_open_widget.py @@ -1,14 +1,10 @@ from napari.components import ViewerModel import pytest -from pytest_mock import MockerFixture from pytestqt.qtbot import QtBot +from cryoet_data_portal import Tomogram + from napari_cryoet_data_portal._open_widget import OpenWidget -from napari_cryoet_data_portal._tests._mocks import ( - MOCK_TOMOGRAM_TS_026, - mock_read_points_annotations_json, - mock_read_tomogram_ome_zarr, -) @pytest.fixture() @@ -30,13 +26,11 @@ def test_init(viewer_model: ViewerModel, qtbot: QtBot): assert not widget._progress.isVisibleTo(widget) -def test_set_tomogram_adds_layers_to_viewer(widget: OpenWidget, mocker: MockerFixture, qtbot: QtBot): - mocker.patch('napari_cryoet_data_portal._open_widget.read_tomogram_ome_zarr', mock_read_tomogram_ome_zarr) - mocker.patch('napari_cryoet_data_portal._open_widget.read_points_annotations_json', mock_read_points_annotations_json) +def test_set_tomogram_adds_layers_to_viewer(widget: OpenWidget, tomogram: Tomogram, qtbot: QtBot): assert len(widget._viewer.layers) == 0 - with qtbot.waitSignal(widget._progress.finished): - widget.setTomogram(MOCK_TOMOGRAM_TS_026) + with qtbot.waitSignal(widget._progress.finished, timeout=30000): + widget.setTomogram(tomogram) assert len(widget._viewer.layers) == 3 \ No newline at end of file diff --git a/src/napari_cryoet_data_portal/_tests/test_reader.py b/src/napari_cryoet_data_portal/_tests/test_reader.py index 2cee498..dbfaf1b 100644 --- a/src/napari_cryoet_data_portal/_tests/test_reader.py +++ b/src/napari_cryoet_data_portal/_tests/test_reader.py @@ -2,15 +2,16 @@ from napari import Viewer from napari.layers import Points +from cryoet_data_portal import Annotation, Client from napari_cryoet_data_portal import ( - read_points_annotations_json, + read_points_annotations_ndjson, read_tomogram_ome_zarr, ) -from napari_cryoet_data_portal._model import PORTAL_S3_URI -TOMOGRAM_DIR = f"{PORTAL_S3_URI}/10000/TS_026/Tomograms/VoxelSpacing13.48" -ANNOTATION_FILE = f"{TOMOGRAM_DIR}/Annotations/sara_goetz-ribosome-1.0.json" +CLOUDFRONT_URI = "https://files.cryoetdataportal.cziscience.com" +TOMOGRAM_DIR = f"{CLOUDFRONT_URI}/10000/TS_026/Tomograms/VoxelSpacing13.48" +ANNOTATION_FILE = f"{TOMOGRAM_DIR}/Annotations/sara_goetz-ribosome-1.0.ndjson" def test_read_tomogram_ome_zarr(): @@ -26,15 +27,14 @@ def test_read_tomogram_ome_zarr(): assert layer_type == "image" -def test_read_points_annotations_json(): - data, attrs, layer_type = read_points_annotations_json(ANNOTATION_FILE) +def test_read_points_annotations_ndjson(): + data, attrs, layer_type = read_points_annotations_ndjson(ANNOTATION_FILE) assert len(data) == 838 assert data[0] == (469, 261, 517) assert data[418] == (524, 831, 475) assert data[837] == (519, 723, 538) - assert attrs["name"] == "Ribosome" - assert attrs["face_color"] == "red" + assert attrs["name"] == "annotations" assert layer_type == "points" diff --git a/src/napari_cryoet_data_portal/_tests/test_uri_widget.py b/src/napari_cryoet_data_portal/_tests/test_uri_widget.py index d57efb8..1366c20 100644 --- a/src/napari_cryoet_data_portal/_tests/test_uri_widget.py +++ b/src/napari_cryoet_data_portal/_tests/test_uri_widget.py @@ -1,12 +1,7 @@ import pytest -from pytest_mock import MockerFixture from pytestqt.qtbot import QtBot -from napari_cryoet_data_portal._uri_widget import UriWidget -from napari_cryoet_data_portal._tests._mocks import ( - MOCK_S3_URI, - mock_path_exists, -) +from napari_cryoet_data_portal._uri_widget import GRAPHQL_URI, UriWidget @pytest.fixture() @@ -21,36 +16,26 @@ def test_init(qtbot: QtBot): qtbot.add_widget(widget) assert widget._connect_button.isVisibleTo(widget) - assert widget._choose_dir_button.isVisibleTo(widget) assert widget._uri_edit.isVisibleTo(widget) assert not widget._disconnect_button.isVisibleTo(widget) assert not widget._progress.isVisibleTo(widget) -def test_click_connect_when_uri_exists(widget: UriWidget, qtbot: QtBot, mocker: MockerFixture): - mocker.patch( - 'napari_cryoet_data_portal._uri_widget.path_exists', - mock_path_exists, - ) - widget._uri_edit.setText(MOCK_S3_URI) +def test_click_connect_when_uri_exists(widget: UriWidget, qtbot: QtBot): + widget._uri_edit.setText(GRAPHQL_URI) with qtbot.waitSignal(widget.connected): widget._connect_button.click() assert not widget._connect_button.isVisibleTo(widget) - assert not widget._choose_dir_button.isVisibleTo(widget) assert widget._uri_edit.isVisibleTo(widget) - assert widget._uri_edit.isReadOnly() assert widget._disconnect_button.isVisibleTo(widget) assert not widget._progress.isVisibleTo(widget) -def test_click_connect_when_uri_does_not_exist(widget: UriWidget, qtbot: QtBot, mocker: MockerFixture): - mocker.patch( - 'napari_cryoet_data_portal._uri_widget.path_exists', - mock_path_exists, - ) - widget._uri_edit.setText('s3://mock-bad-uri') +@pytest.mark.skip(reason="https://github.com/chanzuckerberg/cryoet-data-portal/issues/16") +def test_click_connect_when_uri_does_not_exist(widget: UriWidget, qtbot: QtBot): + widget._uri_edit.setText("https://not.a.graphl.url/v1/graphql") with qtbot.captureExceptions() as exceptions: with qtbot.waitSignal(widget._progress.finished): @@ -59,36 +44,18 @@ def test_click_connect_when_uri_does_not_exist(widget: UriWidget, qtbot: QtBot, assert exceptions[0][0] is ValueError assert widget._connect_button.isVisibleTo(widget) - assert widget._choose_dir_button.isVisibleTo(widget) assert widget._uri_edit.isVisibleTo(widget) - assert not widget._uri_edit.isReadOnly() assert not widget._disconnect_button.isVisibleTo(widget) assert not widget._progress.isVisibleTo(widget) def test_click_disconnect(widget: UriWidget, qtbot: QtBot): - widget._onUriChecked((True, MOCK_S3_URI)) + widget._onConnected(GRAPHQL_URI) with qtbot.waitSignal(widget.disconnected): widget._disconnect_button.click() assert widget._connect_button.isVisibleTo(widget) - assert widget._choose_dir_button.isVisibleTo(widget) assert widget._uri_edit.isVisibleTo(widget) - assert not widget._uri_edit.isReadOnly() assert not widget._disconnect_button.isVisibleTo(widget) - assert not widget._progress.isVisibleTo(widget) - - -def test_choose_dir_button_clicked(widget: UriWidget, mocker: MockerFixture): - mock_dir = '/path/to/test' - def mock_get_existing_directory(_): - return mock_dir - mocker.patch( - 'napari_cryoet_data_portal._uri_widget.QFileDialog.getExistingDirectory', - mock_get_existing_directory, - ) - - widget._choose_dir_button.click() - - assert widget._uri_edit.text() == mock_dir \ No newline at end of file + assert not widget._progress.isVisibleTo(widget) \ No newline at end of file diff --git a/src/napari_cryoet_data_portal/_tests/test_widget.py b/src/napari_cryoet_data_portal/_tests/test_widget.py index f220bdc..2643313 100644 --- a/src/napari_cryoet_data_portal/_tests/test_widget.py +++ b/src/napari_cryoet_data_portal/_tests/test_widget.py @@ -1,6 +1,7 @@ from typing import Callable, List, Tuple import pytest +from cryoet_data_portal import Client from napari import Viewer from napari.components import ViewerModel from pytest_mock import MockerFixture @@ -8,6 +9,7 @@ from qtpy.QtWidgets import QWidget from napari_cryoet_data_portal import DataPortalWidget +from napari_cryoet_data_portal._uri_widget import GRAPHQL_URI @pytest.fixture() @@ -60,11 +62,10 @@ def test_listing_item_changed_to_none(widget: DataPortalWidget): def test_connected_loads_listing(widget: DataPortalWidget, mocker: MockerFixture): mocker.patch.object(widget._listing, 'load') - uri = 's3://mock-portal' - widget._uri.connected.emit(uri) + widget._uri.connected.emit(GRAPHQL_URI) - widget._listing.load.assert_called_once_with(uri) + widget._listing.load.assert_called_once_with(GRAPHQL_URI) def test_disconnected_only_shows_uri(widget: DataPortalWidget): diff --git a/src/napari_cryoet_data_portal/_uri_widget.py b/src/napari_cryoet_data_portal/_uri_widget.py index b0330ed..d799181 100644 --- a/src/napari_cryoet_data_portal/_uri_widget.py +++ b/src/napari_cryoet_data_portal/_uri_widget.py @@ -1,27 +1,26 @@ -from typing import Optional, Tuple +from typing import Optional from qtpy.QtCore import Signal from qtpy.QtWidgets import ( - QFileDialog, QGroupBox, QHBoxLayout, QLineEdit, QPushButton, - QStyle, QVBoxLayout, QWidget, ) +from cryoet_data_portal import Client -from napari_cryoet_data_portal._io import path_exists from napari_cryoet_data_portal._logging import logger -from napari_cryoet_data_portal._model import PORTAL_S3_URI from napari_cryoet_data_portal._progress_widget import ProgressWidget +GRAPHQL_URI = "https://graphql.cryoetdataportal.cziscience.com/v1/graphql" + class UriWidget(QGroupBox): """Connects to a data portal with a specific URI.""" - # Emitted on successful connection to the URI it contains. + # Emitted on successful connection with the URI. connected = Signal(str) # Emitted when disconnecting from the portal. disconnected = Signal() @@ -32,22 +31,21 @@ def __init__(self, parent: Optional[QWidget] = None) -> None: self.setTitle("Portal") self._connect_button = QPushButton("Connect") self._disconnect_button = QPushButton("Disconnect") - self._uri_edit = QLineEdit(PORTAL_S3_URI) + self._uri_edit = QLineEdit(GRAPHQL_URI) + # Only allow the default portal URI because invalid ones will cause + # indefinite hangs: + # https://github.com/chanzuckerberg/cryoet-data-portal/issues/16 + self._uri_edit.setReadOnly(True) self._uri_edit.setCursorPosition(0) self._uri_edit.setPlaceholderText("Enter a URI to CryoET portal data") - choose_dir_icon = self.style().standardIcon( - QStyle.StandardPixmap.SP_DirOpenIcon - ) - self._choose_dir_button = QPushButton(choose_dir_icon, "") self._progress: ProgressWidget = ProgressWidget( - work=self._checkUri, - returnCallback=self._onUriChecked, + work=self._connect, + returnCallback=self._onConnected, ) self._updateVisibility(False) self._connect_button.clicked.connect(self._onConnectClicked) self._disconnect_button.clicked.connect(self._onDisconnectClicked) - self._choose_dir_button.clicked.connect(self._onChooseDirClicked) self._uri_edit.returnPressed.connect(self._onConnectClicked) control_layout = QHBoxLayout() @@ -55,7 +53,6 @@ def __init__(self, parent: Optional[QWidget] = None) -> None: control_layout.addWidget(self._connect_button) control_layout.addWidget(self._disconnect_button) control_layout.addWidget(self._uri_edit) - control_layout.addWidget(self._choose_dir_button) layout = QVBoxLayout() layout.addLayout(control_layout) @@ -74,27 +71,16 @@ def _onDisconnectClicked(self) -> None: self._updateVisibility(False) self.disconnected.emit() - def _checkUri(self, uri: str) -> Tuple[bool, str]: - logger.debug("UriWidget._checkUri: %s", uri) - return path_exists(uri), uri + def _connect(self, uri: str) -> str: + _ = Client(uri) + return uri - def _onUriChecked(self, result: Tuple[bool, str]) -> None: - logger.debug("UriWidget._onUriChecked: %s", result) - exists, uri = result - self._updateVisibility(exists) - if exists: - self.connected.emit(uri) - else: - raise ValueError(f"CryoET data portal not found at: {uri}") + def _onConnected(self, uri: str) -> None: + logger.debug("UriWidget._onConnected: %s", uri) + self._updateVisibility(True) + self.connected.emit(uri) def _updateVisibility(self, uri_exists: bool) -> None: logger.debug("UriWidget._updateVisibility: %s", uri_exists) self._connect_button.setVisible(not uri_exists) - self._choose_dir_button.setVisible(not uri_exists) self._disconnect_button.setVisible(uri_exists) - self._uri_edit.setReadOnly(uri_exists) - - def _onChooseDirClicked(self) -> None: - logger.debug("DataPathWidget._onChooseDirClicked") - path = QFileDialog.getExistingDirectory(self) - self._uri_edit.setText(path) diff --git a/src/napari_cryoet_data_portal/_widget.py b/src/napari_cryoet_data_portal/_widget.py index 1e65c18..00c98f9 100644 --- a/src/napari_cryoet_data_portal/_widget.py +++ b/src/napari_cryoet_data_portal/_widget.py @@ -6,11 +6,11 @@ QVBoxLayout, QWidget, ) +from cryoet_data_portal import Client, Tomogram from napari_cryoet_data_portal._listing_widget import ListingWidget from napari_cryoet_data_portal._logging import logger from napari_cryoet_data_portal._metadata_widget import MetadataWidget -from napari_cryoet_data_portal._model import Tomogram from napari_cryoet_data_portal._open_widget import OpenWidget from napari_cryoet_data_portal._uri_widget import UriWidget @@ -67,6 +67,7 @@ def __init__( def _onUriConnected(self, uri: str) -> None: logger.debug("DataPortalWidget._onUriConnected") + self._open.setUri(uri) self._listing.load(uri) def _onUriDisconnected(self) -> None: diff --git a/src/napari_cryoet_data_portal/napari.yaml b/src/napari_cryoet_data_portal/napari.yaml index bc0de2b..4bbb584 100644 --- a/src/napari_cryoet_data_portal/napari.yaml +++ b/src/napari_cryoet_data_portal/napari.yaml @@ -24,7 +24,7 @@ contributions: accepts_directories: true - command: napari-cryoet-data-portal.points_annotations_reader filename_patterns: - - '*.json' + - '*.ndjson' accepts_directories: false sample_data: - key: tomogram-10000-ts-026