nova-trame 0.22.1__py3-none-any.whl → 0.23.1__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):
@@ -184,6 +47,10 @@ class DataSelectorModel:
184
47
 
185
48
  if len(path_parts) > 1:
186
49
  dirs.clear()
50
+ elif path_parts != ["."]:
51
+ # Subdirectories are fully queried upon being opened, so we only need to query one item to determine
52
+ # if the target directory has any children.
53
+ dirs[:] = dirs[:1]
187
54
 
188
55
  # Only create a new entry for top-level directories
189
56
  if len(path_parts) == 1 and path_parts[0] != ".": # This indicates a top-level directory
@@ -208,21 +75,24 @@ class DataSelectorModel:
208
75
 
209
76
  return self.sort_directories(directories)
210
77
 
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)
78
+ def get_directories(self, base_path: Optional[Path] = None) -> List[Dict[str, Any]]:
79
+ if base_path:
80
+ pass
218
81
  else:
82
+ base_path = Path(self.state.directory)
83
+
84
+ if not base_path:
219
85
  return []
220
86
 
87
+ return self.get_directories_from_path(base_path)
88
+
89
+ def get_datafiles_from_path(self, base_path: Path) -> List[str]:
90
+ datafiles = []
221
91
  try:
222
92
  if self.state.prefix:
223
- datafile_path = str(base_path / self.state.prefix)
93
+ datafile_path = base_path / self.state.prefix
224
94
  else:
225
- datafile_path = str(base_path / self.state.directory)
95
+ datafile_path = base_path / self.state.subdirectory
226
96
 
227
97
  for entry in os.scandir(datafile_path):
228
98
  if entry.is_file():
@@ -237,13 +107,10 @@ class DataSelectorModel:
237
107
 
238
108
  return natsorted(datafiles)
239
109
 
240
- def set_directory(self, directory_path: str) -> None:
241
- self.state.directory = directory_path
110
+ def get_datafiles(self) -> List[str]:
111
+ base_path = Path(self.state.directory)
112
+
113
+ return self.get_datafiles_from_path(base_path)
242
114
 
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
115
+ def set_subdirectory(self, subdirectory_path: str) -> None:
116
+ 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,15 +1,15 @@
1
1
  """View Implementation for DataSelector."""
2
2
 
3
+ from asyncio import ensure_future, sleep
3
4
  from typing import Any, List, Optional, cast
4
- from warnings import warn
5
5
 
6
6
  from trame.app import get_server
7
7
  from trame.widgets import client, datagrid, html
8
8
  from trame.widgets import vuetify3 as vuetify
9
9
 
10
10
  from nova.mvvm.trame_binding import TrameBinding
11
- from nova.trame.model.data_selector import CUSTOM_DIRECTORIES_LABEL, DataSelectorModel
12
- from nova.trame.view.layouts import GridLayout, VBoxLayout
11
+ from nova.trame.model.data_selector import DataSelectorModel, DataSelectorState
12
+ from nova.trame.view.layouts import GridLayout, HBoxLayout, VBoxLayout
13
13
  from nova.trame.view_model.data_selector import DataSelectorViewModel
14
14
 
15
15
  from .input_field import InputField
@@ -18,16 +18,15 @@ vuetify.enable_lab()
18
18
 
19
19
 
20
20
  class DataSelector(datagrid.VGrid):
21
- """Allows the user to select datafiles from an IPTS experiment."""
21
+ """Allows the user to select datafiles from the server."""
22
22
 
23
23
  def __init__(
24
24
  self,
25
25
  v_model: str,
26
- allow_custom_directories: bool = False,
27
- facility: str = "",
28
- instrument: str = "",
26
+ directory: str,
29
27
  extensions: Optional[List[str]] = None,
30
28
  prefix: str = "",
29
+ refresh_rate: int = 30,
31
30
  select_strategy: str = "all",
32
31
  **kwargs: Any,
33
32
  ) -> None:
@@ -38,18 +37,17 @@ class DataSelector(datagrid.VGrid):
38
37
  v_model : str
39
38
  The name of the state variable to bind to this widget. The state variable will contain a list of the files
40
39
  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).
40
+ directory : str
41
+ The top-level folder to expose to users. Only contents of this directory and its children will be exposed to
42
+ users.
48
43
  extensions : List[str], optional
49
44
  A list of file extensions to restrict selection to. If unset, then all files will be shown.
50
45
  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.
46
+ A subdirectory within the selected top-level folder to show files. If not specified, the user will be shown
47
+ a folder browser and will be able to see all files in the selected top-level folder.
48
+ refresh_rate : int, optional
49
+ The number of seconds between attempts to automatically refresh the file list. Set to zero to disable this
50
+ feature. Defaults to 30 seconds.
53
51
  select_strategy : str, optional
