nova-trame 0.19.2__py3-none-any.whl → 0.20.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.
@@ -54,23 +54,17 @@ INSTRUMENTS = {
54
54
  }
55
55
 
56
56
 
57
- def get_facilities() -> List[str]:
58
- return list(INSTRUMENTS.keys())
59
-
60
-
61
- def get_instruments(facility: str) -> List[str]:
62
- return list(INSTRUMENTS.get(facility, {}).keys())
63
-
64
-
65
57
  class DataSelectorState(BaseModel, validate_assignment=True):
66
58
  """Selection state for identifying datafiles."""
67
59
 
68
60
  facility: str = Field(default="", title="Facility")
69
61
  instrument: str = Field(default="", title="Instrument")
70
62
  experiment: str = Field(default="", title="Experiment")
63
+ user_directory: str = Field(default="", title="User Directory")
71
64
  directory: str = Field(default="")
72
65
  extensions: List[str] = Field(default=[])
73
66
  prefix: str = Field(default="")
67
+ show_user_directories: bool = Field(default=False)
74
68
 
75
69
  @field_validator("experiment", mode="after")
76
70
  @classmethod
@@ -81,11 +75,11 @@ class DataSelectorState(BaseModel, validate_assignment=True):
81
75
 
82
76
  @model_validator(mode="after")
83
77
  def validate_state(self) -> Self:
84
- valid_facilities = get_facilities()
78
+ valid_facilities = self.get_facilities()
85
79
  if self.facility and self.facility not in valid_facilities:
86
80
  warn(f"Facility '{self.facility}' could not be found. Valid options: {valid_facilities}", stacklevel=1)
87
81
 
88
- valid_instruments = get_instruments(self.facility)
82
+ valid_instruments = self.get_instruments()
89
83
  if self.instrument and self.instrument not in valid_instruments:
