nova-trame 0.25.5__py3-none-any.whl → 0.26.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,166 @@
1
+ """Analysis cluster filesystem backend for NeutronDataSelector."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Any, Dict, List, Optional
6
+ from warnings import warn
7
+
8
+ from natsort import natsorted
9
+ from pydantic import Field, model_validator
10
+ from typing_extensions import Self
11
+
12
+ from .neutron_data_selector import NeutronDataSelectorModel, NeutronDataSelectorState
13
+
14
+ CUSTOM_DIRECTORIES_LABEL = "Custom Directory"
15
+
16
+ INSTRUMENTS = {
17
+ "HFIR": {
18
+ "CG-1A": "CG1A",
19
+ "DEV BEAM": "CG1B",
20
+ "MARS": "CG1D",
21
+ "GP-SANS": "CG2",
22
+ "BIO-SANS": "CG3",
23
+ "CNPDB": "CG4B",
24
+ "CTAX": "CG4C",
25
+ "IMAGINE": "CG4D",
26
+ "PTAX": "HB1",
27
+ "VERITAS": "HB1A",
28
+ "POWDER": "HB2A",
29
+ "HIDRA": "HB2B",
30
+ "WAND²": "HB2C",
31
+ "TAX": "HB3",
32
+ "DEMAND": "HB3A",
33
+ "NOWG": "NOWG",
34
+ "NOWV": "NOWV",
35
+ },
36
+ "SNS": {
37
+ "ARCS": "ARCS",
38
+ "BL-0": "BL0",
39
+ "BASIS": "BSS",
40
+ "CNCS": "CNCS",
41
+ "CORELLI": "CORELLI",
42
+ "EQ-SANS": "EQSANS",
43
+ "HYSPEC": "HYS",
44
+ "MANDI": "MANDI",
45
+ "NOMAD": "NOM",
46
+ "NOWB": "NOWB",
47
+ "NOWD": "NOWD",
48
+ "NSE": "NSE",
49
+ "POWGEN": "PG3",
50
+ "LIQREF": "REF_L",
51
+ "MAGREF": "REF_M",
52
+ "SEQUOIA": "SEQ",
53
+ "SNAP": "SNAP",
54
+ "TOPAZ": "TOPAZ",
55
+ "USANS": "USANS",
56
+ "VENUS": "VENUS",
57
+ "VISION": "VIS",
58
+ "VULCAN": "VULCAN",
59
+ },
60
+ }
61
+
62
+
63
+ class AnalysisDataSelectorState(NeutronDataSelectorState):
64
+ """Selection state for identifying datafiles."""
65
+
66
+ allow_custom_directories: bool = Field(default=False)
67
+ custom_directory: str = Field(default="", title="Custom Directory")
68
+
69
+ @model_validator(mode="after")
70
+ def validate_state(self) -> Self:
71
+ valid_facilities = self.get_facilities()
72
+ if self.facility and self.facility not in valid_facilities:
73
+ warn(
74
+ f"Facility '{self.facility}' could not be found. Valid options: {valid_facilities}",
75
+ stacklevel=1,
76
+ )
77
+
78
+ valid_instruments = self.get_instruments()
79
+ if self.instrument and self.facility != CUSTOM_DIRECTORIES_LABEL and self.instrument not in valid_instruments:
80
+ warn(
81
+ (
82
+ f"Instrument '{self.instrument}' could not be found in '{self.facility}'. "
83
+ f"Valid options: {valid_instruments}"
84
+ ),
85
+ stacklevel=1,
86
+ )
87
+ # Validating the experiment is expensive and will fail in our CI due to the filesystem not being mounted there.
88
+
89
+ return self
90
+
91
+ def get_facilities(self) -> List[str]:
92
+ facilities = list(INSTRUMENTS.keys())
93
+ if self.allow_custom_directories:
94
+ facilities.append(CUSTOM_DIRECTORIES_LABEL)
95
+ return facilities
96
+
97
+ def get_instruments(self) -> List[str]:
98
+ return list(INSTRUMENTS.get(self.facility, {}).keys())
99
+
100
+
101
+ class AnalysisDataSelectorModel(NeutronDataSelectorModel):
102
+ """Analysis cluster filesystem backend for NeutronDataSelector."""
103
+
104
+ def __init__(self, state: AnalysisDataSelectorState) -> None:
105
+ super().__init__(state)
106
+ self.state: AnalysisDataSelectorState = state
107
+
108
+ def set_binding_parameters(self, **kwargs: Any) -> None:
109
+ super().set_binding_parameters(**kwargs)
110
+
111
+ if "allow_custom_directories" in kwargs:
112
+ self.state.allow_custom_directories = kwargs["allow_custom_directories"]
113
+
114
+ def get_custom_directory_path(self) -> Optional[Path]:
115
+ # Don't expose the full file system
116
+ if not self.state.custom_directory:
117
+ return None
118
+
119
+ return Path(self.state.custom_directory)
120
+
121
+ def get_experiment_directory_path(self) -> Optional[Path]:
122
+ if not self.state.experiment:
123
+ return None
124
+
125
+ return Path("/") / self.state.facility / self.get_instrument_dir() / self.state.experiment
126
+
127
+ def get_instrument_dir(self) -> str:
128
+ return INSTRUMENTS.get(self.state.facility, {}).get(self.state.instrument, "")
129
+
130
+ def get_experiments(self) -> List[str]:
131
+ experiments = []
132
+
133
+ instrument_path = Path("/") / self.state.facility / self.get_instrument_dir()
134
+ try:
135
+ for dirname in os.listdir(instrument_path):
136
+ if dirname.startswith("IPTS-") and os.access(instrument_path / dirname, mode=os.R_OK):
137
+ experiments.append(dirname)
138
+ except OSError:
139
+ pass
140
+
141
+ return natsorted(experiments)
142
+
143
+ def get_directories(self, base_path: Optional[Path] = None) -> List[Dict[str, Any]]:
144
+ using_custom_directory = self.state.facility == CUSTOM_DIRECTORIES_LABEL
145
+ if base_path:
146
+ pass
147
+ elif using_custom_directory:
148
+ base_path = self.get_custom_directory_path()
149
+ else:
150
+ base_path = self.get_experiment_directory_path()
151
+
152
+ if not base_path:
153
+ return []
154
+
155
+ return self.get_directories_from_path(base_path)
156
+
157
+ def get_datafiles(self, *args: Any, **kwargs: Any) -> List[Any]:
158
+ using_custom_directory = self.state.facility == CUSTOM_DIRECTORIES_LABEL
159
+ if self.state.experiment:
160
+ base_path = Path("/") / self.state.facility / self.get_instrument_dir() / self.state.experiment
161
+ elif using_custom_directory and self.state.custom_directory:
162
+ base_path = Path(self.state.custom_directory)
163
+ else:
164
+ return []
165
+
166
+ return [{"path": path} for path in self.get_datafiles_from_path(base_path)]
@@ -1,72 +1,20 @@
1
1
  """Model implementation for DataSelector."""
2
2
 
3
- import os
4
3
  from pathlib import Path
5
4
  from typing import Any, Dict, List, Optional
6
- from warnings import warn
7
5
 
8
6
  from natsort import natsorted
9
- from pydantic import Field, field_validator, model_validator
10
- from typing_extensions import Self
7
+ from pydantic import Field, field_validator
11
8
 
12
9
  from ..data_selector import DataSelectorModel, DataSelectorState
13
10
 
14
- CUSTOM_DIRECTORIES_LABEL = "Custom Directory"
15
-
16
- INSTRUMENTS = {
17
- "HFIR": {
18
- "CG-1A": "CG1A",
19
- "CG-1B": "CG1B",
20
- "CG-1D": "CG1D",
21
- "CG-2": "CG2",
22
- "CG-3": "CG3",
23
- "CG-4B": "CG4B",
24
- "CG-4C": "CG4C",
25
- "CG-4D": "CG4D",
26
- "HB-1": "HB1",
27
- "HB-1A": "HB1A",
28
- "HB-2A": "HB2A",
29
- "HB-2B": "HB2B",
30
- "HB-2C": "HB2C",
31
- "HB-3": "HB3",
32
- "HB-3A": "HB3A",
33
- "NOW-G": "NOWG",
34
- "NOW-V": "NOWV",
35
- },
36
- "SNS": {
37
- "BL-18": "ARCS",
38
- "BL-0": "BL0",
39
- "BL-2": "BSS",
40
- "BL-5": "CNCS",
41
- "BL-9": "CORELLI",
42
- "BL-6": "EQSANS",
43
- "BL-14B": "HYS",
44
- "BL-11B": "MANDI",
45
- "BL-1B": "NOM",
46
- "NOW-G": "NOWG",
47
- "BL-15": "NSE",
48
- "BL-11A": "PG3",
49
- "BL-4B": "REF_L",
50
- "BL-4A": "REF_M",
51
- "BL-17": "SEQ",
52
- "BL-3": "SNAP",
53
- "BL-12": "TOPAZ",
54
- "BL-1A": "USANS",
55
- "BL-10": "VENUS",
56
- "BL-16B": "VIS",
57
- "BL-7": "VULCAN",
58
- },
59
- }
60
-
61
11
 
62
12
  class NeutronDataSelectorState(DataSelectorState):
63
13
  """Selection state for identifying datafiles."""
64
14
 
65
- allow_custom_directories: bool = Field(default=False)
66
15
  facility: str = Field(default="", title="Facility")
67
16
  instrument: str = Field(default="", title="Instrument")
68
17
  experiment: str = Field(default="", title="Experiment")
69
- custom_directory: str = Field(default="", title="Custom Directory")
70
18
 
71
19
  @field_validator("experiment", mode="after")
72
20
  @classmethod
@@ -75,33 +23,11 @@ class NeutronDataSelectorState(DataSelectorState):
75
23
  raise ValueError("experiment must begin with IPTS-")
76
24
  return experiment
77
25
 
78
- @model_validator(mode="after")
79
- def validate_state(self) -> Self:
80
- valid_facilities = self.get_facilities()
81
- if self.facility and self.facility not in valid_facilities:
82
- warn(f"Facility '{self.facility}' could not be found. Valid options: {valid_facilities}", stacklevel=1)
83
-
84
- valid_instruments = self.get_instruments()
85
- if self.instrument and self.facility != CUSTOM_DIRECTORIES_LABEL and self.instrument not in valid_instruments:
86
- warn(
87
- (
88
- f"Instrument '{self.instrument}' could not be found in '{self.facility}'. "
89
- f"Valid options: {valid_instruments}"
90
- ),
91
- stacklevel=1,
92
- )
93
- # Validating the experiment is expensive and will fail in our CI due to the filesystem not being mounted there.
94
-
95
- return self
96
-
97
26
  def get_facilities(self) -> List[str]:
98
- facilities = list(INSTRUMENTS.keys())
99
- if self.allow_custom_directories:
100
- facilities.append(CUSTOM_DIRECTORIES_LABEL)
101
- return facilities
27
+ raise NotImplementedError()
102
28
 
103
29
  def get_instruments(self) -> List[str]:
104
- return list(INSTRUMENTS.get(self.facility, {}).keys())
30
+ raise NotImplementedError()
105
31
 
106
32
 
107
33
  class NeutronDataSelectorModel(DataSelectorModel):
@@ -120,65 +46,18 @@ class NeutronDataSelectorModel(DataSelectorModel):
120
46
  self.state.instrument = kwargs["instrument"]
121
47
  if "experiment" in kwargs:
122
48
  self.state.experiment = kwargs["experiment"]
123
- if "allow_custom_directories" in kwargs:
124
- self.state.allow_custom_directories = kwargs["allow_custom_directories"]
125
49
 
126
50
  def get_facilities(self) -> List[str]:
127
51
  return natsorted(self.state.get_facilities())
128
52
 
129
- def get_instrument_dir(self) -> str:
130
- return INSTRUMENTS.get(self.state.facility, {}).get(self.state.instrument, "")
131
-
132
53
  def get_instruments(self) -> List[str]:
133
54
  return natsorted(self.state.get_instruments())
134
55
 
135
56
  def get_experiments(self) -> List[str]:
136
- experiments = []
137
-
138
- instrument_path = Path("/") / self.state.facility / self.get_instrument_dir()
139
- try:
140
- for dirname in os.listdir(instrument_path):
141
- if dirname.startswith("IPTS-") and os.access(instrument_path / dirname, mode=os.R_OK):
142
- experiments.append(dirname)
143
- except OSError:
144
- pass
145
-
146
- return natsorted(experiments)
147
-
148
- def get_experiment_directory_path(self) -> Optional[Path]:
149
- if not self.state.experiment:
150
- return None
151
-
152
- return Path("/") / self.state.facility / self.get_instrument_dir() / self.state.experiment
153
-
154
- def get_custom_directory_path(self) -> Optional[Path]:
155
- # Don't expose the full file system
156
- if not self.state.custom_directory:
157
- return None
158
-
159
- return Path(self.state.custom_directory)
57
+ raise NotImplementedError()
160
58
 
161
59
  def get_directories(self, base_path: Optional[Path] = None) -> List[Dict[str, Any]]:
162
- using_custom_directory = self.state.facility == CUSTOM_DIRECTORIES_LABEL
163
- if base_path:
164
- pass
165
- elif using_custom_directory:
166
- base_path = self.get_custom_directory_path()
167
- else:
168
- base_path = self.get_experiment_directory_path()
169
-
170
- if not base_path:
171
- return []
172
-
173
- return self.get_directories_from_path(base_path)
174
-
175
- def get_datafiles(self) -> List[str]:
176
- using_custom_directory = self.state.facility == CUSTOM_DIRECTORIES_LABEL
177
- if self.state.experiment:
178
- base_path = Path("/") / self.state.facility / self.get_instrument_dir() / self.state.experiment
179
- elif using_custom_directory and self.state.custom_directory:
180
- base_path = Path(self.state.custom_directory)
181
- else:
182
- return []
183
-
184
- return self.get_datafiles_from_path(base_path)
60
+ raise NotImplementedError()
61
+
62
+ def get_datafiles(self, *args: Any, **kwargs: Any) -> List[str]:
63
+ raise NotImplementedError()
@@ -0,0 +1,131 @@
1
+ """ONCat backend for NeutronDataSelector."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ from natsort import natsorted
8
+ from pydantic import Field
9
+ from pyoncat import CLIENT_CREDENTIALS_FLOW, ONCat
10
+
11
+ from .neutron_data_selector import NeutronDataSelectorModel, NeutronDataSelectorState
12
+
13
+ TOKEN_VARNAME = "USER_OIDC_TOKEN"
14
+ ID_VARNAME = "ONCAT_CLIENT_ID"
15
+ SECRET_VARNAME = "ONCAT_CLIENT_SECRET"
16
+
17
+
18
+ class ONCatDataSelectorState(NeutronDataSelectorState):
19
+ """Selection state for identifying datafiles."""
20
+
21
+ instrument_mapping: Dict[str, str] = Field(default={})
22
+ projection: List[str] = Field(default=[])
23
+
24
+
25
+ class ONCatDataSelectorModel(NeutronDataSelectorModel):
26
+ """ONCat backend for NeutronDataSelector."""
27
+
28
+ def __init__(self, state: ONCatDataSelectorState) -> None:
29
+ super().__init__(state)
30
+ self.state: ONCatDataSelectorState = state
31
+
32
+ user_token = os.environ.get(TOKEN_VARNAME, "")
33
+ client_id = os.environ.get(ID_VARNAME, "")
34
+ client_secret = os.environ.get(SECRET_VARNAME, "")
35
+ if user_token:
36
+ self.oncat_client = ONCat(url="https://calvera-test.ornl.gov/oncat", api_token=user_token)
37
+ elif client_id and client_secret:
38
+ self.oncat_client = ONCat(
39
+ url="https://oncat.ornl.gov",
40
+ client_id=client_id,
41
+ client_secret=client_secret,
42
+ flow=CLIENT_CREDENTIALS_FLOW,
43
+ )
44
+ else:
45
+ raise EnvironmentError(
46
+ f"In order to use the ONCat backend for NeutronDataSelector, you must set either {TOKEN_VARNAME} or "
47
+ f"both {ID_VARNAME} and {SECRET_VARNAME} in your environment."
48
+ )
49
+
50
+ def set_binding_parameters(self, **kwargs: Any) -> None:
51
+ super().set_binding_parameters(**kwargs)
52
+
53
+ if "projection" in kwargs:
54
+ self.state.projection = kwargs["projection"]
55
+
56
+ def get_facilities(self) -> List[str]:
57
+ facilities = []
58
+ for facility_data in self.oncat_client.Facility.list(projection=["name"]):
59
+ facilities.append(facility_data.name)
60
+ return natsorted(facilities)
61
+
62
+ def get_instruments(self) -> List[str]:
63
+ if not self.state.facility:
64
+ return []
65
+
66
+ self.state.instrument_mapping = {}
67
+ instruments = []
68
+ for instrument_data in self.oncat_client.Instrument.list(
69
+ facility=self.state.facility, projection=["short_name"]
70
+ ):
71
+ self.state.instrument_mapping[instrument_data.short_name] = instrument_data.id
72
+ instruments.append(instrument_data.short_name)
73
+ return natsorted(instruments)
74
+
75
+ def get_experiments(self) -> List[str]:
76
+ if not self.state.facility or not self.state.instrument:
77
+ return []
78
+
79
+ experiments = []
80
+ for experiment_data in self.oncat_client.Experiment.list(
81
+ facility=self.state.facility,
82
+ instrument=self.state.instrument_mapping[self.state.instrument],
83
+ projection=["name"],
84
+ ):
85
+ experiments.append(experiment_data.name)
86
+ return natsorted(experiments)
87
+
88
+ def get_directories(self, _: Optional[Path] = None) -> List[Dict[str, Any]]:
89
+ return []
90
+
91
+ def create_datafile_obj(self, data: Dict[str, Any], projection: List[str]) -> Dict[str, str]:
92
+ new_obj = {"path": data["location"]}
93
+
94
+ for key in projection:
95
+ value: Any = data
96
+
97
+ if key == "location":
98
+ continue
99
+
100
+ for part in key.split("."):
101
+ try:
102
+ value = value[part]
103
+ except KeyError:
104
+ value = ""
105
+ break
106
+
107
+ new_obj[key] = value
108
+
109
+ return new_obj
110
+
111
+ def get_datafiles(self, *args: Any, **kwargs: Any) -> List[Any]:
112
+ if not self.state.facility or not self.state.instrument or not self.state.experiment:
113
+ return []
114
+
115
+ projection = ["location"] + self.state.projection
116
+
117
+ datafiles = []
118
+ for datafile_data in self.oncat_client.Datafile.list(
119
+ facility=self.state.facility,
120
+ instrument=self.state.instrument_mapping[self.state.instrument],
121
+ experiment=self.state.experiment,
122
+ projection=projection,
123
+ ):
124
+ path = datafile_data.location
125
+ if self.state.extensions:
126
+ for extension in self.state.extensions:
127
+ if path.lower().endswith(extension):
128
+ datafiles.append(self.create_datafile_obj(datafile_data, projection))
129
+ else:
130
+ datafiles.append(self.create_datafile_obj(datafile_data, projection))
131
+ return natsorted(datafiles, key=lambda d: d["path"])
@@ -155,10 +155,10 @@ class DataSelector(datagrid.VGrid):
155
155
  )
