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.
- {ms_components-0.1.0 → ms_components-0.2.0}/.github/workflows/docs.yml +4 -15
- {ms_components-0.1.0 → ms_components-0.2.0}/PKG-INFO +4 -4
- {ms_components-0.1.0 → ms_components-0.2.0}/README.md +1 -2
- {ms_components-0.1.0 → ms_components-0.2.0}/docs/index.md +2 -4
- {ms_components-0.1.0 → ms_components-0.2.0}/pyproject.toml +2 -1
- ms_components-0.2.0/src/ms_components/ms_executors.py +362 -0
- ms_components-0.2.0/src/ms_components/ms_monitor/__init__.py +25 -0
- ms_components-0.2.0/src/ms_components/ms_monitor/bridge.py +222 -0
- ms_components-0.2.0/src/ms_components/ms_monitor/detail_panel.py +121 -0
- ms_components-0.2.0/src/ms_components/ms_monitor/formatting.py +29 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_monitor/job_cards.py +133 -157
- ms_components-0.2.0/src/ms_components/ms_monitor/models.py +76 -0
- ms_components-0.2.0/src/ms_components/ms_monitor/polling.py +149 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_monitor/widget.py +61 -118
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_projects/browser.py +8 -62
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_settings/panel.py +2 -31
- ms_components-0.2.0/src/ms_components/palettes.py +29 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/step_dialog.py +40 -4
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/theme.py +1 -24
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components.egg-info/PKG-INFO +4 -4
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components.egg-info/SOURCES.txt +6 -9
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components.egg-info/requires.txt +2 -1
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components.egg-info/scm_file_list.json +5 -8
- ms_components-0.2.0/src/ms_components.egg-info/scm_version.json +8 -0
- ms_components-0.2.0/tests/test_executors_ui.py +120 -0
- ms_components-0.2.0/tests/test_molsuite_monitor_bridge.py +160 -0
- ms_components-0.2.0/tests/test_molsuite_monitor_widgets.py +75 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/tests/test_monitor_config.py +9 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/tests/test_project_browser.py +1 -3
- ms_components-0.2.0/tests/test_step_dialog.py +41 -0
- ms_components-0.1.0/src/ms_components/ms_monitor/__init__.py +0 -43
- ms_components-0.1.0/src/ms_components/ms_monitor/bridge.py +0 -400
- ms_components-0.1.0/src/ms_components/ms_monitor/chunk_panel.py +0 -85
- ms_components-0.1.0/src/ms_components/ms_monitor/events_panel.py +0 -178
- ms_components-0.1.0/src/ms_components/ms_monitor/executor_panel.py +0 -149
- ms_components-0.1.0/src/ms_components/ms_monitor/executors_tab.py +0 -355
- ms_components-0.1.0/src/ms_components/ms_monitor/formatting.py +0 -51
- ms_components-0.1.0/src/ms_components/ms_monitor/job_filter_proxy.py +0 -48
- ms_components-0.1.0/src/ms_components/ms_monitor/job_tree_model.py +0 -106
- ms_components-0.1.0/src/ms_components/ms_monitor/models.py +0 -165
- ms_components-0.1.0/src/ms_components/ms_monitor/polling.py +0 -498
- ms_components-0.1.0/src/ms_components/ms_settings/workers_editor.py +0 -335
- ms_components-0.1.0/src/ms_components.egg-info/scm_version.json +0 -8
- ms_components-0.1.0/tests/test_molsuite_monitor_bridge.py +0 -599
- ms_components-0.1.0/tests/test_molsuite_monitor_widgets.py +0 -119
- ms_components-0.1.0/tests/test_workers_editor.py +0 -106
- {ms_components-0.1.0 → ms_components-0.2.0}/.github/workflows/publish.yml +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/.gitignore +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/LICENSE +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/docs/api_reference.md +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/docs/examples.md +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/docs/usage.md +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/setup.cfg +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/__init__.py +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/base.qss +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/listview.py +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_dockwidget/__init__.py +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_dockwidget/resources/__init__.py +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_dockwidget/resources/icons/__init__.py +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_dockwidget/resources/icons/folder.svg +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_dockwidget/widget.py +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_monitor/cards.py +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_monitor/config.py +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_monitor/palette.py +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_projects/__init__.py +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_projects/app.py +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_projects/scratch.py +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_projects/widget.py +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_pymol/__init__.py +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_pymol/icons/actions.svg +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_pymol/icons/box.svg +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_pymol/icons/center.svg +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_pymol/icons/display.svg +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_pymol/icons/hide.svg +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_pymol/icons/menu.svg +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_pymol/icons/orient.svg +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_pymol/icons/presets.svg +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_pymol/icons/rock.svg +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_pymol/icons/selection.svg +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_pymol/icons/settings.svg +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_pymol/icons/zoom.svg +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_pymol/plugins.py +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_pymol/widget.py +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_settings/__init__.py +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_stepper.py +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/__init__.py +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/action_header.py +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/cell_delegate.py +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/column_panel.py +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/example_usage.py +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/filter_panel.py +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/icons/columns.svg +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/icons/export.svg +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/icons/filter.svg +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/icons/menu.svg +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/icons/reload.svg +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/icons/search.svg +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/icons/settings.svg +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/icons/sort-asc.svg +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/icons/sort-desc.svg +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/icons/sort.svg +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/metric_filter_bar.py +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/pagination_control.py +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/query_builder.py +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/session_provider.py +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/smart_table_view.py +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/sort_panel.py +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/table_config.py +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/table_model.py +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/test_external_filters.py +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/ms_table/test_toolbar.py +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/pagination.py +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/tool_panel.py +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components/wheel.py +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components.egg-info/dependency_links.txt +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/src/ms_components.egg-info/top_level.txt +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/tests/test_app_settings.py +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/tests/test_dock_window.py +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/tests/test_ms_projects_widget.py +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/tests/test_pymol_toolbar.py +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/tests/test_query_builder_keyset.py +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/tests/test_shift_hscroll.py +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/tests/test_table_toolbar.py +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/tests/test_table_view_state.py +0 -0
- {ms_components-0.1.0 → ms_components-0.2.0}/tests/test_theme_contrast.py +0 -0
- {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.
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
16
|
-
pip install "ms_components @ git+https://github.com/MolSuite/ms_components"
|
|
14
|
+
pip install ms_components
|
|
17
15
|
```
|
|
18
16
|
|
|
19
17
|
## Components
|
|
@@ -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)
|