54
52
  The selection strategy to pass to the `VDataTable component <https://trame.readthedocs.io/en/latest/trame.widgets.vuetify3.html#trame.widgets.vuetify3.VDataTable>`__.
55
53
  If unset, the `all` strategy will be used.
@@ -61,6 +59,12 @@ class DataSelector(datagrid.VGrid):
61
59
  -------
62
60
  None
63
61
  """
62
+ if "allow_custom_directory" in kwargs or "facility" in kwargs or "instrument" in kwargs:
63
+ raise TypeError(
64
+ "The old DataSelector component has been renamed to NeutronDataSelector. Please import it from "
65
+ "`nova.trame.view.components.ornl`."
66
+ )
67
+
64
68
  if "items" in kwargs:
65
69
  raise AttributeError("The items parameter is not allowed on DataSelector widget.")
66
70
 
@@ -69,21 +73,16 @@ class DataSelector(datagrid.VGrid):
69
73
  else:
70
74
  self._label = None
71
75
 
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
76
  self._v_model = v_model
76
77
  self._v_model_name_in_state = v_model.split(".")[0]
77
- self._allow_custom_directories = allow_custom_directories
78
+ self._directory = directory
78
79
  self._extensions = extensions if extensions is not None else []
79
80
  self._prefix = prefix
81
+ self._refresh_rate = refresh_rate
80
82
  self._select_strategy = select_strategy
81
83
 
82
84
  self._revogrid_id = f"nova__dataselector_{self._next_id}_rv"
83
85
  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
86
  self._directories_name = f"nova__dataselector_{self._next_id}_directories"
88
87
  self._datafiles_name = f"nova__dataselector_{self._next_id}_datafiles"
89
88
 
@@ -93,36 +92,22 @@ class DataSelector(datagrid.VGrid):
93
92
  ).exec
94
93
  self._reset_state = client.JSEval(exec=f"{self._v_model} = []; {self._flush_state}").exec
95
94
 
96
- self.create_model(facility, instrument)
95
+ self.create_model()
97
96
  self.create_viewmodel()
98
97
 
99
- self.create_ui(facility, instrument, **kwargs)
98
+ self.create_ui(**kwargs)
100
99
 
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)
100
+ ensure_future(self._refresh_loop())
101
+
102
+ def create_ui(self, *args: Any, **kwargs: Any) -> None:
103
+ with VBoxLayout(classes="nova-data-selector", height="100%") as self._layout:
104
+ with HBoxLayout(valign="center"):
105
+ self._layout.filter = html.Div(classes="flex-1-1")
106
+ with vuetify.VBtn(
107
+ classes="mx-1", density="compact", icon=True, variant="text", click=self.refresh_contents
108
+ ):
109
+ vuetify.VIcon("mdi-refresh")
110
+ vuetify.VTooltip("Refresh Contents", activator="parent")
126
111
 
127
112
  with GridLayout(columns=2, classes="flex-1-0 h-0", valign="start"):
128
113
  if not self._prefix:
@@ -137,7 +122,7 @@ class DataSelector(datagrid.VGrid):
137
122
  item_value="path",
138
123
  items=(self._directories_name,),
139
124
  click_open=(self._vm.expand_directory, "[$event.path]"),
140
- update_activated=(self._vm.set_directory, "$event"),
125
+ update_activated=(self._vm.set_subdirectory, "$event"),
141
126
  )
142
127
  vuetify.VListItem("No directories found", classes="flex-0-1 text-center", v_else=True)
143
128
 
@@ -198,10 +183,9 @@ class DataSelector(datagrid.VGrid):
198
183
  f"(+{{{{ {self._v_model}.length - 2 }}}} others)", v_if="index === 2", classes="text-caption"
199
184
  )
200
185
 
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
- )
186
+ def create_model(self) -> None:
187
+ state = DataSelectorState()
188
+ self._model = DataSelectorModel(state, self._directory, self._extensions, self._prefix)
205
189
 
206
190
  def create_viewmodel(self) -> None:
207
191
  server = get_server(None, client_type="vue3")
@@ -209,39 +193,29 @@ class DataSelector(datagrid.VGrid):
209
193
 
210
194
  self._vm = DataSelectorViewModel(self._model, binding)
211
195
  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
196
  self._vm.directories_bind.connect(self._directories_name)
216
197
  self._vm.datafiles_bind.connect(self._datafiles_name)
217
198
  self._vm.reset_bind.connect(self.reset)
218
199
 
219
200
  self._vm.update_view()
220
201
 
202
+ def refresh_contents(self) -> None:
203
+ self._vm.update_view(refresh_directories=True)
204
+
221
205
  def reset(self, _: Any = None) -> None:
222
206
  self._reset_state()
223
207
  self._reset_rv_grid()
224
208
 
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.
209
+ def set_state(self, *args: Any, **kwargs: Any) -> None:
210
+ raise TypeError(
211
+ "The old DataSelector component has been renamed to NeutronDataSelector. Please import it from "
212
+ "`nova.trame.view.components.ornl`."
213
+ )
231
214
 
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.
215
+ async def _refresh_loop(self) -> None:
216
+ if self._refresh_rate > 0:
217
+ while True:
218
+ await sleep(self._refresh_rate)
242
219
 
243
- Returns
244
- -------
245
- None
246
- """
247
- self._vm.set_state(facility, instrument, experiment)
220
+ self.refresh_contents()
221
+ self.state.dirty(self._datafiles_name)
@@ -0,0 +1,3 @@
1
+ from .neutron_data_selector import NeutronDataSelector
2
+
3
+ __all__ = ["NeutronDataSelector"]
@@ -0,0 +1,155 @@
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
+ refresh_rate: int = 30,
36
+ select_strategy: str = "all",
37
+ **kwargs: Any,
38
+ ) -> None:
39
+ """Constructor for DataSelector.
40
+
41
+ Parameters
42
+ ----------
43
+ v_model : str
44
+ The name of the state variable to bind to this widget. The state variable will contain a list of the files
45
+ selected by the user.
46
+ allow_custom_directories : bool, optional
47
+ Whether or not to allow users to provide their own directories to search for datafiles in. Ignored if the
48
+ facility parameter is set.
49
+ facility : str, optional
50
+ The facility to restrict data selection to. Options: HFIR, SNS
51
+ instrument : str, optional
52
+ The instrument to restrict data selection to. Please use the instrument acronym (e.g. CG-2).
53
+ extensions : List[str], optional
54
+ A list of file extensions to restrict selection to. If unset, then all files will be shown.
55
+ prefix : str, optional
56
+ A subdirectory within the user's chosen experiment to show files. If not specified, the user will be shown a
57
+ folder browser and will be able to see all files in the experiment that they have access to.
58
+ refresh_rate : int, optional
59
+ The number of seconds between attempts to automatically refresh the file list. Set to zero to disable this
60
+ feature. Defaults to 30 seconds.
61
+ select_strategy : str, optional
62
+ The selection strategy to pass to the `VDataTable component <https://trame.readthedocs.io/en/latest/trame.widgets.vuetify3.html#trame.widgets.vuetify3.VDataTable>`__.
63
+ If unset, the `all` strategy will be used.
64
+ **kwargs
65
+ All other arguments will be passed to the underlying
66
+ `VDataTable component <https://trame.readthedocs.io/en/latest/trame.widgets.vuetify3.html#trame.widgets.vuetify3.VDataTable>`_.
67
+
68
+ Returns
69
+ -------
70
+ None
71
+ """
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
+ self._facility = facility
76
+ self._instrument = instrument
77
+ self._allow_custom_directories = allow_custom_directories
78
+
79
+ self._facilities_name = f"nova__neutrondataselector_{self._next_id}_facilities"
80
+ self._instruments_name = f"nova__neutrondataselector_{self._next_id}_instruments"
81
+ self._experiments_name = f"nova__neutrondataselector_{self._next_id}_experiments"
82
+
83
+ super().__init__(v_model, "", extensions, prefix, refresh_rate, select_strategy, **kwargs)
84
+
85
+ def create_ui(self, **kwargs: Any) -> None:
86
+ super().create_ui(**kwargs)
87
+ with self._layout.filter:
88
+ with GridLayout(columns=3):
89
+ columns = 3
90
+ if self._facility == "":
91
+ columns -= 1
92
+ InputField(
93
+ v_model=f"{self._state_name}.facility", items=(self._facilities_name,), type="autocomplete"
94
+ )
95
+ if self._instrument == "":
96
+ columns -= 1
97
+ InputField(
98
+ v_if=f"{self._state_name}.facility !== '{CUSTOM_DIRECTORIES_LABEL}'",
99
+ v_model=f"{self._state_name}.instrument",
100
+ items=(self._instruments_name,),
101
+ type="autocomplete",
102
+ )
103
+ InputField(
104
+ v_if=f"{self._state_name}.facility !== '{CUSTOM_DIRECTORIES_LABEL}'",
105
+ v_model=f"{self._state_name}.experiment",
106
+ column_span=columns,
107
+ items=(self._experiments_name,),
108
+ type="autocomplete",
109
+ )
110
+ InputField(v_else=True, v_model=f"{self._state_name}.custom_directory", column_span=2)
111
+
112
+ def create_model(self) -> None:
113
+ state = NeutronDataSelectorState()
114
+ self._model: NeutronDataSelectorModel = NeutronDataSelectorModel(
115
+ state, self._facility, self._instrument, self._extensions, self._prefix, self._allow_custom_directories
116
+ )
117
+
118
+ def create_viewmodel(self) -> None:
119
+ server = get_server(None, client_type="vue3")
120
+ binding = TrameBinding(server.state)
121
+
122
+ self._vm: NeutronDataSelectorViewModel = NeutronDataSelectorViewModel(self._model, binding)
123
+ self._vm.state_bind.connect(self._state_name)
124
+ self._vm.facilities_bind.connect(self._facilities_name)
125
+ self._vm.instruments_bind.connect(self._instruments_name)
126
+ self._vm.experiments_bind.connect(self._experiments_name)
127
+ self._vm.directories_bind.connect(self._directories_name)
128
+ self._vm.datafiles_bind.connect(self._datafiles_name)
129
+ self._vm.reset_bind.connect(self.reset)
130
+
131
+ self._vm.update_view()
132
+
133
+ def set_state(
134
+ self, facility: Optional[str] = None, instrument: Optional[str] = None, experiment: Optional[str] = None
135
+ ) -> None:
136
+ """Programmatically set the facility, instrument, and/or experiment to restrict data selection to.
137
+
138
+ If a parameter is None, then it will not be updated.
139
+
140
+ Parameters
141
+ ----------
142
+ facility : str, optional
143
+ The facility to restrict data selection to. Options: HFIR, SNS
144
+ instrument : str, optional
145
+ The instrument to restrict data selection to. Must be at the selected facility.
146
+ experiment : str, optional
147
+ The experiment to restrict data selection to. Must begin with "IPTS-". It is your responsibility to validate
148
+ that the provided experiment exists within the instrument directory. If it doesn't then no datafiles will be
149
+ shown to the user.
150
+
151
+ Returns
152
+ -------
153
+ None
154
+ """
155
+ 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
- self.expanded: List[str] = []
19
+ self.expanded: Dict[str, 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()
@@ -47,43 +44,28 @@ class DataSelectorViewModel:
47
44
  current_level["children"] = new_directories
48
45
 
49
46
  # Mark this directory as expanded and display the new content
50
- self.expanded.append(paths[-1])
47
+ self.expanded[paths[-1]] = paths
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()
50
+ def reexpand_directories(self) -> None:
51
+ paths_to_expand = self.expanded.values()
52
+ self.expanded = {}
60
53
 
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)
54
+ for paths in paths_to_expand:
55
+ self.expand_directory(paths)
66
56
 