156
156
  vuetify.VListItem("No directories found", classes="flex-0-1 text-center", v_else=True)
157
157
 
158
- super().__init__(
159
- v_model=self._v_model,
160
- can_focus=False,
161
- columns=(
158
+ if "columns" in kwargs:
159
+ columns = kwargs.pop("columns")
160
+ else:
161
+ columns = (
162
162
  "[{"
163
163
  " cellTemplate: (createElement, props) =>"
164
164
  f" window.grid_manager.get('{self._revogrid_id}').cellTemplate(createElement, props),"
@@ -167,7 +167,12 @@ class DataSelector(datagrid.VGrid):
167
167
  " name: 'Available Datafiles',"
168
168
  " prop: 'title',"
169
169
  "}]",
170
- ),
170
+ )
171
+
172
+ super().__init__(
173
+ v_model=self._v_model,
174
+ can_focus=False,
175
+ columns=columns,
171
176
  column_span=1 if isinstance(self._subdirectory, tuple) or not self._subdirectory else 2,
172
177
  frame_size=10,
173
178
  hide_attribution=True,
@@ -21,6 +21,7 @@ class FileUpload(vuetify.VBtn):
21
21
  extensions: Union[List[str], Tuple, None] = None,
22
22
  label: str = "",
23
23
  return_contents: Union[bool, Tuple] = True,
24
+ use_bytes: Union[bool, Tuple] = False,
24
25
  **kwargs: Any,
25
26
  ) -> None:
26
27
  """Constructor for FileUpload.
@@ -40,6 +41,8 @@ class FileUpload(vuetify.VBtn):
40
41
  return_contents : Union[bool, Tuple], optional
41
42
  If true, the file contents will be stored in v_model. If false, a file path will be stored in v_model.
42
43
  Defaults to true.
44
+ use_bytes : Union[bool, Tuple], optional
45
+ If true, then files uploaded from the local machine will contain bytes rather than text.
43
46
  **kwargs
44
47
  All other arguments will be passed to the underlying
45
48
  `Button component <https://trame.readthedocs.io/en/latest/trame.widgets.vuetify3.html#trame.widgets.vuetify3.VBtn>`_.
@@ -54,6 +57,7 @@ class FileUpload(vuetify.VBtn):
54
57
  self._base_paths = base_paths if base_paths else ["/"]
55
58
  self._extensions = extensions if extensions else []
56
59
  self._return_contents = return_contents
60
+ self._use_bytes = use_bytes
57
61
  self._ref_name = f"nova__fileupload_{self._next_id}"
58
62
 
59
63
  super().__init__(label, **kwargs)
@@ -83,6 +87,7 @@ class FileUpload(vuetify.VBtn):
83
87
  extensions=self._extensions,
84
88
  input_props={"classes": "d-none"},
85
89
  return_contents=self._return_contents,
90
+ use_bytes=self._use_bytes,
86
91
  )
87
92
 
88
93
  with self:
@@ -1,6 +1,6 @@
1
1
  """View Implementation for DataSelector."""
2
2
 
3
- from typing import Any, List, Tuple, Union
3
+ from typing import Any, List, Literal, Tuple, Union
4
4
  from warnings import warn
5
5
 
6
6
  from trame.app import get_server
@@ -8,11 +8,13 @@ from trame.widgets import vuetify3 as vuetify
8
8
 
9
9
  from nova.mvvm._internal.utils import rgetdictvalue
10
10
  from nova.mvvm.trame_binding import TrameBinding
11
- from nova.trame.model.ornl.neutron_data_selector import (
11
+ from nova.trame.model.ornl.analysis_data_selector import (
12
12
  CUSTOM_DIRECTORIES_LABEL,
13
- NeutronDataSelectorModel,
14
- NeutronDataSelectorState,
13
+ AnalysisDataSelectorModel,
14
+ AnalysisDataSelectorState,
15
15
  )
16
+ from nova.trame.model.ornl.neutron_data_selector import NeutronDataSelectorModel
17
+ from nova.trame.model.ornl.oncat_data_selector import ONCatDataSelectorModel, ONCatDataSelectorState
16
18
  from nova.trame.view.layouts import GridLayout
17
19
  from nova.trame.view_model.ornl.neutron_data_selector import NeutronDataSelectorViewModel
18
20
 
@@ -29,10 +31,12 @@ class NeutronDataSelector(DataSelector):
29
31
  self,
30
32
  v_model: Union[str, Tuple],
31
33
  allow_custom_directories: Union[bool, Tuple] = False,
34
+ data_source: Literal["filesystem", "oncat"] = "filesystem",
32
35
  facility: Union[str, Tuple] = "",
33
36
  instrument: Union[str, Tuple] = "",
34
37
  experiment: Union[str, Tuple] = "",
35
38
  extensions: Union[List[str], Tuple, None] = None,
39
+ projection: Union[List[str], Tuple, None] = None,
36
40
  subdirectory: Union[str, Tuple] = "",
37
41
  refresh_rate: Union[int, Tuple] = 30,
38
42
  select_strategy: Union[str, Tuple] = "all",
@@ -48,6 +52,11 @@ class NeutronDataSelector(DataSelector):
48
52
  allow_custom_directories : Union[bool, Tuple], optional
49
53
  Whether or not to allow users to provide their own directories to search for datafiles in. Ignored if the
50
54
  facility parameter is set.
55
+ data_source : Literal["filesystem", "oncat"], optional
56
+ The source from which to pull datafiles. Defaults to "filesystem". If using ONCat, you will need to set the
57
+ following environment variables for local development: `ONCAT_CLIENT_ID` and `ONCAT_CLIENT_SECRET`. Note
58
+ that this parameter does not supporting Trame bindings. If you need to swap between the options, please
59
+ create two instances of this class and switch between them using a v_if or a v_show.
51
60
  facility : Union[str, Tuple], optional
52
61
  The facility to restrict data selection to. Options: HFIR, SNS
53
62
  instrument : Union[str, Tuple], optional
@@ -56,6 +65,9 @@ class NeutronDataSelector(DataSelector):
56
65
  The experiment to restrict data selection to.
57
66
  extensions : Union[List[str], Tuple], optional
58
67
  A list of file extensions to restrict selection to. If unset, then all files will be shown.
68
+ projection : Union[List[str], Tuple], optional
69
+ Sets the projection argument when pulling data files via pyoncat. Please refer to the ONCat documentation
70
+ for how to use this. This should only be used with `data_source="oncat"`.
59
71
  subdirectory : Union[str, Tuple], optional
60
72
  A subdirectory within the user's chosen experiment to show files. If not specified as a string, the user
61
73
  will be shown a folder browser and will be able to see all files in the experiment that they have access to.
@@ -73,6 +85,12 @@ class NeutronDataSelector(DataSelector):
73
85
  -------
74
86
  None
75
87
  """
88
+ if data_source == "oncat" and allow_custom_directories:
89
+ warn("allow_custom_directories will be ignored since data will be pulled from ONCat.", stacklevel=1)
90
+
91
+ if data_source == "oncat" and subdirectory:
92
+ warn("subdirectory will be ignored since data will be pulled from ONCat.", stacklevel=1)
93
+
76
94
  if isinstance(facility, str) and allow_custom_directories:
77
95
  warn("allow_custom_directories will be ignored since the facility parameter is fixed.", stacklevel=1)
78
96
 
@@ -81,6 +99,8 @@ class NeutronDataSelector(DataSelector):
81
99
  self._experiment = experiment
82
100
  self._allow_custom_directories = allow_custom_directories
83
101
  self._last_allow_custom_directories = self._allow_custom_directories
102
+ self._data_source = data_source
103
+ self._projection = projection
84
104
 
85
105
  self._state_name = f"nova__dataselector_{self._next_id}_state"
86
106
  self._facilities_name = f"nova__neutrondataselector_{self._next_id}_facilities"
@@ -100,19 +120,43 @@ class NeutronDataSelector(DataSelector):
100
120
  v_model,
101
121
  "",
102
122
  extensions=extensions,
103
- subdirectory=subdirectory,
123
+ subdirectory=subdirectory if data_source == "filesystem" else "oncat",
104
124
  refresh_rate=refresh_rate,
105
125
  select_strategy=select_strategy,
106
126
  **kwargs,
107
127
  )
