Repository: duqtools/duqtools

Issue:
duqtools dash no longer working on Gateway
I receive the following error when running `duqtools dash` on the Gateway, using the `firefox/latest` module for the browser.
```
Traceback (most recent call last):
  File "/afs/eufus.eu/user/g/g2aho/.local/lib/python3.10/site-packages/streamlit/runtime/scriptrunner/script_runner.py", line 565, in _run_script
    exec(code, module.__dict__)
  File "/afs/eufus.eu/g2itmuse/user/g2aho/gitrepos/fusion-dUQtools/src/duqtools/dashboard/Plotting.py", line 5, in <module>
    from _shared import add_sidebar_logo, default_workdir, get_dataset
  File "/afs/eufus.eu/g2itmuse/user/g2aho/gitrepos/fusion-dUQtools/src/duqtools/dashboard/_shared.py", line 28, in <module>
    @st.cache_data
AttributeError: module 'streamlit' has no attribute 'cache_data'
```

I am running the command on a directory without a `data.csv` or a `runs.yaml`. Does this affect anything? Or perhaps I am missing something else?

Discussion:

Patch:
diff --git a/src/duqtools/utils.py b/src/duqtools/utils.py
index 81e84bf..ed23e91 100644
--- a/src/duqtools/utils.py
+++ b/src/duqtools/utils.py
@@ -35,7 +35,7 @@ def work_directory(path: PathLike):
         os.chdir(prev_cwd)
 
 
