nova-trame 0.22.1__py3-none-any.whl → 0.23.0__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.
@@ -3,142 +3,29 @@
3
3
  import os
4
4
  from pathlib import Path
5
5
  from typing import Any, Dict, List, Optional
6
- from warnings import warn
7
6
 
8
7
  from natsort import natsorted
9
- from pydantic import BaseModel, Field, field_validator, model_validator
10
- from typing_extensions import Self
11
-
12
- CUSTOM_DIRECTORIES_LABEL = "Custom Directory"
13
-
14
- INSTRUMENTS = {
15
- "HFIR": {
16
- "CG-1A": "CG1A",
17
- "CG-1B": "CG1B",
18
- "CG-1D": "CG1D",
19
- "CG-2": "CG2",
20
- "CG-3": "CG3",
21
- "CG-4B": "CG4B",
22
- "CG-4C": "CG4C",
23
- "CG-4D": "CG4D",
24
- "HB-1": "HB1",
25
- "HB-1A": "HB1A",
26
- "HB-2A": "HB2A",
27
- "HB-2B": "HB2B",
28
- "HB-2C": "HB2C",
29
- "HB-3": "HB3",
30
- "HB-3A": "HB3A",
31
- "NOW-G": "NOWG",
32
- "NOW-V": "NOWV",
33
- },
34
- "SNS": {
35
- "BL-18": "ARCS",
36
- "BL-0": "BL0",
37
- "BL-2": "BSS",
38
- "BL-5": "CNCS",
39
- "BL-9": "CORELLI",
40
- "BL-6": "EQSANS",
41
- "BL-14B": "HYS",
42
- "BL-11B": "MANDI",
43
- "BL-1B": "NOM",
44
- "NOW-G": "NOWG",
45
- "BL-15": "NSE",
46
- "BL-11A": "PG3",
47
- "BL-4B": "REF_L",
48
- "BL-4A": "REF_M",
49
- "BL-17": "SEQ",
50
- "BL-3": "SNAP",
51
- "BL-12": "TOPAZ",
52
- "BL-1A": "USANS",
53
- "BL-10": "VENUS",
54
- "BL-16B": "VIS",
55
- "BL-7": "VULCAN",
56
- },
57
- }
8
+ from pydantic import BaseModel, Field
58
9
 
59
10
 
60
11
  class DataSelectorState(BaseModel, validate_assignment=True):
61
12
  """Selection state for identifying datafiles."""
62
13
 
63
- allow_custom_directories: bool = Field(default=False)
64
- facility: str = Field(default="", title="Facility")
65
- instrument: str = Field(default="", title="Instrument")
66
- experiment: str = Field(default="", title="Experiment")
67
- custom_directory: str = Field(default="", title="Custom Directory")
68
14
  directory: str = Field(default="")
15
+ subdirectory: str = Field(default="")
69
16
  extensions: List[str] = Field(default=[])
70
17
  prefix: str = Field(default="")
71
18
 
72
- @field_validator("experiment", mode="after")
73
- @classmethod
74
- def validate_experiment(cls, experiment: str) -> str:
75
- if experiment and not experiment.startswith("IPTS-"):
76
- raise ValueError("experiment must begin with IPTS-")
77
- return experiment
78
-
79
- @model_validator(mode="after")
80
- def validate_state(self) -> Self:
81
- valid_facilities = self.get_facilities()
82
- if self.facility and self.facility not in valid_facilities:
83
- warn(f"Facility '{self.facility}' could not be found. Valid options: {valid_facilities}", stacklevel=1)
84
-
85
- valid_instruments = self.get_instruments()
86
- if self.instrument and self.facility != CUSTOM_DIRECTORIES_LABEL and self.instrument not in valid_instruments:
87
- warn(
88
- (
89
- f"Instrument '{self.instrument}' could not be found in '{self.facility}'. "
90
- f"Valid options: {valid_instruments}"
91
- ),
92
- stacklevel=1,
93
- )
94
- # Validating the experiment is expensive and will fail in our CI due to the filesystem not being mounted there.
95
-
96
- return self
97
-
98
- def get_facilities(self) -> List[str]:
99
- facilities = list(INSTRUMENTS.keys())
100
- if self.allow_custom_directories:
101
- facilities.append(CUSTOM_DIRECTORIES_LABEL)
102
- return facilities
103
-
104
- def get_instruments(self) -> List[str]:
105
- return list(INSTRUMENTS.get(self.facility, {}).keys())
106
-
107
19
 
108
20
  class DataSelectorModel:
109
21
  """Manages file system interactions for the DataSelector widget."""
110
22
 
111
- def __init__(
112
- self, facility: str, instrument: str, extensions: List[str], prefix: str, allow_custom_directories: bool
113
- ) -> None:
114
- self.state = DataSelectorState()
115
- self.state.facility = facility
116
- self.state.instrument = instrument
23
+ def __init__(self, state: DataSelectorState, directory: str, extensions: List[str], prefix: str) -> None:
24
+ self.state: DataSelectorState = state
25
+
26
+ self.state.directory = directory
117
27
  self.state.extensions = extensions
118
28
  self.state.prefix = prefix
119
- self.state.allow_custom_directories = allow_custom_directories
120
-
121
- def get_facilities(self) -> List[str]:
122
- return natsorted(self.state.get_facilities())
123
-
124
- def get_instrument_dir(self) -> str:
125
- return INSTRUMENTS.get(self.state.facility, {}).get(self.state.instrument, "")
126
-
127
- def get_instruments(self) -> List[str]:
128
- return natsorted(self.state.get_instruments())
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
29
 
