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.
Files changed (145) hide show
  1. {ms_components-0.4.3 → ms_components-0.5.0}/PKG-INFO +3 -3
  2. {ms_components-0.4.3 → ms_components-0.5.0}/pyproject.toml +2 -2
  3. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_executors.py +119 -10
  4. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_monitor/__init__.py +3 -1
  5. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_monitor/bridge.py +6 -4
  6. ms_components-0.5.0/src/ms_components/ms_monitor/detail.py +671 -0
  7. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_monitor/formatting.py +8 -3
  8. ms_components-0.5.0/src/ms_components/ms_monitor/jobs.py +294 -0
  9. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_monitor/models.py +18 -2
  10. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_monitor/polling.py +39 -6
  11. ms_components-0.5.0/src/ms_components/ms_monitor/resources.py +448 -0
  12. ms_components-0.5.0/src/ms_components/ms_monitor/widget.py +345 -0
  13. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_pymol/widget.py +34 -3
  14. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components.egg-info/PKG-INFO +3 -3
  15. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components.egg-info/SOURCES.txt +3 -3
  16. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components.egg-info/requires.txt +2 -2
  17. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components.egg-info/scm_file_list.json +3 -3
  18. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components.egg-info/scm_version.json +2 -2
  19. {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_executors_ui.py +62 -1
  20. {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_molsuite_monitor_bridge.py +9 -7
  21. ms_components-0.5.0/tests/test_molsuite_monitor_widgets.py +178 -0
  22. ms_components-0.5.0/tests/test_optional_integrations.py +103 -0
  23. {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_pymol_toolbar.py +16 -0
  24. ms_components-0.4.3/src/ms_components/ms_monitor/cards.py +0 -75
  25. ms_components-0.4.3/src/ms_components/ms_monitor/detail_panel.py +0 -121
  26. ms_components-0.4.3/src/ms_components/ms_monitor/job_cards.py +0 -540
  27. ms_components-0.4.3/src/ms_components/ms_monitor/resources.py +0 -305
  28. ms_components-0.4.3/src/ms_components/ms_monitor/widget.py +0 -355
  29. ms_components-0.4.3/tests/test_molsuite_monitor_widgets.py +0 -92
  30. {ms_components-0.4.3 → ms_components-0.5.0}/.github/workflows/docs.yml +0 -0
  31. {ms_components-0.4.3 → ms_components-0.5.0}/.github/workflows/publish.yml +0 -0
  32. {ms_components-0.4.3 → ms_components-0.5.0}/.gitignore +0 -0
  33. {ms_components-0.4.3 → ms_components-0.5.0}/LICENSE +0 -0
  34. {ms_components-0.4.3 → ms_components-0.5.0}/MANIFEST.in +0 -0
  35. {ms_components-0.4.3 → ms_components-0.5.0}/README.md +0 -0
  36. {ms_components-0.4.3 → ms_components-0.5.0}/examples/tutorial_demo.py +0 -0
  37. {ms_components-0.4.3 → ms_components-0.5.0}/examples/tutorial_demo.toml +0 -0
  38. {ms_components-0.4.3 → ms_components-0.5.0}/licenses/qt-themes-LICENSE.txt +0 -0
  39. {ms_components-0.4.3 → ms_components-0.5.0}/setup.cfg +0 -0
  40. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/__init__.py +0 -0
  41. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/backdrop.py +0 -0
  42. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/base.qss +0 -0
  43. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/chips.py +0 -0
  44. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ketcher.py +0 -0
  45. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/listview.py +0 -0
  46. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_dockwidget/__init__.py +0 -0
  47. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_dockwidget/resources/__init__.py +0 -0
  48. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_dockwidget/resources/icons/__init__.py +0 -0
  49. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_dockwidget/resources/icons/folder.svg +0 -0
  50. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_dockwidget/widget.py +0 -0
  51. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_monitor/config.py +0 -0
  52. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_monitor/palette.py +0 -0
  53. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_projects/__init__.py +0 -0
  54. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_projects/app.py +0 -0
  55. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_projects/backend.py +0 -0
  56. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_projects/browser.py +0 -0
  57. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_projects/scratch.py +0 -0
  58. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_projects/widget.py +0 -0
  59. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_pymol/__init__.py +0 -0
  60. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_pymol/command_line.py +0 -0
  61. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_pymol/icons/actions.svg +0 -0
  62. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_pymol/icons/box.svg +0 -0
  63. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_pymol/icons/center.svg +0 -0
  64. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_pymol/icons/display.svg +0 -0
  65. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_pymol/icons/hide.svg +0 -0
  66. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_pymol/icons/hydrogens-polar.svg +0 -0
  67. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_pymol/icons/hydrogens.svg +0 -0
  68. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_pymol/icons/menu.svg +0 -0
  69. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_pymol/icons/orient.svg +0 -0
  70. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_pymol/icons/presets.svg +0 -0
  71. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_pymol/icons/rock.svg +0 -0
  72. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_pymol/icons/selection.svg +0 -0
  73. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_pymol/icons/settings.svg +0 -0
  74. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_pymol/icons/style.svg +0 -0
  75. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_pymol/icons/zoom.svg +0 -0
  76. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_pymol/plugins.py +0 -0
  77. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_pymol/scene.py +0 -0
  78. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_settings/__init__.py +0 -0
  79. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_settings/panel.py +0 -0
  80. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_stepper.py +0 -0
  81. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/__init__.py +0 -0
  82. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/action_header.py +0 -0
  83. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/cell_delegate.py +0 -0
  84. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/column_panel.py +0 -0
  85. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/example_usage.py +0 -0
  86. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/filter_panel.py +0 -0
  87. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/icons/columns.svg +0 -0
  88. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/icons/export.svg +0 -0
  89. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/icons/filter.svg +0 -0
  90. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/icons/menu.svg +0 -0
  91. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/icons/reload.svg +0 -0
  92. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/icons/search.svg +0 -0
  93. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/icons/settings.svg +0 -0
  94. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/icons/sort-asc.svg +0 -0
  95. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/icons/sort-desc.svg +0 -0
  96. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/icons/sort.svg +0 -0
  97. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/metric_filter_bar.py +0 -0
  98. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/pagination_control.py +0 -0
  99. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/query_builder.py +0 -0
  100. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/session_provider.py +0 -0
  101. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/smart_table_view.py +0 -0
  102. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/sort_panel.py +0 -0
  103. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/table_config.py +0 -0
  104. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/table_interaction.py +0 -0
  105. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/table_model.py +0 -0
  106. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/test_external_filters.py +0 -0
  107. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_table/test_toolbar.py +0 -0
  108. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_tutorial/__init__.py +0 -0
  109. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_tutorial/bindings.py +0 -0
  110. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_tutorial/controller.py +0 -0
  111. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_tutorial/models.py +0 -0
  112. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/ms_tutorial/overlay.py +0 -0
  113. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/opengl.py +0 -0
  114. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/pagination.py +0 -0
  115. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/palettes.py +0 -0
  116. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/step_dialog.py +0 -0
  117. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/theme.py +0 -0
  118. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/tool_panel.py +0 -0
  119. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components/wheel.py +0 -0
  120. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components.egg-info/dependency_links.txt +0 -0
  121. {ms_components-0.4.3 → ms_components-0.5.0}/src/ms_components.egg-info/top_level.txt +0 -0
  122. {ms_components-0.4.3 → ms_components-0.5.0}/tests/conftest.py +0 -0
  123. {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_app_settings.py +0 -0
  124. {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_chips.py +0 -0
  125. {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_dock_window.py +0 -0
  126. {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_ketcher.py +0 -0
  127. {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_monitor_config.py +0 -0
  128. {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_ms_projects_widget.py +0 -0
  129. {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_opengl.py +0 -0
  130. {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_progress_column.py +0 -0
  131. {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_project_browser.py +0 -0
  132. {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_pymol_command_line.py +0 -0
  133. {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_pymol_grid.py +0 -0
  134. {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_pymol_scene.py +0 -0
  135. {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_query_builder_keyset.py +0 -0
  136. {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_shift_hscroll.py +0 -0
  137. {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_split_button_style.py +0 -0
  138. {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_step_dialog.py +0 -0
  139. {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_table_interaction.py +0 -0
  140. {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_table_toolbar.py +0 -0
  141. {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_table_view_state.py +0 -0
  142. {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_theme_contrast.py +0 -0
  143. {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_tool_panel.py +0 -0
  144. {ms_components-0.4.3 → ms_components-0.5.0}/tests/test_tutorial.py +0 -0
  145. {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.4.3
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.2.0; extra == "projects"
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.2.0; extra == "steps"
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.2.0"]
34
+ projects = ["ms_flow>=0.7.1"]
35
35
  pymol = ["pymol-open-source>=3.2.0a0"]
36
- steps = ["ms_env>=0.2.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, hosts=()) -> 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) is list: # names of hosts: the ones kept, to tick
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 host in hosts:
55
- item = QListWidgetItem("local (this computer)" if host == "local" else host, editor)
56
- item.setData(Qt.ItemDataRole.UserRole, host)
57
- item.setCheckState(Qt.CheckState.Checked if host in (value or ()) else Qt.CheckState.Unchecked)
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
- editor = self._editors[key] = _editor(field, getattr(entry, key, None), body, hosts)
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
- "JobDetailPanel": "detail_panel",
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 set_unfolded_jobs(self, job_ids) -> None:
109
- """The jobs whose tasks in flight the next polls bring (`JobMonitorState.tasks`)."""
110
- self._poller.unfolded = frozenset(job_ids)
111
- self.request_refresh()
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."""