-def read_imas_handles_from_file(inp: PathLike, ) -> dict[str, ImasHandle]:
+def read_imas_handles_from_file(inp: PathLike) -> dict[str, ImasHandle]:
     """Read a collection of imas paths from a file.
 
     Input can be a `Runs.yaml` file `data.csv` file.
@@ -49,12 +49,10 @@ def read_imas_handles_from_file(inp: PathLike, ) -> dict[str, ImasHandle]:
     ----------
     inp : PathLike
         Name of the file to read.
-    as_dataframe : bool, optional
-        Return imas handles in a dataframe instead.
 
     Returns
     -------
-    Union[dict[str, ImasHandle], 'pd.DataFrame']
+    dict[str, ImasHandle]
         Returns a dict with the Imas handles.
 
     Raises
@@ -72,7 +70,7 @@ def read_imas_handles_from_file(inp: PathLike, ) -> dict[str, ImasHandle]:
     if inp.suffix == '.csv':
         handles = {}
         with open(inp) as f:
-            has_header = csv.Sniffer().has_header(f.read(1024))
+            has_header = csv.Sniffer().has_header(''.join(f.readlines(3)))
             f.seek(0)
 
             if not has_header:


Test patch:
diff --git a/tests/test_data.py b/tests/test_data.py
new file mode 100644
index 0000000..db7fa56
--- /dev/null
+++ b/tests/test_data.py
@@ -0,0 +1,23 @@
+from pytest import TEST_DATA
+
+from duqtools.api import ImasHandle
+from duqtools.utils import read_imas_handles_from_file
+
+
+def _check_data_csv(data_csv: str, length: int):
+    ret = read_imas_handles_from_file(data_csv)
+    assert isinstance(ret, dict)
+    assert len(ret.keys()) == length
+    assert all(isinstance(field, str) for field in ret)
+    assert all(isinstance(handle, ImasHandle) for handle in ret.values())
+    return ret
+
+
+def test_data_csv_4_col():
+    data = TEST_DATA / 'data_4_col.csv'
+    _check_data_csv(data, length=10)
+
+
+def test_data_csv_5_col():
+    data = TEST_DATA / 'data_5_col.csv'
+    _check_data_csv(data, length=25)
diff --git a/tests/test_data/data_4_col.csv b/tests/test_data/data_4_col.csv
new file mode 100644
index 0000000..6a95dbe
--- /dev/null
+++ b/tests/test_data/data_4_col.csv
@@ -0,0 +1,11 @@
+,user,db,shot,run
+data_01,g2aho,aug,36982,0002
+data_02,g2aho,jet,75225,0002
+data_03,g2aho,jet,90350,0002
+data_04,g2aho,jet,92432,0002
+data_05,g2aho,jet,94875,0001
+data_06,g2aho,tcv,64958,0002
+data_07,g2aho,west,54568,0001
+data_08,g2aho,west,54728,0001
+data_09,g2aho,west,55181,0001
+data_10,g2aho,west,55525,0001
diff --git a/tests/test_data/data_5_col.csv b/tests/test_data/data_5_col.csv
new file mode 100644
index 0000000..0358429
--- /dev/null
+++ b/tests/test_data/data_5_col.csv
@@ -0,0 +1,26 @@
+,relative_location,user,db,shot,run
+jet89730_id2.run_0000,,/gss_efgw_work/work/g2aho/jetto/runs/runlsv/predictive_qlknn_qedgemod/jet89730_id2/run_0000/imasdb,jet_lsvdb,89730,2
+jet89730_id2.run_0001,,/gss_efgw_work/work/g2aho/jetto/runs/runlsv/predictive_qlknn_qedgemod/jet89730_id2/run_0001/imasdb,jet_lsvdb,89730,2
+jet89730_id2.run_0002,,/gss_efgw_work/work/g2aho/jetto/runs/runlsv/predictive_qlknn_qedgemod/jet89730_id2/run_0002/imasdb,jet_lsvdb,89730,2
+jet89730_id2.run_0003,,/gss_efgw_work/work/g2aho/jetto/runs/runlsv/predictive_qlknn_qedgemod/jet89730_id2/run_0003/imasdb,jet_lsvdb,89730,2
+jet89730_id2.run_0004,,/gss_efgw_work/work/g2aho/jetto/runs/runlsv/predictive_qlknn_qedgemod/jet89730_id2/run_0004/imasdb,jet_lsvdb,89730,2
+jet89730_id2.run_0005,,/gss_efgw_work/work/g2aho/jetto/runs/runlsv/predictive_qlknn_qedgemod/jet89730_id2/run_0005/imasdb,jet_lsvdb,89730,2
+jet89730_id2.run_0006,,/gss_efgw_work/work/g2aho/jetto/runs/runlsv/predictive_qlknn_qedgemod/jet89730_id2/run_0006/imasdb,jet_lsvdb,89730,2
+jet89730_id2.run_0007,,/gss_efgw_work/work/g2aho/jetto/runs/runlsv/predictive_qlknn_qedgemod/jet89730_id2/run_0007/imasdb,jet_lsvdb,89730,2
+jet89730_id2.run_0008,,/gss_efgw_work/work/g2aho/jetto/runs/runlsv/predictive_qlknn_qedgemod/jet89730_id2/run_0008/imasdb,jet_lsvdb,89730,2
+jet89730_id2.run_0009,,/gss_efgw_work/work/g2aho/jetto/runs/runlsv/predictive_qlknn_qedgemod/jet89730_id2/run_0009/imasdb,jet_lsvdb,89730,2
+jet89730_id2.run_0010,,/gss_efgw_work/work/g2aho/jetto/runs/runlsv/predictive_qlknn_qedgemod/jet89730_id2/run_0010/imasdb,jet_lsvdb,89730,2
+jet89730_id2.run_0011,,/gss_efgw_work/work/g2aho/jetto/runs/runlsv/predictive_qlknn_qedgemod/jet89730_id2/run_0011/imasdb,jet_lsvdb,89730,2
+jet89730_id2.run_0012,,/gss_efgw_work/work/g2aho/jetto/runs/runlsv/predictive_qlknn_qedgemod/jet89730_id2/run_0012/imasdb,jet_lsvdb,89730,2
+jet89730_id2.run_0013,,/gss_efgw_work/work/g2aho/jetto/runs/runlsv/predictive_qlknn_qedgemod/jet89730_id2/run_0013/imasdb,jet_lsvdb,89730,2
+jet89730_id2.run_0014,,/gss_efgw_work/work/g2aho/jetto/runs/runlsv/predictive_qlknn_qedgemod/jet89730_id2/run_0014/imasdb,jet_lsvdb,89730,2
+jet89730_id2.run_0015,,/gss_efgw_work/work/g2aho/jetto/runs/runlsv/predictive_qlknn_qedgemod/jet89730_id2/run_0015/imasdb,jet_lsvdb,89730,2
+jet89730_id2.run_0016,,/gss_efgw_work/work/g2aho/jetto/runs/runlsv/predictive_qlknn_qedgemod/jet89730_id2/run_0016/imasdb,jet_lsvdb,89730,2
+jet89730_id2.run_0017,,/gss_efgw_work/work/g2aho/jetto/runs/runlsv/predictive_qlknn_qedgemod/jet89730_id2/run_0017/imasdb,jet_lsvdb,89730,2
+jet89730_id2.run_0018,,/gss_efgw_work/work/g2aho/jetto/runs/runlsv/predictive_qlknn_qedgemod/jet89730_id2/run_0018/imasdb,jet_lsvdb,89730,2
+jet89730_id2.run_0019,,/gss_efgw_work/work/g2aho/jetto/runs/runlsv/predictive_qlknn_qedgemod/jet89730_id2/run_0019/imasdb,jet_lsvdb,89730,2
+jet89730_id2.run_0020,,/gss_efgw_work/work/g2aho/jetto/runs/runlsv/predictive_qlknn_qedgemod/jet89730_id2/run_0020/imasdb,jet_lsvdb,89730,2
+jet89730_id2.run_0021,,/gss_efgw_work/work/g2aho/jetto/runs/runlsv/predictive_qlknn_qedgemod/jet89730_id2/run_0021/imasdb,jet_lsvdb,89730,2
+jet89730_id2.run_0022,,/gss_efgw_work/work/g2aho/jetto/runs/runlsv/predictive_qlknn_qedgemod/jet89730_id2/run_0022/imasdb,jet_lsvdb,89730,2
+jet89730_id2.run_0023,,/gss_efgw_work/work/g2aho/jetto/runs/runlsv/predictive_qlknn_qedgemod/jet89730_id2/run_0023/imasdb,jet_lsvdb,89730,2
+jet89730_id2.run_0024,,/gss_efgw_work/work/g2aho/jetto/runs/runlsv/predictive_qlknn_qedgemod/jet89730_id2/run_0024/imasdb,jet_lsvdb,89730,2