nova-trame 0.22.0__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
  }
@@ -0,0 +1,73 @@
1
+ """Components used to control the lifecycle of a Themed Application."""
2
+
3
+ import logging
4
+ from typing import Any
5
+
6
+ from trame.app import get_server
7
+ from trame.widgets import vuetify3 as vuetify
8
+
9
+ logger = logging.getLogger(__name__)
10
+ logger.setLevel(logging.INFO)
11
+
12
+
13
+ class ExitButton:
14
+ """Exit button for Trame Applications."""
15
+
16
+ def __init__(self, exit_callback: Any = None, job_status_callback: Any = None) -> None:
17
+ self.server = get_server(None, client_type="vue3")
18
+ self.server.state.nova_kill_jobs_on_exit = False
19
+ self.server.state.nova_show_exit_dialog = False
20
+ self.server.state.nova_show_stop_jobs_on_exit_checkbox = False
21
+ self.server.state.nova_running_jobs = []
22
+ self.server.state.nova_show_exit_progress = False
23
+ self.exit_application_callback = exit_callback
24
+ self.job_status_callback = job_status_callback
25
+ self.create_ui()
26
+
27
+ def create_ui(self) -> None:
28
+ with vuetify.VBtn(
29
+ "Exit",
30
+ prepend_icon="mdi-close-box",
31
+ classes="mr-4 bg-error",
32
+ id="shutdown_app_theme_button",
33
+ color="white",
34
+ click=self.open_exit_dialog,
35
+ ):
36
+ with vuetify.VDialog(v_model="nova_show_exit_dialog", persistent="true"):
37
+ with vuetify.VCard(classes="pa-4 ma-auto"):
38
+ vuetify.VCardTitle("Exit Application")
39
+ with vuetify.VCardText(
40
+ "Are you sure you want to exit this application?",
41
+ variant="outlined",
42
+ ):
43
+ vuetify.VCheckbox(
44
+ v_model="nova_kill_jobs_on_exit",
45
+ label="Stop All Jobs On Exit.",
46
+ v_if="nova_running_jobs.length > 0",
47
+ )
48
+ with vuetify.VList():
49
+ vuetify.VListSubheader("Running Jobs:", v_if="nova_running_jobs.length > 0")
50
+ vuetify.VListItem("{{ item }}", v_for="(item, index) in nova_running_jobs")
51
+ with vuetify.VCardActions(v_if="!nova_show_exit_progress"):
52
+ vuetify.VBtn(
53
+ "Exit App",
54
+ click=self.exit_application_callback,
55
+ color="error",
56
+ )
57
+ vuetify.VBtn(
58
+ "Stay In App",
59
+ click=self.close_exit_dialog,
60
+ )
61
+ with vuetify.VCardActions(v_else=True):
62
+ vuetify.VCardText(
63
+ "Exiting Application...",
64
+ variant="outlined",
65
+ )
66
+ vuetify.VProgressCircular(indeterminate=True)
67
+
68
+ async def open_exit_dialog(self) -> None:
69
+ self.server.state.nova_show_exit_dialog = True
70
+ await self.job_status_callback()
71
+
72
+ async def close_exit_dialog(self) -> None:
73
+ self.server.state.nova_show_exit_dialog = False
@@ -1,12 +1,15 @@
1
1
  """Implementation of ThemedApp."""
2
2
 
3
+ import asyncio
3
4
  import json
4
5
  import logging
6
+ import sys
5
7
  from asyncio import create_task
6
8
  from functools import partial
7
9
  from pathlib import Path
8
10
  from typing import Optional
9
11
 
12
+ import blinker
10
13
  import sass
11
14
  from mergedeep import Strategy, merge
12
15
  from trame.app import get_server
@@ -18,7 +21,9 @@ from trame_client.widgets import html
18
21
  from trame_server.core import Server
19
22
  from trame_server.state import State
20
23
 
24
+ from nova.common.signals import Signal
21
25
  from nova.mvvm.pydantic_utils import validate_pydantic_parameter
26
+ from nova.trame.view.theme.exit_button import ExitButton
22
27
  from nova.trame.view.utilities.local_storage import LocalStorageManager
23
28
 
24
29
  THEME_PATH = Path(__file__).parent
@@ -138,6 +143,29 @@ class ThemedApp:
138
143
  async def init_theme(self) -> None:
139
144
  create_task(self._init_theme())
140
145
 
