nova-trame 0.23.1__py3-none-any.whl → 0.24.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.
- nova/trame/_internal/utils.py +30 -0
- nova/trame/model/data_selector.py +10 -10
- nova/trame/model/ornl/neutron_data_selector.py +13 -23
- nova/trame/view/components/data_selector.py +126 -28
- nova/trame/view/components/input_field.py +1 -1
- nova/trame/view/components/ornl/neutron_data_selector.py +150 -55
- nova/trame/view/layouts/grid.py +2 -2
- nova/trame/view/layouts/hbox.py +1 -1
- nova/trame/view/layouts/vbox.py +1 -1
- nova/trame/view_model/data_selector.py +4 -0
- nova/trame/view_model/ornl/neutron_data_selector.py +2 -14
- {nova_trame-0.23.1.dist-info → nova_trame-0.24.1.dist-info}/METADATA +1 -1
- {nova_trame-0.23.1.dist-info → nova_trame-0.24.1.dist-info}/RECORD +16 -15
- {nova_trame-0.23.1.dist-info → nova_trame-0.24.1.dist-info}/LICENSE +0 -0
- {nova_trame-0.23.1.dist-info → nova_trame-0.24.1.dist-info}/WHEEL +0 -0
- {nova_trame-0.23.1.dist-info → nova_trame-0.24.1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,30 @@
|
|
1
|
+
"""Internal utilities for nova-trame."""
|
2
|
+
|
3
|
+
from typing import Any, Tuple, Union
|
4
|
+
|
5
|
+
from trame_server.core import State
|
6
|
+
|
7
|
+
from nova.mvvm._internal.utils import rgetdictvalue, rsetdictvalue
|
8
|
+
|
9
|
+
|
10
|
+
# Reads a state parameter from Trame. For internal use only, if you're using this in your application you're violating
|
11
|
+
# the MVVM framework. :)
|
12
|
+
def get_state_param(state: State, value: Union[Any, Tuple]) -> Any:
|
13
|
+
if isinstance(value, tuple):
|
14
|
+
return rgetdictvalue(state, value[0])
|
15
|
+
|
16
|
+
return value
|
17
|
+
|
18
|
+
|
19
|
+
# Writes a state parameter to Trame. For internal use only, if you're using this in your application you're violating
|
20
|
+
# the MVVM framework. :)
|
21
|
+
def set_state_param(state: State, value: Union[Any, Tuple], new_value: Any = None) -> Any:
|
22
|
+
with state:
|
23
|
+
if isinstance(value, tuple):
|
24
|
+
if new_value is not None:
|
25
|
+
rsetdictvalue(state, value[0], new_value)
|
26
|
+
elif len(value) > 1:
|
27
|
+
rsetdictvalue(state, value[0], value[1])
|
28
|
+
state.dirty(value[0].split(".")[0])
|
29
|
+
|
30
|
+
return get_state_param(state, value)
|
@@ -12,20 +12,23 @@ class DataSelectorState(BaseModel, validate_assignment=True):
|
|
12
12
|
"""Selection state for identifying datafiles."""
|
13
13
|
|
14
14
|
directory: str = Field(default="")
|
15
|
-
subdirectory: str = Field(default="")
|
16
15
|
extensions: List[str] = Field(default=[])
|
17
|
-
|
16
|
+
subdirectory: str = Field(default="")
|
18
17
|
|
19
18
|
|
20
19
|
class DataSelectorModel:
|
21
20
|
"""Manages file system interactions for the DataSelector widget."""
|
22
21
|
|
23
|
-
def __init__(self, state: DataSelectorState
|
22
|
+
def __init__(self, state: DataSelectorState) -> None:
|
24
23
|
self.state: DataSelectorState = state
|
25
24
|
|
26
|
-
|
27
|
-
|
28
|
-
|
25
|
+
def set_binding_parameters(self, **kwargs: Any) -> None:
|
26
|
+
if "directory" in kwargs:
|
27
|
+
self.state.directory = kwargs["directory"]
|
28
|
+
if "extensions" in kwargs:
|
29
|
+
self.state.extensions = kwargs["extensions"]
|
30
|
+
if "subdirectory" in kwargs:
|
31
|
+
self.state.subdirectory = kwargs["subdirectory"]
|
29
32
|
|
30
33
|
def sort_directories(self, directories: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
31
34
|
# Sort the current level of dictionaries
|
@@ -89,10 +92,7 @@ class DataSelectorModel:
|
|
89
92
|
def get_datafiles_from_path(self, base_path: Path) -> List[str]:
|
90
93
|
datafiles = []
|
91
94
|
try:
|
92
|
-
|
93
|
-
datafile_path = base_path / self.state.prefix
|
94
|
-
else:
|
95
|
-
datafile_path = base_path / self.state.subdirectory
|
95
|
+
datafile_path = base_path / self.state.subdirectory
|
96
96
|
|
97
97
|
for entry in os.scandir(datafile_path):
|
98
98
|
if entry.is_file():
|
@@ -107,23 +107,21 @@ class NeutronDataSelectorState(DataSelectorState):
|
|
107
107
|
class NeutronDataSelectorModel(DataSelectorModel):
|
108
108
|
"""Manages file system interactions for the DataSelector widget."""
|
109
109
|
|
110
|
-
def __init__(
|
111
|
-
|
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)
|
110
|
+
def __init__(self, state: NeutronDataSelectorState) -> None:
|
111
|
+
super().__init__(state)
|
120
112
|
self.state: NeutronDataSelectorState = state
|
121
113
|
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
114
|
+
def set_binding_parameters(self, **kwargs: Any) -> None:
|
115
|
+
super().set_binding_parameters(**kwargs)
|
116
|
+
|
117
|
+
if "facility" in kwargs:
|
118
|
+
self.state.facility = kwargs["facility"]
|
119
|
+
if "instrument" in kwargs:
|
120
|
+
self.state.instrument = kwargs["instrument"]
|
121
|
+
if "experiment" in kwargs:
|
122
|
+
self.state.experiment = kwargs["experiment"]
|
123
|
+
if "allow_custom_directories" in kwargs:
|
124
|
+
self.state.allow_custom_directories = kwargs["allow_custom_directories"]
|
127
125
|
|
128
126
|
def get_facilities(self) -> List[str]:
|
129
127
|
return natsorted(self.state.get_facilities())
|
@@ -183,11 +181,3 @@ class NeutronDataSelectorModel(DataSelectorModel):
|
|
183
181
|
return []
|
184
182
|
|
185
183
|
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,13 +1,17 @@
|
|
1
1
|
"""View Implementation for DataSelector."""
|
2
2
|
|
3
3
|
from asyncio import ensure_future, sleep
|
4
|
-
from typing import Any, List,
|
4
|
+
from typing import Any, List, Tuple, Union, cast
|
5
|
+
from warnings import warn
|
5
6
|
|
6
7
|
from trame.app import get_server
|
7
8
|
from trame.widgets import client, datagrid, html
|
8
9
|
from trame.widgets import vuetify3 as vuetify
|
10
|
+
from trame_server.core import State
|
9
11
|
|
12
|
+
from nova.mvvm._internal.utils import rgetdictvalue
|
10
13
|
from nova.mvvm.trame_binding import TrameBinding
|
14
|
+
from nova.trame._internal.utils import get_state_param, set_state_param
|
11
15
|
from nova.trame.model.data_selector import DataSelectorModel, DataSelectorState
|
12
16
|
from nova.trame.view.layouts import GridLayout, HBoxLayout, VBoxLayout
|
13
17
|
from nova.trame.view_model.data_selector import DataSelectorViewModel
|
@@ -22,33 +26,36 @@ class DataSelector(datagrid.VGrid):
|
|
22
26
|
|
23
27
|
def __init__(
|
24
28
|
self,
|
25
|
-
v_model: str,
|
26
|
-
directory: str,
|
27
|
-
extensions:
|
28
|
-
prefix: str = "",
|
29
|
-
|
30
|
-
|
29
|
+
v_model: Union[str, Tuple],
|
30
|
+
directory: Union[str, Tuple],
|
31
|
+
extensions: Union[List[str], Tuple, None] = None,
|
32
|
+
prefix: Union[str, Tuple] = "",
|
33
|
+
subdirectory: Union[str, Tuple] = "",
|
34
|
+
refresh_rate: Union[int, Tuple] = 30,
|
35
|
+
select_strategy: Union[str, Tuple] = "all",
|
31
36
|
**kwargs: Any,
|
32
37
|
) -> None:
|
33
38
|
"""Constructor for DataSelector.
|
34
39
|
|
35
40
|
Parameters
|
36
41
|
----------
|
37
|
-
v_model : str
|
42
|
+
v_model : Union[str, Tuple]
|
38
43
|
The name of the state variable to bind to this widget. The state variable will contain a list of the files
|
39
44
|
selected by the user.
|
40
|
-
directory : str
|
45
|
+
directory : Union[str, Tuple]
|
41
46
|
The top-level folder to expose to users. Only contents of this directory and its children will be exposed to
|
42
47
|
users.
|
43
|
-
extensions : List[str], optional
|
48
|
+
extensions : Union[List[str], Tuple], optional
|
44
49
|
A list of file extensions to restrict selection to. If unset, then all files will be shown.
|
45
|
-
prefix : str, optional
|
46
|
-
|
47
|
-
|
48
|
-
|
50
|
+
prefix : Union[str, Tuple], optional
|
51
|
+
Deprecated. Please refer to the `subdirectory` parameter.
|
52
|
+
subdirectory : Union[str, Tuple], optional
|
53
|
+
A subdirectory within the selected top-level folder to show files. If not specified as a string, the user
|
54
|
+
will be shown a folder browser and will be able to see all files in the selected top-level folder.
|
55
|
+
refresh_rate : Union[int, Tuple], optional
|
49
56
|
The number of seconds between attempts to automatically refresh the file list. Set to zero to disable this
|
50
57
|
feature. Defaults to 30 seconds.
|
51
|
-
select_strategy : str, optional
|
58
|
+
select_strategy : Union[str, Tuple], optional
|
52
59
|
The selection strategy to pass to the `VDataTable component <https://trame.readthedocs.io/en/latest/trame.widgets.vuetify3.html#trame.widgets.vuetify3.VDataTable>`__.
|
53
60
|
If unset, the `all` strategy will be used.
|
54
61
|
**kwargs
|
@@ -73,11 +80,28 @@ class DataSelector(datagrid.VGrid):
|
|
73
80
|
else:
|
74
81
|
self._label = None
|
75
82
|
|
83
|
+
if prefix:
|
84
|
+
warn(
|
85
|
+
"The prefix parameter has been deprecated. Please switch to using the subdirectory parameter.",
|
86
|
+
category=DeprecationWarning,
|
87
|
+
stacklevel=1,
|
88
|
+
)
|
89
|
+
|
90
|
+
if not subdirectory:
|
91
|
+
subdirectory = prefix
|
92
|
+
|
76
93
|
self._v_model = v_model
|
77
|
-
|
94
|
+
if isinstance(v_model, str):
|
95
|
+
self._v_model_name_in_state = v_model.split(".")[0]
|
96
|
+
else:
|
97
|
+
self._v_model_name_in_state = v_model[0].split(".")[0]
|
98
|
+
|
78
99
|
self._directory = directory
|
100
|
+
self._last_directory = get_state_param(self.state, self._directory)
|
79
101
|
self._extensions = extensions if extensions is not None else []
|
80
|
-
self.
|
102
|
+
self._last_extensions = get_state_param(self.state, self._extensions)
|
103
|
+
self._subdirectory = subdirectory
|
104
|
+
self._last_subdirectory = get_state_param(self.state, self._subdirectory)
|
81
105
|
self._refresh_rate = refresh_rate
|
82
106
|
self._select_strategy = select_strategy
|
83
107
|
|
@@ -92,13 +116,18 @@ class DataSelector(datagrid.VGrid):
|
|
92
116
|
).exec
|
93
117
|
self._reset_state = client.JSEval(exec=f"{self._v_model} = []; {self._flush_state}").exec
|
94
118
|
|
95
|
-
self.
|
96
|
-
self.
|
119
|
+
self._create_model()
|
120
|
+
self._create_viewmodel()
|
121
|
+
self._setup_bindings()
|
97
122
|
|
98
123
|
self.create_ui(**kwargs)
|
99
124
|
|
100
125
|
ensure_future(self._refresh_loop())
|
101
126
|
|
127
|
+
@property
|
128
|
+
def state(self) -> State:
|
129
|
+
return get_server(None, client_type="vue3").state
|
130
|
+
|
102
131
|
def create_ui(self, *args: Any, **kwargs: Any) -> None:
|
103
132
|
with VBoxLayout(classes="nova-data-selector", height="100%") as self._layout:
|
104
133
|
with HBoxLayout(valign="center"):
|
@@ -110,7 +139,7 @@ class DataSelector(datagrid.VGrid):
|
|
110
139
|
vuetify.VTooltip("Refresh Contents", activator="parent")
|
111
140
|
|
112
141
|
with GridLayout(columns=2, classes="flex-1-0 h-0", valign="start"):
|
113
|
-
if not self.
|
142
|
+
if isinstance(self._subdirectory, tuple) or not self._subdirectory:
|
114
143
|
with html.Div(classes="d-flex flex-column h-100 overflow-hidden"):
|
115
144
|
vuetify.VListSubheader("Available Directories", classes="flex-0-1 justify-center px-0")
|
116
145
|
vuetify.VTreeview(
|
@@ -122,7 +151,7 @@ class DataSelector(datagrid.VGrid):
|
|
122
151
|
item_value="path",
|
123
152
|
items=(self._directories_name,),
|
124
153
|
click_open=(self._vm.expand_directory, "[$event.path]"),
|
125
|
-
update_activated=(self.
|
154
|
+
update_activated=(self.set_subdirectory, "$event"),
|
126
155
|
)
|
127
156
|
vuetify.VListItem("No directories found", classes="flex-0-1 text-center", v_else=True)
|
128
157
|
|
@@ -139,6 +168,7 @@ class DataSelector(datagrid.VGrid):
|
|
139
168
|
" prop: 'title',"
|
140
169
|
"}]",
|
141
170
|
),
|
171
|
+
column_span=1 if isinstance(self._subdirectory, tuple) or not self._subdirectory else 2,
|
142
172
|
frame_size=10,
|
143
173
|
hide_attribution=True,
|
144
174
|
id=self._revogrid_id,
|
@@ -183,11 +213,11 @@ class DataSelector(datagrid.VGrid):
|
|
183
213
|
f"(+{{{{ {self._v_model}.length - 2 }}}} others)", v_if="index === 2", classes="text-caption"
|
184
214
|
)
|
185
215
|
|
186
|
-
def
|
216
|
+
def _create_model(self) -> None:
|
187
217
|
state = DataSelectorState()
|
188
|
-
self._model = DataSelectorModel(state
|
218
|
+
self._model = DataSelectorModel(state)
|
189
219
|
|
190
|
-
def
|
220
|
+
def _create_viewmodel(self) -> None:
|
191
221
|
server = get_server(None, client_type="vue3")
|
192
222
|
binding = TrameBinding(server.state)
|
193
223
|
|
@@ -197,8 +227,6 @@ class DataSelector(datagrid.VGrid):
|
|
197
227
|
self._vm.datafiles_bind.connect(self._datafiles_name)
|
198
228
|
self._vm.reset_bind.connect(self.reset)
|
199
229
|
|
200
|
-
self._vm.update_view()
|
201
|
-
|
202
230
|
def refresh_contents(self) -> None:
|
203
231
|
self._vm.update_view(refresh_directories=True)
|
204
232
|
|
@@ -206,16 +234,86 @@ class DataSelector(datagrid.VGrid):
|
|
206
234
|
self._reset_state()
|
207
235
|
self._reset_rv_grid()
|
208
236
|
|
237
|
+
def set_subdirectory(self, subdirectory_path: str = "") -> None:
|
238
|
+
set_state_param(self.state, self._subdirectory, subdirectory_path)
|
239
|
+
self._vm.set_subdirectory(subdirectory_path)
|
240
|
+
|
209
241
|
def set_state(self, *args: Any, **kwargs: Any) -> None:
|
210
242
|
raise TypeError(
|
211
243
|
"The old DataSelector component has been renamed to NeutronDataSelector. Please import it from "
|
212
244
|
"`nova.trame.view.components.ornl`."
|
213
245
|
)
|
214
246
|
|
247
|
+
# This method sets up Trame state change listeners for each binding parameter that can be changed directly by this
|
248
|
+
# component. This allows us to communicate the changes to the developer's bindings without requiring our own. We
|
249
|
+
# don't want bindings in the internal implementation as our callbacks could compete with the developer's.
|
250
|
+
def _setup_bindings(self) -> None:
|
251
|
+
# If the bindings were given initial values, write these to the state.
|
252
|
+
set_state_param(self.state, self._directory)
|
253
|
+
set_state_param(self.state, self._extensions)
|
254
|
+
set_state_param(self.state, self._subdirectory)
|
255
|
+
self._vm.set_binding_parameters(
|
256
|
+
directory=get_state_param(self.state, self._directory),
|
257
|
+
extensions=get_state_param(self.state, self._extensions),
|
258
|
+
subdirectory=get_state_param(self.state, self._subdirectory),
|
259
|
+
)
|
260
|
+
|
261
|
+
# The component used by this parameter will attempt to set the initial value itself, which will trigger the
|
262
|
+
# below change listeners causing unpredictable behavior.
|
263
|
+
if isinstance(self._subdirectory, tuple):
|
264
|
+
self._subdirectory = (self._subdirectory[0],)
|
265
|
+
|
266
|
+
# Now we set up the change listeners for all bound parameters. These are responsible for updating the component
|
267
|
+
# when other portions of the application manipulate these parameters.
|
268
|
+
if isinstance(self._directory, tuple):
|
269
|
+
|
270
|
+
@self.state.change(self._directory[0].split(".")[0])
|
271
|
+
def on_directory_change(**kwargs: Any) -> None:
|
272
|
+
directory = rgetdictvalue(kwargs, self._directory[0])
|
273
|
+
if directory != self._last_directory:
|
274
|
+
self._last_directory = directory
|
275
|
+
self._vm.set_binding_parameters(
|
276
|
+
directory=set_state_param(self.state, self._directory, directory),
|
277
|
+
)
|
278
|
+
|
279
|
+
if isinstance(self._extensions, tuple):
|
280
|
+
|
281
|
+
@self.state.change(self._extensions[0].split(".")[0])
|
282
|
+
def on_extensions_change(**kwargs: Any) -> None:
|
283
|
+
extensions = rgetdictvalue(kwargs, self._extensions[0])
|
284
|
+
if extensions != self._last_extensions:
|
285
|
+
self._last_extensions = extensions
|
286
|
+
self._vm.set_binding_parameters(
|
287
|
+
extensions=set_state_param(self.state, self._extensions, extensions),
|
288
|
+
)
|
289
|
+
|
290
|
+
if isinstance(self._subdirectory, tuple):
|
291
|
+
|
292
|
+
@self.state.change(self._subdirectory[0].split(".")[0])
|
293
|
+
def on_subdirectory_change(**kwargs: Any) -> None:
|
294
|
+
subdirectory = rgetdictvalue(kwargs, self._subdirectory[0])
|
295
|
+
if subdirectory != self._last_subdirectory:
|
296
|
+
self._last_subdirectory = subdirectory
|
297
|
+
self._vm.set_binding_parameters(
|
298
|
+
subdirectory=set_state_param(self.state, self._subdirectory, subdirectory),
|
299
|
+
)
|
300
|
+
|
215
301
|
async def _refresh_loop(self) -> None:
|
216
|
-
|
302
|
+
refresh_rate: int = set_state_param(self.state, self._refresh_rate)
|
303
|
+
skip = False
|
304
|
+
|
305
|
+
if refresh_rate > 0:
|
217
306
|
while True:
|
218
|
-
await sleep(
|
307
|
+
await sleep(refresh_rate)
|
308
|
+
if skip:
|
309
|
+
continue
|
219
310
|
|
220
311
|
self.refresh_contents()
|
221
312
|
self.state.dirty(self._datafiles_name)
|
313
|
+
|
314
|
+
try:
|
315
|
+
refresh_rate = int(get_state_param(self.state, self._refresh_rate))
|
316
|
+
skip = False
|
317
|
+
except TypeError:
|
318
|
+
refresh_rate = 1
|
319
|
+
skip = True
|
@@ -172,7 +172,7 @@ class InputField:
|
|
172
172
|
The following example would set the auto_grow and label attributes on
|
173
173
|
`VTextarea <https://trame.readthedocs.io/en/latest/trame.widgets.vuetify3.html#trame.widgets.vuetify3.VTextarea>`_:
|
174
174
|
|
175
|
-
.. literalinclude:: ../tests/gallery/app.py
|
175
|
+
.. literalinclude:: ../tests/gallery/views/app.py
|
176
176
|
:start-after: InputField kwargs example start
|
177
177
|
:end-before: InputField kwargs example end
|
178
178
|
:dedent:
|
@@ -1,11 +1,12 @@
|
|
1
1
|
"""View Implementation for DataSelector."""
|
2
2
|
|
3
|
-
from typing import Any, List,
|
3
|
+
from typing import Any, List, Tuple, Union
|
4
4
|
from warnings import warn
|
5
5
|
|
6
6
|
from trame.app import get_server
|
7
7
|
from trame.widgets import vuetify3 as vuetify
|
8
8
|
|
9
|
+
from nova.mvvm._internal.utils import rgetdictvalue
|
9
10
|
from nova.mvvm.trame_binding import TrameBinding
|
10
11
|
from nova.trame.model.ornl.neutron_data_selector import (
|
11
12
|
CUSTOM_DIRECTORIES_LABEL,
|
@@ -15,7 +16,7 @@ from nova.trame.model.ornl.neutron_data_selector import (
|
|
15
16
|
from nova.trame.view.layouts import GridLayout
|
16
17
|
from nova.trame.view_model.ornl.neutron_data_selector import NeutronDataSelectorViewModel
|
17
18
|
|
18
|
-
from ..data_selector import DataSelector
|
19
|
+
from ..data_selector import DataSelector, get_state_param, set_state_param
|
19
20
|
from ..input_field import InputField
|
20
21
|
|
21
22
|
vuetify.enable_lab()
|
@@ -26,39 +27,42 @@ class NeutronDataSelector(DataSelector):
|
|
26
27
|
|
27
28
|
def __init__(
|
28
29
|
self,
|
29
|
-
v_model: str,
|
30
|
-
allow_custom_directories: bool = False,
|
31
|
-
facility: str = "",
|
32
|
-
instrument: str = "",
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
30
|
+
v_model: Union[str, Tuple],
|
31
|
+
allow_custom_directories: Union[bool, Tuple] = False,
|
32
|
+
facility: Union[str, Tuple] = "",
|
33
|
+
instrument: Union[str, Tuple] = "",
|
34
|
+
experiment: Union[str, Tuple] = "",
|
35
|
+
extensions: Union[List[str], Tuple, None] = None,
|
36
|
+
subdirectory: Union[str, Tuple] = "",
|
37
|
+
refresh_rate: Union[int, Tuple] = 30,
|
38
|
+
select_strategy: Union[str, Tuple] = "all",
|
37
39
|
**kwargs: Any,
|
38
40
|
) -> None:
|
39
41
|
"""Constructor for DataSelector.
|
40
42
|
|
41
43
|
Parameters
|
42
44
|
----------
|
43
|
-
v_model : str
|
45
|
+
v_model : Union[str, Tuple]
|
44
46
|
The name of the state variable to bind to this widget. The state variable will contain a list of the files
|
45
47
|
selected by the user.
|
46
|
-
allow_custom_directories : bool, optional
|
48
|
+
allow_custom_directories : Union[bool, Tuple], optional
|
47
49
|
Whether or not to allow users to provide their own directories to search for datafiles in. Ignored if the
|
48
50
|
facility parameter is set.
|
49
|
-
facility : str, optional
|
51
|
+
facility : Union[str, Tuple], optional
|
50
52
|
The facility to restrict data selection to. Options: HFIR, SNS
|
51
|
-
instrument : str, optional
|
53
|
+
instrument : Union[str, Tuple], optional
|
52
54
|
The instrument to restrict data selection to. Please use the instrument acronym (e.g. CG-2).
|
53
|
-
|
55
|
+
experiment : Union[str, Tuple], optional
|
56
|
+
The experiment to restrict data selection to.
|
57
|
+
extensions : Union[List[str], Tuple], optional
|
54
58
|
A list of file extensions to restrict selection to. If unset, then all files will be shown.
|
55
|
-
|
56
|
-
A subdirectory within the user's chosen experiment to show files. If not specified, the user
|
57
|
-
folder browser and will be able to see all files in the experiment that they have access to.
|
58
|
-
refresh_rate :
|
59
|
+
subdirectory : Union[str, Tuple], optional
|
60
|
+
A subdirectory within the user's chosen experiment to show files. If not specified as a string, the user
|
61
|
+
will be shown a folder browser and will be able to see all files in the experiment that they have access to.
|
62
|
+
refresh_rate : Union[str, Tuple], optional
|
59
63
|
The number of seconds between attempts to automatically refresh the file list. Set to zero to disable this
|
60
64
|
feature. Defaults to 30 seconds.
|
61
|
-
select_strategy : str, optional
|
65
|
+
select_strategy : Union[str, Tuple], optional
|
62
66
|
The selection strategy to pass to the `VDataTable component <https://trame.readthedocs.io/en/latest/trame.widgets.vuetify3.html#trame.widgets.vuetify3.VDataTable>`__.
|
63
67
|
If unset, the `all` strategy will be used.
|
64
68
|
**kwargs
|
@@ -69,53 +73,76 @@ class NeutronDataSelector(DataSelector):
|
|
69
73
|
-------
|
70
74
|
None
|
71
75
|
"""
|
72
|
-
if facility and allow_custom_directories:
|
73
|
-
warn("allow_custom_directories will be ignored since the facility parameter is
|
76
|
+
if isinstance(facility, str) and allow_custom_directories:
|
77
|
+
warn("allow_custom_directories will be ignored since the facility parameter is fixed.", stacklevel=1)
|
74
78
|
|
75
79
|
self._facility = facility
|
76
80
|
self._instrument = instrument
|
81
|
+
self._experiment = experiment
|
77
82
|
self._allow_custom_directories = allow_custom_directories
|
83
|
+
self._last_allow_custom_directories = self._allow_custom_directories
|
78
84
|
|
85
|
+
self._state_name = f"nova__dataselector_{self._next_id}_state"
|
79
86
|
self._facilities_name = f"nova__neutrondataselector_{self._next_id}_facilities"
|
87
|
+
self._selected_facility_name = (
|
88
|
+
self._facility[0] if isinstance(self._facility, tuple) else f"{self._state_name}.facility"
|
89
|
+
)
|
80
90
|
self._instruments_name = f"nova__neutrondataselector_{self._next_id}_instruments"
|
91
|
+
self._selected_instrument_name = (
|
92
|
+
self._instrument[0] if isinstance(self._instrument, tuple) else f"{self._state_name}.instrument"
|
93
|
+
)
|
81
94
|
self._experiments_name = f"nova__neutrondataselector_{self._next_id}_experiments"
|
95
|
+
self._selected_experiment_name = (
|
96
|
+
self._experiment[0] if isinstance(self._experiment, tuple) else f"{self._state_name}.experiment"
|
97
|
+
)
|
82
98
|
|
83
|
-
super().__init__(
|
99
|
+
super().__init__(
|
100
|
+
v_model,
|
101
|
+
"",
|
102
|
+
extensions=extensions,
|
103
|
+
subdirectory=subdirectory,
|
104
|
+
refresh_rate=refresh_rate,
|
105
|
+
select_strategy=select_strategy,
|
106
|
+
**kwargs,
|
107
|
+
)
|
84
108
|
|
85
109
|
def create_ui(self, **kwargs: Any) -> None:
|
86
110
|
super().create_ui(**kwargs)
|
87
111
|
with self._layout.filter:
|
88
112
|
with GridLayout(columns=3):
|
89
113
|
columns = 3
|
90
|
-
if self._facility
|
114
|
+
if isinstance(self._facility, tuple) or not self._facility:
|
91
115
|
columns -= 1
|
92
116
|
InputField(
|
93
|
-
v_model=
|
117
|
+
v_model=self._selected_facility_name,
|
118
|
+
items=(self._facilities_name,),
|
119
|
+
type="autocomplete",
|
120
|
+
update_modelValue=(self.update_facility, "[$event]"),
|
94
121
|
)
|
95
|
-
if self._instrument
|
122
|
+
if isinstance(self._instrument, tuple) or not self._instrument:
|
96
123
|
columns -= 1
|
97
124
|
InputField(
|
98
|
-
v_if=f"{self.
|
99
|
-
v_model=
|
125
|
+
v_if=f"{self._selected_facility_name} !== '{CUSTOM_DIRECTORIES_LABEL}'",
|
126
|
+
v_model=self._selected_instrument_name,
|
100
127
|
items=(self._instruments_name,),
|
101
128
|
type="autocomplete",
|
129
|
+
update_modelValue=(self.update_instrument, "[$event]"),
|
102
130
|
)
|
103
131
|
InputField(
|
104
|
-
v_if=f"{self.
|
105
|
-
v_model=
|
132
|
+
v_if=f"{self._selected_facility_name} !== '{CUSTOM_DIRECTORIES_LABEL}'",
|
133
|
+
v_model=self._selected_experiment_name,
|
106
134
|
column_span=columns,
|
107
135
|
items=(self._experiments_name,),
|
108
136
|
type="autocomplete",
|
137
|
+
update_modelValue=(self.update_experiment, "[$event]"),
|
109
138
|
)
|
110
139
|
InputField(v_else=True, v_model=f"{self._state_name}.custom_directory", column_span=2)
|
111
140
|
|
112
|
-
def
|
141
|
+
def _create_model(self) -> None:
|
113
142
|
state = NeutronDataSelectorState()
|
114
|
-
self._model: NeutronDataSelectorModel = NeutronDataSelectorModel(
|
115
|
-
state, self._facility, self._instrument, self._extensions, self._prefix, self._allow_custom_directories
|
116
|
-
)
|
143
|
+
self._model: NeutronDataSelectorModel = NeutronDataSelectorModel(state)
|
117
144
|
|
118
|
-
def
|
145
|
+
def _create_viewmodel(self) -> None:
|
119
146
|
server = get_server(None, client_type="vue3")
|
120
147
|
binding = TrameBinding(server.state)
|
121
148
|
|
@@ -130,26 +157,94 @@ class NeutronDataSelector(DataSelector):
|
|
130
157
|
|
131
158
|
self._vm.update_view()
|
132
159
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
160
|
+
# This method sets up Trame state change listeners for each binding parameter that can be changed directly by this
|
161
|
+
# component. This allows us to communicate the changes to the developer's bindings without requiring our own. We
|
162
|
+
# don't want bindings in the internal implementation as our callbacks could compete with the developer's.
|
163
|
+
def _setup_bindings(self) -> None:
|
164
|
+
# If the bindings were given initial values, write these to the state.
|
165
|
+
set_state_param(self.state, self._facility)
|
166
|
+
set_state_param(self.state, self._instrument)
|
167
|
+
set_state_param(self.state, self._experiment)
|
168
|
+
set_state_param(self.state, self._allow_custom_directories)
|
169
|
+
self._last_facility = get_state_param(self.state, self._facility)
|
170
|
+
self._last_instrument = get_state_param(self.state, self._instrument)
|
171
|
+
self._last_experiment = get_state_param(self.state, self._experiment)
|
172
|
+
self._vm.set_binding_parameters(
|
173
|
+
facility=get_state_param(self.state, self._facility),
|
174
|
+
instrument=get_state_param(self.state, self._instrument),
|
175
|
+
experiment=get_state_param(self.state, self._experiment),
|
176
|
+
allow_custom_directories=get_state_param(self.state, self._allow_custom_directories),
|
177
|
+
)
|
137
178
|
|
138
|
-
|
179
|
+
# Now we set up the change listeners for all bound parameters. These are responsible for updating the component
|
180
|
+
# when other portions of the application manipulate these parameters.
|
181
|
+
if isinstance(self._facility, tuple):
|
182
|
+
|
183
|
+
@self.state.change(self._facility[0].split(".")[0])
|
184
|
+
def on_facility_change(**kwargs: Any) -> None:
|
185
|
+
facility = rgetdictvalue(kwargs, self._facility[0])
|
186
|
+
if facility != self._last_facility:
|
187
|
+
self._last_facility = facility
|
188
|
+
self._vm.set_binding_parameters(
|
189
|
+
facility=set_state_param(self.state, (self._selected_facility_name,), facility)
|
190
|
+
)
|
191
|
+
self._vm.reset()
|
139
192
|
|
140
|
-
|
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.
|
193
|
+
if isinstance(self._instrument, tuple):
|
150
194
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
195
|
+
@self.state.change(self._instrument[0].split(".")[0])
|
196
|
+
def on_instrument_change(**kwargs: Any) -> None:
|
197
|
+
instrument = rgetdictvalue(kwargs, self._instrument[0])
|
198
|
+
if instrument != self._last_instrument:
|
199
|
+
self._last_instrument = instrument
|
200
|
+
self._vm.set_binding_parameters(
|
201
|
+
instrument=set_state_param(self.state, (self._selected_instrument_name,), instrument)
|
202
|
+
)
|
203
|
+
self._vm.reset()
|
204
|
+
|
205
|
+
if isinstance(self._experiment, tuple):
|
206
|
+
|
207
|
+
@self.state.change(self._experiment[0].split(".")[0])
|
208
|
+
def on_experiment_change(**kwargs: Any) -> None:
|
209
|
+
experiment = rgetdictvalue(kwargs, self._experiment[0])
|
210
|
+
if experiment != self._last_experiment:
|
211
|
+
self._last_experiment = experiment
|
212
|
+
self._vm.set_binding_parameters(
|
213
|
+
experiment=set_state_param(self.state, (self._selected_experiment_name,), experiment)
|
214
|
+
)
|
215
|
+
self._vm.reset()
|
216
|
+
|
217
|
+
if isinstance(self._allow_custom_directories, tuple):
|
218
|
+
|
219
|
+
@self.state.change(self._allow_custom_directories[0].split(".")[0])
|
220
|
+
def on_allow_custom_directories_change(**kwargs: Any) -> None:
|
221
|
+
allow_custom_directories = rgetdictvalue(kwargs, self._allow_custom_directories[0]) # type: ignore
|
222
|
+
if allow_custom_directories != self._last_allow_custom_directories:
|
223
|
+
self._last_allow_custom_directories = allow_custom_directories
|
224
|
+
self._vm.set_binding_parameters(
|
225
|
+
allow_custom_directories=set_state_param(
|
226
|
+
self.state, self._allow_custom_directories, allow_custom_directories
|
227
|
+
)
|
228
|
+
)
|
229
|
+
|
230
|
+
# These update methods notify the rest of the application when the component changes bound parameters.
|
231
|
+
def update_facility(self, facility: str) -> None:
|
232
|
+
self._vm.set_binding_parameters(
|
233
|
+
facility=set_state_param(self.state, (self._selected_facility_name,), facility),
|
234
|
+
instrument=set_state_param(self.state, (self._selected_instrument_name,), ""), # Reset the instrument
|
235
|
+
experiment=set_state_param(self.state, (self._selected_experiment_name,), ""), # Reset the experiment
|
236
|
+
)
|
237
|
+
self._vm.reset()
|
238
|
+
|
239
|
+
def update_instrument(self, instrument: str) -> None:
|
240
|
+
self._vm.set_binding_parameters(
|
241
|
+
instrument=set_state_param(self.state, (self._selected_instrument_name,), instrument),
|
242
|
+
experiment=set_state_param(self.state, (self._selected_experiment_name,), ""), # Reset the experiment
|
243
|
+
)
|
244
|
+
self._vm.reset()
|
245
|
+
|
246
|
+
def update_experiment(self, experiment: str) -> None:
|
247
|
+
self._vm.set_binding_parameters(
|
248
|
+
experiment=set_state_param(self.state, (self._selected_experiment_name,), experiment),
|
249
|
+
)
|
250
|
+
self._vm.reset()
|
nova/trame/view/layouts/grid.py
CHANGED
@@ -53,7 +53,7 @@ class GridLayout(html.Div):
|
|
53
53
|
--------
|
54
54
|
Basic usage:
|
55
55
|
|
56
|
-
.. literalinclude:: ../tests/gallery/app.py
|
56
|
+
.. literalinclude:: ../tests/gallery/views/app.py
|
57
57
|
:start-after: setup grid
|
58
58
|
:end-before: setup grid complete
|
59
59
|
:dedent:
|
@@ -133,7 +133,7 @@ class GridLayout(html.Div):
|
|
133
133
|
|
134
134
|
Example
|
135
135
|
-------
|
136
|
-
.. literalinclude:: ../tests/gallery/app.py
|
136
|
+
.. literalinclude:: ../tests/gallery/views/app.py
|
137
137
|
:start-after: grid row and column span example
|
138
138
|
:end-before: grid row and column span example end
|
139
139
|
:dedent:
|
nova/trame/view/layouts/hbox.py
CHANGED
nova/trame/view/layouts/vbox.py
CHANGED
@@ -57,6 +57,10 @@ class DataSelectorViewModel:
|
|
57
57
|
def on_state_updated(self, results: Dict[str, Any]) -> None:
|
58
58
|
pass
|
59
59
|
|
60
|
+
def set_binding_parameters(self, **kwargs: Any) -> None:
|
61
|
+
self.model.set_binding_parameters(**kwargs)
|
62
|
+
self.update_view(refresh_directories=True)
|
63
|
+
|
60
64
|
def set_subdirectory(self, subdirectory_path: str = "") -> None:
|
61
65
|
self.model.set_subdirectory(subdirectory_path)
|
62
66
|
self.update_view()
|
@@ -1,6 +1,6 @@
|
|
1
1
|
"""View model implementation for the DataSelector widget."""
|
2
2
|
|
3
|
-
from typing import Any, Dict
|
3
|
+
from typing import Any, Dict
|
4
4
|
|
5
5
|
from nova.mvvm.interface import BindingInterface
|
6
6
|
from nova.trame.model.ornl.neutron_data_selector import NeutronDataSelectorModel
|
@@ -18,10 +18,6 @@ class NeutronDataSelectorViewModel(DataSelectorViewModel):
|
|
18
18
|
self.instruments_bind = binding.new_bind()
|
19
19
|
self.experiments_bind = binding.new_bind()
|
20
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
21
|
def reset(self) -> None:
|
26
22
|
self.model.set_subdirectory("")
|
27
23
|
self.directories = self.model.get_directories()
|
@@ -31,17 +27,9 @@ class NeutronDataSelectorViewModel(DataSelectorViewModel):
|
|
31
27
|
def on_state_updated(self, results: Dict[str, Any]) -> None:
|
32
28
|
for update in results.get("updated", []):
|
33
29
|
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
30
|
case "custom_directory":
|
43
31
|
self.reset()
|
44
|
-
|
32
|
+
self.update_view()
|
45
33
|
|
46
34
|
def update_view(self, refresh_directories: bool = False) -> None:
|
47
35
|
self.facilities_bind.update_in_view(self.model.get_facilities())
|
@@ -1,15 +1,16 @@
|
|
1
1
|
nova/__init__.py,sha256=ED6jHcYiuYpr_0vjGz0zx2lrrmJT9sDJCzIljoDfmlM,65
|
2
2
|
nova/trame/__init__.py,sha256=gFrAg1qva5PIqR5TjvPzAxLx103IKipJLqp3XXvrQL8,59
|
3
|
-
nova/trame/
|
4
|
-
nova/trame/model/
|
3
|
+
nova/trame/_internal/utils.py,sha256=Yi6zdHfeIHE5dQXmxZ9x0Yyuwkjn2geFoXAYtZ_PO2s,1060
|
4
|
+
nova/trame/model/data_selector.py,sha256=rDmWDtHVGgi5e2fBEYvChM2vVKNu798m67Sq8MUN2UI,4463
|
5
|
+
nova/trame/model/ornl/neutron_data_selector.py,sha256=Eu-CnX4Gr_T-Kz_l3SsMhyGf61hlHhRCYpcn7em7TCk,6168
|
5
6
|
nova/trame/model/remote_file_input.py,sha256=9KAf31ZHzpsh_aXUrNcF81Q5jvUZDWCzW1QATKls-Jk,3675
|
6
7
|
nova/trame/view/components/__init__.py,sha256=60BeS69aOrFnkptjuD17rfPE1f4Z35iBH56TRmW5MW8,451
|
7
|
-
nova/trame/view/components/data_selector.py,sha256=
|
8
|
+
nova/trame/view/components/data_selector.py,sha256=EJ8fYCrkhRgDExAcewiUb7jTs7Qzo-GKVYWx7oFlrT0,14592
|
8
9
|
nova/trame/view/components/execution_buttons.py,sha256=fIkrWKI3jFZqk3GHhtmYh3nK2c-HOXpD3D3zd_TUpi0,4049
|
9
10
|
nova/trame/view/components/file_upload.py,sha256=7VcpfA6zmiqMDLkwVPlb35Tf0IUTBN1xsHpoUFnSr1w,3111
|
10
|
-
nova/trame/view/components/input_field.py,sha256=
|
11
|
+
nova/trame/view/components/input_field.py,sha256=Rtcl_eszvhgyC1rhTI7OMSLHjrE7DNH44eY08k7UXks,16094
|
11
12
|
nova/trame/view/components/ornl/__init__.py,sha256=HnxzzSsxw0vQSDCVFfWsAxx1n3HnU37LMuQkfiewmSU,90
|
12
|
-
nova/trame/view/components/ornl/neutron_data_selector.py,sha256=
|
13
|
+
nova/trame/view/components/ornl/neutron_data_selector.py,sha256=YTYuLD5eDn3AsKguT8ksPx2Yme2RxheuXQGlhuTLOOc,12527
|
13
14
|
nova/trame/view/components/progress_bar.py,sha256=fCfPw4MPAvORaeFOXugreok4GLpDVZGMkqvnv-AhMxg,2967
|
14
15
|
nova/trame/view/components/remote_file_input.py,sha256=ByrBFj8svyWezcardCWrS_4Ag3fgTYNg_11lDW1FIA8,9669
|
15
16
|
nova/trame/view/components/tool_outputs.py,sha256=-6pDURd2l_FK_8EWa9BI3KhU_KJXJ6uyJ_rW4nQVc08,2337
|
@@ -17,10 +18,10 @@ nova/trame/view/components/visualization/__init__.py,sha256=reqkkbhD5uSksHHlhVMy
|
|
17
18
|
nova/trame/view/components/visualization/interactive_2d_plot.py,sha256=foZCMoqbuahT5dtqIQvm8C4ZJcY9P211eJEcpQJltmM,3421
|
18
19
|
nova/trame/view/components/visualization/matplotlib_figure.py,sha256=GGH2cx-dQFkMAOTnlCrzMGDb2TN451I9J3gAS8tx2cs,12147
|
19
20
|
nova/trame/view/layouts/__init__.py,sha256=cMrlB5YMUoK8EGB83b34UU0kPTVrH8AxsYvKRtpUNEc,141
|
20
|
-
nova/trame/view/layouts/grid.py,sha256=
|
21
|
-
nova/trame/view/layouts/hbox.py,sha256=
|
21
|
+
nova/trame/view/layouts/grid.py,sha256=vqEX-jghs6j9_sVtijdRH7uhlD9loWNi90k2qgg4Dhg,5534
|
22
|
+
nova/trame/view/layouts/hbox.py,sha256=cdwnGk93ec6dXAeEamRQx1WTj5T7Ygsmsy0xz130tWM,3519
|
22
23
|
nova/trame/view/layouts/utils.py,sha256=Hg34VQWTG3yHBsgNvmfatR4J-uL3cko7UxSJpT-h3JI,376
|
23
|
-
nova/trame/view/layouts/vbox.py,sha256=
|
24
|
+
nova/trame/view/layouts/vbox.py,sha256=XRV14e32MY1HWc9FTVTv1vOatWWbhLMd0lYwZP-isTg,3520
|
24
25
|
nova/trame/view/theme/__init__.py,sha256=70_marDlTigIcPEOGiJb2JTs-8b2sGM5SlY7XBPtBDM,54
|
25
26
|
nova/trame/view/theme/assets/core_style.scss,sha256=AJ-2hyQRVkyOGWJB0lGMzlQeshbCJP5BGNYCt9e9AOI,4208
|
26
27
|
nova/trame/view/theme/assets/favicon.png,sha256=Xbp1nUmhcBDeObjsebEbEAraPDZ_M163M_ZLtm5AbQc,1927
|
@@ -31,14 +32,14 @@ nova/trame/view/theme/assets/vuetify_config.json,sha256=a0FSgpLYWGFlRGSMhMq61MyD
|
|
31
32
|
nova/trame/view/theme/exit_button.py,sha256=Kqv1GVJZGrSsj6_JFjGU3vm3iNuMolLC2T1x2IsdmV0,3094
|
32
33
|
nova/trame/view/theme/theme.py,sha256=8JqSrEbhxK1SccXE1_jUdel9Wtc2QNObVEwtbVWG_QY,13146
|
33
34
|
nova/trame/view/utilities/local_storage.py,sha256=vD8f2VZIpxhIKjZwEaD7siiPCTZO4cw9AfhwdawwYLY,3218
|
34
|
-
nova/trame/view_model/data_selector.py,sha256=
|
35
|
+
nova/trame/view_model/data_selector.py,sha256=LeVbrBatzwffiOywI8M7F9ldZxlpi92rFRishHkFhmo,2975
|
35
36
|
nova/trame/view_model/execution_buttons.py,sha256=MfKSp95D92EqpD48C15cBo6dLO0Yld4FeRZMJNxJf7Y,3551
|
36
|
-
nova/trame/view_model/ornl/neutron_data_selector.py,sha256=
|
37
|
+
nova/trame/view_model/ornl/neutron_data_selector.py,sha256=l1l_e0CFsVZ0h-9MPSjXTA2w1rgH3KoGAS67UyL8DvY,1551
|
37
38
|
nova/trame/view_model/progress_bar.py,sha256=6AUKHF3hfzbdsHqNEnmHRgDcBKY5TT8ywDx9S6ovnsc,2854
|
38
39
|
nova/trame/view_model/remote_file_input.py,sha256=ojEOJ8ZPkajpbAaZi9VLj7g-uBjhb8BMrTdMmwf_J6A,3367
|
39
40
|
nova/trame/view_model/tool_outputs.py,sha256=ev6LY7fJ0H2xAJn9f5ww28c8Kpom2SYc2FbvFcoN4zg,829
|
40
|
-
nova_trame-0.
|
41
|
-
nova_trame-0.
|
42
|
-
nova_trame-0.
|
43
|
-
nova_trame-0.
|
44
|
-
nova_trame-0.
|
41
|
+
nova_trame-0.24.1.dist-info/LICENSE,sha256=Iu5QiDbwNbREg75iYaxIJ_V-zppuv4QFuBhAW-qiAlM,1061
|
42
|
+
nova_trame-0.24.1.dist-info/METADATA,sha256=V5Lfat3qNcYwNAXcyz2F_eSOPK04_PrEzo6Dm-IOSC0,1688
|
43
|
+
nova_trame-0.24.1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
44
|
+
nova_trame-0.24.1.dist-info/entry_points.txt,sha256=J2AmeSwiTYZ4ZqHHp9HO6v4MaYQTTBPbNh6WtoqOT58,42
|
45
|
+
nova_trame-0.24.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|