67
57
  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()
58
+ pass
59
+
60
+ def set_subdirectory(self, subdirectory_path: str = "") -> None:
61
+ self.model.set_subdirectory(subdirectory_path)
80
62
  self.update_view()
81
63
 
82
- def update_view(self) -> None:
64
+ def update_view(self, refresh_directories: bool = False) -> None:
83
65
  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())
66
+ if not self.directories or refresh_directories:
67
+ self.directories = self.model.get_directories()
68
+ self.reexpand_directories()
87
69
  self.directories_bind.update_in_view(self.directories)
88
70
 
89
71
  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, refresh_directories: bool = False) -> 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(refresh_directories)
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: nova-trame
3
- Version: 0.22.1
3
+ Version: 0.23.1
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=8eaIVGak6XOAQMxdUotOsiXisW14sSRd1V0oViglJVQ,4448
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=d5UDF7ENBNifmLyIecx8VyINRY4vSxlQHyJ2a5G7IOY,9606
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=8B0BVXzVtMApnbMGD6ms6G4gdv5l-dIWM6YlHcBgnoc,6878
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=yDyYceZAUIxv4hhJGAPdIo54zHmrwqkxNZh7F2sRlw0,2810
32
35
  nova/trame/view_model/execution_buttons.py,sha256=MfKSp95D92EqpD48C15cBo6dLO0Yld4FeRZMJNxJf7Y,3551
36
+ nova/trame/view_model/ornl/neutron_data_selector.py,sha256=WTuz9QO4WzbOftTH0uP9Qxn7nYdsnIoixu1hWHKCVNU,2128
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.1.dist-info/LICENSE,sha256=Iu5QiDbwNbREg75iYaxIJ_V-zppuv4QFuBhAW-qiAlM,1061
41
+ nova_trame-0.23.1.dist-info/METADATA,sha256=VVMLgB8JhBIzt_FbCAEUIjPh3Zu8gEqQy091t3Fpd8E,1688
42
+ nova_trame-0.23.1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
43
+ nova_trame-0.23.1.dist-info/entry_points.txt,sha256=J2AmeSwiTYZ4ZqHHp9HO6v4MaYQTTBPbNh6WtoqOT58,42
44
+ nova_trame-0.23.1.dist-info/RECORD,,