diff --git a/src/conftest.py b/src/conftest.py
index afa39a1c..acd7ab51 100644
--- a/src/conftest.py
+++ b/src/conftest.py
@@ -379,6 +379,7 @@ def filters(sim_registry):
mapping = {
"255id_testing": MapAdapter(bluesky_mapping),
+ "255bm_testing": MapAdapter(bluesky_mapping),
}
tree = MapAdapter(mapping)
@@ -389,13 +390,12 @@ def tiled_client():
app = build_app(tree)
with Context.from_app(app) as context:
client = from_context(context)
- yield client["255id_testing"]
+ yield client
@pytest.fixture()
def catalog(tiled_client):
- cat = Catalog(client=tiled_client)
- # cat = mock.AsyncMock()
+ cat = Catalog(client=tiled_client["255id_testing"])
return cat
diff --git a/src/firefly/controller.py b/src/firefly/controller.py
index 24167193..ccfd12ce 100644
--- a/src/firefly/controller.py
+++ b/src/firefly/controller.py
@@ -333,11 +333,15 @@ async def finalize_new_window(self, action):
# Send the current devices to the window
await action.window.update_devices(self.registry)
- def finalize_run_browser_window(self, action):
- """Connect up signals that are specific to the run browser window."""
+ @asyncSlot(QAction)
+ async def finalize_run_browser_window(self, action):
+ """Connect up run browser signals and load initial data."""
display = action.display
self.run_updated.connect(display.update_running_scan)
self.run_stopped.connect(display.update_running_scan)
+ # Set initial state for the run_browser
+ config = load_config()['database']['tiled']
+ await display.change_catalog(config['entry_node'])
def finalize_status_window(self, action):
"""Connect up signals that are specific to the voltmeters window."""
@@ -652,12 +656,6 @@ async def add_queue_item(self, item):
if getattr(self, "_queue_client", None) is not None:
await self._queue_client.add_queue_item(item)
- @QtCore.Slot()
- def show_sample_viewer_window(self):
- return self.show_window(
- FireflyMainWindow, ui_dir / "sample_viewer.ui", name="sample_viewer"
- )
-
@QtCore.Slot(bool)
def set_open_environment_action_state(self, is_open: bool):
"""Update the readback value for opening the queueserver environment."""
diff --git a/src/firefly/run_browser/client.py b/src/firefly/run_browser/client.py
index 86e6744b..c556ce40 100644
--- a/src/firefly/run_browser/client.py
+++ b/src/firefly/run_browser/client.py
@@ -1,3 +1,4 @@
+import asyncio
import datetime as dt
import logging
import warnings
@@ -7,6 +8,7 @@
import numpy as np
import pandas as pd
from tiled import queries
+from qasync import asyncSlot
from haven import exceptions
from haven.catalog import Catalog
@@ -16,13 +18,31 @@
class DatabaseWorker:
selected_runs: Sequence = []
+ catalog: Catalog = None
- def __init__(self, catalog=None, *args, **kwargs):
- if catalog is None:
- catalog = Catalog()
- self.catalog = catalog
+ def __init__(self, tiled_client, *args, **kwargs):
+ self.client = tiled_client
super().__init__(*args, **kwargs)
+ @asyncSlot(str)
+ async def change_catalog(self, catalog_name: str):
+ """Change the catalog being used for pulling data.
+
+ *catalog_name* should be an entry in *worker.tiled_client()*.
+ """
+ def get_catalog(name):
+ return Catalog(self.client[catalog_name])
+
+ loop = asyncio.get_running_loop()
+ self.catalog = await loop.run_in_executor(None, get_catalog, catalog_name)
+
+ async def catalog_names(self):
+ def get_names():
+ return list(self.client.keys())
+
+ loop = asyncio.get_running_loop()
+ return await loop.run_in_executor(None, get_names)
+
async def filtered_nodes(self, filters: Mapping):
case_sensitive = False
log.debug(f"Filtering nodes: {filters}")
@@ -49,6 +69,7 @@ async def filtered_nodes(self, filters: Mapping):
return runs
async def load_distinct_fields(self):
+
"""Get distinct metadata fields for filterable metadata."""
new_fields = {}
target_fields = [
diff --git a/src/firefly/run_browser/display.py b/src/firefly/run_browser/display.py
index d814ca57..302c0246 100644
--- a/src/firefly/run_browser/display.py
+++ b/src/firefly/run_browser/display.py
@@ -13,6 +13,7 @@
from qtpy.QtGui import QStandardItem, QStandardItemModel
from ophyd_async.core import Device
from ophyd import Device as ThreadedDevice
+from tiled.client.container import Container
from pydm import PyDMChannel
from firefly import display
@@ -60,16 +61,29 @@ class RunBrowserDisplay(display.FireflyDisplay):
# Counter for keeping track of UI hints for long DB hits
_busy_hinters: Counter
- def __init__(self, root_node=None, args=None, macros=None, **kwargs):
+ def __init__(self, args=None, macros=None, **kwargs):
super().__init__(args=args, macros=macros, **kwargs)
self.selected_runs = []
self._running_db_tasks = {}
self._busy_hinters = Counter()
- self.db = DatabaseWorker(catalog=root_node)
- # Load the list of all runs for the selection widget
- self.db_task(self.load_runs(), name="init_load_runs")
- # Load the list of filters' field values into the comboboxes
- self.db_task(self.update_combobox_items(), name="update_combobox_items")
+
+ async def setup_database(self, tiled_client: Container, catalog_name: str):
+ """Prepare to use a set of databases accessible through *tiled_client*.
+
+ Parameters
+ ==========
+ Each key in *tiled_client* should be """
+ self.db = DatabaseWorker(tiled_client)
+ self.ui.catalog_combobox.addItems(await self.db.catalog_names())
+ await self.change_catalog(catalog_name)
+
+ async def change_catalog(self, catalog_name: str):
+ """Activate a different catalog in the Tiled server."""
+ await self.db_task(self.db.change_catalog(catalog_name), name="change_catalog")
+ await self.db_task(asyncio.gather(
+ self.load_runs(),
+ self.update_combobox_items()
+ ), name="change_catalog")
def db_task(self, coro, name="default task"):
"""Executes a co-routine as a database task. Existing database
@@ -143,7 +157,6 @@ async def update_combobox_items(self):
""""""
with self.busy_hints(run_table=False, run_widgets=False, filter_widgets=True):
fields = await self.db.load_distinct_fields()
- print(fields)
for field_name, cb in [
("plan_name", self.ui.filter_plan_combobox),
("sample_name", self.ui.filter_sample_combobox),
@@ -247,7 +260,8 @@ async def update_devices(self, registry):
def setup_bss_channels(self, bss: Device | ThreadedDevice):
"""Setup channels to update the proposal and ESAF ID boxes."""
- self.proposal_channel.disconnect()
+ if getattr(self, "proposal_channel", None) is not None:
+ self.proposal_channel.disconnect()
self.proposal_channel = PyDMChannel(
address=f"haven://{bss.proposal.proposal_id.name}",
value_slot=partial(
@@ -256,7 +270,8 @@ def setup_bss_channels(self, bss: Device | ThreadedDevice):
checkbox=self.ui.filter_current_proposal_checkbox,
)
)
- self.esaf_channel.disconnect()
+ if getattr(self, "esaf_channel", None) is not None:
+ self.esaf_channel.disconnect()
self.esaf_channel = PyDMChannel(
address=f"haven://{bss.esaf.esaf_id.name}",
value_slot=partial(
diff --git a/src/firefly/run_browser/run_browser.ui b/src/firefly/run_browser/run_browser.ui
index 363bbe61..cd5e6def 100644
--- a/src/firefly/run_browser/run_browser.ui
+++ b/src/firefly/run_browser/run_browser.ui
@@ -36,7 +36,7 @@
-
- false
+ true
Catalog:
@@ -44,9 +44,9 @@
-
-
+
- false
+ true
@@ -223,7 +223,7 @@
0
0
373
- 560
+ 531
diff --git a/src/firefly/run_browser/tests/test_client.py b/src/firefly/run_browser/tests/test_client.py
index 209b4008..f043cdbe 100644
--- a/src/firefly/run_browser/tests/test_client.py
+++ b/src/firefly/run_browser/tests/test_client.py
@@ -3,23 +3,35 @@
from firefly.run_browser.client import DatabaseWorker
+@pytest.fixture()
+async def worker(tiled_client):
+ worker = DatabaseWorker(tiled_client)
+ await worker.change_catalog("255id_testing")
+ return worker
+
+
+@pytest.mark.asyncio
+async def test_catalog_names(worker):
+ assert (await worker.catalog_names()) == ["255id_testing", "255bm_testing"]
+
+
@pytest.mark.asyncio
-async def test_filter_runs(catalog):
- worker = DatabaseWorker(catalog=catalog)
+async def test_filter_runs(worker):
runs = await worker.load_all_runs(filters={"plan": "xafs_scan"})
# Check that the runs were filtered
assert len(runs) == 1
@pytest.mark.asyncio
-async def test_distinct_fields(catalog):
- worker = DatabaseWorker(catalog=catalog)
+async def test_distinct_fields(worker):
distinct_fields = await worker.load_distinct_fields()
# Check that the dictionary has the right structure
for key in ["sample_name"]:
assert key in distinct_fields.keys()
+
+
# -----------------------------------------------------------------------------
# :author: Mark Wolfman
# :email: wolfman@anl.gov
diff --git a/src/firefly/run_browser/tests/test_display.py b/src/firefly/run_browser/tests/test_display.py
index 4d439ea0..8dbdba29 100644
--- a/src/firefly/run_browser/tests/test_display.py
+++ b/src/firefly/run_browser/tests/test_display.py
@@ -21,7 +21,7 @@ def bss(sim_registry):
@pytest.fixture()
-async def display(qtbot, catalog, mocker):
+async def display(qtbot, tiled_client, catalog, mocker):
mocker.patch(
"firefly.run_browser.widgets.ExportDialog.exec_",
return_value=QFileDialog.Accepted,
@@ -31,20 +31,16 @@ async def display(qtbot, catalog, mocker):
return_value=["/net/s255data/export/test_file.nx"],
)
mocker.patch("firefly.run_browser.client.DatabaseWorker.export_runs")
- display = RunBrowserDisplay(root_node=catalog)
+ display = RunBrowserDisplay()
qtbot.addWidget(display)
display.clear_filters()
# Wait for the initial database load to process
- await display._running_db_tasks["init_load_runs"]
- await display._running_db_tasks["update_combobox_items"]
+ await display.setup_database(tiled_client, catalog_name="255id_testing")
# Set up some fake data
run = [run async for run in catalog.values()][0]
display.db.selected_runs = [run]
await display.update_1d_signals()
run_data = await run.data()
- expected_xdata = run_data.energy_energy
- expected_ydata = np.log(run_data.I0_net_counts / run_data.It_net_counts)
- expected_ydata = np.gradient(expected_ydata, expected_xdata)
# Set the controls to describe the data we want to test
x_combobox = display.ui.signal_x_combobox
x_combobox.addItem("energy_energy")
@@ -58,7 +54,6 @@ async def display(qtbot, catalog, mocker):
return display
-@pytest.mark.asyncio
async def test_db_task(display):
async def test_coro():
return 15
@@ -67,7 +62,6 @@ async def test_coro():
assert result == 15
-@pytest.mark.asyncio
async def test_db_task_interruption(display):
async def test_coro(sleep_time):
await asyncio.sleep(sleep_time)
@@ -90,7 +84,6 @@ def test_load_runs(display):
assert display.ui.runs_total_label.text() == str(display.runs_model.rowCount())
-@pytest.mark.asyncio
async def test_update_selected_runs(display):
# Change the proposal item
item = display.runs_model.item(0, 1)
@@ -102,7 +95,6 @@ async def test_update_selected_runs(display):
assert len(display.db.selected_runs) > 0
-@pytest.mark.asyncio
async def test_update_selected_runs(display):
# Change the proposal item
item = display.runs_model.item(0, 1)
@@ -120,7 +112,6 @@ async def test_clear_plots(display):
assert display.plot_1d_view.clear_runs.called
-@pytest.mark.asyncio
async def test_metadata(display):
# Change the proposal item
display.ui.run_tableview.selectRow(0)
@@ -130,7 +121,6 @@ async def test_metadata(display):
assert "xafs_scan" in text
-@pytest.mark.asyncio
async def test_1d_plot_signals(catalog, display):
# Check that the 1D plot was created
plot_widget = display.ui.plot_1d_view
@@ -152,8 +142,7 @@ async def test_1d_plot_signals(catalog, display):
# Warns: Task was destroyed but it is pending!
-@pytest.mark.asyncio
-async def test_1d_plot_signal_memory(catalog, display):
+async def test_1d_plot_signal_memory(display):
"""Do we remember the signals that were previously selected."""
# Check that the 1D plot was created
plot_widget = display.ui.plot_1d_view
@@ -174,7 +163,6 @@ async def test_1d_plot_signal_memory(catalog, display):
# Warns: Task was destroyed but it is pending!
-@pytest.mark.asyncio
async def test_1d_hinted_signals(catalog, display):
display.ui.plot_1d_hints_checkbox.setChecked(True)
# Check that the 1D plot was created
@@ -196,7 +184,6 @@ async def test_1d_hinted_signals(catalog, display):
), f"unhinted signal found in {combobox.objectName()}."
-@pytest.mark.asyncio
async def test_update_1d_plot(catalog, display):
display.plot_1d_view.plot_runs = MagicMock()
display.plot_1d_view.autoRange = MagicMock()
@@ -233,7 +220,6 @@ async def test_update_running_scan(display):
# Warns: Task was destroyed but it is pending!
-@pytest.mark.asyncio
async def test_2d_plot_signals(catalog, display):
# Check that the 1D plot was created
plot_widget = display.ui.plot_2d_view
@@ -248,7 +234,6 @@ async def test_2d_plot_signals(catalog, display):
assert combobox.findText("It_net_counts") > -1
-@pytest.mark.asyncio
async def test_update_2d_plot(catalog, display):
display.plot_2d_item.setRect = MagicMock()
# Load test data
@@ -279,7 +264,6 @@ async def test_update_2d_plot(catalog, display):
display.plot_2d_item.setRect.assert_called_with(-100, -80, 200, 160)
-@pytest.mark.asyncio
async def test_update_multi_plot(catalog, display):
run = await catalog["7d1daf1d-60c7-4aa7-a668-d1cd97e5335f"]
expected_xdata = await run["energy_energy"]
@@ -353,7 +337,6 @@ def test_busy_hints_multiple(display):
assert display.ui.detail_tabwidget.isEnabled()
-@pytest.mark.asyncio
async def test_update_combobox_items(display):
"""Check that the comboboxes get the distinct filter fields."""
assert display.ui.filter_plan_combobox.count() > 0
@@ -366,7 +349,6 @@ async def test_update_combobox_items(display):
assert display.ui.filter_beamline_combobox.count() > 0
-@pytest.mark.asyncio
async def test_export_button_enabled(catalog, display):
assert not display.export_button.isEnabled()
# Update the list with 1 run and see if the control gets enabled
@@ -380,7 +362,6 @@ async def test_export_button_enabled(catalog, display):
assert not display.export_button.isEnabled()
-@pytest.mark.asyncio
async def test_export_button_clicked(catalog, display, mocker, qtbot):
# Set up a run to be tested against
run = MagicMock()
@@ -454,6 +435,12 @@ def test_update_bss_filters(display):
checkbox.setChecked(False)
update_slot("99531")
assert combobox.currentText() == "89321"
+
+
+def test_catalog_choices(display, tiled_client):
+ combobox = display.ui.catalog_combobox
+ items = [combobox.itemText(idx) for idx in range(combobox.count())]
+ assert items == ["255id_testing", "255bm_testing"]
# -----------------------------------------------------------------------------
# :author: Mark Wolfman
diff --git a/src/haven/catalog.py b/src/haven/catalog.py
index 7ec4185a..453d05e3 100644
--- a/src/haven/catalog.py
+++ b/src/haven/catalog.py
@@ -309,6 +309,11 @@ class Catalog:
are structured, so can make some assumptions and takes care of
boiler-plate code (e.g. reshaping maps, etc).
+ Parameters
+ ==========
+ client
+ A Tiled client that has scan UIDs as its keys.
+
"""
_client = None