90
84
  warn(
91
85
  (
@@ -98,25 +92,37 @@ class DataSelectorState(BaseModel, validate_assignment=True):
98
92
 
99
93
  return self
100
94
 
95
+ def get_facilities(self) -> List[str]:
96
+ facilities = list(INSTRUMENTS.keys())
97
+ if self.show_user_directories:
98
+ facilities.append("User Directory")
99
+ return facilities
100
+
101
+ def get_instruments(self) -> List[str]:
102
+ return list(INSTRUMENTS.get(self.facility, {}).keys())
103
+
101
104
 
102
105
  class DataSelectorModel:
103
106
  """Manages file system interactions for the DataSelector widget."""
104
107
 
105
- def __init__(self, facility: str, instrument: str, extensions: List[str], prefix: str) -> None:
108
+ def __init__(
109
+ self, facility: str, instrument: str, extensions: List[str], prefix: str, show_user_directories: bool
110
+ ) -> None:
106
111
  self.state = DataSelectorState()
107
112
  self.state.facility = facility
108
113
  self.state.instrument = instrument
109
114
  self.state.extensions = extensions
110
115
  self.state.prefix = prefix
116
+ self.state.show_user_directories = show_user_directories
111
117
 
112
118
  def get_facilities(self) -> List[str]:
113
- return sorted(get_facilities())
119
+ return sorted(self.state.get_facilities())
114
120
 
115
121
  def get_instrument_dir(self) -> str:
116
122
  return INSTRUMENTS.get(self.state.facility, {}).get(self.state.instrument, "")
117
123
 
118
124
  def get_instruments(self) -> List[str]:
119
- return sorted(get_instruments(self.state.facility))
125
+ return sorted(self.state.get_instruments())
120
126
 
121
127
  def get_experiments(self) -> List[str]:
122
128
  experiments = []
@@ -142,17 +148,32 @@ class DataSelectorModel:
142
148
 
143
149
  return sorted_dirs
144
150
 
145
- def get_directories(self) -> List[Any]:
151
+ def get_experiment_directory_path(self) -> Optional[Path]:
146
152
  if not self.state.experiment:
153
+ return None
154
+
155
+ return Path("/") / self.state.facility / self.get_instrument_dir() / self.state.experiment
156
+
157
+ def get_user_directory_path(self) -> Optional[Path]:
158
+ if not self.state.user_directory:
159
+ return None
160
+
161
+ return Path("/SNS/users") / self.state.user_directory
162
+
163
+ def get_directories(self) -> List[str]:
164
+ if self.state.facility == "User Directory":
165
+ base_path = self.get_user_directory_path()
166
+ else:
167
+ base_path = self.get_experiment_directory_path()
168
+
169
+ if not base_path:
147
170
  return []
148
171
 
149
172
  directories = []
150
-
151
- experiment_path = Path("/") / self.state.facility / self.get_instrument_dir() / self.state.experiment
152
173
  try:
153
- for dirpath, _, _ in os.walk(experiment_path):
174
+ for dirpath, _, _ in os.walk(base_path):
154
175
  # Get the relative path from the start path
155
- path_parts = os.path.relpath(dirpath, experiment_path).split(os.sep)
176
+ path_parts = os.path.relpath(dirpath, base_path).split(os.sep)
156
177
 
157
178
  # Only create a new entry for top-level directories
158
179
  if len(path_parts) == 1 and path_parts[0] != ".": # This indicates a top-level directory
@@ -1,6 +1,17 @@
1
1
  from .data_selector import DataSelector
2
+ from .execution_buttons import ExecutionButtons
2
3
  from .file_upload import FileUpload
3
4
  from .input_field import InputField
5
+ from .progress_bar import ProgressBar
4
6
  from .remote_file_input import RemoteFileInput
7
+ from .tool_outputs import ToolOutputWindows
5
8
 
6
- __all__ = ["DataSelector", "FileUpload", "InputField", "RemoteFileInput"]
9
+ __all__ = [
10
+ "DataSelector",
11
+ "ExecutionButtons",
12
+ "FileUpload",
13
+ "InputField",
14
+ "ProgressBar",
15
+ "RemoteFileInput",
16
+ "ToolOutputWindows",
17
+ ]
@@ -1,6 +1,7 @@
1
1
  """View Implementation for DataSelector."""
2
2
 
3
3
  from typing import Any, List, Optional, cast
4
+ from warnings import warn
4
5
 
5
6
  from trame.app import get_server
6
7
  from trame.widgets import client, html
@@ -27,6 +28,7 @@ class DataSelector(vuetify.VDataTableVirtual):
27
28
  extensions: Optional[List[str]] = None,
28
29
  prefix: str = "",
29
30
  select_strategy: str = "all",
31
+ show_user_directories: bool = False,
30
32
  **kwargs: Any,
31
33
  ) -> None:
32
34
  """Constructor for DataSelector.
@@ -48,6 +50,9 @@ class DataSelector(vuetify.VDataTableVirtual):
48
50
  select_strategy : str, optional
49
51
  The selection strategy to pass to the `VDataTable component <https://trame.readthedocs.io/en/latest/trame.widgets.vuetify3.html#trame.widgets.vuetify3.VDataTable>`__.
50
52
  If unset, the `all` strategy will be used.
53
+ show_user_directories : bool, optional
54
+ Whether or not to allow users to select data files from user directories. Ignored if the facility parameter
55
+ is set.
51
56
  **kwargs
52
57
  All other arguments will be passed to the underlying
53
58
  `VDataTable component <https://trame.readthedocs.io/en/latest/trame.widgets.vuetify3.html#trame.widgets.vuetify3.VDataTable>`_.
@@ -64,10 +69,15 @@ class DataSelector(vuetify.VDataTableVirtual):
64
69
  else:
65
70
  self._label = None
66
71
 
72
+ if facility and show_user_directories:
73
+ warn("show_user_directories will be ignored since the facility parameter is set.", stacklevel=1)
74
+
67
75
  self._v_model = v_model
76
+ self._v_model_name_in_state = v_model.split(".")[0]
68
77
  self._extensions = extensions if extensions is not None else []
69
78
  self._prefix = prefix
70
79
  self._select_strategy = select_strategy
80
+ self._show_user_directories = show_user_directories
71
81
 
72
82
  self._state_name = f"nova__dataselector_{self._next_id}_state"
73
83
  self._facilities_name = f"nova__dataselector_{self._next_id}_facilities"
@@ -76,7 +86,7 @@ class DataSelector(vuetify.VDataTableVirtual):
76
86
  self._directories_name = f"nova__dataselector_{self._next_id}_directories"
77
87
  self._datafiles_name = f"nova__dataselector_{self._next_id}_datafiles"
78
88
 
79
- self._flush_state = f"flushState('{self._v_model.split('.')[0]}');"
89
+ self._flush_state = f"flushState('{self._v_model_name_in_state}');"
80
90
  self._reset_state = client.JSEval(exec=f"{self._v_model} = []; {self._flush_state}").exec
81
91
 
82
92
  self.create_model(facility, instrument)
@@ -96,14 +106,19 @@ class DataSelector(vuetify.VDataTableVirtual):
96
106
  if instrument == "":
97
107
  columns -= 1
98
108
  InputField(
99
- v_model=f"{self._state_name}.instrument", items=(self._instruments_name,), type="autocomplete"
109
+ v_if=f"{self._state_name}.facility !== 'User Directory'",
110
+ v_model=f"{self._state_name}.instrument",
111
+ items=(self._instruments_name,),
112
+ type="autocomplete",
100
113
  )
101
114
  InputField(
115
+ v_if=f"{self._state_name}.facility !== 'User Directory'",
102
116
  v_model=f"{self._state_name}.experiment",
103
117
  column_span=columns,
104
118
  items=(self._experiments_name,),
105
119
  type="autocomplete",
106
120
  )
121
+ InputField(v_else=True, v_model=f"{self._state_name}.user_directory", column_span=2)
107
122
 
108
123
  with GridLayout(columns=2, classes="flex-1-0 h-0", valign="start"):
109
124
  if not self._prefix:
@@ -155,7 +170,9 @@ class DataSelector(vuetify.VDataTableVirtual):
155
170
  )
156
171
 
157
172
  def create_model(self, facility: str, instrument: str) -> None:
158
- self._model = DataSelectorModel(facility, instrument, self._extensions, self._prefix)
173
+ self._model = DataSelectorModel(
174
+ facility, instrument, self._extensions, self._prefix, self._show_user_directories
175
+ )
159
176
 
160
177
  def create_viewmodel(self) -> None:
161
178
  server = get_server(None, client_type="vue3")
@@ -0,0 +1,112 @@
1
+ """Module for the Progress Tab."""
2
+
3
+ from trame.app import get_server
4
+ from trame.widgets import client
5
+ from trame.widgets import vuetify3 as vuetify
6
+ from trame_client.widgets import html
7
+
8
+ from nova.mvvm.trame_binding import TrameBinding
9
+ from nova.trame.view_model.execution_buttons import ExecutionButtonsViewModel
10
+
11
+
12
+ class ExecutionButtons:
13
+ """Execution buttons class. Adds Run/Stop/Cancel/Download buttons to the view.
14
+
15
+ This is intended to be used with the `nova-galaxy ToolRunner <https://nova-application-development.readthedocs.io/projects/nova-galaxy/en/latest/core_concepts/tool_runner.html>`__.
16
+ """
17
+
18
+ def __init__(self, id: str, stop_btn: bool = False, download_btn: bool = False) -> None:
19
+ """Constructor for ExecutionButtons.
20
+
21
+ Parameters
22
+ ----------
23
+ id : str
24
+ Component id. Should be used consistently with ToolRunner and other components.
25
+ stop_btn: bool
26
+ Display stop button.
27
+ download_btn : bool
28
+ Display download button.
29
+
30
+ Returns
31
+ -------
32
+ None
33
+ """
34
+ self.id = f"execution_{id}"
35
+
36
+ self.server = get_server(None, client_type="vue3")
37
+ binding = TrameBinding(self.server.state)
38
+ self.ctrl = self.server.controller
39
+ self.stop_btn = stop_btn
40
+ self.download_btn = download_btn
41
+ self.view_model = ExecutionButtonsViewModel(id, binding)
42
+ self.view_model.buttons_state_bind.connect(self.id)
43
+ self._download = client.JSEval(
44
+ exec=(
45
+ "async ($event) => {"
46
+ " const blob = new window.Blob([$event], {type: 'application/zip'});"
47
+ " const url = window.URL.createObjectURL(blob);"
48
+ " const anchor = window.document.createElement('a');"
49
+ " anchor.setAttribute('href', url);"
50
+ " anchor.setAttribute('download', 'results.zip');"
51
+ " window.document.body.appendChild(anchor);"
52
+ " anchor.click();"
53
+ " window.document.body.removeChild(anchor);"
54
+ " setTimeout(() => window.URL.revokeObjectURL(url), 1000);"
55
+ "}"
56
+ )
57
+ ).exec
58
+
59
+ self.create_ui()
60
+
61
+ def create_ui(self) -> None:
62
+ with html.Div(classes="d-flex justify-center my-4 w-100"):
63
+ vuetify.VBtn(
64
+ "Run",
65
+ disabled=(f"{self.id}.run_disabled",),
66
+ prepend_icon="mdi-play",
67
+ classes="mr-4",
68
+ id=f"{self.id}_run",
69
+ click=self.run,
70
+ )
71
+ if self.stop_btn:
72
+ vuetify.VBtn(
73
+ "Stop",
74
+ disabled=(f"{self.id}.stop_disabled",),
75
+ loading=(f"{self.id}.stop_in_progress",),
76
+ classes="mr-4",
77
+ id=f"{self.id}_stop",
78
+ prepend_icon="mdi-stop",
79
+ click=self.stop,
80
+ )
81
+ vuetify.VBtn(
82
+ "Cancel",
83
+ disabled=(f"{self.id}.cancel_disabled",),
84
+ color="error",
85
+ loading=(f"{self.id}.cancel_in_progress",),
86
+ prepend_icon="mdi-cancel",
87
+ classes="mr-4",
88
+ id=f"{self.id}_cancel",
89
+ click=self.cancel,
90
+ )
91
+ if self.download_btn:
92
+ vuetify.VBtn(
93
+ "Download Results",
94
+ disabled=(f"{self.id}.download_disabled",),
95
+ loading=(f"{self.id}.download_in_progress",),
96
+ id=f"{self.id}.download",
97
+ click=self.download,
98
+ )
99
+
100
+ async def download(self) -> None:
101
+ content = await self.view_model.prepare_results()
102
+ if content:
103
+ self._download(content)
104
+
105
+ async def run(self) -> None:
106
+ await self.view_model.run()
107
+
108
+ async def cancel(self) -> None:
109
+ await self.view_model.cancel()
110
+
111
+ async def stop(self) -> None:
112
+ await self.view_model.stop()
@@ -0,0 +1,65 @@
1
+ """Module for the Progress Tab."""
2
+
3
+ from trame.app import get_server
4
+ from trame.widgets import vuetify3 as vuetify
5
+ from trame_client.widgets import html
6
+
7
+ from nova.mvvm.trame_binding import TrameBinding
8
+ from nova.trame.view_model.progress_bar import ProgressBarViewModel
9
+
10
+
11
+ class ProgressBar:
12
+ """Progress bar class. Adds progress bar that displays job status to the view.
13
+
14
+ This is intended to be used with the `nova-galaxy ToolRunner <https://nova-application-development.readthedocs.io/projects/nova-galaxy/en/latest/core_concepts/tool_runner.html>`__.
15
+ """
16
+
17
+ def __init__(self, id: str) -> None:
18
+ """Constructor for ProgressBar.
19
+
20
+ Parameters
21
+ ----------
22
+ id : str
23
+ Component id. Should be used consistently with ToolRunner and other components
24
+
25
+ Returns
26
+ -------
27
+ None
28
+ """
29
+ self.id = f"progress_bar_{id}"
30
+ self.create_viewmodel(id)
31
+ self.view_model.progress_state_bind.connect(self.id)
32
+ self.create_ui()
33
+
34
+ def create_viewmodel(self, id: str) -> None:
35
+ server = get_server(None, client_type="vue3")
36
+ binding = TrameBinding(server.state)
37
+ self.view_model = ProgressBarViewModel(id, binding)
38
+
39
+ def create_ui(self) -> None:
40
+ with vuetify.VProgressLinear(
41
+ height="25",
42
+ model_value=(f"{self.id}.progress", "0"),
43
+ striped=True,
44
+ id=f"{self.id}_show_progress",
45
+ v_show=(f"{self.id}.show_progress",),
46
+ ):
47
+ html.H5(v_text=f"{self.id}.details")
48
+ with vuetify.VProgressLinear(
49
+ height="25",
50
+ model_value="100",
51
+ striped=False,
52
+ color="error",
53
+ id=f"{self.id}_show_failed",
54
+ v_show=(f"{self.id}.show_failed",),
55
+ ):
56
+ html.H5(v_text=f"{self.id}.details", classes="text-white")
57
+ with vuetify.VProgressLinear(
58
+ height="25",
59
+ model_value="100",
60
+ striped=False,
61
+ color="primary",
62
+ id=f"{self.id}_show_ok",
63
+ v_show=(f"{self.id}.show_ok",),
64
+ ):
65
+ html.H5(v_text=f"{self.id}.details", classes="text-white")
@@ -0,0 +1,63 @@
1
+ """Module for the Tool outputs."""
2
+
3
+ from trame.app import get_server
4
+ from trame.widgets import vuetify3 as vuetify
5
+
6
+ from nova.mvvm.trame_binding import TrameBinding
7
+ from nova.trame.view.components import InputField
8
+ from nova.trame.view.layouts import HBoxLayout
9
+ from nova.trame.view_model.tool_outputs import ToolOutputsViewModel
10
+
11
+
12
+ class ToolOutputWindows:
13
+ """Tool outputs class. Displays windows with tool stdout/stderr.
14
+
15
+ This is intended to be used with the `nova-galaxy ToolRunner <https://nova-application-development.readthedocs.io/projects/nova-galaxy/en/latest/core_concepts/tool_runner.html>`__.
16
+ """
17
+
18
+ def __init__(self, id: str) -> None:
19
+ """Constructor for ToolOutputWindows.
20
+
21
+ Parameters
22
+ ----------
23
+ id : str
24
+ Component id. Should be used consistently with ToolRunner and other components
25
+
26
+ Returns
27
+ -------
28
+ None
29
+ """
30
+ self.id = f"tool_outputs_{id}"
31
+ self.create_viewmodel(id)
32
+ self.view_model.tool_outputs_bind.connect(self.id)
33
+ self.create_ui()
34
+
35
+ def create_viewmodel(self, id: str) -> None:
36
+ server = get_server(None, client_type="vue3")
37
+ binding = TrameBinding(server.state)
38
+ self.view_model = ToolOutputsViewModel(id, binding)
39
+
40
+ def create_ui(self) -> None:
41
+ with HBoxLayout(classes="d-flex", width="100%"):
42
+ with vuetify.VTabs(v_model=(f"{self.id}_active_output_tab", "1"), direction="vertical"):
43
+ vuetify.VTab("Console output", value=1)
44
+ vuetify.VTab("Console error", value=2)
45
+ with HBoxLayout(classes="flex-grow-1"):
46
+ InputField(
47
+ v_show=f"{self.id}_active_output_tab === '1'",
48
+ v_model=f"{self.id}.stdout",
49
+ id=f"{self.id}_outputs",
50
+ type="autoscroll",
51
+ auto_grow=True,
52
+ readonly=True,
53
+ max_rows="30",
54
+ )
55
+ InputField(
56
+ v_show=f"{self.id}_active_output_tab === '2'",
57
+ v_model=f"{self.id}.stderr",
58
+ id=f"{self.id}_errors",
59
+ type="autoscroll",
60
+ auto_grow=True,
61
+ readonly=True,
62
+ max_rows="30",
63
+ )
@@ -130,10 +130,14 @@ html {
130
130
  min-width: 0px !important;
131
131
  padding: 5px 5px !important;
132
132
  box-shadow: none !important;
133
- }
134
133
 
135
- .v-btn__content {
136
- text-transform: none;
134
+ .v-btn__content {
135
+ text-transform: none;
136
+ }
137
+
138
+ .v-btn__prepend {
139
+ margin-left: 1px;
140
+ }
137
141
  }
138
142
 
139
143
  .v-label {
@@ -145,6 +149,10 @@ html {
145
149
  padding: 5px;
146
150
  }
147
151
 
152
+ textarea.v-field__input {
153
+ mask-image: none !important;
154
+ }
155
+
148
156
  .v-field {
149
157
  margin: 8px 4px 8px 4px;
150
158
  }
@@ -29,17 +29,23 @@ class DataSelectorViewModel:
29
29
  self.model.set_state(facility, instrument, experiment)
30
30
  self.update_view()
31
31
 
32
+ def reset(self) -> None:
33
+ self.model.set_directory("")
34
+ self.reset_bind.update_in_view(None)
35
+
32
36
  def on_state_updated(self, results: Dict[str, Any]) -> None:
33
37
  for update in results.get("updated", []):
34
38
  match update:
35
39
  case "facility":
36
40
  self.model.set_state(facility=None, instrument="", experiment="")
37
- self.model.set_directory("")
38
- self.reset_bind.update_in_view(None)
41
+ self.reset()
39
42
  case "instrument":
40
43
  self.model.set_state(facility=None, instrument=None, experiment="")
41
- self.model.set_directory("")
42
- self.reset_bind.update_in_view(None)
44
+ self.reset()
45
+ case "experiment":
46
+ self.reset()
47
+ case "user_directory":
48
+ self.reset()
43
49
  self.update_view()
44
50
 
45
51
  def update_view(self) -> None:
@@ -0,0 +1,87 @@
1
+ """Module for the JobProgress ViewModel."""
2
+
3
+ import asyncio
4
+ from typing import Any, Optional
5
+
6
+ import blinker
7
+ from pydantic import BaseModel
8
+
9
+ from nova.common.job import WorkState
10
+ from nova.common.signals import Signal, ToolCommand, get_signal_id
11
+ from nova.mvvm.interface import BindingInterface
12
+
13
+
14
+ def job_running(status: WorkState) -> bool:
15
+ """A helper function to check if job is doing something in Galaxy."""
16
+ return status in [
17
+ WorkState.UPLOADING_DATA,
18
+ WorkState.QUEUED,
19
+ WorkState.RUNNING,
20
+ WorkState.STOPPING,
21
+ WorkState.CANCELING,
22
+ ]
23
+
24
+
25
+ class ButtonsState(BaseModel):
26
+ """Class that manages start/stop/cancel button states."""
27
+
28
+ run_disabled: bool = False
29
+ cancel_disabled: bool = True
30
+ stop_disabled: bool = True
31
+ download_disabled: bool = True
32
+
33
+ stop_in_progress: bool = False
34
+ cancel_in_progress: bool = False
35
+ download_in_progress: bool = False
36
+
37
+ def update_from_workstate(self, status: WorkState) -> None:
38
+ running = job_running(status)
39
+ self.run_disabled = running
40
+ self.cancel_disabled = not running
41
+ self.stop_disabled = status not in [WorkState.RUNNING, WorkState.STOPPING]
42
+ self.stop_in_progress = status == WorkState.STOPPING
43
+ self.cancel_in_progress = status == WorkState.CANCELING
44
+ self.download_disabled = status != WorkState.FINISHED
45
+
46
+
47
+ class ExecutionButtonsViewModel:
48
+ """A viewmodel responsible for execution buttons."""
49
+
50
+ def __init__(self, id: str, binding: BindingInterface):
51
+ self.sender_id = f"ExecutionButtonsViewModel_{id}"
52
+ self.button_states = ButtonsState()
53
+ self.buttons_state_bind = binding.new_bind(self.button_states)
54
+ self.execution_signal = blinker.signal(get_signal_id(id, Signal.TOOL_COMMAND))
55
+ self.progress_signal = blinker.signal(get_signal_id(id, Signal.PROGRESS))
56
+ self.progress_signal.connect(self.update_state, weak=False)
57
+
58
+ async def update_state(self, _sender: Any, state: WorkState, details: str) -> None:
59
+ self.button_states.update_from_workstate(state)
60
+ self.buttons_state_bind.update_in_view(self.button_states)
61
+
62
+ async def run(self) -> None:
63
+ # disable run now since it might take some time before the client updates the status
64
+ self.button_states.run_disabled = True
65
+ self.buttons_state_bind.update_in_view(self.button_states)
66
+ await self.execution_signal.send_async(self.sender_id, command=ToolCommand.START)
67
+
68
+ async def cancel(self) -> None:
69
+ await self.execution_signal.send_async(self.sender_id, command=ToolCommand.CANCEL)
70
+
71
+ async def stop(self) -> None:
72
+ await self.execution_signal.send_async(self.sender_id, command=ToolCommand.CANCEL)
73
+
74
+ async def prepare_results(self) -> Optional[bytes]:
75
+ self.button_states.download_in_progress = True
76
+ self.buttons_state_bind.update_in_view(self.button_states)
77
+ await asyncio.sleep(0.5) # to give Trame time to update view
78
+ responses = await self.execution_signal.send_async(self.sender_id, command=ToolCommand.GET_RESULTS)
79
+ res = None
80
+ for response in responses: # responses can come from multiple places
81
+ if response[1] is None:
82
+ continue
83
+ if response[1]["sender"] == self.sender_id and response[1]["command"] == ToolCommand.GET_RESULTS:
84
+ res = response[1]["results"]
85
+ self.button_states.download_in_progress = False
86
+ self.buttons_state_bind.update_in_view(self.button_states)
87
+ return res
@@ -0,0 +1,75 @@
1
+ """Module for the JobProgress ViewModel."""
2
+
3
+ from typing import Any
4
+
5
+ import blinker
6
+ from pydantic import BaseModel
7
+
8
+ from nova.common.job import WorkState
9
+ from nova.common.signals import Signal, get_signal_id
10
+ from nova.mvvm.interface import BindingInterface
11
+
12
+
13
+ def details_from_state(state: WorkState) -> str:
14
+ work_state_map = {
15
+ WorkState.NOT_STARTED: "job not started",
16
+ WorkState.UPLOADING_DATA: "uploading data",
17
+ WorkState.QUEUED: "job is queued",
18
+ WorkState.RUNNING: "job is running",
19
+ WorkState.FINISHED: "job finished",
20
+ WorkState.ERROR: "job produced an error",
21
+ WorkState.DELETED: "job deleted",
22
+ WorkState.CANCELED: "job canceled",
23
+ WorkState.STOPPING: "stopping job",
24
+ WorkState.CANCELING: "canceling job",
25
+ }
26
+ if state in work_state_map:
27
+ return work_state_map[state]
28
+ else:
29
+ return state.value
30
+
31
+
32
+ class ProgressState(BaseModel):
33
+ """Class that manages progress bars states."""
34
+
35
+ progress: str = ""
36
+ details: str = ""
37
+ show_progress: bool = False
38
+ show_failed: bool = False
39
+ show_ok: bool = False
40
+
41
+ def update_from_workstate(self, state: WorkState) -> None:
42
+ progress = "0"
43
+ match state:
44
+ case WorkState.UPLOADING_DATA:
45
+ progress = "10"
46
+ case WorkState.QUEUED:
47
+ progress = "20"
48
+ case WorkState.RUNNING:
49
+ progress = "50"
50
+
51
+ self.show_progress = state in [
52
+ WorkState.UPLOADING_DATA,
53
+ WorkState.QUEUED,
54
+ WorkState.RUNNING,
55
+ WorkState.CANCELING,
56
+ WorkState.STOPPING,
57
+ ]
58
+ self.show_failed = state == WorkState.ERROR
59
+ self.show_ok = state == WorkState.FINISHED
60
+ self.progress = progress
61
+ self.details = details_from_state(state)
62
+
63
+
64
+ class ProgressBarViewModel:
65
+ """A viewmodel responsible for progress bar."""
66
+
67
+ def __init__(self, id: str, binding: BindingInterface):
68
+ self.progress_state = ProgressState()
69
+ self.progress_state_bind = binding.new_bind(self.progress_state)
70
+ self.progress_signal = blinker.signal(get_signal_id(id, Signal.PROGRESS))
71
+ self.progress_signal.connect(self.update_state, weak=False)
72
+
73
+ async def update_state(self, _sender: Any, state: WorkState, details: str) -> None:
74
+ self.progress_state.update_from_workstate(state)
75
+ self.progress_state_bind.update_in_view(self.progress_state)
@@ -0,0 +1,23 @@
1
+ """Module for the Tool ouputs ViewModel."""
2
+
3
+ from typing import Any
4
+
5
+ import blinker
6
+
7
+ from nova.common.job import ToolOutputs
8
+ from nova.common.signals import Signal, get_signal_id
9
+ from nova.mvvm.interface import BindingInterface
10
+
11
+
12
+ class ToolOutputsViewModel:
13
+ """A viewmodel responsible for tool stdout and stderr."""
14
+
15
+ def __init__(self, id: str, binding: BindingInterface):
16
+ self.tool_outputs = ToolOutputs()
17
+ self.tool_outputs_bind = binding.new_bind(self.tool_outputs)
18
+ self.outputs_signal = blinker.signal(get_signal_id(id, Signal.OUTPUTS))
19
+ self.outputs_signal.connect(self.on_outputs_update, weak=False)
20
+
21
+ async def on_outputs_update(self, _sender: Any, outputs: ToolOutputs) -> None:
22
+ self.tool_outputs = outputs
23
+ self.tool_outputs_bind.update_in_view(self.tool_outputs)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: nova-trame
3
- Version: 0.19.2
3
+ Version: 0.20.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
@@ -14,8 +14,10 @@ Classifier: Programming Language :: Python :: 3.11
14
14
  Classifier: Programming Language :: Python :: 3.12
15
15
  Classifier: Programming Language :: Python :: 3.13
16
16
  Requires-Dist: altair
17
+ Requires-Dist: blinker (>=1.9.0,<2.0.0)
17
18
  Requires-Dist: libsass
18
19
  Requires-Dist: mergedeep
20
+ Requires-Dist: nova-common (>=0.2.0)
19
21
  Requires-Dist: nova-mvvm
20
22
  Requires-Dist: pydantic
21
23
  Requires-Dist: tomli
@@ -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=sCLU0YIMCccC1BH8dKZPmajaft6WgwuM_zOr3NPk2bg,7552
3
+ nova/trame/model/data_selector.py,sha256=bKWMk9bVhC16DTuEI7o656idMxzLbUi7RBdiaGO6TZY,8377
4
4
  nova/trame/model/remote_file_input.py,sha256=9KAf31ZHzpsh_aXUrNcF81Q5jvUZDWCzW1QATKls-Jk,3675
5
- nova/trame/view/components/__init__.py,sha256=u8yzshFp_TmuC1g9TRxKjy_BdGWMIzPQouI52hzcr2U,234
6
- nova/trame/view/components/data_selector.py,sha256=UFQriSH25wk3F4s6EnP5Dfrt3MFbisJTrzmRxH0ME8U,8831
5
+ nova/trame/view/components/__init__.py,sha256=60BeS69aOrFnkptjuD17rfPE1f4Z35iBH56TRmW5MW8,451
6
+ nova/trame/view/components/data_selector.py,sha256=IQipVqf6fG7qVRa4CJSr4jGz7Sru_lYU6K-qczJdstg,9727
7
+ nova/trame/view/components/execution_buttons.py,sha256=fIkrWKI3jFZqk3GHhtmYh3nK2c-HOXpD3D3zd_TUpi0,4049
7
8
  nova/trame/view/components/file_upload.py,sha256=7VcpfA6zmiqMDLkwVPlb35Tf0IUTBN1xsHpoUFnSr1w,3111
8
9
  nova/trame/view/components/input_field.py,sha256=q6WQ_N-BOlimUL9zgazDlsDfK28FrrKjH4he8e_HzRA,16088
10
+ nova/trame/view/components/progress_bar.py,sha256=Sh5cOPaMWrFq8KTWEDui1dIbK53BPtGG2RZOSKEaoJ4,2186
9
11
  nova/trame/view/components/remote_file_input.py,sha256=ByrBFj8svyWezcardCWrS_4Ag3fgTYNg_11lDW1FIA8,9669
12
+ nova/trame/view/components/tool_outputs.py,sha256=-6pDURd2l_FK_8EWa9BI3KhU_KJXJ6uyJ_rW4nQVc08,2337
10
13
  nova/trame/view/components/visualization/__init__.py,sha256=reqkkbhD5uSksHHlhVMy1qNUCwSekS5HlXk6wCREYxU,152
11
14
  nova/trame/view/components/visualization/interactive_2d_plot.py,sha256=foZCMoqbuahT5dtqIQvm8C4ZJcY9P211eJEcpQJltmM,3421
12
15
  nova/trame/view/components/visualization/matplotlib_figure.py,sha256=0iWCXB8i7Tut1gA66hY9cGrhZPaHC7p-XdADDNy_UVY,12042
@@ -16,7 +19,7 @@ nova/trame/view/layouts/hbox.py,sha256=qlOMp_iOropIkC9Jxa6D89b7OPv0pNvJ73tUEzddy
16
19
  nova/trame/view/layouts/utils.py,sha256=Hg34VQWTG3yHBsgNvmfatR4J-uL3cko7UxSJpT-h3JI,376
17
20
  nova/trame/view/layouts/vbox.py,sha256=hzhzPu99R2fAclMe-FwHZseJWk7iailZ31bKdGhi1hk,3514
18
21
  nova/trame/view/theme/__init__.py,sha256=70_marDlTigIcPEOGiJb2JTs-8b2sGM5SlY7XBPtBDM,54
19
- nova/trame/view/theme/assets/core_style.scss,sha256=kxm66OdHeSAW1_zWJXWpzBZ30hopI1cgLwyXrqdmf1I,3214
22
+ nova/trame/view/theme/assets/core_style.scss,sha256=5i2cZQbmaQTpcNgsaVRdcubh0HHidqPnnLj8McHNkbY,3367
20
23
  nova/trame/view/theme/assets/favicon.png,sha256=Xbp1nUmhcBDeObjsebEbEAraPDZ_M163M_ZLtm5AbQc,1927
21
24
  nova/trame/view/theme/assets/js/delay_manager.js,sha256=vmb34DZ5YCQIlRW9Tf2M_uvJW6HFCmtlKZ5e_TPR8yg,536
22
25
  nova/trame/view/theme/assets/js/lodash.debounce.min.js,sha256=GLzlQH04WDUNYN7i39ttHHejSdu-CpAvfWgDgKDn-OY,4448
@@ -24,10 +27,13 @@ nova/trame/view/theme/assets/js/lodash.throttle.min.js,sha256=9csqjX-M-LVGJnF3z4
24
27
  nova/trame/view/theme/assets/vuetify_config.json,sha256=a0FSgpLYWGFlRGSMhMq61MyDFBEBwvz55G4qjkM08cs,5627
25
28
  nova/trame/view/theme/theme.py,sha256=HUeuVfzEgeYW65W-LcvXzfYNRHu6aQibGwwgHGyh3OA,11765
26
29
  nova/trame/view/utilities/local_storage.py,sha256=vD8f2VZIpxhIKjZwEaD7siiPCTZO4cw9AfhwdawwYLY,3218
27
- nova/trame/view_model/data_selector.py,sha256=FM1Xe-f-gi1jVwA9nDf2KE1UDvsAvmMKlp78slIpX58,2418
30
+ nova/trame/view_model/data_selector.py,sha256=WrdvCE8J_sye19srGBpBZbheu_YzBEEEo_u098WLh9g,2524
31
+ nova/trame/view_model/execution_buttons.py,sha256=MfKSp95D92EqpD48C15cBo6dLO0Yld4FeRZMJNxJf7Y,3551
32
+ nova/trame/view_model/progress_bar.py,sha256=L7ED6TDn5v2142iu-qt3i-jUg_5JEhLyC476t2OtohU,2467
28
33
  nova/trame/view_model/remote_file_input.py,sha256=ojEOJ8ZPkajpbAaZi9VLj7g-uBjhb8BMrTdMmwf_J6A,3367
29
- nova_trame-0.19.2.dist-info/LICENSE,sha256=Iu5QiDbwNbREg75iYaxIJ_V-zppuv4QFuBhAW-qiAlM,1061
30
- nova_trame-0.19.2.dist-info/METADATA,sha256=HYdpyI8udw6yW-u42ZfN18eI3yk-unnP7wO0zZ08fio,1446
31
- nova_trame-0.19.2.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
32
- nova_trame-0.19.2.dist-info/entry_points.txt,sha256=J2AmeSwiTYZ4ZqHHp9HO6v4MaYQTTBPbNh6WtoqOT58,42
33
- nova_trame-0.19.2.dist-info/RECORD,,
34
+ nova/trame/view_model/tool_outputs.py,sha256=ev6LY7fJ0H2xAJn9f5ww28c8Kpom2SYc2FbvFcoN4zg,829
35
+ nova_trame-0.20.1.dist-info/LICENSE,sha256=Iu5QiDbwNbREg75iYaxIJ_V-zppuv4QFuBhAW-qiAlM,1061
36
+ nova_trame-0.20.1.dist-info/METADATA,sha256=k1zMQM3iexbogY55bKVC-vQaoFTK9W6MZxKN8MndVW4,1523
37
+ nova_trame-0.20.1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
38
+ nova_trame-0.20.1.dist-info/entry_points.txt,sha256=J2AmeSwiTYZ4ZqHHp9HO6v4MaYQTTBPbNh6WtoqOT58,42
39
+ nova_trame-0.20.1.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.1.2
2
+ Generator: poetry-core 2.1.3
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any