ms-components 0.1.0__tar.gz → 0.2.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 (126) hide show
  1. {ms_components-0.1.0 → ms_components-0.2.0}/.github/workflows/docs.yml +4 -15
  2. {ms_components-0.1.0 → ms_components-0.2.0}/PKG-INFO +4 -4
  3. {ms_components-0.1.0 → ms_components-0.2.0}/README.md +1 -2
  4. {ms_components-0.1.0 → ms_components-0.2.0}/docs/index.md +2 -4
  5. {ms_components-0.1.0 → ms_components-0.2.0}/pyproject.toml +2 -1
  6. ms_components-0.2.0/src/ms_components/ms_executors.py +362 -0
  7. ms_components-0.2.0/src/ms_components/ms_monitor/__init__.py +25 -0
  8. ms_components-0.2.0/src/ms_components/ms_monitor/bridge.py +222 -0
  9. ms_components-0.2.0/src/ms_components/ms_monitor/detail_panel.py +121 -0
  10. ms_components-0.2.0/src/ms_components/ms_monitor/formatting.py +29 -0
  11. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_monitor/job_cards.py +133 -157
  12. ms_components-0.2.0/src/ms_components/ms_monitor/models.py +76 -0
  13. ms_components-0.2.0/src/ms_components/ms_monitor/polling.py +149 -0
  14. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_monitor/widget.py +61 -118
  15. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_projects/browser.py +8 -62
  16. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_settings/panel.py +2 -31
  17. ms_components-0.2.0/src/ms_components/palettes.py +29 -0
  18. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/step_dialog.py +40 -4
  19. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/theme.py +1 -24
  20. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components.egg-info/PKG-INFO +4 -4
  21. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components.egg-info/SOURCES.txt +6 -9
  22. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components.egg-info/requires.txt +2 -1
  23. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components.egg-info/scm_file_list.json +5 -8
  24. ms_components-0.2.0/src/ms_components.egg-info/scm_version.json +8 -0
  25. ms_components-0.2.0/tests/test_executors_ui.py +120 -0
  26. ms_components-0.2.0/tests/test_molsuite_monitor_bridge.py +160 -0
  27. ms_components-0.2.0/tests/test_molsuite_monitor_widgets.py +75 -0
  28. {ms_components-0.1.0 → ms_components-0.2.0}/tests/test_monitor_config.py +9 -0
  29. {ms_components-0.1.0 → ms_components-0.2.0}/tests/test_project_browser.py +1 -3
  30. ms_components-0.2.0/tests/test_step_dialog.py +41 -0
  31. ms_components-0.1.0/src/ms_components/ms_monitor/__init__.py +0 -43
  32. ms_components-0.1.0/src/ms_components/ms_monitor/bridge.py +0 -400
  33. ms_components-0.1.0/src/ms_components/ms_monitor/chunk_panel.py +0 -85
  34. ms_components-0.1.0/src/ms_components/ms_monitor/events_panel.py +0 -178
  35. ms_components-0.1.0/src/ms_components/ms_monitor/executor_panel.py +0 -149
  36. ms_components-0.1.0/src/ms_components/ms_monitor/executors_tab.py +0 -355
  37. ms_components-0.1.0/src/ms_components/ms_monitor/formatting.py +0 -51
  38. ms_components-0.1.0/src/ms_components/ms_monitor/job_filter_proxy.py +0 -48
  39. ms_components-0.1.0/src/ms_components/ms_monitor/job_tree_model.py +0 -106
  40. ms_components-0.1.0/src/ms_components/ms_monitor/models.py +0 -165
  41. ms_components-0.1.0/src/ms_components/ms_monitor/polling.py +0 -498
  42. ms_components-0.1.0/src/ms_components/ms_settings/workers_editor.py +0 -335
  43. ms_components-0.1.0/src/ms_components.egg-info/scm_version.json +0 -8
  44. ms_components-0.1.0/tests/test_molsuite_monitor_bridge.py +0 -599
  45. ms_components-0.1.0/tests/test_molsuite_monitor_widgets.py +0 -119
  46. ms_components-0.1.0/tests/test_workers_editor.py +0 -106
  47. {ms_components-0.1.0 → ms_components-0.2.0}/.github/workflows/publish.yml +0 -0
  48. {ms_components-0.1.0 → ms_components-0.2.0}/.gitignore +0 -0
  49. {ms_components-0.1.0 → ms_components-0.2.0}/LICENSE +0 -0
  50. {ms_components-0.1.0 → ms_components-0.2.0}/docs/api_reference.md +0 -0
  51. {ms_components-0.1.0 → ms_components-0.2.0}/docs/examples.md +0 -0
  52. {ms_components-0.1.0 → ms_components-0.2.0}/docs/usage.md +0 -0
  53. {ms_components-0.1.0 → ms_components-0.2.0}/setup.cfg +0 -0
  54. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/__init__.py +0 -0
  55. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/base.qss +0 -0
  56. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/listview.py +0 -0
  57. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_dockwidget/__init__.py +0 -0
  58. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_dockwidget/resources/__init__.py +0 -0
  59. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_dockwidget/resources/icons/__init__.py +0 -0
  60. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_dockwidget/resources/icons/folder.svg +0 -0
  61. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_dockwidget/widget.py +0 -0
  62. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_monitor/cards.py +0 -0
  63. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_monitor/config.py +0 -0
  64. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_monitor/palette.py +0 -0
  65. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_projects/__init__.py +0 -0
  66. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_projects/app.py +0 -0
  67. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_projects/scratch.py +0 -0
  68. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_projects/widget.py +0 -0
  69. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_pymol/__init__.py +0 -0
  70. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_pymol/icons/actions.svg +0 -0
  71. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_pymol/icons/box.svg +0 -0
  72. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_pymol/icons/center.svg +0 -0
  73. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_pymol/icons/display.svg +0 -0
  74. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_pymol/icons/hide.svg +0 -0
  75. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_pymol/icons/menu.svg +0 -0
  76. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_pymol/icons/orient.svg +0 -0
  77. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_pymol/icons/presets.svg +0 -0
  78. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_pymol/icons/rock.svg +0 -0
  79. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_pymol/icons/selection.svg +0 -0
  80. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_pymol/icons/settings.svg +0 -0
  81. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_pymol/icons/zoom.svg +0 -0
  82. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_pymol/plugins.py +0 -0
  83. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_pymol/widget.py +0 -0
  84. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_settings/__init__.py +0 -0
  85. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_stepper.py +0 -0
  86. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/__init__.py +0 -0
  87. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/action_header.py +0 -0
  88. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/cell_delegate.py +0 -0
  89. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/column_panel.py +0 -0
  90. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/example_usage.py +0 -0
  91. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/filter_panel.py +0 -0
  92. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/icons/columns.svg +0 -0
  93. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/icons/export.svg +0 -0
  94. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/icons/filter.svg +0 -0
  95. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/icons/menu.svg +0 -0
  96. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/icons/reload.svg +0 -0
  97. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/icons/search.svg +0 -0
  98. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/icons/settings.svg +0 -0
  99. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/icons/sort-asc.svg +0 -0
  100. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/icons/sort-desc.svg +0 -0
  101. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/icons/sort.svg +0 -0
  102. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/metric_filter_bar.py +0 -0
  103. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/pagination_control.py +0 -0
  104. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/query_builder.py +0 -0
  105. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/session_provider.py +0 -0
  106. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/smart_table_view.py +0 -0
  107. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/sort_panel.py +0 -0
  108. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/table_config.py +0 -0
  109. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/table_model.py +0 -0
  110. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/test_external_filters.py +0 -0
  111. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/test_toolbar.py +0 -0
  112. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/pagination.py +0 -0
  113. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/tool_panel.py +0 -0
  114. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/wheel.py +0 -0
  115. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components.egg-info/dependency_links.txt +0 -0
  116. {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components.egg-info/top_level.txt +0 -0
  117. {ms_components-0.1.0 → ms_components-0.2.0}/tests/test_app_settings.py +0 -0
  118. {ms_components-0.1.0 → ms_components-0.2.0}/tests/test_dock_window.py +0 -0
  119. {ms_components-0.1.0 → ms_components-0.2.0}/tests/test_ms_projects_widget.py +0 -0
  120. {ms_components-0.1.0 → ms_components-0.2.0}/tests/test_pymol_toolbar.py +0 -0
  121. {ms_components-0.1.0 → ms_components-0.2.0}/tests/test_query_builder_keyset.py +0 -0
  122. {ms_components-0.1.0 → ms_components-0.2.0}/tests/test_shift_hscroll.py +0 -0
  123. {ms_components-0.1.0 → ms_components-0.2.0}/tests/test_table_toolbar.py +0 -0
  124. {ms_components-0.1.0 → ms_components-0.2.0}/tests/test_table_view_state.py +0 -0
  125. {ms_components-0.1.0 → ms_components-0.2.0}/tests/test_theme_contrast.py +0 -0
  126. {ms_components-0.1.0 → ms_components-0.2.0}/zensical.toml +0 -0
@@ -11,22 +11,20 @@ concurrency:
11
11
  group: documentation
12
12
  cancel-in-progress: false
13
13
 
14
+ # mike commits every version to the gh-pages branch and GitHub Pages serves that
15
+ # branch (Settings > Pages > Deploy from a branch: gh-pages, / (root)).
16
+ # An Actions Pages deployment is keyed by commit SHA, so a release tag on a
17
+ # commit already deployed from main would be silently ignored.
14
18
  permissions:
15
19
  contents: write
16
- pages: write
17
- id-token: write
18
20
 
19
21
  jobs:
20
22
  deploy:
21
- environment:
22
- name: github-pages
23
- url: ${{ steps.deployment.outputs.page_url }}
24
23
  runs-on: ubuntu-latest
25
24
  steps:
26
25
  - uses: actions/checkout@v6
27
26
  with:
28
27
  fetch-depth: 0
29
- - uses: actions/configure-pages@v6
30
28
  - uses: actions/setup-python@v6
31
29
  with:
32
30
  python-version: 3.x
@@ -47,12 +45,3 @@ jobs:
47
45
  run: |
48
46
  mike deploy --push --update-aliases --alias-type redirect "$GITHUB_REF_NAME" latest
49
47
  mike set-default --push latest
50
- - name: Export versioned site
51
- run: |
52
- mkdir -p site
53
- git archive gh-pages | tar -x -C site
54
- - uses: actions/upload-pages-artifact@v5
55
- with:
56
- path: site
57
- - uses: actions/deploy-pages@v5
58
- id: deployment
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ms_components
3
- Version: 0.1.0
3
+ Version: 0.2.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
@@ -16,7 +16,8 @@ Classifier: Environment :: X11 Applications :: Qt
16
16
  Requires-Python: >=3.12
17
17
  Description-Content-Type: text/markdown
18
18
  License-File: LICENSE
19
- Requires-Dist: ms_flow
19
+ Requires-Dist: ms_env>=0.2.0
20
+ Requires-Dist: ms_flow>=0.2.0
20
21
  Requires-Dist: PySide6>=6.5
21
22
  Requires-Dist: pymol-open-source>=3.2.0a0
22
23
  Requires-Dist: sqlmodel>=0.0.14
@@ -38,8 +39,7 @@ PyMOL is installed automatically from the `pymol-open-source` alpha wheels on
38
39
  PyPI (`>=3.2.0a0`):
39
40
 
40
41
  ```bash
41
- pip install git+https://github.com/MolSuite/ms_flow
42
- pip install git+https://github.com/MolSuite/ms_components
42
+ pip install ms_components
43
43
  ```
44
44
 
45
45
  ## License
@@ -12,8 +12,7 @@ PyMOL is installed automatically from the `pymol-open-source` alpha wheels on
12
12
  PyPI (`>=3.2.0a0`):
13
13
 
14
14
  ```bash
15
- pip install git+https://github.com/MolSuite/ms_flow
16
- pip install git+https://github.com/MolSuite/ms_components
15
+ pip install ms_components
17
16
  ```
18
17
 
19
18
  ## License
@@ -8,12 +8,10 @@ dock, steppers, tool panels, and the shared theme.
8
8
 
9
9
  `ms_components` requires Python 3.12. PyMOL comes from the
10
10
  `pymol-open-source` alpha wheels on PyPI and is installed automatically as a
11
- dependency; no conda environment is needed. Until the MolSuite packages are
12
- published on PyPI, install them from Git:
11
+ dependency, together with `ms_flow`; no conda environment is needed:
13
12
 
14
13
  ```bash
15
- pip install "ms_flow @ git+https://github.com/MolSuite/ms_flow"
16
- pip install "ms_components @ git+https://github.com/MolSuite/ms_components"
14
+ pip install ms_components
17
15
  ```
18
16
 
19
17
  ## Components
@@ -26,7 +26,8 @@ classifiers = [
26
26
 
27
27
  # UI components sit on top of the orchestrator (molsuite_flow), Qt and PyMOL.
28
28
  dependencies = [
29
- "ms_flow",
29
+ "ms_env>=0.2.0",
30
+ "ms_flow>=0.2.0",
30
31
  "PySide6>=6.5",
31
32
  "pymol-open-source>=3.2.0a0",
32
33
  "sqlmodel>=0.0.14",
@@ -0,0 +1,362 @@
1
+ """The machines that run jobs and who runs them on them.
2
+
3
+ `KeptPage` is a Settings page: the entries kept under one name each (`hosts` or `executors`)
4
+ and the form of the one chosen. `ExecutorsPanel` is the quick window that starts and stops the
5
+ executors. Both go through a `ms_flow.Workspace` (`save_host`, `remove_host`, `save_executor`,
6
+ `remove_executor`, `executors`, `activate`, `deactivate`), which is who refuses what is not to
7
+ be done now, and says why.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import threading
12
+ import types
13
+ from typing import Any, Literal, Union, get_args, get_origin
14
+
15
+ from PySide6.QtCore import Qt, QTimer, Signal
16
+ from PySide6.QtWidgets import (
17
+ QCheckBox,
18
+ QComboBox,
19
+ QDialog,
20
+ QDoubleSpinBox,
21
+ QFormLayout,
22
+ QGridLayout,
23
+ QHBoxLayout,
24
+ QLabel,
25
+ QLineEdit,
26
+ QListWidget,
27
+ QListWidgetItem,
28
+ QMessageBox,
29
+ QPlainTextEdit,
30
+ QPushButton,
31
+ QRadioButton,
32
+ QSpinBox,
33
+ QVBoxLayout,
34
+ QWidget,
35
+ )
36
+
37
+
38
+ def _editor(field, value: Any, parent: QWidget, hosts=()) -> QWidget:
39
+ annotation, options = field.annotation, get_args(field.annotation)
40
+ if get_origin(annotation) in (Union, types.UnionType) and options == (bool, type(None)):
41
+ editor = QComboBox(parent)
42
+ for text, data in (("Automatic", None), ("Yes", True), ("No", False)):
43
+ editor.addItem(text, data)
44
+ elif get_origin(annotation) is Literal:
45
+ editor = QComboBox(parent)
46
+ for option in options:
47
+ editor.addItem(str(option), option)
48
+ elif get_origin(annotation) is list: # names of hosts: the ones kept, to tick
49
+ editor = QListWidget(parent)
50
+ editor.setMaximumHeight(110)
51
+ for host in hosts:
52
+ item = QListWidgetItem("local (this computer)" if host == "local" else host, editor)
53
+ item.setData(Qt.ItemDataRole.UserRole, host)
54
+ item.setCheckState(Qt.CheckState.Checked if host in (value or ()) else Qt.CheckState.Unchecked)
55
+ return editor
56
+ elif get_origin(annotation) is dict:
57
+ editor = QPlainTextEdit("\n".join(f"{key} = {path}" for key, path in (value or {}).items()), parent)
58
+ editor.setPlaceholderText("vina = /opt/vina/bin/vina")
59
+ editor.setMaximumHeight(90)
60
+ return editor
61
+ elif annotation is bool:
62
+ editor = QCheckBox(parent)
63
+ editor.setChecked(bool(value))
64
+ return editor
65
+ elif annotation in (int, float):
66
+ editor = QSpinBox(parent) if annotation is int else QDoubleSpinBox(parent)
67
+ # A number with a limit here is one that starts at 0; the others also say "all but".
68
+ editor.setRange(0 if field.metadata else -4096, 1_000_000)
69
+ editor.setValue(value or 0)
70
+ return editor
71
+ else:
72
+ return QLineEdit(str(value or ""), parent)
73
+ editor.setCurrentIndex(max(0, editor.findData(value)))
74
+ return editor
75
+
76
+
77
+ def _value(editor: QWidget) -> Any:
78
+ if isinstance(editor, QComboBox):
79
+ return editor.currentData()
80
+ if isinstance(editor, QCheckBox):
81
+ return editor.isChecked()
82
+ if isinstance(editor, (QSpinBox, QDoubleSpinBox)):
83
+ return editor.value()
84
+ if isinstance(editor, QListWidget):
85
+ items = [editor.item(row) for row in range(editor.count())]
86
+ return [item.data(Qt.ItemDataRole.UserRole) for item in items if item.checkState() == Qt.CheckState.Checked]
87
+ if isinstance(editor, QPlainTextEdit):
88
+ pairs = (line.partition("=") for line in editor.toPlainText().splitlines() if "=" in line)
89
+ return {key.strip(): path.strip() for key, _, path in pairs}
90
+ return editor.text().strip()
91
+
92
+
93
+ class KeptPage(QWidget):
94
+ """The entries kept in `table` (`hosts` or `executors`) and the form of the chosen one.
95
+
96
+ "Save" and "Remove" act at once, through the workspace: an entry is whole or it is not
97
+ kept, so the "Save changes" of the dialog around this page is not for it.
98
+ """
99
+
100
+ changed = Signal()
101
+
102
+ def __init__(self, workspace, table: str, parent: QWidget | None = None):
103
+ super().__init__(parent)
104
+ self._workspace, self._table, self._one = workspace, table, table[:-1]
105
+ settings = type(workspace.settings_manager.settings)
106
+ self._model = get_args(settings.model_fields[table].annotation)[1]
107
+ self._editors: dict[str, QWidget] = {}
108
+ self._body: QWidget | None = None
109
+
110
+ root = QHBoxLayout(self)
111
+ left = QVBoxLayout()
112
+ self.names = QListWidget(self)
113
+ self.names.setMaximumWidth(190)
114
+ self.names.currentTextChanged.connect(lambda name: name and self._show(name))
115
+ left.addWidget(self.names, 1)
116
+ buttons = QHBoxLayout()
117
+ self.new_button = QPushButton("New", self)
118
+ self.new_button.clicked.connect(lambda: self._show(None))
119
+ self.remove_button = QPushButton("Remove", self)
120
+ self.remove_button.clicked.connect(self._remove)
121
+ buttons.addWidget(self.new_button)
122
+ buttons.addWidget(self.remove_button)
123
+ left.addLayout(buttons)
124
+ root.addLayout(left)
125
+
126
+ self._right = QVBoxLayout()
127
+ self.said = QLabel(self)
128
+ self.said.setWordWrap(True)
129
+ self.save_button = QPushButton("Save", self)
130
+ self.save_button.clicked.connect(self._save)
131
+ foot = QHBoxLayout()
132
+ foot.addWidget(self.said, 1)
133
+ foot.addWidget(self.save_button)
134
+ self._right.addStretch(1)
135
+ self._right.addLayout(foot)
136
+ root.addLayout(self._right, 1)
137
+ self.reload()
138
+
139
+ def _kept(self) -> dict:
140
+ return getattr(self._workspace.settings_manager.settings, self._table)
141
+
142
+ def showEvent(self, event) -> None: # the other page may have changed what this one lists
143
+ self.reload()
144
+ super().showEvent(event)
145
+
146
+ def reload(self, select: str | None = None) -> None:
147
+ select = select or (self.names.currentItem() and self.names.currentItem().text())
148
+ names = list(self._kept())
149
+ self.names.blockSignals(True)
150
+ self.names.clear()
151
+ self.names.addItems(names)
152
+ self.names.setCurrentRow(names.index(select) if select in names else 0)
153
+ self.names.blockSignals(False)
154
+ self._show(self.names.currentItem().text())
155
+
156
+ def _show(self, name: str | None) -> None:
157
+ """The form of the entry kept as `name`, or of one that has no name yet."""
158
+ if self._body is not None:
159
+ self._body.setParent(None)
160
+ self._body.deleteLater()
161
+ if name is None:
162
+ self.names.setCurrentRow(-1)
163
+ entry = self._kept()[name] if name else self._model.model_construct()
164
+ self._body = body = QWidget(self)
165
+ self._form = form = QFormLayout(body)
166
+ form.setContentsMargins(0, 0, 0, 0)
167
+ self.name = QLineEdit(name or "", body)
168
+ self.name.setReadOnly(bool(name))
169
+ self.name.setPlaceholderText("letters, digits, _ and -")
170
+ form.addRow("Name", self.name)
171
+ hosts = self._workspace.settings_manager.settings.hosts
172
+ self._editors = {}
173
+ for key, field in self._model.model_fields.items():
174
+ editor = self._editors[key] = _editor(field, getattr(entry, key, None), body, hosts)
175
+ editor.setToolTip(field.description or "")
176
+ form.addRow(field.title or key.replace("_", " ").capitalize(), editor)
177
+ kind = self._editors.get("kind")
178
+ if name == "local": # this machine and its pool: what they are is not a choice
179
+ for fixed in ("address", "kind", "hosts"):
180
+ if fixed in self._editors:
181
+ self._editors[fixed].setEnabled(False)
182
+ elif kind is not None:
183
+ kind.removeItem(kind.findData("loky"))
184
+ if kind is not None:
185
+ kind.currentIndexChanged.connect(self._only_of_kind)
186
+ self._editors["hosts"].itemChanged.connect(self._one_host)
187
+ self._only_of_kind()
188
+ self.remove_button.setEnabled(bool(name) and name != "local")
189
+ self.said.setText("")
190
+ self._right.insertWidget(0, body)
191
+
192
+ def _only_of_kind(self) -> None:
193
+ kind = self._editors["kind"].currentData()
194
+ for key, field in self._model.model_fields.items():
195
+ only = (field.json_schema_extra or {}).get("kind")
196
+ self._form.setRowVisible(self._editors[key], only in (None, kind))
197
+ self._form.labelForField(self._editors["hosts"]).setText("Login node" if kind == "queue" else "Hosts")
198
+ self._one_host()
199
+
200
+ def _one_host(self, ticked: QListWidgetItem | None = None) -> None:
201
+ """A queue is asked through one machine, its login node, and has nothing to do with
202
+ the others kept: ticking one unticks the rest."""
203
+ ticks = self._editors["hosts"]
204
+ if self._editors["kind"].currentData() != "queue":
205
+ return
206
+ on = [item for item in map(ticks.item, range(ticks.count())) if item.checkState() == Qt.CheckState.Checked]
207
+ keep = ticked if ticked in on else on[0] if on else None
208
+ ticks.blockSignals(True)
209
+ for item in on:
210
+ if item is not keep:
211
+ item.setCheckState(Qt.CheckState.Unchecked)
212
+ ticks.blockSignals(False)
213
+
214
+ def _save(self) -> None:
215
+ name = self.name.text().strip()
216
+ if not self.name.isReadOnly() and name in self._kept():
217
+ self.said.setText(f"`{name}` is kept already.")
218
+ return
219
+ fields = {key: _value(editor) for key, editor in self._editors.items() if not editor.isHidden()}
220
+ try:
221
+ getattr(self._workspace, f"save_{self._one}")(name, **fields)
222
+ except (ValueError, RuntimeError) as error:
223
+ self.said.setText(str(error))
224
+ return
225
+ self.reload(select=name)
226
+ self.said.setText(f"`{name}` is kept.")
227
+ self.changed.emit()
228
+
229
+ def _remove(self) -> None:
230
+ name = self.name.text().strip()
231
+ asked = QMessageBox.question(self, "Remove", f"Remove `{name}` from the {self._table} kept?")
232
+ if asked != QMessageBox.StandardButton.Yes:
233
+ return
234
+ try:
235
+ getattr(self._workspace, f"remove_{self._one}")(name)
236
+ except (ValueError, RuntimeError) as error:
237
+ self.said.setText(str(error))
238
+ return
239
+ self.reload()
240
+ self.changed.emit()
241
+
242
+
243
+ class ExecutorsPanel(QDialog):
244
+ """Who runs jobs now: a row for each executor kept, started and stopped from here.
245
+
246
+ Starting one waits for its machines, so it is done in a thread, and what is done on a
247
+ machine that is being prepared is shown line by line. The round mark is the executor a job
248
+ goes to when nobody is named.
249
+ """
250
+
251
+ line, ended = Signal(str), Signal(str, str)
252
+ settings_requested = Signal()
253
+ _STATES = {"on": "On", "off": "Off", "starting": "Starting…", "blocked": "Blocked"}
254
+
255
+ def __init__(self, workspace, parent: QWidget | None = None):
256
+ super().__init__(parent)
257
+ self.setWindowTitle("Executors")
258
+ self.setModal(False)
259
+ self.resize(640, 260)
260
+ self._workspace = workspace
261
+ self._busy: set[str] = set() # the ones a thread of this panel is starting or stopping
262
+ self._shown: Any = None
263
+ self._body: QWidget | None = None
264
+ self.buttons: dict[str, QPushButton] = {}
265
+ self.states: dict[str, QLabel] = {}
266
+ self.defaults: dict[str, QRadioButton] = {}
267
+
268
+ self._layout = QVBoxLayout(self)
269
+ self.said = QPlainTextEdit(self)
270
+ self.said.setReadOnly(True)
271
+ self.said.setVisible(False)
272
+ self._layout.addWidget(self.said, 1)
273
+ self._layout.addStretch(0) # takes the room left only while nothing is said
274
+ foot = QHBoxLayout()
275
+ foot.addStretch(1)
276
+ self.settings_button = QPushButton("Hosts and executors…", self)
277
+ self.settings_button.setToolTip("They are made and changed in Settings.")
278
+ self.settings_button.clicked.connect(self.settings_requested.emit)
279
+ foot.addWidget(self.settings_button)
280
+ self._layout.addLayout(foot)
281
+
282
+ self.line.connect(self._say)
283
+ self.ended.connect(self._ended)
284
+ self._timer = QTimer(self)
285
+ self._timer.setInterval(1000)
286
+ self._timer.timeout.connect(self.refresh)
287
+ self.refresh()
288
+
289
+ def showEvent(self, event) -> None:
290
+ self.refresh()
291
+ self._timer.start()
292
+ super().showEvent(event)
293
+
294
+ def hideEvent(self, event) -> None:
295
+ self._timer.stop()
296
+ super().hideEvent(event)
297
+
298
+ def refresh(self) -> None:
299
+ rows = self._workspace.executors()
300
+ if (rows, self._busy) == self._shown:
301
+ return
302
+ self._shown = (rows, set(self._busy))
303
+ if self._body is not None:
304
+ self._body.setParent(None)
305
+ self._body.deleteLater()
306
+ self._body = body = QWidget(self)
307
+ grid = QGridLayout(body)
308
+ grid.setContentsMargins(0, 0, 0, 0)
309
+ grid.setColumnStretch(4, 1)
310
+ for column, title in enumerate(("Default", "Name", "Kind", "Hosts", "State")):
311
+ grid.addWidget(QLabel(f"<b>{title}</b>", body), 0, column)
312
+ for at, row in enumerate(rows, start=1):
313
+ name, state, busy = row["name"], row["state"], row["name"] in self._busy
314
+ default = self.defaults[name] = QRadioButton(body)
315
+ default.setChecked(row["default"])
316
+ default.setToolTip("A job that names nobody goes to this one.")
317
+ default.clicked.connect(lambda _checked, name=name: self._default(name))
318
+ grid.addWidget(default, at, 0)
319
+ grid.addWidget(QLabel(name, body), at, 1)
320
+ grid.addWidget(QLabel(row["kind"], body), at, 2)
321
+ grid.addWidget(QLabel(", ".join(row["hosts"]), body), at, 3)
322
+ said = self.states[name] = QLabel(
323
+ "Stopping…" if busy and state == "on" else self._STATES[state] + (f": {row['why']}" if row["why"] else ""),
324
+ body)
325
+ said.setWordWrap(True)
326
+ grid.addWidget(said, at, 4)
327
+ button = self.buttons[name] = QPushButton("Deactivate" if state == "on" else "Activate", body)
328
+ button.setEnabled(state in ("on", "off") and not busy)
329
+ button.setToolTip(row["why"])
330
+ button.clicked.connect(lambda _checked, name=name, stop=state == "on": self._do(name, stop))
331
+ grid.addWidget(button, at, 5)
332
+ self._layout.insertWidget(0, body)
333
+
334
+ def _default(self, name: str) -> None:
335
+ self._workspace.settings_manager.set_default_executor(name)
336
+ self.refresh()
337
+
338
+ def _do(self, name: str, stop: bool) -> None:
339
+ def work() -> None:
340
+ try:
341
+ self._workspace.deactivate(name) if stop else self._workspace.activate(name, self.line.emit)
342
+ except Exception as error: # noqa: BLE001 - said in the panel, where it was asked for
343
+ self.ended.emit(name, str(error))
344
+ else:
345
+ self.ended.emit(name, "")
346
+
347
+ self._busy.add(name)
348
+ self.refresh()
349
+ threading.Thread(target=work, name=f"executor-{name}", daemon=True).start()
350
+
351
+ def _say(self, text: str) -> None:
352
+ self.said.setVisible(True)
353
+ self.said.appendPlainText(text)
354
+
355
+ def _ended(self, name: str, error: str) -> None:
356
+ self._busy.discard(name)
357
+ if error:
358
+ self._say(f"{name}: {error}")
359
+ self.refresh()
360
+
361
+
362
+ __all__ = ["ExecutorsPanel", "KeptPage"]
@@ -0,0 +1,25 @@
1
+ """The monitor of MolSuite jobs. Its names are loaded when first asked for: `config` is read
2
+ by processes that never open a window, and they must not pay for Qt."""
3
+ from importlib import import_module
4
+
5
+ _LAZY = {
6
+ "FailureState": "models",
7
+ "JobDetailPanel": "detail_panel",
8
+ "JobDetailState": "models",
9
+ "JobMonitorState": "models",
10
+ "MonitorConfig": "config",
11
+ "MonitorSyncState": "models",
12
+ "MolSuiteMonitorBridge": "bridge",
13
+ "MolSuiteMonitorWidget": "widget",
14
+ "ProjectMonitorState": "models",
15
+ }
16
+
17
+
18
+ def __getattr__(name: str):
19
+ if name not in _LAZY:
20
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
21
+ value = globals()[name] = getattr(import_module(f"{__name__}.{_LAZY[name]}"), name)
22
+ return value
23
+
24
+
25
+ __all__ = sorted(_LAZY)