Skip to content
Draft
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,102 changes: 862 additions & 240 deletions pixi.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ dependencies = [
"napari",
"qtpy",
"scikit-image",
"tiled[array,minimal-client]",
"tiled[array,client]>=0.2.0",
"bluesky-tiled-plugins>=2.0.0rc3",
]

[project.optional-dependencies]
Expand Down
17 changes: 13 additions & 4 deletions src/napari_tiled_browser/models/tiled_selector.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from httpx import ConnectError
from qtpy.QtCore import QObject, Signal
from tiled.client import from_uri
from tiled.client.array import ArrayClient
from tiled.client.base import BaseClient
from tiled.queries import FullText, Key, Regex
from tiled.structures.core import StructureFamily
Expand Down Expand Up @@ -45,6 +46,11 @@ class TiledSelectorSignals(QObject):
str, # Error message
name="TiledSelector.client_connection_error",
)
plottable_data_received = Signal(
ArrayClient, # node
str, # child_node_path
name="TiledSelector.plottable_data_received",
)
table_changed = Signal(
tuple, # New node path parts, tuple of strings
name="TiledSelector.table_changed",
Expand Down Expand Up @@ -89,6 +95,7 @@ def __init__(
self.signals = self.Signals(parent)
self.client_connected = self.signals.client_connected
self.client_connection_error = self.signals.client_connection_error
self.plottable_data_received = self.signals.plottable_data_received
self.table_changed = self.signals.table_changed
self.url_changed = self.signals.url_changed
self.url_validation_error = self.signals.url_validation_error
Expand Down Expand Up @@ -284,11 +291,12 @@ def get_parent_node(self, node_path_parts: tuple[str]) -> list:
# even if there is only one item in the tuple
# This may change in the future when the capability to pass a list
# of uids to tiled is removed
if node_path_parts:
return self.client[node_path_parts]
client = self.client
# Walk down one node at a time (slow, but safe).
for segment in node_path_parts:
client = client[segment]

# An empty tuple indicates the root node
return self.client
return client

# @functools.lru_cache(maxsize=1)
def get_node(self, node_path_parts: tuple[str], node_offset: int) -> list:
Expand Down Expand Up @@ -342,6 +350,7 @@ def open_node(self, child_node_path: str) -> None:

if family == StructureFamily.array:
_logger.info("Found array, plotting TODO")
self.plottable_data_received.emit(node, child_node_path)
elif family == StructureFamily.container:
_logger.debug("Entering container: %s", child_node_path)
self.enter_node(child_node_path)
Expand Down
60 changes: 60 additions & 0 deletions src/napari_tiled_browser/models/tiled_subscriber.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from qtpy.QtCore import QObject, QRunnable, QThread, QThreadPool, Signal


class QtExecutor:
def __init__(self):
self.threadpool = QThreadPool.globalInstance()

def submit(self, f, *args):
print(" In QtExecutor.submit")
runnable = QRunnable.create(lambda: f(*args))
self.threadpool.start(runnable)

def shutdown(self, wait: bool = True):
pass


class TiledSubscriberSignals(QObject):
finished = Signal()
results = Signal(object)


class TiledSubscriber(QThread):
def __init__(
self,
client,
**kwargs,
):
super().__init__(**kwargs)
self.signals = TiledSubscriberSignals()
self.client = client
print("In TiledSubscriber.init")
print(self.client)
self.sub = self.client.subscribe(QtExecutor())

def run(self):
self.sub.start()
# this still gives 500 error


def on_new_child(update):
"A new child node has been created in a container."
child = update.child()
print(child)
ts = TiledSubscriber(child)
# Is the child also a container?
if child.structure_family == "container":
# Recursively subscribe to the children of this new container.
ts.sub.child_created.add_callback(on_new_child)
else:
# Subscribe to data updates (i.e. appended table rows or array slices).
ts.sub.new_data.add_callback(on_new_data)
# Launch the subscription.
# Ask the server to replay from the very first update, if we already
# missed some.
ts.run()


def on_new_data(update):
"Data has been updated (maybe appended) to an array or table."
print(update.data())
171 changes: 31 additions & 140 deletions src/napari_tiled_browser/qt/tiled_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from tiled.structures.core import StructureFamily

from napari_tiled_browser.models.tiled_selector import TiledSelector
from napari_tiled_browser.models.tiled_subscriber import TiledSubscriber, on_new_child
from napari_tiled_browser.models.tiled_worker import TiledWorker
from napari_tiled_browser.qt.tiled_search import QTiledSearchWidget

Expand Down Expand Up @@ -78,7 +79,8 @@ def __init__(self, napari_viewer):
super().__init__()
self.viewer = napari_viewer

self.model = TiledSelector(url="https://tiled.nsls2.bnl.gov/api")
# TODO: try using TILED_DEFAULT_PROFILE here?
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think a sensible order of preference here could be:

  1. TILED_PROFILE
  2. TILED_DEFAULT_URL
  3. tiled.profiles.get_default_profile_name()

self.model = TiledSelector()

self.thread_pool = QThreadPool.globalInstance()

Expand Down Expand Up @@ -146,6 +148,8 @@ def create_layout(self):
self.catalog_table.setSelectionMode(
QAbstractItemView.SelectionMode.SingleSelection
) # disable multi-select
self.catalog_live_button = QPushButton("LIVE")
self.catalog_live_button.setCheckable(True)
self.catalog_table_widget = QWidget()
self.catalog_breadcrumbs = None

Expand All @@ -163,6 +167,7 @@ def create_layout(self):

# Catalog table layout
catalog_table_layout = QVBoxLayout()
catalog_table_layout.addWidget(self.catalog_live_button)
catalog_table_layout.addWidget(self.current_path_widget)
catalog_table_layout.addLayout(catalog_info_layout)
catalog_table_layout.addWidget(self.navigation_widget)
Expand Down Expand Up @@ -247,6 +252,12 @@ def fetch_table_data(self):
runnable.signals.results.connect(self.populate_table)
self.thread_pool.start(runnable)

def subscribe_to_table_data(self):
catalog = self.model.client[self.model.node_path_parts]
self.sub_thread = TiledSubscriber(catalog)
self.sub_thread.sub.child_created.add_callback(on_new_child)
self.sub_thread.run()

def populate_table(self, results):
_logger.debug("QTiledBrowser.populate_table()...")
original_state = {}
Expand Down Expand Up @@ -333,10 +344,16 @@ def on_table_changed(node_path_parts: tuple[str]):
return
# self._set_current_location_label()
self.fetch_table_data()
# self.subscribe_to_table_data()
self._rebuild_current_path_layout()

self.model.url_changed.connect(self.reset_url_entry)

@self.model.plottable_data_received.connect
def on_plottable_data_received(node, child_node_path):
layer = self.viewer.add_image(node, name=child_node_path)
layer.reset_contrast_limits()

def connect_model_slots(self):
"""Connect model slots to dialog signals."""
_logger.debug("QTiledBrowser.connect_model_slots()...")
Expand All @@ -355,15 +372,10 @@ def connect_model_slots(self):
)

def connect_self_signals(self):
# self.connect_button.clicked.connect(self._on_connect_clicked)
# self.previous_page.clicked.connect(self._on_prev_page_clicked)
# self.next_page.clicked.connect(self._on_next_page_clicked)

# self.rows_per_page_selector.currentTextChanged.connect(
# self._on_rows_per_page_changed
# )

self.load_button.clicked.connect(self._on_load)
self.catalog_live_button.clicked.connect(
self._on_catalog_live_button_clicked
)

# self.catalog_table.itemDoubleClicked.connect(
# self._on_item_double_click
Expand All @@ -374,48 +386,16 @@ def initialize_values(self):
self.reset_url_entry()
self.reset_rows_per_page()

# def set_root(self, root):
# self.root = root
# self.node_path = ()
# self._current_page = 0
# if root is not None:
# self.catalog_table_widget.setVisible(True)
# self._rebuild()

# def get_current_node(self):
# return self.get_node(self.node_path)

# # @functools.lru_cache(maxsize=1)
# def get_node(self, node_path):
# if node_path:
# return self.root[node_path]
# return self.root

# def enter_node(self, node_id):
# self.node_path += (node_id,)
# self._current_page = 0
# self._rebuild()

# def exit_node(self):
# self.node_path = self.node_path[:-1]
# self._current_page = 0
# self._rebuild()

# def open_node(self, node_id):
# node = self.get_current_node()[node_id]
# family = node.item["attributes"]["structure_family"]
# # TODO: make this dictionary with StructureFamily type as key
# # and action for StructureFamily as value
# if isinstance(node, DummyClient):
# show_info(f"Cannot open type: '{family}'")
# return
# if family == StructureFamily.array:
# layer = self.viewer.add_image(node, name=node_id)
# layer.reset_contrast_limits()
# elif family == StructureFamily.container:
# self.enter_node(node_id)
# else:
# show_info(f"Type not supported:'{family}")
def _on_catalog_live_button_clicked(self):
# TODO: add check for CatalogOfBlueskyRuns and enable/disable live button as needed
# subscribe to table data if live button checked
if self.catalog_live_button.isChecked():
self.subscribe_to_table_data()
else:
# cleanup subscriptions
if self.sub_thread.isRunning():
self.sub_thread.quit()
self.sub_thread.wait()

def _on_load(self):
selected = self.catalog_table.selectedItems()
Expand All @@ -430,12 +410,6 @@ def _on_load(self):
def _on_breadcrumb_clicked(self, node_index):
self.model.jump_to_node(node_index)

# def _on_rows_per_page_changed(self, value):
# self._rows_per_page = int(value)
# self._current_page = 0
# self._rebuild_table()
# self._set_current_location_label()

# def _on_item_double_click(self, item):
# if item is self.catalog_breadcrumbs:
# self.exit_node()
Expand All @@ -458,89 +432,6 @@ def _clear_metadata(self):
self.info_box.setText("")
# self.load_button.setEnabled(False)

# def _rebuild_current_path_label(self):
# path = ["root"]
# for node_id in self.node_path:
# if len(node_id) > self.NODE_ID_MAXLEN:
# node_id = node_id[: self.NODE_ID_MAXLEN - 3] + "..."
# path.append(node_id)
# path.append("")

# self.current_path_label.setText(" / ".join(path))

# def _rebuild_table(self):
# prev_block = self.catalog_table.blockSignals(True)
# # Remove all rows first
# while self.catalog_table.rowCount() > 0:
# self.catalog_table.removeRow(0)

# if self.node_path:
# # add breadcrumbs
# self.catalog_breadcrumbs = QTableWidgetItem("..")
# self.catalog_table.insertRow(0)
# self.catalog_table.setItem(0, 0, self.catalog_breadcrumbs)

# # Then add new rows
# for _ in range(self._rows_per_page):
# last_row_position = self.catalog_table.rowCount()
# self.catalog_table.insertRow(last_row_position)
# node_offset = self._rows_per_page * self._current_page
# # Fetch a page of keys.
# items = self.get_current_node().items()[
# node_offset : node_offset + self._rows_per_page
# ]
# # Loop over rows, filling in keys until we run out of keys.
# start = 1 if self.node_path else 0
# for row_index, (key, value) in zip(
# range(start, self.catalog_table.rowCount()), items, strict=False
# ):
# family = value.item["attributes"]["structure_family"]
# if family == StructureFamily.container:
# icon = self.style().standardIcon(QStyle.SP_DirHomeIcon)
# elif family == StructureFamily.array:
# icon = QIcon(QPixmap(ICONS["new_image"]))
# else:
# icon = self.style().standardIcon(
# QStyle.SP_TitleBarContextHelpButton
# )
# self.catalog_table.setItem(
# row_index, 0, QTableWidgetItem(icon, key)
# )

# # remove extra rows
# for _ in range(self._rows_per_page - len(items)):
# self.catalog_table.removeRow(self.catalog_table.rowCount() - 1)

# headers = [
# str(x + 1)
# for x in range(
# node_offset, node_offset + self.catalog_table.rowCount()
# )
# ]
# if self.node_path:
# headers = [""] + headers

# self.catalog_table.setVerticalHeaderLabels(headers)
# self._clear_metadata()
# self.catalog_table.blockSignals(prev_block)

# def _rebuild(self):
# self._rebuild_table()
# self._rebuild_current_path_label()
# self._set_current_location_label()

# def _on_prev_page_clicked(self):
# if self._current_page != 0:
# self._current_page -= 1
# self._rebuild()

# def _on_next_page_clicked(self):
# if (
# self._current_page * self._rows_per_page
# ) + self._rows_per_page < len(self.get_current_node()):
# self._current_page += 1
# self._rebuild()

def _set_current_location_label(self):
_logger.debug("QTiledBrowser._set_current_location_label()...")
starting_index = (
Expand Down
Loading