108
128
 
129
+ def create_projection_column_title(self, key: str) -> str:
130
+ return key.split(".")[-1].replace("_", " ").title()
131
+
109
132
  def create_ui(self, **kwargs: Any) -> None:
110
- super().create_ui(**kwargs)
133
+ if self._data_source == "oncat":
134
+ columns = (
135
+ "["
136
+ " {"
137
+ " cellTemplate: (createElement, props) =>"
138
+ f" window.grid_manager.get('{self._revogrid_id}').cellTemplate(createElement, props),"
139
+ " columnTemplate: (createElement) =>"
140
+ f" window.grid_manager.get('{self._revogrid_id}').columnTemplate(createElement),"
141
+ " name: 'Available Datafiles',"
142
+ " prop: 'title',"
143
+ " size: 150,"
144
+ " },"
145
+ )
146
+ if self._projection:
147
+ for key in self._projection:
148
+ columns += f"{{name: '{self.create_projection_column_title(key)}', prop: '{key}', size: 150}},"
149
+ columns += "]"
150
+
151
+ super().create_ui(columns=(columns,), resize=True, **kwargs)
152
+ else:
153
+ super().create_ui(**kwargs)
154
+
111
155
  with self._layout.filter:
112
156
  with GridLayout(columns=3):
113
- columns = 3
157
+ column_span = 3
114
158
  if isinstance(self._facility, tuple) or not self._facility:
