ms-components 0.4.3__tar.gz → 0.5.0__tar.gz
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.
- {ms_components-0.4.3 → ms_components-0.5.0}/PKG-INFO +3 -3
- {ms_components-0.4.3 → ms_components-0.5.0}/pyproject.toml +2 -2
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_executors.py +119 -10
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_monitor/__init__.py +3 -1
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_monitor/bridge.py +6 -4
- ms_components-0.5.0/src/ms_components/ms_monitor/detail.py +671 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_monitor/formatting.py +8 -3
- ms_components-0.5.0/src/ms_components/ms_monitor/jobs.py +294 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_monitor/models.py +18 -2
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_monitor/polling.py +39 -6
- ms_components-0.5.0/src/ms_components/ms_monitor/resources.py +448 -0
- ms_components-0.5.0/src/ms_components/ms_monitor/widget.py +345 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_pymol/widget.py +34 -3
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components.egg-info/PKG-INFO +3 -3
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components.egg-info/SOURCES.txt +3 -3
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components.egg-info/requires.txt +2 -2
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components.egg-info/scm_file_list.json +3 -3
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components.egg-info/scm_version.json +2 -2
- {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_executors_ui.py +62 -1
- {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_molsuite_monitor_bridge.py +9 -7
- ms_components-0.5.0/tests/test_molsuite_monitor_widgets.py +178 -0
- ms_components-0.5.0/tests/test_optional_integrations.py +103 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_pymol_toolbar.py +16 -0
- ms_components-0.4.3/src/ms_components/ms_monitor/cards.py +0 -75
- ms_components-0.4.3/src/ms_components/ms_monitor/detail_panel.py +0 -121
- ms_components-0.4.3/src/ms_components/ms_monitor/job_cards.py +0 -540
- ms_components-0.4.3/src/ms_components/ms_monitor/resources.py +0 -305
- ms_components-0.4.3/src/ms_components/ms_monitor/widget.py +0 -355
- ms_components-0.4.3/tests/test_molsuite_monitor_widgets.py +0 -92
- {ms_components-0.4.3 → ms_components-0.5.0}/.github/workflows/docs.yml +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/.github/workflows/publish.yml +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/.gitignore +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/LICENSE +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/MANIFEST.in +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/README.md +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/examples/tutorial_demo.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/examples/tutorial_demo.toml +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/licenses/qt-themes-LICENSE.txt +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/setup.cfg +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/__init__.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/backdrop.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/base.qss +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/chips.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ketcher.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/listview.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_dockwidget/__init__.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_dockwidget/resources/__init__.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_dockwidget/resources/icons/__init__.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_dockwidget/resources/icons/folder.svg +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_dockwidget/widget.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_monitor/config.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_monitor/palette.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_projects/__init__.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_projects/app.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_projects/backend.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_projects/browser.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_projects/scratch.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_projects/widget.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_pymol/__init__.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_pymol/command_line.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_pymol/icons/actions.svg +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_pymol/icons/box.svg +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_pymol/icons/center.svg +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_pymol/icons/display.svg +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_pymol/icons/hide.svg +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_pymol/icons/hydrogens-polar.svg +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_pymol/icons/hydrogens.svg +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_pymol/icons/menu.svg +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_pymol/icons/orient.svg +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_pymol/icons/presets.svg +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_pymol/icons/rock.svg +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_pymol/icons/selection.svg +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_pymol/icons/settings.svg +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_pymol/icons/style.svg +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_pymol/icons/zoom.svg +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_pymol/plugins.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_pymol/scene.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_settings/__init__.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_settings/panel.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_stepper.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/__init__.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/action_header.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/cell_delegate.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/column_panel.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/example_usage.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/filter_panel.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/icons/columns.svg +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/icons/export.svg +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/icons/filter.svg +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/icons/menu.svg +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/icons/reload.svg +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/icons/search.svg +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/icons/settings.svg +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/icons/sort-asc.svg +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/icons/sort-desc.svg +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/icons/sort.svg +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/metric_filter_bar.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/pagination_control.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/query_builder.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/session_provider.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/smart_table_view.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/sort_panel.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/table_config.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/table_interaction.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/table_model.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/test_external_filters.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/test_toolbar.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_tutorial/__init__.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_tutorial/bindings.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_tutorial/controller.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_tutorial/models.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_tutorial/overlay.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/opengl.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/pagination.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/palettes.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/step_dialog.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/theme.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/tool_panel.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/wheel.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components.egg-info/dependency_links.txt +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components.egg-info/top_level.txt +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/tests/conftest.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_app_settings.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_chips.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_dock_window.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_ketcher.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_monitor_config.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_ms_projects_widget.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_opengl.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_progress_column.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_project_browser.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_pymol_command_line.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_pymol_grid.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_pymol_scene.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_query_builder_keyset.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_shift_hscroll.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_split_button_style.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_step_dialog.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_table_interaction.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_table_toolbar.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_table_view_state.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_theme_contrast.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_tool_panel.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_tutorial.py +0 -0
- {ms_components-0.4.3 → ms_components-0.5.0}/zensical.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ms_components
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: PySide6 UI components for MolSuite: smart table, project browser, job monitor, PyMOL dock, steppers.
|
|
5
5
|
Author: Mario S. Valdés-Tresanco, Mario E. Valdés-Tresanco
|
|
6
6
|
License-Expression: MIT
|
|
@@ -21,11 +21,11 @@ Requires-Dist: PySide6>=6.5
|
|
|
21
21
|
Provides-Extra: monitor
|
|
22
22
|
Requires-Dist: pydantic>=2.0; extra == "monitor"
|
|
23
23
|
Provides-Extra: projects
|
|
24
|
-
Requires-Dist: ms_flow>=0.
|
|
24
|
+
Requires-Dist: ms_flow>=0.7.1; extra == "projects"
|
|
25
25
|
Provides-Extra: pymol
|
|
26
26
|
Requires-Dist: pymol-open-source>=3.2.0a0; extra == "pymol"
|
|
27
27
|
Provides-Extra: steps
|
|
28
|
-
Requires-Dist: ms_env>=0.
|
|
28
|
+
Requires-Dist: ms_env>=0.3.3; extra == "steps"
|
|
29
29
|
Provides-Extra: table
|
|
30
30
|
Requires-Dist: sqlmodel>=0.0.14; extra == "table"
|
|
31
31
|
Provides-Extra: all
|
|
@@ -31,9 +31,9 @@ dependencies = [
|
|
|
31
31
|
|
|
32
32
|
[project.optional-dependencies]
|
|
33
33
|
monitor = ["pydantic>=2.0"]
|
|
34
|
-
projects = ["ms_flow>=0.
|
|
34
|
+
projects = ["ms_flow>=0.7.1"]
|
|
35
35
|
pymol = ["pymol-open-source>=3.2.0a0"]
|
|
36
|
-
steps = ["ms_env>=0.
|
|
36
|
+
steps = ["ms_env>=0.3.3"]
|
|
37
37
|
table = ["sqlmodel>=0.0.14"]
|
|
38
38
|
all = ["ms_components[monitor,projects,pymol,steps,table]"]
|
|
39
39
|
# Test + development tooling.
|
|
@@ -8,6 +8,7 @@ be done now, and says why.
|
|
|
8
8
|
"""
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
+
import re
|
|
11
12
|
import shlex
|
|
12
13
|
import sys
|
|
13
14
|
import threading
|
|
@@ -30,6 +31,7 @@ from PySide6.QtWidgets import (
|
|
|
30
31
|
QListWidgetItem,
|
|
31
32
|
QMessageBox,
|
|
32
33
|
QPlainTextEdit,
|
|
34
|
+
QProgressBar,
|
|
33
35
|
QPushButton,
|
|
34
36
|
QRadioButton,
|
|
35
37
|
QSpinBox,
|
|
@@ -38,7 +40,7 @@ from PySide6.QtWidgets import (
|
|
|
38
40
|
)
|
|
39
41
|
|
|
40
42
|
|
|
41
|
-
def _editor(field, value: Any, parent: QWidget,
|
|
43
|
+
def _editor(field, value: Any, parent: QWidget, names=()) -> QWidget:
|
|
42
44
|
annotation, options = field.annotation, get_args(field.annotation)
|
|
43
45
|
if get_origin(annotation) in (Union, types.UnionType) and options == (bool, type(None)):
|
|
44
46
|
editor = QComboBox(parent)
|
|
@@ -48,13 +50,14 @@ def _editor(field, value: Any, parent: QWidget, hosts=()) -> QWidget:
|
|
|
48
50
|
editor = QComboBox(parent)
|
|
49
51
|
for option in options:
|
|
50
52
|
editor.addItem(str(option), option)
|
|
51
|
-
elif get_origin(annotation)
|
|
53
|
+
elif list in (get_origin(annotation), *map(get_origin, options)):
|
|
54
|
+
# `names` to tick: the hosts kept, or the programs of the app; None ticks them all
|
|
52
55
|
editor = QListWidget(parent)
|
|
53
56
|
editor.setMaximumHeight(110)
|
|
54
|
-
for
|
|
55
|
-
item = QListWidgetItem("local (this computer)" if
|
|
56
|
-
item.setData(Qt.ItemDataRole.UserRole,
|
|
57
|
-
item.setCheckState(Qt.CheckState.Checked if
|
|
57
|
+
for name in names:
|
|
58
|
+
item = QListWidgetItem("local (this computer)" if name == "local" else name, editor)
|
|
59
|
+
item.setData(Qt.ItemDataRole.UserRole, name)
|
|
60
|
+
item.setCheckState(Qt.CheckState.Checked if value is None or name in value else Qt.CheckState.Unchecked)
|
|
58
61
|
return editor
|
|
59
62
|
elif get_origin(annotation) is dict:
|
|
60
63
|
editor = QPlainTextEdit("\n".join(f"{key} = {path}" for key, path in (value or {}).items()), parent)
|
|
@@ -93,6 +96,55 @@ def _value(editor: QWidget) -> Any:
|
|
|
93
96
|
return editor.text().strip()
|
|
94
97
|
|
|
95
98
|
|
|
99
|
+
class _Steps(QWidget):
|
|
100
|
+
"""How far the preparation of a host got, what it is doing and for how long: moved by the
|
|
101
|
+
lines `[done/total] what` it says; `[0/0] what` is a wait with no steps to count."""
|
|
102
|
+
|
|
103
|
+
_LINE = re.compile(r"\[(\d+)/(\d+)\] (.*)")
|
|
104
|
+
|
|
105
|
+
def __init__(self, parent: QWidget) -> None:
|
|
106
|
+
super().__init__(parent)
|
|
107
|
+
layout = QVBoxLayout(self)
|
|
108
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
109
|
+
self.label = QLabel(self)
|
|
110
|
+
self.bar = QProgressBar(self)
|
|
111
|
+
self.bar.setTextVisible(False)
|
|
112
|
+
layout.addWidget(self.label)
|
|
113
|
+
layout.addWidget(self.bar)
|
|
114
|
+
self._what, self._since = "", 0.0
|
|
115
|
+
self._clock = QTimer(self) # a step of minutes moves nothing else: the time spent says it is alive
|
|
116
|
+
self._clock.setInterval(1000)
|
|
117
|
+
self._clock.timeout.connect(self._tell)
|
|
118
|
+
self.setVisible(False)
|
|
119
|
+
|
|
120
|
+
def take(self, line: str) -> bool:
|
|
121
|
+
"""Whether `line` was one of those, shown here then, not in the log."""
|
|
122
|
+
found = self._LINE.fullmatch(line)
|
|
123
|
+
if found:
|
|
124
|
+
done, total, what = int(found[1]), int(found[2]), found[3]
|
|
125
|
+
if what != self._what:
|
|
126
|
+
self._what, self._since = what, time.monotonic()
|
|
127
|
+
self.bar.setRange(0, total) # 0 of 0: Qt shows it busy
|
|
128
|
+
self.bar.setValue(done)
|
|
129
|
+
self._tell()
|
|
130
|
+
self.setVisible(True)
|
|
131
|
+
self._clock.start()
|
|
132
|
+
return bool(found)
|
|
133
|
+
|
|
134
|
+
def full(self) -> bool:
|
|
135
|
+
return 0 < self.bar.maximum() == self.bar.value()
|
|
136
|
+
|
|
137
|
+
def _tell(self) -> None:
|
|
138
|
+
spent = int(time.monotonic() - self._since)
|
|
139
|
+
done, total = self.bar.value(), self.bar.maximum()
|
|
140
|
+
counted = f" · step {done + 1} of {total}" if done < total else ""
|
|
141
|
+
self.label.setText(f"{self._what} · {spent // 60}:{spent % 60:02d}{counted}")
|
|
142
|
+
|
|
143
|
+
def hideEvent(self, event) -> None:
|
|
144
|
+
self._clock.stop()
|
|
145
|
+
super().hideEvent(event)
|
|
146
|
+
|
|
147
|
+
|
|
96
148
|
_VERSIONS = """import sys, importlib.metadata as m
|
|
97
149
|
print("Python", sys.version.split()[0], "at", sys.prefix)
|
|
98
150
|
for name in sys.argv[1:]:
|
|
@@ -140,14 +192,16 @@ class KeptPage(QWidget):
|
|
|
140
192
|
|
|
141
193
|
changed = Signal()
|
|
142
194
|
_checked = Signal(str)
|
|
195
|
+
_synced = Signal(str, bool) # a line said there, and whether it is the last
|
|
143
196
|
|
|
144
197
|
def __init__(self, workspace, table: str, parent: QWidget | None = None, *, packages=("ms_flow", "ms_env"),
|
|
145
|
-
default_env: str = ""):
|
|
198
|
+
default_env: str = "", programs=()):
|
|
146
199
|
"""`packages`: whose version "Check connection" reads on a host; `default_env`: the
|
|
147
|
-
environment the app prepares on a host that names none, under the home there
|
|
200
|
+
environment the app prepares on a host that names none, under the home there;
|
|
201
|
+
`programs`: the ones it can install there, to tick for each host (none: no choice shown)."""
|
|
148
202
|
super().__init__(parent)
|
|
149
203
|
self._workspace, self._table, self._one = workspace, table, table[:-1]
|
|
150
|
-
self._packages, self._default_env = tuple(packages), default_env
|
|
204
|
+
self._packages, self._default_env, self._programs = tuple(packages), default_env, tuple(programs)
|
|
151
205
|
settings = type(workspace.settings_manager.settings)
|
|
152
206
|
self._model = get_args(settings.model_fields[table].annotation)[1]
|
|
153
207
|
self._editors: dict[str, QWidget] = {}
|
|
@@ -182,11 +236,19 @@ class KeptPage(QWidget):
|
|
|
182
236
|
"the environment of the app and its packages, the tools named.")
|
|
183
237
|
self.check_button.clicked.connect(self._check)
|
|
184
238
|
foot.addWidget(self.check_button)
|
|
239
|
+
self.sync_button = QPushButton("Sync", self)
|
|
240
|
+
self.sync_button.setToolTip("Install there, or bring up to date, the environment of the app and the "
|
|
241
|
+
"programs ticked, as saved. What is already as it is here is left alone.")
|
|
242
|
+
self.sync_button.clicked.connect(self._sync)
|
|
243
|
+
foot.addWidget(self.sync_button)
|
|
244
|
+
self.steps = _Steps(self)
|
|
245
|
+
self._right.addWidget(self.steps)
|
|
185
246
|
self.report = QPlainTextEdit(self)
|
|
186
247
|
self.report.setReadOnly(True)
|
|
187
248
|
self.report.setVisible(False)
|
|
188
249
|
self._right.addWidget(self.report, 8) # the room the stretch below has while nothing is said
|
|
189
250
|
self._checked.connect(self._show_report)
|
|
251
|
+
self._synced.connect(self._sync_said)
|
|
190
252
|
foot.addWidget(self.save_button)
|
|
191
253
|
self._right.addStretch(1)
|
|
192
254
|
self._right.addLayout(foot)
|
|
@@ -228,9 +290,12 @@ class KeptPage(QWidget):
|
|
|
228
290
|
hosts = self._workspace.settings_manager.settings.hosts
|
|
229
291
|
self._editors = {}
|
|
230
292
|
for key, field in self._model.model_fields.items():
|
|
231
|
-
|
|
293
|
+
names = self._programs if key == "install" else hosts
|
|
294
|
+
editor = self._editors[key] = _editor(field, getattr(entry, key, None), body, names)
|
|
232
295
|
editor.setToolTip(field.description or "")
|
|
233
296
|
form.addRow(field.title or key.replace("_", " ").capitalize(), editor)
|
|
297
|
+
if "install" in self._editors: # this machine has its programs from the app's own settings
|
|
298
|
+
form.setRowVisible(self._editors["install"], bool(self._programs) and name != "local")
|
|
234
299
|
kind = self._editors.get("kind")
|
|
235
300
|
if name == "local": # this machine and its pool: what they are is not a choice
|
|
236
301
|
for fixed in ("address", "kind", "hosts"):
|
|
@@ -246,6 +311,8 @@ class KeptPage(QWidget):
|
|
|
246
311
|
self.said.setText("")
|
|
247
312
|
if self._table == "hosts":
|
|
248
313
|
self.report.setVisible(False)
|
|
314
|
+
self.steps.setVisible(False)
|
|
315
|
+
self.sync_button.setVisible(bool(name) and name != "local" and getattr(self._workspace, "host_env", None) is not None)
|
|
249
316
|
self._right.insertWidget(0, body)
|
|
250
317
|
|
|
251
318
|
def _only_of_kind(self) -> None:
|
|
@@ -302,6 +369,38 @@ class KeptPage(QWidget):
|
|
|
302
369
|
|
|
303
370
|
threading.Thread(target=work, name="host-check", daemon=True).start()
|
|
304
371
|
|
|
372
|
+
def _sync(self) -> None:
|
|
373
|
+
"""`Workspace.host_env` for the host as it is saved: what the app prepares there, made as it is here."""
|
|
374
|
+
name = self.names.currentItem().text()
|
|
375
|
+
host = self._kept()[name]
|
|
376
|
+
if host.env:
|
|
377
|
+
self._show_report(f"`{name}` names its own environment, {host.env}: the app prepares nothing there.")
|
|
378
|
+
return
|
|
379
|
+
users = [row["name"] for row in self._workspace.executors()
|
|
380
|
+
if row["state"] in ("on", "starting") and name in row["hosts"]]
|
|
381
|
+
if users: # what it runs would change under it
|
|
382
|
+
self._show_report(f"`{name}` is in use by `{users[0]}`, which is active: deactivate it first.")
|
|
383
|
+
return
|
|
384
|
+
self.sync_button.setEnabled(False)
|
|
385
|
+
self._show_report(f"Preparing {host.address}…")
|
|
386
|
+
|
|
387
|
+
def work() -> None:
|
|
388
|
+
try:
|
|
389
|
+
self._workspace.host_env(host.address, None, lambda line: self._synced.emit(line, False))
|
|
390
|
+
said = f"\n{name} is ready."
|
|
391
|
+
except Exception as error: # noqa: BLE001 - said in the page, where it was asked for
|
|
392
|
+
said = f"\nCould not prepare {name}:\n{error}"
|
|
393
|
+
self._synced.emit(said, True)
|
|
394
|
+
|
|
395
|
+
threading.Thread(target=work, name="host-sync", daemon=True).start()
|
|
396
|
+
|
|
397
|
+
def _sync_said(self, line: str, last: bool) -> None:
|
|
398
|
+
if not self.steps.take(line):
|
|
399
|
+
self.report.appendPlainText(line)
|
|
400
|
+
self.sync_button.setEnabled(last)
|
|
401
|
+
if last:
|
|
402
|
+
self.steps.setVisible(False)
|
|
403
|
+
|
|
305
404
|
def _show_report(self, text: str) -> None:
|
|
306
405
|
self.report.setPlainText(text)
|
|
307
406
|
self.report.setVisible(True)
|
|
@@ -340,6 +439,7 @@ class ExecutorsPanel(QDialog):
|
|
|
340
439
|
self.resize(640, 260)
|
|
341
440
|
self._workspace = workspace
|
|
342
441
|
self._busy: set[str] = set() # the ones a thread of this panel is starting or stopping
|
|
442
|
+
self._waiting = "" # the line the bar shows while they have no steps to count
|
|
343
443
|
self._shown: Any = None
|
|
344
444
|
self._body: QWidget | None = None
|
|
345
445
|
self.buttons: dict[str, QPushButton] = {}
|
|
@@ -347,6 +447,8 @@ class ExecutorsPanel(QDialog):
|
|
|
347
447
|
self.defaults: dict[str, QRadioButton] = {}
|
|
348
448
|
|
|
349
449
|
self._layout = QVBoxLayout(self)
|
|
450
|
+
self.steps = _Steps(self)
|
|
451
|
+
self._layout.addWidget(self.steps)
|
|
350
452
|
self.said = QPlainTextEdit(self)
|
|
351
453
|
self.said.setReadOnly(True)
|
|
352
454
|
self.said.setVisible(False)
|
|
@@ -426,15 +528,22 @@ class ExecutorsPanel(QDialog):
|
|
|
426
528
|
self.ended.emit(name, "")
|
|
427
529
|
|
|
428
530
|
self._busy.add(name)
|
|
531
|
+
self._waiting = f"[0/0] {'stopping' if stop else 'starting'} {name}"
|
|
532
|
+
self.steps.take(self._waiting)
|
|
429
533
|
self.refresh()
|
|
430
534
|
threading.Thread(target=work, name=f"executor-{name}", daemon=True).start()
|
|
431
535
|
|
|
432
536
|
def _say(self, text: str) -> None:
|
|
537
|
+
if self.steps.take(text):
|
|
538
|
+
if self.steps.full() and self._busy: # its hosts are ready: it is still starting on them
|
|
539
|
+
self.steps.take(self._waiting)
|
|
540
|
+
return
|
|
433
541
|
self.said.setVisible(True)
|
|
434
542
|
self.said.appendPlainText(text)
|
|
435
543
|
|
|
436
544
|
def _ended(self, name: str, error: str) -> None:
|
|
437
545
|
self._busy.discard(name)
|
|
546
|
+
self.steps.setVisible(bool(self._busy))
|
|
438
547
|
if error:
|
|
439
548
|
self._say(f"{name}: {error}")
|
|
440
549
|
self.refresh()
|
|
@@ -4,8 +4,10 @@ from importlib import import_module
|
|
|
4
4
|
|
|
5
5
|
_LAZY = {
|
|
6
6
|
"FailureState": "models",
|
|
7
|
-
"
|
|
7
|
+
"HostsStrip": "resources",
|
|
8
|
+
"JobDetail": "detail",
|
|
8
9
|
"JobDetailState": "models",
|
|
10
|
+
"JobList": "jobs",
|
|
9
11
|
"JobMonitorState": "models",
|
|
10
12
|
"MonitorConfig": "config",
|
|
11
13
|
"MonitorSyncState": "models",
|
|
@@ -105,10 +105,12 @@ class MolSuiteMonitorBridge(QObject):
|
|
|
105
105
|
self._set_sync_state(in_progress=False, last_refresh_at=datetime.now())
|
|
106
106
|
return self._poller.current_snapshot
|
|
107
107
|
|
|
108
|
-
def
|
|
109
|
-
"""The
|
|
110
|
-
|
|
111
|
-
self.
|
|
108
|
+
def select_job(self, job_id: str | None) -> None:
|
|
109
|
+
"""The job whose tasks in flight and chunk states the next polls bring
|
|
110
|
+
(`JobMonitorState.tasks`, `JobMonitorState.chunk_states`): one at a time."""
|
|
111
|
+
if self._poller.selected != job_id:
|
|
112
|
+
self._poller.selected = job_id
|
|
113
|
+
self.request_refresh()
|
|
112
114
|
|
|
113
115
|
def get_job_detail(self, job_id: str | None, *, failure_limit: int = 200) -> JobDetailState | None:
|
|
114
116
|
"""The job as last polled plus its failed chunks, read from the project's journal."""
|