143
30
  def sort_directories(self, directories: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
144
31
  # Sort the current level of dictionaries
@@ -151,31 +38,7 @@ class DataSelectorModel:
151
38
 
152
39
  return sorted_dirs
153
40
 
154
- def get_experiment_directory_path(self) -> Optional[Path]:
155
- if not self.state.experiment:
156
- return None
157
-
158
- return Path("/") / self.state.facility / self.get_instrument_dir() / self.state.experiment
159
-
160
- def get_custom_directory_path(self) -> Optional[Path]:
161
- # Don't expose the full file system
162
- if not self.state.custom_directory:
163
- return None
164
-
165
- return Path(self.state.custom_directory)
166
-
167
- def get_directories(self, base_path: Optional[Path] = None) -> List[Dict[str, Any]]:
168
- using_custom_directory = self.state.facility == CUSTOM_DIRECTORIES_LABEL
169
- if base_path:
170
- pass
171
- elif using_custom_directory:
172
- base_path = self.get_custom_directory_path()
173
- else:
174
- base_path = self.get_experiment_directory_path()
175
-
176
- if not base_path:
177
- return []
178
-
41
+ def get_directories_from_path(self, base_path: Path) -> List[Dict[str, Any]]:
179
42
  directories = []
180
43
  try:
181
44
  for dirpath, dirs, _ in os.walk(base_path):
@@ -208,21 +71,24 @@ class DataSelectorModel:
208
71
 
209
72
  return self.sort_directories(directories)
210
73
 
211
- def get_datafiles(self) -> List[str]:
212
- datafiles = []
213
-
214
- if self.state.experiment:
215
- base_path = Path("/") / self.state.facility / self.get_instrument_dir() / self.state.experiment
216
- elif self.state.custom_directory:
217
- base_path = Path(self.state.custom_directory)
74
+ def get_directories(self, base_path: Optional[Path] = None) -> List[Dict[str, Any]]:
75
+ if base_path:
76
+ pass
218
77
  else:
78
+ base_path = Path(self.state.directory)
79
+
80
+ if not base_path:
219
81
  return []
220
82
 
83
+ return self.get_directories_from_path(base_path)
84
+
85
+ def get_datafiles_from_path(self, base_path: Path) -> List[str]:
86
+ datafiles = []
221
87
  try:
222
88
  if self.state.prefix:
223
- datafile_path = str(base_path / self.state.prefix)
89
+ datafile_path = base_path / self.state.prefix
224
90
  else:
225
- datafile_path = str(base_path / self.state.directory)
91
+ datafile_path = base_path / self.state.subdirectory
226
92
 
227
93
  for entry in os.scandir(datafile_path):
228
94
  if entry.is_file():
@@ -237,13 +103,10 @@ class DataSelectorModel:
237
103
 
238
104
  return natsorted(datafiles)
239
105
 
240
- def set_directory(self, directory_path: str) -> None:
241
- self.state.directory = directory_path
106
+ def get_datafiles(self) -> List[str]:
107
+ base_path = Path(self.state.directory)
108
+
109
+ return self.get_datafiles_from_path(base_path)
242
110
 
243
- def set_state(self, facility: Optional[str], instrument: Optional[str], experiment: Optional[str]) -> None:
244
- if facility is not None:
245
- self.state.facility = facility
246
- if instrument is not None:
247
- self.state.instrument = instrument
248
- if experiment is not None:
249
- self.state.experiment = experiment
111
+ def set_subdirectory(self, subdirectory_path: str) -> None:
112
+ self.state.subdirectory = subdirectory_path
@@ -0,0 +1,193 @@
1
+ """Model implementation for DataSelector."""
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, field_validator, model_validator
10
+ from typing_extensions import Self
11
+
12
+ from ..data_selector import DataSelectorModel, DataSelectorState
13
+
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
+
62
+ class NeutronDataSelectorState(DataSelectorState):
63
+ """Selection state for identifying datafiles."""
64
+
65
+ allow_custom_directories: bool = Field(default=False)
66
+ facility: str = Field(default="", title="Facility")
67
+ instrument: str = Field(default="", title="Instrument")
68
+ experiment: str = Field(default="", title="Experiment")
69
+ custom_directory: str = Field(default="", title="Custom Directory")
70
+
71
+ @field_validator("experiment", mode="after")
72
+ @classmethod
73
+ def validate_experiment(cls, experiment: str) -> str:
74
+ if experiment and not experiment.startswith("IPTS-"):
75
+ raise ValueError("experiment must begin with IPTS-")
76
+ return experiment
77
+
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
+ 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
102
+
103
+ def get_instruments(self) -> List[str]:
104
+ return list(INSTRUMENTS.get(self.facility, {}).keys())
105
+
106
+
107
+ class NeutronDataSelectorModel(DataSelectorModel):
108
+ """Manages file system interactions for the DataSelector widget."""
109
+
110
+ def __init__(
111
+ self,
112
+ state: NeutronDataSelectorState,
113
+ facility: str,
114
+ instrument: str,
115
+ extensions: List[str],
116
+ prefix: str,
117
+ allow_custom_directories: bool,
118
+ ) -> None:
119
+ super().__init__(state, "", extensions, prefix)
120
+ self.state: NeutronDataSelectorState = state
121
+
122
+ self.state.facility = facility
123
+ self.state.instrument = instrument
124
+ self.state.extensions = extensions
125
+ self.state.prefix = prefix
126
+ self.state.allow_custom_directories = allow_custom_directories
127
+
128
+ def get_facilities(self) -> List[str]:
129
+ return natsorted(self.state.get_facilities())
130
+
131
+ def get_instrument_dir(self) -> str:
132
+ return INSTRUMENTS.get(self.state.facility, {}).get(self.state.instrument, "")
133
+
134
+ def get_instruments(self) -> List[str]:
135
+ return natsorted(self.state.get_instruments())
136
+
137
+ def get_experiments(self) -> List[str]:
138
+ experiments = []
139
+
140
+ instrument_path = Path("/") / self.state.facility / self.get_instrument_dir()
141
+ try:
142
+ for dirname in os.listdir(instrument_path):
143
+ if dirname.startswith("IPTS-") and os.access(instrument_path / dirname, mode=os.R_OK):
144
+ experiments.append(dirname)
145
+ except OSError:
146
+ pass
147
+
148
+ return natsorted(experiments)
149
+
150
+ def get_experiment_directory_path(self) -> Optional[Path]:
151
+ if not self.state.experiment:
152
+ return None
153
+
154
+ return Path("/") / self.state.facility / self.get_instrument_dir() / self.state.experiment
155
+
156
+ def get_custom_directory_path(self) -> Optional[Path]:
157
+ # Don't expose the full file system
158
+ if not self.state.custom_directory:
159
+ return None
160
+
161
+ return Path(self.state.custom_directory)
162
+
163
+ def get_directories(self, base_path: Optional[Path] = None) -> List[Dict[str, Any]]:
164
+ using_custom_directory = self.state.facility == CUSTOM_DIRECTORIES_LABEL
165
+ if base_path:
166
+ pass
167
+ elif using_custom_directory:
168
+ base_path = self.get_custom_directory_path()
169
+ else:
170
+ base_path = self.get_experiment_directory_path()
171
+
172
+ if not base_path:
173
+ return []
174
+
175
+ return self.get_directories_from_path(base_path)
176
+
177
+ def get_datafiles(self) -> List[str]:
178
+ if self.state.experiment:
179
+ base_path = Path("/") / self.state.facility / self.get_instrument_dir() / self.state.experiment
180
+ elif self.state.custom_directory:
181
+ base_path = Path(self.state.custom_directory)
182
+ else:
183
+ return []
184
+
185
+ return self.get_datafiles_from_path(base_path)
186
+
187
+ def set_state(self, facility: Optional[str], instrument: Optional[str], experiment: Optional[str]) -> None:
188
+ if facility is not None:
189
+ self.state.facility = facility
190
+ if instrument is not None:
191
+ self.state.instrument = instrument
192
+ if experiment is not None:
193
+ self.state.experiment = experiment
@@ -1,14 +1,13 @@
1
1
  """View Implementation for DataSelector."""
2
2
 
3
3
  from typing import Any, List, Optional, cast
4
- from warnings import warn
5
4
 
6
5
  from trame.app import get_server
7
6
  from trame.widgets import client, datagrid, html
8
7
  from trame.widgets import vuetify3 as vuetify
9
8
 
10
9
  from nova.mvvm.trame_binding import TrameBinding
11
- from nova.trame.model.data_selector import CUSTOM_DIRECTORIES_LABEL, DataSelectorModel
10
+ from nova.trame.model.data_selector import DataSelectorModel, DataSelectorState
12
11
  from nova.trame.view.layouts import GridLayout, VBoxLayout
13
12
  from nova.trame.view_model.data_selector import DataSelectorViewModel
14
13
 
@@ -18,14 +17,12 @@ vuetify.enable_lab()
18
17
 
19
18
 
20
19
  class DataSelector(datagrid.VGrid):
21
- """Allows the user to select datafiles from an IPTS experiment."""
20
+ """Allows the user to select datafiles from the server."""
22
21
 
23
22
  def __init__(
24
23
  self,
25
24
  v_model: str,
26
- allow_custom_directories: bool = False,
27
- facility: str = "",
28
- instrument: str = "",
25
+ directory: str,
29
26
  extensions: Optional[List[str]] = None,
30
27
  prefix: str = "",
31
28
  select_strategy: str = "all",
@@ -38,18 +35,14 @@ class DataSelector(datagrid.VGrid):
38
35
  v_model : str
39
36
  The name of the state variable to bind to this widget. The state variable will contain a list of the files
40
37
  selected by the user.
41
- allow_custom_directories : bool, optional
42
- Whether or not to allow users to provide their own directories to search for datafiles in. Ignored if the
43
- facility parameter is set.
44
- facility : str, optional
45
- The facility to restrict data selection to. Options: HFIR, SNS
46
- instrument : str, optional
47
- The instrument to restrict data selection to. Please use the instrument acronym (e.g. CG-2).
38
+ directory : str
39
+ The top-level folder to expose to users. Only contents of this directory and its children will be exposed to
40
+ users.
48
41
  extensions : List[str], optional
49
42
  A list of file extensions to restrict selection to. If unset, then all files will be shown.
50
43
  prefix : str, optional
51
- A subdirectory within the user's chosen experiment to show files. If not specified, the user will be shown a
52
- folder browser and will be able to see all files in the experiment that they have access to.
44
+ A subdirectory within the selected top-level folder to show files. If not specified, the user will be shown
45
+ a folder browser and will be able to see all files in the selected top-level folder.
53
46
  select_strategy : str, optional
54
47
  The selection strategy to pass to the `VDataTable component <https://trame.readthedocs.io/en/latest/trame.widgets.vuetify3.html#trame.widgets.vuetify3.VDataTable>`__.
55
48
  If unset, the `all` strategy will be used.
@@ -61,6 +54,12 @@ class DataSelector(datagrid.VGrid):
61
54
  -------
62
55
  None
63
56
  """
57
+ if "allow_custom_directory" in kwargs or "facility" in kwargs or "instrument" in kwargs:
58
+ raise TypeError(
59
+ "The old DataSelector component has been renamed to NeutronDataSelector. Please import it from "
60
+ "`nova.trame.view.components.ornl`."
61
+ )
62
+
64
63
  if "items" in kwargs:
65
64
  raise AttributeError("The items parameter is not allowed on DataSelector widget.")
66
65
 
@@ -69,21 +68,15 @@ class DataSelector(datagrid.VGrid):
69
68
  else:
70
69
  self._label = None
71
70
 
72
- if facility and allow_custom_directories:
73
- warn("allow_custom_directories will be ignored since the facility parameter is set.", stacklevel=1)
74
-
75
71
  self._v_model = v_model
76
72
  self._v_model_name_in_state = v_model.split(".")[0]
77
- self._allow_custom_directories = allow_custom_directories
73
+ self._directory = directory
78
74
  self._extensions = extensions if extensions is not None else []
79
75
  self._prefix = prefix
80
76
  self._select_strategy = select_strategy
81
77
 
82
78
  self._revogrid_id = f"nova__dataselector_{self._next_id}_rv"
83
79
  self._state_name = f"nova__dataselector_{self._next_id}_state"
84
- self._facilities_name = f"nova__dataselector_{self._next_id}_facilities"
85
- self._instruments_name = f"nova__dataselector_{self._next_id}_instruments"
86
- self._experiments_name = f"nova__dataselector_{self._next_id}_experiments"
87
80
  self._directories_name = f"nova__dataselector_{self._next_id}_directories"
88
81
  self._datafiles_name = f"nova__dataselector_{self._next_id}_datafiles"
89
82
 
@@ -93,36 +86,14 @@ class DataSelector(datagrid.VGrid):
93
86
  ).exec
94
87
  self._reset_state = client.JSEval(exec=f"{self._v_model} = []; {self._flush_state}").exec
95
88
 
96
- self.create_model(facility, instrument)
89
+ self.create_model()
97
90
  self.create_viewmodel()
98
91
 
99
- self.create_ui(facility, instrument, **kwargs)
92
+ self.create_ui(**kwargs)
100
93
 
101
- def create_ui(self, facility: str, instrument: str, **kwargs: Any) -> None:
102
- with VBoxLayout(classes="nova-data-selector", height="100%"):
103
- with GridLayout(columns=3):
104
- columns = 3
105
- if facility == "":
106
- columns -= 1
107
- InputField(
108
- v_model=f"{self._state_name}.facility", items=(self._facilities_name,), type="autocomplete"
109
- )
110
- if instrument == "":
111
- columns -= 1
112
- InputField(
113
- v_if=f"{self._state_name}.facility !== '{CUSTOM_DIRECTORIES_LABEL}'",
114
- v_model=f"{self._state_name}.instrument",
115
- items=(self._instruments_name,),
116
- type="autocomplete",
117
- )
118
- InputField(
119
- v_if=f"{self._state_name}.facility !== '{CUSTOM_DIRECTORIES_LABEL}'",
120
- v_model=f"{self._state_name}.experiment",
121
- column_span=columns,
122
- items=(self._experiments_name,),
123
- type="autocomplete",
124
- )
125
- InputField(v_else=True, v_model=f"{self._state_name}.custom_directory", column_span=2)
94
+ def create_ui(self, *args: Any, **kwargs: Any) -> None:
95
+ with VBoxLayout(classes="nova-data-selector", height="100%") as self._layout:
96
+ self._layout.filter = html.Div()
126
97
 
127
98
  with GridLayout(columns=2, classes="flex-1-0 h-0", valign="start"):
128
99
  if not self._prefix:
@@ -137,7 +108,7 @@ class DataSelector(datagrid.VGrid):
137
108
  item_value="path",
138
109
  items=(self._directories_name,),
139
110
  click_open=(self._vm.expand_directory, "[$event.path]"),
140
- update_activated=(self._vm.set_directory, "$event"),
111
+ update_activated=(self._vm.set_subdirectory, "$event"),
141
112
  )
142
113
  vuetify.VListItem("No directories found", classes="flex-0-1 text-center", v_else=True)
143
114
 
@@ -198,10 +169,9 @@ class DataSelector(datagrid.VGrid):
198
169
  f"(+{{{{ {self._v_model}.length - 2 }}}} others)", v_if="index === 2", classes="text-caption"
199
170
  )
200
171
 
201
- def create_model(self, facility: str, instrument: str) -> None:
202
- self._model = DataSelectorModel(
203
- facility, instrument, self._extensions, self._prefix, self._allow_custom_directories
204
- )
172
+ def create_model(self) -> None:
173
+ state = DataSelectorState()
174
+ self._model = DataSelectorModel(state, self._directory, self._extensions, self._prefix)
205
175
 
206
176
  def create_viewmodel(self) -> None:
207
177
  server = get_server(None, client_type="vue3")
@@ -209,9 +179,6 @@ class DataSelector(datagrid.VGrid):
209
179
 
210
180
  self._vm = DataSelectorViewModel(self._model, binding)
211
181
  self._vm.state_bind.connect(self._state_name)
212
- self._vm.facilities_bind.connect(self._facilities_name)
213
- self._vm.instruments_bind.connect(self._instruments_name)
214
- self._vm.experiments_bind.connect(self._experiments_name)
215
182
  self._vm.directories_bind.connect(self._directories_name)
216
183
  self._vm.datafiles_bind.connect(self._datafiles_name)
217
184
  self._vm.reset_bind.connect(self.reset)
@@ -222,26 +189,8 @@ class DataSelector(datagrid.VGrid):
222
189
  self._reset_state()
223
190
  self._reset_rv_grid()
224
191
 
225
- def set_state(
226
- self, facility: Optional[str] = None, instrument: Optional[str] = None, experiment: Optional[str] = None
227
- ) -> None:
228
- """Programmatically set the facility, instrument, and/or experiment to restrict data selection to.
229
-
230
- If a parameter is None, then it will not be updated.
231
-
232
- Parameters
233
- ----------
234
- facility : str, optional
235
- The facility to restrict data selection to. Options: HFIR, SNS
236
- instrument : str, optional
237
- The instrument to restrict data selection to. Must be at the selected facility.
238
- experiment : str, optional
239
- The experiment to restrict data selection to. Must begin with "IPTS-". It is your responsibility to validate
240
- that the provided experiment exists within the instrument directory. If it doesn't then no datafiles will be
241
- shown to the user.
242
-
243
- Returns
244
- -------
245
- None
246
- """
247
- self._vm.set_state(facility, instrument, experiment)
192
+ def set_state(self, *args: Any, **kwargs: Any) -> None:
193
+ raise TypeError(
194
+ "The old DataSelector component has been renamed to NeutronDataSelector. Please import it from "
195
+ "`nova.trame.view.components.ornl`."
196
+ )
@@ -0,0 +1,3 @@
1
+ from .neutron_data_selector import NeutronDataSelector
2
+
3
+ __all__ = ["NeutronDataSelector"]
@@ -0,0 +1,151 @@
1
+ """View Implementation for DataSelector."""
2
+
3
+ from typing import Any, List, Optional
4
+ from warnings import warn
5
+
6
+ from trame.app import get_server
7
+ from trame.widgets import vuetify3 as vuetify
8
+
9
+ from nova.mvvm.trame_binding import TrameBinding
10
+ from nova.trame.model.ornl.neutron_data_selector import (
11
+ CUSTOM_DIRECTORIES_LABEL,
12
+ NeutronDataSelectorModel,
13
+ NeutronDataSelectorState,
14
+ )
15
+ from nova.trame.view.layouts import GridLayout
16
+ from nova.trame.view_model.ornl.neutron_data_selector import NeutronDataSelectorViewModel
17
+
18
+ from ..data_selector import DataSelector
19
+ from ..input_field import InputField
20
+
21
+ vuetify.enable_lab()
22
+
23
+
24
+ class NeutronDataSelector(DataSelector):
25
+ """Allows the user to select datafiles from an IPTS experiment."""
26
+
27
+ def __init__(
28
+ self,
29
+ v_model: str,
30
+ allow_custom_directories: bool = False,
31
+ facility: str = "",
32
+ instrument: str = "",
33
+ extensions: Optional[List[str]] = None,
34
+ prefix: str = "",
35
+ select_strategy: str = "all",
36
+ **kwargs: Any,
37
+ ) -> None:
38
+ """Constructor for DataSelector.
39
+
40
+ Parameters
41
+ ----------
42
+ v_model : str
43
+ The name of the state variable to bind to this widget. The state variable will contain a list of the files
44
+ selected by the user.
45
+ allow_custom_directories : bool, optional
46
+ Whether or not to allow users to provide their own directories to search for datafiles in. Ignored if the
47
+ facility parameter is set.
48
+ facility : str, optional
49
+ The facility to restrict data selection to. Options: HFIR, SNS
50
+ instrument : str, optional
51
+ The instrument to restrict data selection to. Please use the instrument acronym (e.g. CG-2).
52
+ extensions : List[str], optional
53
+ A list of file extensions to restrict selection to. If unset, then all files will be shown.
54
+ prefix : str, optional
55
+ A subdirectory within the user's chosen experiment to show files. If not specified, the user will be shown a
56
+ folder browser and will be able to see all files in the experiment that they have access to.
57
+ select_strategy : str, optional
58
+ The selection strategy to pass to the `VDataTable component <https://trame.readthedocs.io/en/latest/trame.widgets.vuetify3.html#trame.widgets.vuetify3.VDataTable>`__.
59
+ If unset, the `all` strategy will be used.
60
+ **kwargs
61
+ All other arguments will be passed to the underlying
62
+ `VDataTable component <https://trame.readthedocs.io/en/latest/trame.widgets.vuetify3.html#trame.widgets.vuetify3.VDataTable>`_.
63
+
64
+ Returns
65
+ -------
66
+ None
67
+ """
68
+ if facility and allow_custom_directories:
69
+ warn("allow_custom_directories will be ignored since the facility parameter is set.", stacklevel=1)
70
+
71
+ self._facility = facility
72
+ self._instrument = instrument
73
+ self._allow_custom_directories = allow_custom_directories
74
+
75
+ self._facilities_name = f"nova__neutrondataselector_{self._next_id}_facilities"
76
+ self._instruments_name = f"nova__neutrondataselector_{self._next_id}_instruments"
77
+ self._experiments_name = f"nova__neutrondataselector_{self._next_id}_experiments"
78
+
79
+ super().__init__(v_model, "", extensions, prefix, select_strategy, **kwargs)
80
+
81
+ def create_ui(self, **kwargs: Any) -> None:
82
+ super().create_ui(**kwargs)
83
+ with self._layout.filter:
84
+ with GridLayout(columns=3):
85
+ columns = 3
86
+ if self._facility == "":
87
+ columns -= 1
88
+ InputField(
89
+ v_model=f"{self._state_name}.facility", items=(self._facilities_name,), type="autocomplete"
90
+ )
91
+ if self._instrument == "":
92
+ columns -= 1
93
+ InputField(
94
+ v_if=f"{self._state_name}.facility !== '{CUSTOM_DIRECTORIES_LABEL}'",
95
+ v_model=f"{self._state_name}.instrument",
96
+ items=(self._instruments_name,),
97
+ type="autocomplete",
98
+ )
99
+ InputField(
100
+ v_if=f"{self._state_name}.facility !== '{CUSTOM_DIRECTORIES_LABEL}'",
101
+ v_model=f"{self._state_name}.experiment",
102
+ column_span=columns,
103
+ items=(self._experiments_name,),
104
+ type="autocomplete",
105
+ )
106
+ InputField(v_else=True, v_model=f"{self._state_name}.custom_directory", column_span=2)
107
+
108
+ def create_model(self) -> None:
109
+ state = NeutronDataSelectorState()
110
+ self._model: NeutronDataSelectorModel = NeutronDataSelectorModel(
111
+ state, self._facility, self._instrument, self._extensions, self._prefix, self._allow_custom_directories
112
+ )
113
+
114
+ def create_viewmodel(self) -> None:
115
+ server = get_server(None, client_type="vue3")
116
+ binding = TrameBinding(server.state)
117
+
118
+ self._vm: NeutronDataSelectorViewModel = NeutronDataSelectorViewModel(self._model, binding)
119
+ self._vm.state_bind.connect(self._state_name)
120
+ self._vm.facilities_bind.connect(self._facilities_name)
121
+ self._vm.instruments_bind.connect(self._instruments_name)
122
+ self._vm.experiments_bind.connect(self._experiments_name)
123
+ self._vm.directories_bind.connect(self._directories_name)
124
+ self._vm.datafiles_bind.connect(self._datafiles_name)
125
+ self._vm.reset_bind.connect(self.reset)
126
+
127
+ self._vm.update_view()
128
+
129
+ def set_state(
130
+ self, facility: Optional[str] = None, instrument: Optional[str] = None, experiment: Optional[str] = None
131
+ ) -> None:
132
+ """Programmatically set the facility, instrument, and/or experiment to restrict data selection to.
133
+
134
+ If a parameter is None, then it will not be updated.
135
+
136
+ Parameters
137
+ ----------
138
+ facility : str, optional
139
+ The facility to restrict data selection to. Options: HFIR, SNS
140
+ instrument : str, optional
141
+ The instrument to restrict data selection to. Must be at the selected facility.
142
+ experiment : str, optional
143
+ The experiment to restrict data selection to. Must begin with "IPTS-". It is your responsibility to validate
144
+ that the provided experiment exists within the instrument directory. If it doesn't then no datafiles will be
145
+ shown to the user.
146
+
147
+ Returns
148
+ -------
149
+ None
150
+ """
151
+ self._vm.set_state(facility, instrument, experiment)
@@ -22,8 +22,14 @@ html {
22
22
 
23
23
  .nova-data-selector {
24
24
  .v-list-group {
25
- .v-treeview-item {
26
- --indent-padding: 1em !important;
25
+ --prepend-width: 12px !important;
26
+ }
27
+
28
+ .v-treeview.v-list {
29
+ --indent-padding: 0px !important;
30
+
31
+ & > .v-list-group > .v-list-item {
32
+ padding-left: 0 !important;
27
33
  }
28
34
  }
29
35
  }
@@ -2,7 +2,7 @@
2
2
 
3
3
  import os
4
4
  from pathlib import Path
5
- from typing import Any, Dict, List, Optional
5
+ from typing import Any, Dict, List
6
6
 
7
7
  from nova.mvvm.interface import BindingInterface
8
8
  from nova.trame.model.data_selector import DataSelectorModel
@@ -12,16 +12,13 @@ class DataSelectorViewModel:
12
12
  """Manages the view state of the DataSelector widget."""
13
13
 
14
14
  def __init__(self, model: DataSelectorModel, binding: BindingInterface) -> None:
15
- self.model = model
15
+ self.model: DataSelectorModel = model
16
16
 
17
17
  self.datafiles: List[Dict[str, Any]] = []
18
18
  self.directories: List[Dict[str, Any]] = []
19
19
  self.expanded: List[str] = []
20
20
 
21
21
  self.state_bind = binding.new_bind(self.model.state, callback_after_update=self.on_state_updated)
22
- self.facilities_bind = binding.new_bind()
23
- self.instruments_bind = binding.new_bind()
24
- self.experiments_bind = binding.new_bind()
25
22
  self.directories_bind = binding.new_bind()
26
23
  self.datafiles_bind = binding.new_bind()
27
24
  self.reset_bind = binding.new_bind()
@@ -50,40 +47,17 @@ class DataSelectorViewModel:
50
47
  self.expanded.append(paths[-1])
51
48
  self.directories_bind.update_in_view(self.directories)
52
49
 
53
- def set_directory(self, directory_path: str = "") -> None:
54
- self.model.set_directory(directory_path)
55
- self.update_view()
56
-
57
- def set_state(self, facility: Optional[str], instrument: Optional[str], experiment: Optional[str]) -> None:
58
- self.model.set_state(facility, instrument, experiment)
59
- self.update_view()
60
-
61
- def reset(self) -> None:
62
- self.model.set_directory("")
63
- self.directories = self.model.get_directories()
64
- self.expanded = []
65
- self.reset_bind.update_in_view(None)
66
-
67
50
  def on_state_updated(self, results: Dict[str, Any]) -> None:
68
- for update in results.get("updated", []):
69
- match update:
70
- case "facility":
71
- self.model.set_state(facility=None, instrument="", experiment="")
72
- self.reset()
73
- case "instrument":
74
- self.model.set_state(facility=None, instrument=None, experiment="")
75
- self.reset()
76
- case "experiment":
77
- self.reset()
78
- case "custom_directory":
79
- self.reset()
51
+ pass
52
+
53
+ def set_subdirectory(self, subdirectory_path: str = "") -> None:
54
+ self.model.set_subdirectory(subdirectory_path)
80
55
  self.update_view()
81
56
 
82
57
  def update_view(self) -> None:
83
58
  self.state_bind.update_in_view(self.model.state)
84
- self.facilities_bind.update_in_view(self.model.get_facilities())
85
- self.instruments_bind.update_in_view(self.model.get_instruments())
86
- self.experiments_bind.update_in_view(self.model.get_experiments())
59
+ if not self.directories:
60
+ self.directories = self.model.get_directories()
87
61
  self.directories_bind.update_in_view(self.directories)
88
62
 
89
63
  self.datafiles = [
@@ -0,0 +1,51 @@
1
+ """View model implementation for the DataSelector widget."""
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+ from nova.mvvm.interface import BindingInterface
6
+ from nova.trame.model.ornl.neutron_data_selector import NeutronDataSelectorModel
7
+ from nova.trame.view_model.data_selector import DataSelectorViewModel
8
+
9
+
10
+ class NeutronDataSelectorViewModel(DataSelectorViewModel):
11
+ """Manages the view state of the DataSelector widget."""
12
+
13
+ def __init__(self, model: NeutronDataSelectorModel, binding: BindingInterface) -> None:
14
+ super().__init__(model, binding)
15
+ self.model: NeutronDataSelectorModel = model
16
+
17
+ self.facilities_bind = binding.new_bind()
18
+ self.instruments_bind = binding.new_bind()
19
+ self.experiments_bind = binding.new_bind()
20
+
21
+ def set_state(self, facility: Optional[str], instrument: Optional[str], experiment: Optional[str]) -> None:
22
+ self.model.set_state(facility, instrument, experiment)
23
+ self.update_view()
24
+
25
+ def reset(self) -> None:
26
+ self.model.set_subdirectory("")
27
+ self.directories = self.model.get_directories()
28
+ self.expanded = []
29
+ self.reset_bind.update_in_view(None)
30
+
31
+ def on_state_updated(self, results: Dict[str, Any]) -> None:
32
+ for update in results.get("updated", []):
33
+ match update:
34
+ case "facility":
35
+ self.model.set_state(facility=None, instrument="", experiment="")
36
+ self.reset()
37
+ case "instrument":
38
+ self.model.set_state(facility=None, instrument=None, experiment="")
39
+ self.reset()
40
+ case "experiment":
41
+ self.reset()
42
+ case "custom_directory":
43
+ self.reset()
44
+ self.update_view()
45
+
46
+ def update_view(self) -> None:
47
+ self.facilities_bind.update_in_view(self.model.get_facilities())
48
+ self.instruments_bind.update_in_view(self.model.get_instruments())
49
+ self.experiments_bind.update_in_view(self.model.get_experiments())
50
+
51
+ super().update_view()
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: nova-trame
3
- Version: 0.22.1
3
+ Version: 0.23.0
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
7
- Author: Duggan, John
7
+ Author: John Duggan
8
8
  Author-email: dugganjw@ornl.gov
9
9
  Requires-Python: >=3.10,<4.0
10
10
  Classifier: License :: OSI Approved :: MIT License
@@ -1,12 +1,15 @@
1
1
  nova/__init__.py,sha256=ED6jHcYiuYpr_0vjGz0zx2lrrmJT9sDJCzIljoDfmlM,65
2
2
  nova/trame/__init__.py,sha256=gFrAg1qva5PIqR5TjvPzAxLx103IKipJLqp3XXvrQL8,59
3
- nova/trame/model/data_selector.py,sha256=UnLBCp_jJ523QxTR3R8iun2Ogq4D0G0lxmtW9e_zwOM,8938
3
+ nova/trame/model/data_selector.py,sha256=jXu2tRmk0sI6fSbBs-bSggEiRw99M5d8NwpvPOk9nDs,4182
4
+ nova/trame/model/ornl/neutron_data_selector.py,sha256=nfjXwT93JcPjJwyDynoP_stDVAfjUs7neeVZTk_04gc,6424
4
5
  nova/trame/model/remote_file_input.py,sha256=9KAf31ZHzpsh_aXUrNcF81Q5jvUZDWCzW1QATKls-Jk,3675
5
6
  nova/trame/view/components/__init__.py,sha256=60BeS69aOrFnkptjuD17rfPE1f4Z35iBH56TRmW5MW8,451
6
- nova/trame/view/components/data_selector.py,sha256=lgZjyT_jc3eE19HNgz_Hdog5bWvXZJ3IfxxiSBkZ7hE,11222
7
+ nova/trame/view/components/data_selector.py,sha256=uahrLdTRKrec-g8L-xIYW0OG_dCnFuKJxq69ROf2c6I,8532
7
8
  nova/trame/view/components/execution_buttons.py,sha256=fIkrWKI3jFZqk3GHhtmYh3nK2c-HOXpD3D3zd_TUpi0,4049
8
9
  nova/trame/view/components/file_upload.py,sha256=7VcpfA6zmiqMDLkwVPlb35Tf0IUTBN1xsHpoUFnSr1w,3111
9
10
  nova/trame/view/components/input_field.py,sha256=q6WQ_N-BOlimUL9zgazDlsDfK28FrrKjH4he8e_HzRA,16088
11
+ nova/trame/view/components/ornl/__init__.py,sha256=HnxzzSsxw0vQSDCVFfWsAxx1n3HnU37LMuQkfiewmSU,90
12
+ nova/trame/view/components/ornl/neutron_data_selector.py,sha256=RZAowBxRaxlVsHTsukHTX0reveWG1K6ib8zb1kKL9Ow,6631
10
13
  nova/trame/view/components/progress_bar.py,sha256=fCfPw4MPAvORaeFOXugreok4GLpDVZGMkqvnv-AhMxg,2967
11
14
  nova/trame/view/components/remote_file_input.py,sha256=ByrBFj8svyWezcardCWrS_4Ag3fgTYNg_11lDW1FIA8,9669
12
15
  nova/trame/view/components/tool_outputs.py,sha256=-6pDURd2l_FK_8EWa9BI3KhU_KJXJ6uyJ_rW4nQVc08,2337
@@ -19,7 +22,7 @@ nova/trame/view/layouts/hbox.py,sha256=qlOMp_iOropIkC9Jxa6D89b7OPv0pNvJ73tUEzddy
19
22
  nova/trame/view/layouts/utils.py,sha256=Hg34VQWTG3yHBsgNvmfatR4J-uL3cko7UxSJpT-h3JI,376
20
23
  nova/trame/view/layouts/vbox.py,sha256=hzhzPu99R2fAclMe-FwHZseJWk7iailZ31bKdGhi1hk,3514
21
24
  nova/trame/view/theme/__init__.py,sha256=70_marDlTigIcPEOGiJb2JTs-8b2sGM5SlY7XBPtBDM,54
22
- nova/trame/view/theme/assets/core_style.scss,sha256=lK86Fp55oAMDh1eUHA-DTeGGZi0uUYOseIyJUTj-0A0,4081
25
+ nova/trame/view/theme/assets/core_style.scss,sha256=AJ-2hyQRVkyOGWJB0lGMzlQeshbCJP5BGNYCt9e9AOI,4208
23
26
  nova/trame/view/theme/assets/favicon.png,sha256=Xbp1nUmhcBDeObjsebEbEAraPDZ_M163M_ZLtm5AbQc,1927
24
27
  nova/trame/view/theme/assets/js/delay_manager.js,sha256=mRV6KoO8-Bxq3tG5Bh9CQYy-CRVbkj3IYlqNb-Og7cI,526
25
28
  nova/trame/view/theme/assets/js/lodash.min.js,sha256=KCyAYJ-fsqtp_HMwbjhy6IKjlA5lrVrtWt1JdMsC57k,73016
@@ -28,13 +31,14 @@ nova/trame/view/theme/assets/vuetify_config.json,sha256=a0FSgpLYWGFlRGSMhMq61MyD
28
31
  nova/trame/view/theme/exit_button.py,sha256=Kqv1GVJZGrSsj6_JFjGU3vm3iNuMolLC2T1x2IsdmV0,3094
29
32
  nova/trame/view/theme/theme.py,sha256=8JqSrEbhxK1SccXE1_jUdel9Wtc2QNObVEwtbVWG_QY,13146
30
33
  nova/trame/view/utilities/local_storage.py,sha256=vD8f2VZIpxhIKjZwEaD7siiPCTZO4cw9AfhwdawwYLY,3218
31
- nova/trame/view_model/data_selector.py,sha256=RyMHml1K_pupH4JtXnGxAaYTYYwNoEVus7Abdpqwueo,3698
34
+ nova/trame/view_model/data_selector.py,sha256=8XBCc12CcQfctOyz5zAOZTIXddKfpzIp8A_yZI-MpFc,2499
32
35
  nova/trame/view_model/execution_buttons.py,sha256=MfKSp95D92EqpD48C15cBo6dLO0Yld4FeRZMJNxJf7Y,3551
36
+ nova/trame/view_model/ornl/neutron_data_selector.py,sha256=CtVva_MXWGRG_KclE4ln8XJP_dfy6-5unE3pjEStOpE,2074
33
37
  nova/trame/view_model/progress_bar.py,sha256=6AUKHF3hfzbdsHqNEnmHRgDcBKY5TT8ywDx9S6ovnsc,2854
34
38
  nova/trame/view_model/remote_file_input.py,sha256=ojEOJ8ZPkajpbAaZi9VLj7g-uBjhb8BMrTdMmwf_J6A,3367
35
39
  nova/trame/view_model/tool_outputs.py,sha256=ev6LY7fJ0H2xAJn9f5ww28c8Kpom2SYc2FbvFcoN4zg,829
36
- nova_trame-0.22.1.dist-info/LICENSE,sha256=Iu5QiDbwNbREg75iYaxIJ_V-zppuv4QFuBhAW-qiAlM,1061
37
- nova_trame-0.22.1.dist-info/METADATA,sha256=A-wOqVcFQXeV1qpuPJv253RugeWK5j2UryAUYdwgtII,1689
38
- nova_trame-0.22.1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
39
- nova_trame-0.22.1.dist-info/entry_points.txt,sha256=J2AmeSwiTYZ4ZqHHp9HO6v4MaYQTTBPbNh6WtoqOT58,42
40
- nova_trame-0.22.1.dist-info/RECORD,,
40
+ nova_trame-0.23.0.dist-info/LICENSE,sha256=Iu5QiDbwNbREg75iYaxIJ_V-zppuv4QFuBhAW-qiAlM,1061
41
+ nova_trame-0.23.0.dist-info/METADATA,sha256=vKtoLVHkJVqpA4yqwZZmjWWoYdPk1avkhUc5B8KuF80,1688
42
+ nova_trame-0.23.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
43
+ nova_trame-0.23.0.dist-info/entry_points.txt,sha256=J2AmeSwiTYZ4ZqHHp9HO6v4MaYQTTBPbNh6WtoqOT58,42
44
+ nova_trame-0.23.0.dist-info/RECORD,,