115
- columns -= 1
159
+ column_span -= 1
116
160
  InputField(
117
161
  v_model=self._selected_facility_name,
118
162
  items=(self._facilities_name,),
@@ -120,7 +164,7 @@ class NeutronDataSelector(DataSelector):
120
164
  update_modelValue=(self.update_facility, "[$event]"),
121
165
  )
122
166
  if isinstance(self._instrument, tuple) or not self._instrument:
123
- columns -= 1
167
+ column_span -= 1
124
168
  InputField(
125
169
  v_if=f"{self._selected_facility_name} !== '{CUSTOM_DIRECTORIES_LABEL}'",
126
170
  v_model=self._selected_instrument_name,
@@ -131,7 +175,7 @@ class NeutronDataSelector(DataSelector):
131
175
  InputField(
132
176
  v_if=f"{self._selected_facility_name} !== '{CUSTOM_DIRECTORIES_LABEL}'",
133
177
  v_model=self._selected_experiment_name,
134
- column_span=columns,
178
+ column_span=column_span,
135
179
  items=(self._experiments_name,),
136
180
  type="autocomplete",
137
181
  update_modelValue=(self.update_experiment, "[$event]"),
@@ -139,8 +183,11 @@ class NeutronDataSelector(DataSelector):
139
183
  InputField(v_else=True, v_model=f"{self._state_name}.custom_directory", column_span=2)
140
184
 
141
185
  def _create_model(self) -> None:
142
- state = NeutronDataSelectorState()
143
- self._model: NeutronDataSelectorModel = NeutronDataSelectorModel(state)
186
+ self._model: NeutronDataSelectorModel
187
+ if self._data_source == "oncat":
188
+ self._model = ONCatDataSelectorModel(ONCatDataSelectorState())
189
+ else:
190
+ self._model = AnalysisDataSelectorModel(AnalysisDataSelectorState())
144
191
 
145
192
  def _create_viewmodel(self) -> None:
146
193
  server = get_server(None, client_type="vue3")
@@ -167,6 +214,7 @@ class NeutronDataSelector(DataSelector):
167
214
  set_state_param(self.state, self._instrument)
168
215
  set_state_param(self.state, self._experiment)
169
216
  set_state_param(self.state, self._allow_custom_directories)
217
+ set_state_param(self.state, self._projection)
170
218
  self._last_facility = get_state_param(self.state, self._facility)
171
219
  self._last_instrument = get_state_param(self.state, self._instrument)
172
220
  self._last_experiment = get_state_param(self.state, self._experiment)
@@ -175,6 +223,7 @@ class NeutronDataSelector(DataSelector):
175
223
  instrument=get_state_param(self.state, self._instrument),
176
224
  experiment=get_state_param(self.state, self._experiment),
177
225
  allow_custom_directories=get_state_param(self.state, self._allow_custom_directories),
226
+ projection=get_state_param(self.state, self._projection),
178
227
  )
179
228
 
180
229
  # Now we set up the change listeners for all bound parameters. These are responsible for updating the component
@@ -232,6 +281,17 @@ class NeutronDataSelector(DataSelector):
232
281
  )