146
+ async def get_jobs_callback(self) -> None:
147
+ get_tools_signal = blinker.signal(Signal.GET_ALL_TOOLS)
148
+ response = get_tools_signal.send()
149
+ if response and len(response[0]) > 1: # Make sure that the callback had a return value
150
+ try:
151
+ self.server.state.nova_running_jobs = [tool.id for tool in response[0][1]]
152
+ if len(self.server.state.nova_running_jobs) > 0:
153
+ self.server.state.nova_show_stop_jobs_on_exit_checkbox = True
154
+ self.server.state.nova_kill_jobs_on_exit = True
155
+ else:
156
+ self.server.state.nova_show_stop_jobs_on_exit_checkbox = False
157
+ except Exception as e:
158
+ logger.warning(f"Issue getting running jobs: {e}")
159
+
160
+ async def exit_callback(self) -> None:
161
+ logger.info(f"Closing App. Killing jobs: {self.server.state.nova_kill_jobs_on_exit}")
162
+ if self.server.state.nova_kill_jobs_on_exit:
163
+ self.server.state.nova_show_exit_progress = True
164
+ await asyncio.sleep(2)
165
+ stop_signal = blinker.signal(Signal.EXIT_SIGNAL)
166
+ stop_signal.send()
167
+ sys.exit(0)
168
+
141
169
  def set_theme(self, theme: Optional[str], force: bool = True) -> None:
142
170
  """Sets the theme of the application.
143
171
 
@@ -227,6 +255,7 @@ class ThemedApp:
227
255
  "Selected",
228
256
  v_if=f"nova__theme === '{theme['value']}'",
229
257
  )
258
+ ExitButton(self.exit_callback, self.get_jobs_callback)
230
259
 
231
260
  with vuetify.VMain(classes="align-stretch d-flex flex-column h-screen"):
232
261
  # [slot override example]
@@ -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.0
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
@@ -18,7 +18,7 @@ Requires-Dist: blinker (>=1.9.0,<2.0.0)
18
18
  Requires-Dist: libsass
19
19
  Requires-Dist: mergedeep
20
20
  Requires-Dist: natsort (>=8.4.0,<9.0.0)
21
- Requires-Dist: nova-common (>=0.2.0)
21
+ Requires-Dist: nova-common (>=0.2.2)
22
22
  Requires-Dist: nova-mvvm
23
23
  Requires-Dist: pydantic
24
24
  Requires-Dist: tomli
@@ -43,7 +43,7 @@ You can install this package directly with
43
43
  pip install nova-trame
44
44
  ```
45
45
 
46
- A user guide, examples, and a full API for this package can be found at https://nova-application-development.readthedocs.io/en/stable/.
46
+ A user guide, examples, and a full API for this package can be found at [https://nova-application-development.readthedocs.io/en/stable/](https://nova-application-development.readthedocs.io/projects/nova-trame/en/stable/).
47
47
 
48
48
  Developers: please read [this document](DEVELOPMENT.md)
49
49
 
@@ -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,21 +22,23 @@ 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
26
29
  nova/trame/view/theme/assets/js/revo_grid.js,sha256=WBsmoslu9qI5DHZkHkJam2AVgdiBp6szfOSV8a9cA5Q,3579
27
30
  nova/trame/view/theme/assets/vuetify_config.json,sha256=a0FSgpLYWGFlRGSMhMq61MyDFBEBwvz55G4qjkM08cs,5627
28
- nova/trame/view/theme/theme.py,sha256=0KzBJgAZRwlnwzCIf7gUjDY-gbhON7b2h3CMh2_9HY4,11746
31
+ nova/trame/view/theme/exit_button.py,sha256=Kqv1GVJZGrSsj6_JFjGU3vm3iNuMolLC2T1x2IsdmV0,3094
32
+ nova/trame/view/theme/theme.py,sha256=8JqSrEbhxK1SccXE1_jUdel9Wtc2QNObVEwtbVWG_QY,13146
29
33
  nova/trame/view/utilities/local_storage.py,sha256=vD8f2VZIpxhIKjZwEaD7siiPCTZO4cw9AfhwdawwYLY,3218
30
- nova/trame/view_model/data_selector.py,sha256=RyMHml1K_pupH4JtXnGxAaYTYYwNoEVus7Abdpqwueo,3698
34
+ nova/trame/view_model/data_selector.py,sha256=8XBCc12CcQfctOyz5zAOZTIXddKfpzIp8A_yZI-MpFc,2499
31
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
32
37
  nova/trame/view_model/progress_bar.py,sha256=6AUKHF3hfzbdsHqNEnmHRgDcBKY5TT8ywDx9S6ovnsc,2854
33
38
  nova/trame/view_model/remote_file_input.py,sha256=ojEOJ8ZPkajpbAaZi9VLj7g-uBjhb8BMrTdMmwf_J6A,3367
34
39
  nova/trame/view_model/tool_outputs.py,sha256=ev6LY7fJ0H2xAJn9f5ww28c8Kpom2SYc2FbvFcoN4zg,829
35
- nova_trame-0.22.0.dist-info/LICENSE,sha256=Iu5QiDbwNbREg75iYaxIJ_V-zppuv4QFuBhAW-qiAlM,1061
36
- nova_trame-0.22.0.dist-info/METADATA,sha256=7ySWYYUkEE99UeZnMvMMwskNWHHhHjrrNv0_5dYRU0s,1603
37
- nova_trame-0.22.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
38
- nova_trame-0.22.0.dist-info/entry_points.txt,sha256=J2AmeSwiTYZ4ZqHHp9HO6v4MaYQTTBPbNh6WtoqOT58,42
39
- nova_trame-0.22.0.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,,