ms-components 0.4.2__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.2 → ms_components-0.5.0}/PKG-INFO +3 -3
- {ms_components-0.4.2 → ms_components-0.5.0}/pyproject.toml +2 -2
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_executors.py +200 -10
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_monitor/__init__.py +3 -1
- {ms_components-0.4.2 → 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.2 → 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.2 → ms_components-0.5.0}/src/ms_components/ms_monitor/models.py +21 -3
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_monitor/polling.py +42 -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.5.0/src/ms_components/ms_pymol/command_line.py +136 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_pymol/widget.py +43 -3
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_tutorial/controller.py +21 -3
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_tutorial/overlay.py +8 -2
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components.egg-info/PKG-INFO +3 -3
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components.egg-info/SOURCES.txt +6 -3
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components.egg-info/requires.txt +2 -2
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components.egg-info/scm_file_list.json +6 -3
- ms_components-0.5.0/src/ms_components.egg-info/scm_version.json +8 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/tests/test_executors_ui.py +65 -1
- {ms_components-0.4.2 → 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.5.0/tests/test_pymol_command_line.py +48 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/tests/test_pymol_toolbar.py +16 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/tests/test_tutorial.py +64 -0
- ms_components-0.4.2/src/ms_components/ms_monitor/cards.py +0 -75
- ms_components-0.4.2/src/ms_components/ms_monitor/detail_panel.py +0 -121
- ms_components-0.4.2/src/ms_components/ms_monitor/job_cards.py +0 -512
- ms_components-0.4.2/src/ms_components/ms_monitor/widget.py +0 -344
- ms_components-0.4.2/src/ms_components.egg-info/scm_version.json +0 -8
- ms_components-0.4.2/tests/test_molsuite_monitor_widgets.py +0 -75
- {ms_components-0.4.2 → ms_components-0.5.0}/.github/workflows/docs.yml +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/.github/workflows/publish.yml +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/.gitignore +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/LICENSE +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/MANIFEST.in +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/README.md +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/examples/tutorial_demo.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/examples/tutorial_demo.toml +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/licenses/qt-themes-LICENSE.txt +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/setup.cfg +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/__init__.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/backdrop.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/base.qss +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/chips.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ketcher.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/listview.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_dockwidget/__init__.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_dockwidget/resources/__init__.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_dockwidget/resources/icons/__init__.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_dockwidget/resources/icons/folder.svg +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_dockwidget/widget.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_monitor/config.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_monitor/palette.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_projects/__init__.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_projects/app.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_projects/backend.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_projects/browser.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_projects/scratch.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_projects/widget.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_pymol/__init__.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_pymol/icons/actions.svg +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_pymol/icons/box.svg +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_pymol/icons/center.svg +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_pymol/icons/display.svg +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_pymol/icons/hide.svg +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_pymol/icons/hydrogens-polar.svg +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_pymol/icons/hydrogens.svg +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_pymol/icons/menu.svg +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_pymol/icons/orient.svg +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_pymol/icons/presets.svg +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_pymol/icons/rock.svg +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_pymol/icons/selection.svg +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_pymol/icons/settings.svg +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_pymol/icons/style.svg +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_pymol/icons/zoom.svg +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_pymol/plugins.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_pymol/scene.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_settings/__init__.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_settings/panel.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_stepper.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_table/__init__.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_table/action_header.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_table/cell_delegate.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_table/column_panel.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_table/example_usage.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_table/filter_panel.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_table/icons/columns.svg +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_table/icons/export.svg +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_table/icons/filter.svg +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_table/icons/menu.svg +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_table/icons/reload.svg +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_table/icons/search.svg +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_table/icons/settings.svg +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_table/icons/sort-asc.svg +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_table/icons/sort-desc.svg +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_table/icons/sort.svg +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_table/metric_filter_bar.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_table/pagination_control.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_table/query_builder.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_table/session_provider.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_table/smart_table_view.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_table/sort_panel.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_table/table_config.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_table/table_interaction.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_table/table_model.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_table/test_external_filters.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_table/test_toolbar.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_tutorial/__init__.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_tutorial/bindings.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/ms_tutorial/models.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/opengl.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/pagination.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/palettes.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/step_dialog.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/theme.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/tool_panel.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components/wheel.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components.egg-info/dependency_links.txt +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/src/ms_components.egg-info/top_level.txt +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/tests/conftest.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/tests/test_app_settings.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/tests/test_chips.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/tests/test_dock_window.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/tests/test_ketcher.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/tests/test_monitor_config.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/tests/test_ms_projects_widget.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/tests/test_opengl.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/tests/test_progress_column.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/tests/test_project_browser.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/tests/test_pymol_grid.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/tests/test_pymol_scene.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/tests/test_query_builder_keyset.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/tests/test_shift_hscroll.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/tests/test_split_button_style.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/tests/test_step_dialog.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/tests/test_table_interaction.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/tests/test_table_toolbar.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/tests/test_table_view_state.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/tests/test_theme_contrast.py +0 -0
- {ms_components-0.4.2 → ms_components-0.5.0}/tests/test_tool_panel.py +0 -0
- {ms_components-0.4.2 → 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,7 +8,11 @@ be done now, and says why.
|
|
|
8
8
|
"""
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
+
import re
|
|
12
|
+
import shlex
|
|
13
|
+
import sys
|
|
11
14
|
import threading
|
|
15
|
+
import time
|
|
12
16
|
import types
|
|
13
17
|
from typing import Any, Literal, Union, get_args, get_origin
|
|
14
18
|
|
|
@@ -27,6 +31,7 @@ from PySide6.QtWidgets import (
|
|
|
27
31
|
QListWidgetItem,
|
|
28
32
|
QMessageBox,
|
|
29
33
|
QPlainTextEdit,
|
|
34
|
+
QProgressBar,
|
|
30
35
|
QPushButton,
|
|
31
36
|
QRadioButton,
|
|
32
37
|
QSpinBox,
|
|
@@ -35,7 +40,7 @@ from PySide6.QtWidgets import (
|
|
|
35
40
|
)
|
|
36
41
|
|
|
37
42
|
|
|
38
|
-
def _editor(field, value: Any, parent: QWidget,
|
|
43
|
+
def _editor(field, value: Any, parent: QWidget, names=()) -> QWidget:
|
|
39
44
|
annotation, options = field.annotation, get_args(field.annotation)
|
|
40
45
|
if get_origin(annotation) in (Union, types.UnionType) and options == (bool, type(None)):
|
|
41
46
|
editor = QComboBox(parent)
|
|
@@ -45,13 +50,14 @@ def _editor(field, value: Any, parent: QWidget, hosts=()) -> QWidget:
|
|
|
45
50
|
editor = QComboBox(parent)
|
|
46
51
|
for option in options:
|
|
47
52
|
editor.addItem(str(option), option)
|
|
48
|
-
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
|
|
49
55
|
editor = QListWidget(parent)
|
|
50
56
|
editor.setMaximumHeight(110)
|
|
51
|
-
for
|
|
52
|
-
item = QListWidgetItem("local (this computer)" if
|
|
53
|
-
item.setData(Qt.ItemDataRole.UserRole,
|
|
54
|
-
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)
|
|
55
61
|
return editor
|
|
56
62
|
elif get_origin(annotation) is dict:
|
|
57
63
|
editor = QPlainTextEdit("\n".join(f"{key} = {path}" for key, path in (value or {}).items()), parent)
|
|
@@ -90,6 +96,93 @@ def _value(editor: QWidget) -> Any:
|
|
|
90
96
|
return editor.text().strip()
|
|
91
97
|
|
|
92
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
|
+
|
|
148
|
+
_VERSIONS = """import sys, importlib.metadata as m
|
|
149
|
+
print("Python", sys.version.split()[0], "at", sys.prefix)
|
|
150
|
+
for name in sys.argv[1:]:
|
|
151
|
+
try:
|
|
152
|
+
print(f" {name} {m.version(name)}")
|
|
153
|
+
except m.PackageNotFoundError:
|
|
154
|
+
print(f" {name}: not installed")
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def describe_host(address: str, env: str = "", packages=(), tools=None) -> str:
|
|
159
|
+
"""What the machine `address` (empty: this one) is and has, in a few lines: its system, CPUs,
|
|
160
|
+
memory and GPUs, the Python of `env` there with the version of each of `packages`, and whether
|
|
161
|
+
each of `tools` (name -> path) can be run. One `ssh` call, which raises `HostError` when it does
|
|
162
|
+
not get in. Blocks: not on the GUI thread."""
|
|
163
|
+
from ms_env import Host
|
|
164
|
+
|
|
165
|
+
tools = tools or {}
|
|
166
|
+
python = f"{env.rstrip('/')}/bin/python" if env else ("" if address else sys.executable)
|
|
167
|
+
script = "; ".join([
|
|
168
|
+
"export LC_ALL=C", "uname -srm", '(. /etc/os-release 2>/dev/null && echo "$PRETTY_NAME")',
|
|
169
|
+
'echo "CPUs: $(nproc 2>/dev/null || sysctl -n hw.ncpu)"',
|
|
170
|
+
"awk '/MemTotal/ {printf \"Memory: %.1f GB\\n\", $2/1048576}' /proc/meminfo 2>/dev/null",
|
|
171
|
+
"(nvidia-smi -L 2>/dev/null || echo 'GPUs: none seen by nvidia-smi')",
|
|
172
|
+
"echo",
|
|
173
|
+
*([f"if [ -x {shlex.quote(python)} ]; then {shlex.quote(python)} -c {shlex.quote(_VERSIONS)} "
|
|
174
|
+
f"{' '.join(map(shlex.quote, packages))}; else echo 'Environment: none at '{shlex.quote(python)}; fi"]
|
|
175
|
+
if python else ["echo 'Environment: none named; the app prepares one before the first job'"]),
|
|
176
|
+
*(f"([ -x {shlex.quote(path)} ] && echo ' ✓ {name} '{shlex.quote(path)} || echo ' ✗ {name} not found at '{shlex.quote(path)})"
|
|
177
|
+
for name, path in tools.items()),
|
|
178
|
+
])
|
|
179
|
+
if tools:
|
|
180
|
+
script = script.replace("; ([ -x", "; echo 'Tools:'; ([ -x", 1)
|
|
181
|
+
began = time.monotonic()
|
|
182
|
+
said = Host(address or None).run(script, timeout=60).decode(errors="replace").strip()
|
|
183
|
+
return f"Connected to {address or 'this machine'} in {time.monotonic() - began:.1f} s\n\n{said}"
|
|
184
|
+
|
|
185
|
+
|
|
93
186
|
class KeptPage(QWidget):
|
|
94
187
|
"""The entries kept in `table` (`hosts` or `executors`) and the form of the chosen one.
|
|
95
188
|
|
|
@@ -98,10 +191,17 @@ class KeptPage(QWidget):
|
|
|
98
191
|
"""
|
|
99
192
|
|
|
100
193
|
changed = Signal()
|
|
101
|
-
|
|
102
|
-
|
|
194
|
+
_checked = Signal(str)
|
|
195
|
+
_synced = Signal(str, bool) # a line said there, and whether it is the last
|
|
196
|
+
|
|
197
|
+
def __init__(self, workspace, table: str, parent: QWidget | None = None, *, packages=("ms_flow", "ms_env"),
|
|
198
|
+
default_env: str = "", programs=()):
|
|
199
|
+
"""`packages`: whose version "Check connection" reads on a host; `default_env`: the
|
|
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)."""
|
|
103
202
|
super().__init__(parent)
|
|
104
203
|
self._workspace, self._table, self._one = workspace, table, table[:-1]
|
|
204
|
+
self._packages, self._default_env, self._programs = tuple(packages), default_env, tuple(programs)
|
|
105
205
|
settings = type(workspace.settings_manager.settings)
|
|
106
206
|
self._model = get_args(settings.model_fields[table].annotation)[1]
|
|
107
207
|
self._editors: dict[str, QWidget] = {}
|
|
@@ -130,6 +230,25 @@ class KeptPage(QWidget):
|
|
|
130
230
|
self.save_button.clicked.connect(self._save)
|
|
131
231
|
foot = QHBoxLayout()
|
|
132
232
|
foot.addWidget(self.said, 1)
|
|
233
|
+
if table == "hosts":
|
|
234
|
+
self.check_button = QPushButton("Check connection", self)
|
|
235
|
+
self.check_button.setToolTip("Log in to it and say what it is and what it has: system, CPUs, GPUs, "
|
|
236
|
+
"the environment of the app and its packages, the tools named.")
|
|
237
|
+
self.check_button.clicked.connect(self._check)
|
|
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)
|
|
246
|
+
self.report = QPlainTextEdit(self)
|
|
247
|
+
self.report.setReadOnly(True)
|
|
248
|
+
self.report.setVisible(False)
|
|
249
|
+
self._right.addWidget(self.report, 8) # the room the stretch below has while nothing is said
|
|
250
|
+
self._checked.connect(self._show_report)
|
|
251
|
+
self._synced.connect(self._sync_said)
|
|
133
252
|
foot.addWidget(self.save_button)
|
|
134
253
|
self._right.addStretch(1)
|
|
135
254
|
self._right.addLayout(foot)
|
|
@@ -171,9 +290,12 @@ class KeptPage(QWidget):
|
|
|
171
290
|
hosts = self._workspace.settings_manager.settings.hosts
|
|
172
291
|
self._editors = {}
|
|
173
292
|
for key, field in self._model.model_fields.items():
|
|
174
|
-
|
|
293
|
+
names = self._programs if key == "install" else hosts
|
|
294
|
+
editor = self._editors[key] = _editor(field, getattr(entry, key, None), body, names)
|
|
175
295
|
editor.setToolTip(field.description or "")
|
|
176
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")
|
|
177
299
|
kind = self._editors.get("kind")
|
|
178
300
|
if name == "local": # this machine and its pool: what they are is not a choice
|
|
179
301
|
for fixed in ("address", "kind", "hosts"):
|
|
@@ -187,6 +309,10 @@ class KeptPage(QWidget):
|
|
|
187
309
|
self._only_of_kind()
|
|
188
310
|
self.remove_button.setEnabled(bool(name) and name != "local")
|
|
189
311
|
self.said.setText("")
|
|
312
|
+
if self._table == "hosts":
|
|
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)
|
|
190
316
|
self._right.insertWidget(0, body)
|
|
191
317
|
|
|
192
318
|
def _only_of_kind(self) -> None:
|
|
@@ -226,6 +352,60 @@ class KeptPage(QWidget):
|
|
|
226
352
|
self.said.setText(f"`{name}` is kept.")
|
|
227
353
|
self.changed.emit()
|
|
228
354
|
|
|
355
|
+
def _check(self) -> None:
|
|
356
|
+
"""What `describe_host` says of the host as the form has it now, saved or not."""
|
|
357
|
+
fields = {key: _value(editor) for key, editor in self._editors.items()}
|
|
358
|
+
address = fields.get("address") or ""
|
|
359
|
+
env = fields.get("env") or (self._default_env if address else "")
|
|
360
|
+
self.check_button.setEnabled(False)
|
|
361
|
+
self._show_report(f"Connecting to {address or 'this machine'}…")
|
|
362
|
+
|
|
363
|
+
def work() -> None:
|
|
364
|
+
try:
|
|
365
|
+
said = describe_host(address, env, self._packages, fields.get("tools") or {})
|
|
366
|
+
except Exception as error: # noqa: BLE001 - said in the page, where it was asked for
|
|
367
|
+
said = f"Could not check {address or 'this machine'}:\n{str(error).split('` on ', 1)[-1]}"
|
|
368
|
+
self._checked.emit(said)
|
|
369
|
+
|
|
370
|
+
threading.Thread(target=work, name="host-check", daemon=True).start()
|
|
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
|
+
|
|
404
|
+
def _show_report(self, text: str) -> None:
|
|
405
|
+
self.report.setPlainText(text)
|
|
406
|
+
self.report.setVisible(True)
|
|
407
|
+
self.check_button.setEnabled(not text.endswith("…"))
|
|
408
|
+
|
|
229
409
|
def _remove(self) -> None:
|
|
230
410
|
name = self.name.text().strip()
|
|
231
411
|
asked = QMessageBox.question(self, "Remove", f"Remove `{name}` from the {self._table} kept?")
|
|
@@ -259,6 +439,7 @@ class ExecutorsPanel(QDialog):
|
|
|
259
439
|
self.resize(640, 260)
|
|
260
440
|
self._workspace = workspace
|
|
261
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
|
|
262
443
|
self._shown: Any = None
|
|
263
444
|
self._body: QWidget | None = None
|
|
264
445
|
self.buttons: dict[str, QPushButton] = {}
|
|
@@ -266,6 +447,8 @@ class ExecutorsPanel(QDialog):
|
|
|
266
447
|
self.defaults: dict[str, QRadioButton] = {}
|
|
267
448
|
|
|
268
449
|
self._layout = QVBoxLayout(self)
|
|
450
|
+
self.steps = _Steps(self)
|
|
451
|
+
self._layout.addWidget(self.steps)
|
|
269
452
|
self.said = QPlainTextEdit(self)
|
|
270
453
|
self.said.setReadOnly(True)
|
|
271
454
|
self.said.setVisible(False)
|
|
@@ -345,18 +528,25 @@ class ExecutorsPanel(QDialog):
|
|
|
345
528
|
self.ended.emit(name, "")
|
|
346
529
|
|
|
347
530
|
self._busy.add(name)
|
|
531
|
+
self._waiting = f"[0/0] {'stopping' if stop else 'starting'} {name}"
|
|
532
|
+
self.steps.take(self._waiting)
|
|
348
533
|
self.refresh()
|
|
349
534
|
threading.Thread(target=work, name=f"executor-{name}", daemon=True).start()
|
|
350
535
|
|
|
351
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
|
|
352
541
|
self.said.setVisible(True)
|
|
353
542
|
self.said.appendPlainText(text)
|
|
354
543
|
|
|
355
544
|
def _ended(self, name: str, error: str) -> None:
|
|
356
545
|
self._busy.discard(name)
|
|
546
|
+
self.steps.setVisible(bool(self._busy))
|
|
357
547
|
if error:
|
|
358
548
|
self._say(f"{name}: {error}")
|
|
359
549
|
self.refresh()
|
|
360
550
|
|
|
361
551
|
|
|
362
|
-
__all__ = ["ExecutorsPanel", "KeptPage"]
|
|
552
|
+
__all__ = ["ExecutorsPanel", "KeptPage", "describe_host"]
|
|
@@ -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."""
|