233
282
  )
234
283
 
284
+ if isinstance(self._projection, tuple):
285
+
286
+ @self.state.change(self._projection[0].split(".")[0])
287
+ def on_projection_change(**kwargs: Any) -> None:
288
+ projection = rgetdictvalue(kwargs, self._projection[0]) # type: ignore
289
+ if projection != self._projection:
290
+ self._projection = projection
291
+ self._vm.set_binding_parameters(
292
+ projection=set_state_param(self.state, self._projection, projection)
293
+ )
294
+
235
295
  # These update methods notify the rest of the application when the component changes bound parameters.
236
296
  def update_facility(self, facility: str) -> None:
237
297
  self._vm.set_binding_parameters(
@@ -257,3 +317,9 @@ class NeutronDataSelector(DataSelector):
257
317
  experiment=set_state_param(self.state, (self._selected_experiment_name,), experiment),
258
318
  )
259
319
  self._vm.reset()
320
+
321
+ def set_state(self, *args: Any, **kwargs: Any) -> None:
322
+ raise TypeError(
323
+ "The set_state method has been removed. Please use update_facility, update_instrument, and "
324
+ "update_experiment instead."
325
+ )
@@ -12,7 +12,7 @@ from trame_server.core import State
12
12
 
13
13
  from nova.mvvm._internal.utils import rgetdictvalue
14
14
  from nova.mvvm.trame_binding import TrameBinding
15
- from nova.trame._internal.utils import get_state_name, set_state_param
15
+ from nova.trame._internal.utils import get_state_name, get_state_param, set_state_param
16
16
  from nova.trame.model.remote_file_input import RemoteFileInputModel
17
17
  from nova.trame.view_model.remote_file_input import RemoteFileInputViewModel
18
18
 
@@ -35,6 +35,7 @@ class RemoteFileInput:
35
35
  extensions: Union[List[str], Tuple, None] = None,
36
36
  input_props: Optional[dict[str, Any]] = None,
37
37
  return_contents: Union[bool, Tuple] = False,
38
+ use_bytes: Union[bool, Tuple] = False,
38
39
  ) -> None:
39
40
  """Constructor for RemoteFileInput.
40
41
 
@@ -58,6 +59,8 @@ class RemoteFileInput:
58
59
  return_contents : Union[bool, Tuple], optional
59
60
  If true, then the v_model will contain the contents of the file. If false, then the v_model will contain the
60
61
  path of the file. Defaults to false.
62
+ use_bytes : Union[bool, Tuple], optional
63
+ If true, then the file contents will be treated as bytestreams when calling decode_file.
61
64
 
62
65
  Returns
63
66
  -------
@@ -73,6 +76,7 @@ class RemoteFileInput:
73
76
  self.extensions = extensions if extensions else []
74
77
  self.input_props = dict(input_props) if input_props else {}
75
78
  self.return_contents = return_contents
79
+ self.use_bytes = use_bytes
76
80
 
77
81
  if "__events" not in self.input_props:
78
82
  self.input_props["__events"] = []
@@ -286,14 +290,22 @@ class RemoteFileInput:
286
290
  self.decode_file(file.read())
287
291
 
288
292
  def decode_file(self, bytestream: bytes, set_contents: bool = False) -> None:
293
+ use_bytes = get_state_param(self.state, self.use_bytes)
294
+
289
295
  decoded_content = bytestream.decode("latin1")
290
296
  if set_contents:
291
297
  self.set_v_model(decoded_content)
292
298
  else:
293
- with NamedTemporaryFile(mode="w", delete=False, encoding="utf-8") as temp_file:
294
- temp_file.write(decoded_content)
295
- temp_file.flush()
296
- self.set_v_model(temp_file.name)
299
+ if use_bytes:
300
+ with NamedTemporaryFile(mode="wb", delete=False) as temp_file:
301
+ temp_file.write(bytestream)
302
+ temp_file.flush()
303
+ self.set_v_model(temp_file.name)
304
+ else:
305
+ with NamedTemporaryFile(mode="w", delete=False, encoding="utf-8") as temp_file:
306
+ temp_file.write(decoded_content)
307
+ temp_file.flush()
308
+ self.set_v_model(temp_file.name)
297
309
 
298
310
  def select_file(self, value: str) -> None:
299
311
  """Programmatically set the v_model value."""
@@ -21,7 +21,7 @@ class RevoGrid {
21
21
  const modelValue = _.get(trameState, this.modelKey)
22
22
  const availableData = _.get(trameState, this.dataKey)
23
23
  const selectAllCheckbox = this.grid.querySelector(".header-content input")
24
- const rowCheckboxes = this.grid.querySelectorAll(".rgCell")
24
+ const rowCheckboxes = this.grid.querySelectorAll(".rgCell:first-child")
25
25
 
26
26
  if (selectAllCheckbox === null) {
27
27
  return
@@ -66,6 +66,9 @@ class DataSelectorViewModel:
66
66
  self.model.set_subdirectory(subdirectory_path)
67
67
  self.update_view()
68
68
 
69
+ def transform_datafiles(self, datafiles: List[Any]) -> List[Dict[str, str]]:
70
+ return [{"path": datafile, "title": os.path.basename(datafile)} for datafile in datafiles]
71
+
69
72
  def update_view(self, refresh_directories: bool = False) -> None:
70
73
  self.state_bind.update_in_view(self.model.state)
71
74
  if not self.directories or refresh_directories:
@@ -73,8 +76,6 @@ class DataSelectorViewModel:
73
76
  self.reexpand_directories()
74
77
  self.directories_bind.update_in_view(self.directories)
75
78
 
76
- self.datafiles = [
77
- {"path": datafile, "title": os.path.basename(datafile)} for datafile in self.model.get_datafiles()
78
- ]
79
+ self.datafiles = self.transform_datafiles(self.model.get_datafiles())
79
80
  self.datafiles_bind.update_in_view(self.datafiles)
80
81
  self.reset_grid_bind.update_in_view(None)
@@ -1,6 +1,7 @@
1
1
  """View model implementation for the DataSelector widget."""
2
2
 
3
- from typing import Any, Dict
3
+ import os
4
+ from typing import Any, Dict, List
4
5
 
5
6
  from nova.mvvm.interface import BindingInterface
6
7
  from nova.trame.model.ornl.neutron_data_selector import NeutronDataSelectorModel
@@ -33,6 +34,9 @@ class NeutronDataSelectorViewModel(DataSelectorViewModel):
33
34
  self.reset()
34
35
  self.update_view()
35
36
 
37
+ def transform_datafiles(self, datafiles: List[Any]) -> List[Dict[str, str]]:
38
+ return [{"title": os.path.basename(datafile["path"]), **datafile} for datafile in datafiles]
39
+
36
40
  def update_view(self, refresh_directories: bool = False) -> None:
37
41
  self.facilities_bind.update_in_view(self.model.get_facilities())
38
42
  self.instruments_bind.update_in_view(self.model.get_instruments())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: nova-trame
3
- Version: 0.25.5
3
+ Version: 0.26.2
4
4
  Summary: A Python Package for injecting curated themes and custom components into Trame applications
5
5
  License: MIT
6
6
  Keywords: NDIP,Python,Trame,Vuetify
@@ -22,6 +22,7 @@ Requires-Dist: natsort (>=8.4.0,<9.0.0)
22
22
  Requires-Dist: nova-common (>=0.2.2)
23
23
  Requires-Dist: nova-mvvm
24
24
  Requires-Dist: pydantic
25
+ Requires-Dist: pyoncat (>=2.1,<3.0)
25
26
  Requires-Dist: tomli
26
27
  Requires-Dist: tornado (>=6.5.0)
27
28
  Requires-Dist: trame
@@ -2,17 +2,19 @@ nova/__init__.py,sha256=ED6jHcYiuYpr_0vjGz0zx2lrrmJT9sDJCzIljoDfmlM,65
2
2
  nova/trame/__init__.py,sha256=gFrAg1qva5PIqR5TjvPzAxLx103IKipJLqp3XXvrQL8,59
3
3
  nova/trame/_internal/utils.py,sha256=lTTJnfqbbIe21Tg2buf5MXqKUEUop7Va5PZgpWMzRkI,1381
4
4
  nova/trame/model/data_selector.py,sha256=hPPk1wiJc52jMXP_7XlEur38_vSi9tRUNKIa_gNmblc,4501
5
- nova/trame/model/ornl/neutron_data_selector.py,sha256=Nkj0DXdv3ydfXV3zeilbOGWuRCVzc_ClAOj6iNnN0uI,6276
5
+ nova/trame/model/ornl/analysis_data_selector.py,sha256=P7IEJdqCAUsEOCof4c2JantPXd9vz0EtvhryEKvscbw,5544
6
+ nova/trame/model/ornl/neutron_data_selector.py,sha256=YMoNEpDKgjP_y18oYj-N9IjkxtqwHz9JYMlURQA4BCE,2148
7
+ nova/trame/model/ornl/oncat_data_selector.py,sha256=3JEkWGMU-esWA9DUTglju9hEP9LyZ7EUXLj1yO5BIDs,4755
6
8
  nova/trame/model/remote_file_input.py,sha256=eAk7ZsFgNKcnpJ6KmOQDhiI6pPZpcrr1GMKkRLEWht8,4338
7
9
  nova/trame/view/components/__init__.py,sha256=60BeS69aOrFnkptjuD17rfPE1f4Z35iBH56TRmW5MW8,451
8
- nova/trame/view/components/data_selector.py,sha256=XFwIuKhIeBbkqsNEkIFDhjdDD5PD6cvbgn1jkivUhC4,14817
10
+ nova/trame/view/components/data_selector.py,sha256=T5CFtWPio3YwiPvPEBrWn7vgRo-3l0GqdGKzonc_cwc,14970
9
11
  nova/trame/view/components/execution_buttons.py,sha256=Br6uAmE5bY67TTYc5ZTHECNJ_RJqKmv17HAKPpQtbeg,4576
10
- nova/trame/view/components/file_upload.py,sha256=nRYwTPOzJV_TCtjk337PbeRbbIOf2PipaGKOyj0WKiA,4288
12
+ nova/trame/view/components/file_upload.py,sha256=WOaFXeNNwN0DYZJr-W6vWdBiTpr7m-lq3WKJaHmeMe8,4560
11
13
  nova/trame/view/components/input_field.py,sha256=xzCmNEoB4ljGx99-gGgTV0UwriwtS8ce22zPA4QneZw,17372
12
14
  nova/trame/view/components/ornl/__init__.py,sha256=HnxzzSsxw0vQSDCVFfWsAxx1n3HnU37LMuQkfiewmSU,90
13
- nova/trame/view/components/ornl/neutron_data_selector.py,sha256=g633Ie3GSNu0QFEuexYl_XUxjUiGEJKjWLP7ZB5PRR0,13122
15
+ nova/trame/view/components/ornl/neutron_data_selector.py,sha256=wVSLmRXiGOv2l-nZ0DgDwjrgTMZDu9e9ECm3k824k3o,16749
14
16
  nova/trame/view/components/progress_bar.py,sha256=zhbJwPy_HPQ8YL-ISN8sCRUQ7qY6qqo9wiV59BmvL8I,3038
15
- nova/trame/view/components/remote_file_input.py,sha256=cXSePj_eP3lf8bRsNEHT7dp-FO3doKkW44J3_9bgPAc,14082
17
+ nova/trame/view/components/remote_file_input.py,sha256=mcz_bmI2rD8gdmIOKLhlzfj-XoWBwC99T9ZgQORaKqE,14674
16
18
  nova/trame/view/components/tool_outputs.py,sha256=IbYV4VjrkWAE354Bh5KH76SPsxGLIkOXChijS4-ce_Y,2408
17
19
  nova/trame/view/components/visualization/__init__.py,sha256=reqkkbhD5uSksHHlhVMy1qNUCwSekS5HlXk6wCREYxU,152
18
20
  nova/trame/view/components/visualization/interactive_2d_plot.py,sha256=z2s1janxAclpMEdDJk3z-CQ6r3KPNoR_SXPx9ppWnuQ,3481
@@ -27,19 +29,19 @@ nova/trame/view/theme/assets/core_style.scss,sha256=3-3qMc5gpaDhfuVWAF_psBH5alxw
27
29
  nova/trame/view/theme/assets/favicon.png,sha256=Xbp1nUmhcBDeObjsebEbEAraPDZ_M163M_ZLtm5AbQc,1927
28
30
  nova/trame/view/theme/assets/js/delay_manager.js,sha256=BN4OL88QsyZG4XQ1sTorHpN1rwD4GnWoVKHvl5F5ydo,776
29
31
  nova/trame/view/theme/assets/js/lodash.min.js,sha256=KCyAYJ-fsqtp_HMwbjhy6IKjlA5lrVrtWt1JdMsC57k,73016
30
- nova/trame/view/theme/assets/js/revo_grid.js,sha256=81s0fUo8HbHmAyWag7pW0jP796Ttb1noAPOgTJlxJss,4069
32
+ nova/trame/view/theme/assets/js/revo_grid.js,sha256=fbuEWO8etw-xgo9tjJGjJXdd5wL8qpgabPmrnU6Jp8k,4081
31
33
  nova/trame/view/theme/assets/vuetify_config.json,sha256=a0FSgpLYWGFlRGSMhMq61MyDFBEBwvz55G4qjkM08cs,5627
32
34
  nova/trame/view/theme/exit_button.py,sha256=Kqv1GVJZGrSsj6_JFjGU3vm3iNuMolLC2T1x2IsdmV0,3094
33
35
  nova/trame/view/theme/theme.py,sha256=8JqSrEbhxK1SccXE1_jUdel9Wtc2QNObVEwtbVWG_QY,13146
34
36
  nova/trame/view/utilities/local_storage.py,sha256=vD8f2VZIpxhIKjZwEaD7siiPCTZO4cw9AfhwdawwYLY,3218
35
- nova/trame/view_model/data_selector.py,sha256=d8qdn6Q2b5fNo5lCXi1LTRdesfmy7wErIvGBa0UjOt8,3075
37
+ nova/trame/view_model/data_selector.py,sha256=jAtq5hpohQ6YiLBbgLJfNUzWZBpN2bjCG_c_FCJu2ns,3186
36
38
  nova/trame/view_model/execution_buttons.py,sha256=MfKSp95D92EqpD48C15cBo6dLO0Yld4FeRZMJNxJf7Y,3551
37
- nova/trame/view_model/ornl/neutron_data_selector.py,sha256=zpwvqETPuw0sEUvv0A2sU5Ha2_BKG_3xFYSaUuixLXw,1579
39
+ nova/trame/view_model/ornl/neutron_data_selector.py,sha256=PIKQyzcHpwu81DNk3d8AfgobDbxbdb9ppRLpEvdPgpw,1778
38
40
  nova/trame/view_model/progress_bar.py,sha256=6AUKHF3hfzbdsHqNEnmHRgDcBKY5TT8ywDx9S6ovnsc,2854
39
41
  nova/trame/view_model/remote_file_input.py,sha256=zWOflmCDJYYR_pacHphwzricV667GSRokh-mlxpBAOo,3646
40
42
  nova/trame/view_model/tool_outputs.py,sha256=ev6LY7fJ0H2xAJn9f5ww28c8Kpom2SYc2FbvFcoN4zg,829
41
- nova_trame-0.25.5.dist-info/LICENSE,sha256=Iu5QiDbwNbREg75iYaxIJ_V-zppuv4QFuBhAW-qiAlM,1061
42
- nova_trame-0.25.5.dist-info/METADATA,sha256=l0gN8Orwij-0jFWcK02so7I0YdkoY2DRk897rAE9LMs,1727
43
- nova_trame-0.25.5.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
44
- nova_trame-0.25.5.dist-info/entry_points.txt,sha256=J2AmeSwiTYZ4ZqHHp9HO6v4MaYQTTBPbNh6WtoqOT58,42
45
- nova_trame-0.25.5.dist-info/RECORD,,
43
+ nova_trame-0.26.2.dist-info/LICENSE,sha256=Iu5QiDbwNbREg75iYaxIJ_V-zppuv4QFuBhAW-qiAlM,1061
44
+ nova_trame-0.26.2.dist-info/METADATA,sha256=mDqDXOmyd5tFAnh7-VtPdiYu1ij7ajZcanzkgC6Ojps,1763
45
+ nova_trame-0.26.2.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
46
+ nova_trame-0.26.2.dist-info/entry_points.txt,sha256=J2AmeSwiTYZ4ZqHHp9HO6v4MaYQTTBPbNh6WtoqOT58,42
47
+ nova_trame-0.26.2.dist-info/RECORD,,