processview 1.5.0__tar.gz → 1.5.2__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.
- {processview-1.5.0 → processview-1.5.2}/PKG-INFO +3 -2
- processview-1.5.2/example/show_processmanagerwindow.py +118 -0
- {processview-1.5.0 → processview-1.5.2}/processview/core/sorting.py +1 -1
- processview-1.5.2/processview/gui/__init__.py +4 -0
- processview-1.5.2/processview/gui/processmanager/ObservationTable.py +137 -0
- processview-1.5.2/processview/gui/processmanager/ProcessManager.py +83 -0
- processview-1.5.2/processview/gui/processmanager/ProcessManagerWidget.py +83 -0
- processview-1.5.2/processview/gui/processmanager/ProcessManagerWindow.py +17 -0
- processview-1.5.2/processview/gui/processmanager/_DatasetProcessModel.py +215 -0
- processview-1.5.2/processview/gui/processmanager/_FilterWidget.py +102 -0
- processview-1.5.2/processview/gui/processmanager/_OptionsWidget.py +36 -0
- processview-1.5.2/processview/gui/processmanager/_OrderingWidget.py +38 -0
- processview-1.5.2/processview/gui/processmanager/__init__.py +2 -0
- {processview-1.5.0 → processview-1.5.2}/processview/gui/processmanager.py +20 -19
- processview-1.5.2/processview/gui/utils/qitem_model_resetter.py +17 -0
- processview-1.5.2/processview/resources/__init__.py +0 -0
- processview-1.5.2/processview/resources/gui/__init__.py +0 -0
- {processview-1.5.0 → processview-1.5.2}/processview/version.py +1 -1
- {processview-1.5.0 → processview-1.5.2}/processview.egg-info/PKG-INFO +3 -2
- {processview-1.5.0 → processview-1.5.2}/processview.egg-info/SOURCES.txt +13 -1
- processview-1.5.0/processview/gui/icons.py +0 -403
- processview-1.5.0/processview/resources/__init__.py +0 -277
- {processview-1.5.0 → processview-1.5.2}/LICENSE +0 -0
- {processview-1.5.0 → processview-1.5.2}/README.md +0 -0
- {processview-1.5.0 → processview-1.5.2}/processview/__init__.py +0 -0
- {processview-1.5.0 → processview-1.5.2}/processview/core/__init__.py +0 -0
- {processview-1.5.0 → processview-1.5.2}/processview/core/dataset.py +0 -0
- {processview-1.5.0 → processview-1.5.2}/processview/core/helpers.py +0 -0
- {processview-1.5.0 → processview-1.5.2}/processview/core/manager/__init__.py +0 -0
- {processview-1.5.0 → processview-1.5.2}/processview/core/manager/manager.py +0 -0
- {processview-1.5.0 → processview-1.5.2}/processview/core/manager/test/__init__.py +0 -0
- {processview-1.5.0 → processview-1.5.2}/processview/core/manager/test/test_manager.py +0 -0
- {processview-1.5.0 → processview-1.5.2}/processview/core/setup.py +0 -0
- {processview-1.5.0 → processview-1.5.2}/processview/core/superviseprocess.py +0 -0
- {processview-1.5.0 → processview-1.5.2}/processview/core/test/__init__.py +0 -0
- {processview-1.5.0 → processview-1.5.2}/processview/gui/DropDownWidget.py +0 -0
- {processview-1.5.0 → processview-1.5.2}/processview/gui/messagebox.py +0 -0
- {processview-1.5.0 → processview-1.5.2}/processview/gui/test/__init__.py +0 -0
- {processview-1.5.0 → processview-1.5.2}/processview/gui/test/test_process_manager.py +0 -0
- {processview-1.5.0/processview/gui → processview-1.5.2/processview/gui/utils}/__init__.py +0 -0
- {processview-1.5.0 → processview-1.5.2}/processview/resources/gui/icons/advancement.png +0 -0
- {processview-1.5.0 → processview-1.5.2}/processview/resources/gui/icons/advancement.svg +0 -0
- {processview-1.5.0 → processview-1.5.2}/processview/resources/gui/icons/magnifying_glass.png +0 -0
- {processview-1.5.0 → processview-1.5.2}/processview/resources/gui/icons/magnifying_glass.svg +0 -0
- {processview-1.5.0 → processview-1.5.2}/processview/test/__init__.py +0 -0
- {processview-1.5.0 → processview-1.5.2}/processview/utils/__init__.py +0 -0
- {processview-1.5.0 → processview-1.5.2}/processview/utils/singleton.py +0 -0
- {processview-1.5.0 → processview-1.5.2}/processview.egg-info/dependency_links.txt +0 -0
- {processview-1.5.0 → processview-1.5.2}/processview.egg-info/requires.txt +0 -0
- {processview-1.5.0 → processview-1.5.2}/processview.egg-info/top_level.txt +0 -0
- {processview-1.5.0 → processview-1.5.2}/processview.egg-info/zip-safe +0 -0
- {processview-1.5.0 → processview-1.5.2}/setup.cfg +0 -0
- {processview-1.5.0 → processview-1.5.2}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: processview
|
|
3
|
-
Version: 1.5.
|
|
3
|
+
Version: 1.5.2
|
|
4
4
|
Summary: Simple helper to provide user feedback about dataset processing
|
|
5
5
|
Author: data analysis unit
|
|
6
6
|
Author-email: henri.payno@esrf.fr
|
|
@@ -35,3 +35,4 @@ Provides-Extra: full
|
|
|
35
35
|
Requires-Dist: Sphinx; extra == "full"
|
|
36
36
|
Requires-Dist: pytest; extra == "full"
|
|
37
37
|
Requires-Dist: PyQt5; extra == "full"
|
|
38
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
import numpy
|
|
5
|
+
import threading
|
|
6
|
+
from silx.gui import qt
|
|
7
|
+
from processview.gui.processmanager import ProcessManagerWindow
|
|
8
|
+
from processview.core.superviseprocess import SuperviseProcess
|
|
9
|
+
from processview.core.dataset import Dataset
|
|
10
|
+
from processview.core.dataset import DatasetIdentifier
|
|
11
|
+
from processview.core.manager import DatasetState
|
|
12
|
+
from processview.gui.processmanager import ProcessManager
|
|
13
|
+
import datetime
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class _DummyDataset(Dataset):
|
|
17
|
+
def __init__(self, name):
|
|
18
|
+
super().__init__()
|
|
19
|
+
self.__name = name
|
|
20
|
+
|
|
21
|
+
def __str__(self) -> str:
|
|
22
|
+
return self.__name
|
|
23
|
+
|
|
24
|
+
def get_identifier(self) -> DatasetIdentifier:
|
|
25
|
+
return _DummyIdentifier(
|
|
26
|
+
self,
|
|
27
|
+
metadata={
|
|
28
|
+
"name": self.__name,
|
|
29
|
+
"creation_time": datetime.datetime.now(),
|
|
30
|
+
"modification_time": datetime.datetime.now(),
|
|
31
|
+
},
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class _DummyIdentifier(DatasetIdentifier):
|
|
36
|
+
def to_str(self):
|
|
37
|
+
return str(self)
|
|
38
|
+
|
|
39
|
+
def __str__(self):
|
|
40
|
+
return self.name()
|
|
41
|
+
|
|
42
|
+
def __eq__(self, other):
|
|
43
|
+
return self.name() == other.name()
|
|
44
|
+
|
|
45
|
+
def __hash__(self):
|
|
46
|
+
return hash(self.name())
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
app = qt.QApplication([])
|
|
50
|
+
|
|
51
|
+
window = ProcessManagerWindow(parent=None)
|
|
52
|
+
window.show()
|
|
53
|
+
|
|
54
|
+
p1 = SuperviseProcess(name="process1")
|
|
55
|
+
p2 = SuperviseProcess(name="process2")
|
|
56
|
+
p3 = SuperviseProcess(name="process3")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
manager = ProcessManager()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class RecursiveThread(threading.Thread):
|
|
63
|
+
def __init__(self, execute_each: int):
|
|
64
|
+
self.running = True
|
|
65
|
+
self.execute_each = execute_each
|
|
66
|
+
super().__init__()
|
|
67
|
+
|
|
68
|
+
def run(self):
|
|
69
|
+
"""Method implementing thread loop that updates the plot"""
|
|
70
|
+
while self.running:
|
|
71
|
+
time.sleep(self.execute_each)
|
|
72
|
+
self.process()
|
|
73
|
+
|
|
74
|
+
def process(self):
|
|
75
|
+
raise NotImplementedError()
|
|
76
|
+
|
|
77
|
+
def stop(self):
|
|
78
|
+
"""Stop the update thread"""
|
|
79
|
+
self.running = False
|
|
80
|
+
self.join(2)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class CreateNewDataset(RecursiveThread):
|
|
84
|
+
"""Thread creating a new dataset each n seconds"""
|
|
85
|
+
|
|
86
|
+
def process(self):
|
|
87
|
+
dataset = _DummyDataset(f"scan {numpy.random.randint(0, 999999)}")
|
|
88
|
+
manager.notify_dataset_state(
|
|
89
|
+
dataset=dataset, state=DatasetState.PENDING, process=p1
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class UpdateDataset(RecursiveThread):
|
|
94
|
+
"""Thread that will update randomly one of the existing dataset"""
|
|
95
|
+
|
|
96
|
+
def process(self):
|
|
97
|
+
datasets = manager.get_datasets()
|
|
98
|
+
if len(datasets) == 0:
|
|
99
|
+
return
|
|
100
|
+
dataset_to_update = numpy.random.choice(datasets)
|
|
101
|
+
state = numpy.random.choice(DatasetState)
|
|
102
|
+
process = numpy.random.choice(manager.get_processes())
|
|
103
|
+
manager.notify_dataset_state(
|
|
104
|
+
dataset=dataset_to_update,
|
|
105
|
+
state=state,
|
|
106
|
+
process=process,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
create_new_dataset_thread = CreateNewDataset(execute_each=3)
|
|
111
|
+
create_new_dataset_thread.start()
|
|
112
|
+
update_dataset_thread = UpdateDataset(execute_each=1)
|
|
113
|
+
update_dataset_thread.start()
|
|
114
|
+
|
|
115
|
+
app.exec_()
|
|
116
|
+
|
|
117
|
+
create_new_dataset_thread.stop()
|
|
118
|
+
update_dataset_thread.stop()
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from silx.gui import qt
|
|
4
|
+
from processview.gui.messagebox import MessageBox
|
|
5
|
+
from processview.core.manager import ProcessManager as _ProcessManager
|
|
6
|
+
from processview.core.superviseprocess import SuperviseProcess
|
|
7
|
+
from processview.core.dataset import DatasetIdentifier
|
|
8
|
+
|
|
9
|
+
from .ProcessManager import ProcessManager
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ObservationTable(qt.QTableView):
|
|
13
|
+
"""
|
|
14
|
+
Redefinition of QTableView for datasets and processes
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, parent):
|
|
18
|
+
qt.QTableView.__init__(self, parent)
|
|
19
|
+
self.verticalHeader().setSectionsClickable(True)
|
|
20
|
+
|
|
21
|
+
# QMenu for the dataset name
|
|
22
|
+
self.dataset_menu = qt.QMenu()
|
|
23
|
+
self._copyAction = qt.QAction("copy")
|
|
24
|
+
self.dataset_menu.addAction(self._copyAction)
|
|
25
|
+
self._removeAction = qt.QAction("remove")
|
|
26
|
+
self.dataset_menu.addAction(self._removeAction)
|
|
27
|
+
|
|
28
|
+
# QMenu for cell from on dataset and one process
|
|
29
|
+
self.menu_dataset_vs_process = qt.QMenu()
|
|
30
|
+
self._reprocessAction = qt.QAction("reprocess")
|
|
31
|
+
self.menu_dataset_vs_process.addAction(self._reprocessAction)
|
|
32
|
+
self._cancelAction = qt.QAction("cancel")
|
|
33
|
+
self.menu_dataset_vs_process.addAction(self._cancelAction)
|
|
34
|
+
self._infoAction = qt.QAction("info")
|
|
35
|
+
self.menu_dataset_vs_process.addAction(self._infoAction)
|
|
36
|
+
self.menu_dataset_vs_process.addAction(self._removeAction)
|
|
37
|
+
|
|
38
|
+
self._target = (None, None)
|
|
39
|
+
# register target of the last menu (process, DatasetIdentifier)
|
|
40
|
+
|
|
41
|
+
# connect signal / slot
|
|
42
|
+
self._copyAction.triggered.connect(self._requestDatasetIdCopy)
|
|
43
|
+
self._removeAction.triggered.connect(self._requestRemoveDataset)
|
|
44
|
+
self._reprocessAction.triggered.connect(self._requestReprocessing)
|
|
45
|
+
self._infoAction.triggered.connect(self._requestInfo)
|
|
46
|
+
self._cancelAction.triggered.connect(self._requestCancelProcessing)
|
|
47
|
+
|
|
48
|
+
def _processAt(self, x_pos):
|
|
49
|
+
column = self.columnAt(x_pos)
|
|
50
|
+
if column >= 1:
|
|
51
|
+
processes = self.model()._processes
|
|
52
|
+
process_idx = column - 1
|
|
53
|
+
if process_idx < len(processes):
|
|
54
|
+
return processes[list(processes.keys())[process_idx]]
|
|
55
|
+
|
|
56
|
+
def _datasetAt(self, y_pos):
|
|
57
|
+
row = self.rowAt(y_pos)
|
|
58
|
+
if row >= 0:
|
|
59
|
+
datasets = self.model()._sorted_datasets
|
|
60
|
+
if row < len(datasets):
|
|
61
|
+
return datasets[list(datasets.keys())[row]]
|
|
62
|
+
|
|
63
|
+
def contextMenuEvent(self, event):
|
|
64
|
+
row = self.columnAt(event.pos().x())
|
|
65
|
+
dataset = self._datasetAt(event.pos().y())
|
|
66
|
+
if row == 0:
|
|
67
|
+
# handle column column
|
|
68
|
+
self._target = (None, dataset)
|
|
69
|
+
self.dataset_menu.exec_(event.globalPos())
|
|
70
|
+
else:
|
|
71
|
+
# handle processes column
|
|
72
|
+
process = self._processAt(event.pos().x())
|
|
73
|
+
if (
|
|
74
|
+
process is not None
|
|
75
|
+
and dataset is not None
|
|
76
|
+
and _ProcessManager().met(process=process, dataset=dataset)
|
|
77
|
+
):
|
|
78
|
+
self._target = (process, dataset)
|
|
79
|
+
self.menu_dataset_vs_process.exec_(event.globalPos())
|
|
80
|
+
else:
|
|
81
|
+
self._target = (None, None)
|
|
82
|
+
super().contextMenuEvent(event)
|
|
83
|
+
|
|
84
|
+
def _requestReprocessing(self, *args, **kwargs):
|
|
85
|
+
process, dataset = self._target
|
|
86
|
+
|
|
87
|
+
if process is not None and dataset is not None:
|
|
88
|
+
assert isinstance(process, SuperviseProcess)
|
|
89
|
+
assert isinstance(dataset, DatasetIdentifier)
|
|
90
|
+
process.reprocess(dataset.recreate_dataset())
|
|
91
|
+
|
|
92
|
+
def _requestCancelProcessing(self, *args, **kwargs):
|
|
93
|
+
process, dataset = self._target
|
|
94
|
+
if process is not None and dataset is not None:
|
|
95
|
+
assert isinstance(process, SuperviseProcess)
|
|
96
|
+
assert isinstance(dataset, DatasetIdentifier)
|
|
97
|
+
process.cancel(dataset.recreate_dataset())
|
|
98
|
+
|
|
99
|
+
def _requestDatasetIdCopy(self, *args, **kwargs):
|
|
100
|
+
_, dataset = self._target
|
|
101
|
+
if dataset is not None:
|
|
102
|
+
clipboard = qt.QGuiApplication.clipboard()
|
|
103
|
+
clipboard.setText(dataset.to_str())
|
|
104
|
+
|
|
105
|
+
def _requestRemoveDataset(self, *args, **kwargs):
|
|
106
|
+
def get_dataset_at(row: int):
|
|
107
|
+
datasets = self.model()._sorted_datasets
|
|
108
|
+
return datasets.get(
|
|
109
|
+
list(datasets.keys())[row],
|
|
110
|
+
None,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
datasets = [get_dataset_at(index.row()) for index in self.selectedIndexes()]
|
|
114
|
+
[ProcessManager().remove_dataset(dataset) for dataset in datasets]
|
|
115
|
+
self.model().remove_datasets(datasets)
|
|
116
|
+
|
|
117
|
+
def _requestInfo(self, *args, **kwargs):
|
|
118
|
+
process, dataset = self._target
|
|
119
|
+
if process is not None and dataset is not None:
|
|
120
|
+
infos = ProcessManager().get_dataset_details(
|
|
121
|
+
dataset=dataset, process=process
|
|
122
|
+
)
|
|
123
|
+
if infos in (None, ""):
|
|
124
|
+
infos = "No extra information provided"
|
|
125
|
+
|
|
126
|
+
msg = MessageBox(self)
|
|
127
|
+
msg.setInfos(infos=infos)
|
|
128
|
+
extra_info = "{} processing {}".format(process.name, dataset)
|
|
129
|
+
msg.setWindowTitle(extra_info)
|
|
130
|
+
msg.setWindowModality(qt.Qt.NonModal)
|
|
131
|
+
msg.show()
|
|
132
|
+
|
|
133
|
+
def sizeHintForColumn(self, column):
|
|
134
|
+
if column == 0:
|
|
135
|
+
return 350
|
|
136
|
+
else:
|
|
137
|
+
return super().sizeHintForColumn(column)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from silx.gui import qt
|
|
4
|
+
from processview.core.manager import ProcessManager as _ProcessManager
|
|
5
|
+
from processview.core.superviseprocess import SuperviseProcess
|
|
6
|
+
from processview.utils import docstring
|
|
7
|
+
from processview.core.manager import DatasetState
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ProcessManager(qt.QObject):
|
|
11
|
+
sigUpdated = qt.Signal()
|
|
12
|
+
"""Signal emitted when the state of some process / dataset is updated
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
sigNewProcessRegistered = qt.Signal()
|
|
16
|
+
"""Signal emitted when a new process is registered"""
|
|
17
|
+
|
|
18
|
+
sigProcessUnregistered = qt.Signal()
|
|
19
|
+
"""Signal emitted when a process is unregistered"""
|
|
20
|
+
|
|
21
|
+
sigProcessRenamed = qt.Signal(SuperviseProcess)
|
|
22
|
+
"""Emit when the process is renamed"""
|
|
23
|
+
|
|
24
|
+
def __init__(self):
|
|
25
|
+
qt.QObject.__init__(self)
|
|
26
|
+
self.manager = _ProcessManager()
|
|
27
|
+
|
|
28
|
+
# monkey patch manager updated function
|
|
29
|
+
# TODO: add / remove callback would be simpler
|
|
30
|
+
self.manager.add_update_callback(self.updated)
|
|
31
|
+
self.manager.add_new_process_callback(self.processAdded)
|
|
32
|
+
self.manager.add_process_removed_callback(self.processRemoved)
|
|
33
|
+
self.manager.add_process_name_changed_callback(self.processRenamed)
|
|
34
|
+
|
|
35
|
+
def updated(self):
|
|
36
|
+
self.sigUpdated.emit()
|
|
37
|
+
|
|
38
|
+
def processAdded(self):
|
|
39
|
+
self.sigNewProcessRegistered.emit()
|
|
40
|
+
|
|
41
|
+
def processRemoved(self):
|
|
42
|
+
self.sigProcessUnregistered.emit()
|
|
43
|
+
|
|
44
|
+
def processRenamed(self, process):
|
|
45
|
+
self.sigProcessRenamed.emit(process)
|
|
46
|
+
|
|
47
|
+
def destroyed(self, object_):
|
|
48
|
+
self.manager.remove_update_callback(self.updated)
|
|
49
|
+
qt.QObject.destroyed(object_)
|
|
50
|
+
|
|
51
|
+
# expose some of the original ProcessManager API
|
|
52
|
+
@docstring(_ProcessManager)
|
|
53
|
+
def notify_dataset_state(self, dataset, process, state, details=None) -> None:
|
|
54
|
+
self.manager.notify_dataset_state(
|
|
55
|
+
dataset=dataset, process=process, state=state, details=details
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
@docstring(_ProcessManager)
|
|
59
|
+
def get_dataset_state(self, dataset, process) -> None | DatasetState:
|
|
60
|
+
return self.manager.get_dataset_state(dataset_id=dataset, process=process)
|
|
61
|
+
|
|
62
|
+
@docstring(_ProcessManager)
|
|
63
|
+
def get_dataset_details(self, dataset, process) -> None | DatasetState:
|
|
64
|
+
return self.manager.get_dataset_details(dataset_id=dataset, process=process)
|
|
65
|
+
|
|
66
|
+
@docstring(_ProcessManager)
|
|
67
|
+
def get_dataset_stream(self, dataset, time_stamp=False) -> tuple:
|
|
68
|
+
return self.manager.get_dataset_stream(dataset=dataset, time_stamp=time_stamp)
|
|
69
|
+
|
|
70
|
+
@docstring(_ProcessManager)
|
|
71
|
+
def get_process_history(self, process, time_stamp=False) -> tuple:
|
|
72
|
+
return self.manager.get_process_history(process=process, time_stamp=time_stamp)
|
|
73
|
+
|
|
74
|
+
@docstring(_ProcessManager)
|
|
75
|
+
def get_processes(self):
|
|
76
|
+
return self.manager.get_processes()
|
|
77
|
+
|
|
78
|
+
@docstring(_ProcessManager)
|
|
79
|
+
def get_datasets(self):
|
|
80
|
+
return self.manager.get_datasets()
|
|
81
|
+
|
|
82
|
+
def remove_dataset(self, *args, **kwargs):
|
|
83
|
+
return self.manager.remove_dataset(*args, **kwargs)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from silx.gui import qt
|
|
4
|
+
from processview.core.superviseprocess import SuperviseProcess
|
|
5
|
+
|
|
6
|
+
from .ProcessManager import ProcessManager
|
|
7
|
+
from ._OptionsWidget import OptionsWidget
|
|
8
|
+
from ..DropDownWidget import DropDownWidget
|
|
9
|
+
from .ObservationTable import ObservationTable
|
|
10
|
+
from ._DatasetProcessModel import DatasetProcessModel
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ProcessManagerWidget(qt.QWidget):
|
|
14
|
+
"""
|
|
15
|
+
Main widget to display dataset vs process metadata
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, parent):
|
|
19
|
+
qt.QWidget.__init__(self, parent)
|
|
20
|
+
self.setLayout(qt.QVBoxLayout())
|
|
21
|
+
|
|
22
|
+
self._manager = ProcessManager()
|
|
23
|
+
|
|
24
|
+
self._optionsWidget = OptionsWidget(parent=self)
|
|
25
|
+
self._dropDownOptionsWidget = DropDownWidget(parent=self)
|
|
26
|
+
self._dropDownOptionsWidget.setWidget(self._optionsWidget)
|
|
27
|
+
|
|
28
|
+
self._dropDownOptionsWidget.setSizePolicy(
|
|
29
|
+
qt.QSizePolicy.Minimum, qt.QSizePolicy.Minimum
|
|
30
|
+
)
|
|
31
|
+
self.setContentsMargins(0, 0, 0, 0)
|
|
32
|
+
self.layout().setContentsMargins(0, 0, 0, 0)
|
|
33
|
+
self.layout().setSpacing(2)
|
|
34
|
+
self.layout().addWidget(self._dropDownOptionsWidget)
|
|
35
|
+
|
|
36
|
+
self.observationTable = ObservationTable(self)
|
|
37
|
+
|
|
38
|
+
self.layout().addWidget(self.observationTable)
|
|
39
|
+
self.observationTable.setModel(
|
|
40
|
+
DatasetProcessModel(parent=self.observationTable, header=tuple())
|
|
41
|
+
)
|
|
42
|
+
self.observationTable.resizeColumnsToContents()
|
|
43
|
+
self.observationTable.setSortingEnabled(True)
|
|
44
|
+
self.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding)
|
|
45
|
+
|
|
46
|
+
# connect signal / slot
|
|
47
|
+
self._manager.sigUpdated.connect(self._updateDatasetStates)
|
|
48
|
+
self._manager.sigNewProcessRegistered.connect(self._updateProcesses)
|
|
49
|
+
self._optionsWidget.filterWidget.sigDatasetPatternEditingFinished.connect(
|
|
50
|
+
self._filterUpdated
|
|
51
|
+
)
|
|
52
|
+
self._optionsWidget.filterWidget.sigProcessPatternEditingFinished.connect(
|
|
53
|
+
self._filterUpdated
|
|
54
|
+
)
|
|
55
|
+
self._manager.sigProcessUnregistered.connect(self._updateProcesses)
|
|
56
|
+
self._optionsWidget._orderingWidget.sigSortTypeChanged.connect(
|
|
57
|
+
self.observationTable.model()._setSorting
|
|
58
|
+
)
|
|
59
|
+
self._manager.sigProcessRenamed.connect(self._renameProcess)
|
|
60
|
+
# update to fit existing processes / datasets
|
|
61
|
+
self._updateProcesses()
|
|
62
|
+
self._updateDatasetStates()
|
|
63
|
+
|
|
64
|
+
def _updateDatasetStates(self):
|
|
65
|
+
self.observationTable.model().setDatasets(self._manager.get_datasets())
|
|
66
|
+
|
|
67
|
+
def _updateProcesses(self):
|
|
68
|
+
self.observationTable.model().setProcesses(self._manager.get_processes())
|
|
69
|
+
|
|
70
|
+
def _renameProcess(self, process: SuperviseProcess):
|
|
71
|
+
self.observationTable.model().headerDataChanged.emit(
|
|
72
|
+
qt.Qt.Horizontal, 0, len(self._manager.get_processes())
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def _filterUpdated(self):
|
|
76
|
+
self.observationTable.model().process_patterns = (
|
|
77
|
+
self._dropDownOptionsWidget.getProcessPatterns()
|
|
78
|
+
)
|
|
79
|
+
self._updateProcesses()
|
|
80
|
+
self.observationTable.model().dataset_patterns = (
|
|
81
|
+
self._dropDownOptionsWidget.getDatasetPatterns()
|
|
82
|
+
)
|
|
83
|
+
self._updateDatasetStates()
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from silx.gui import qt
|
|
4
|
+
from .ProcessManagerWidget import ProcessManagerWidget
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ProcessManagerWindow(qt.QMainWindow):
|
|
8
|
+
"""
|
|
9
|
+
Main window of the process manager
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, parent):
|
|
13
|
+
qt.QMainWindow.__init__(self, parent)
|
|
14
|
+
self.setWindowFlags(qt.Qt.Widget)
|
|
15
|
+
|
|
16
|
+
self._centralWidget = ProcessManagerWidget(parent=self)
|
|
17
|
+
self.setCentralWidget(self._centralWidget)
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import fnmatch
|
|
4
|
+
import datetime
|
|
5
|
+
|
|
6
|
+
from silx.gui import qt
|
|
7
|
+
from processview.core.sorting import SortType
|
|
8
|
+
from processview.core.manager import DatasetState
|
|
9
|
+
from processview.core.dataset import DatasetIdentifier
|
|
10
|
+
from processview.gui.utils.qitem_model_resetter import qitem_model_resetter
|
|
11
|
+
|
|
12
|
+
from .ProcessManager import ProcessManager
|
|
13
|
+
|
|
14
|
+
from collections import OrderedDict
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_DATASET_STATE_BACKGROUND = {
|
|
18
|
+
DatasetState.ON_GOING: qt.QColor("#839684"), # light blue
|
|
19
|
+
DatasetState.SUCCEED: qt.QColor("#068c0c"), # green
|
|
20
|
+
DatasetState.FAILED: qt.QColor("#f52718"), # red
|
|
21
|
+
DatasetState.PENDING: qt.QColor("#609ab3"), # blue gray
|
|
22
|
+
DatasetState.SKIPPED: qt.QColor("#f08e0e"), # light orange
|
|
23
|
+
DatasetState.WAIT_USER_VALIDATION: qt.QColor("#cb34c1"), # pink
|
|
24
|
+
DatasetState.CANCELLED: qt.QColor("#a4a8a2"), # light black
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DatasetProcessModel(qt.QAbstractTableModel):
|
|
29
|
+
def __init__(self, parent, header, *args):
|
|
30
|
+
qt.QAbstractTableModel.__init__(self, parent, *args)
|
|
31
|
+
self.header = header
|
|
32
|
+
self._processes = OrderedDict()
|
|
33
|
+
# processes with order as key and Process as value
|
|
34
|
+
self._sorted_datasets = OrderedDict()
|
|
35
|
+
self._unsorted_datasets = OrderedDict()
|
|
36
|
+
self._processPatterns = tuple()
|
|
37
|
+
# is there some process name pattern to follow ?
|
|
38
|
+
self._datasetPatterns = tuple()
|
|
39
|
+
# is there some dataset id pattern to follow ?
|
|
40
|
+
self._sort_type = SortType.FIRST_APPEARANCE
|
|
41
|
+
|
|
42
|
+
def _match_dataset_patterns(self, dataset):
|
|
43
|
+
if len(self._datasetPatterns) == 0:
|
|
44
|
+
return True
|
|
45
|
+
for pattern in self._datasetPatterns:
|
|
46
|
+
if fnmatch.fnmatch(str(dataset), pattern):
|
|
47
|
+
return True
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
def _match_process_patterns(self, process):
|
|
51
|
+
if len(self.process_patterns) == 0:
|
|
52
|
+
return True
|
|
53
|
+
for pattern in self._processPatterns:
|
|
54
|
+
if process.name is None or pattern is None:
|
|
55
|
+
continue
|
|
56
|
+
elif fnmatch.fnmatch(process.name, pattern):
|
|
57
|
+
return True
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def process_patterns(self):
|
|
62
|
+
return self._processPatterns
|
|
63
|
+
|
|
64
|
+
@process_patterns.setter
|
|
65
|
+
def process_patterns(self, patterns):
|
|
66
|
+
self._processPatterns = patterns
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def dataset_patterns(self):
|
|
70
|
+
return self._datasetPatterns
|
|
71
|
+
|
|
72
|
+
@dataset_patterns.setter
|
|
73
|
+
def dataset_patterns(self, patterns):
|
|
74
|
+
self._datasetPatterns = patterns
|
|
75
|
+
|
|
76
|
+
def remove_datasets(self, datasets):
|
|
77
|
+
remaining_datasets = list(self._unsorted_datasets)
|
|
78
|
+
for dataset in datasets:
|
|
79
|
+
if not isinstance(dataset, DatasetIdentifier):
|
|
80
|
+
raise ValueError(
|
|
81
|
+
f"dataset is expected to be a dataset identifier. Get {type(dataset)} instead"
|
|
82
|
+
)
|
|
83
|
+
try:
|
|
84
|
+
remaining_datasets.remove(dataset)
|
|
85
|
+
except Exception:
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
with qitem_model_resetter(self):
|
|
89
|
+
self.setDatasets(remaining_datasets)
|
|
90
|
+
|
|
91
|
+
def clear(self):
|
|
92
|
+
with qitem_model_resetter(self):
|
|
93
|
+
self._processes = OrderedDict()
|
|
94
|
+
|
|
95
|
+
def rowCount(self, parent=None):
|
|
96
|
+
return len(self._unsorted_datasets)
|
|
97
|
+
|
|
98
|
+
def columnCount(self, parent=None):
|
|
99
|
+
return len(self._processes) + 1
|
|
100
|
+
|
|
101
|
+
def setProcesses(self, processes):
|
|
102
|
+
assert isinstance(processes, (tuple, list))
|
|
103
|
+
with qitem_model_resetter(self):
|
|
104
|
+
self._processes = {}
|
|
105
|
+
processes = list(filter(self._match_process_patterns, processes))
|
|
106
|
+
for i_process, process in enumerate(processes):
|
|
107
|
+
self._processes[i_process] = process
|
|
108
|
+
|
|
109
|
+
@staticmethod
|
|
110
|
+
def sort_datasets(datasets: list, sort_type):
|
|
111
|
+
if sort_type is SortType.FIRST_APPEARANCE:
|
|
112
|
+
pass
|
|
113
|
+
elif sort_type is SortType.LAST_APPEARANCE:
|
|
114
|
+
datasets.reverse()
|
|
115
|
+
elif sort_type in (SortType.ALPHABETICAL, SortType.REVERSE_ALPHABETICAL):
|
|
116
|
+
datasets = sorted(
|
|
117
|
+
datasets,
|
|
118
|
+
key=lambda x: x.name() or "",
|
|
119
|
+
reverse=sort_type is SortType.REVERSE_ALPHABETICAL,
|
|
120
|
+
)
|
|
121
|
+
elif sort_type in (SortType.CREATION_TIME, SortType.REVERSE_CREATION_TIME):
|
|
122
|
+
datasets = sorted(
|
|
123
|
+
datasets,
|
|
124
|
+
key=lambda x: x.creation_time() or datetime.datetime.fromtimestamp(0),
|
|
125
|
+
reverse=sort_type is SortType.REVERSE_CREATION_TIME,
|
|
126
|
+
)
|
|
127
|
+
elif sort_type in (
|
|
128
|
+
SortType.MODIFICATION_TIME,
|
|
129
|
+
SortType.REVERSE_MODIFICATION_TIME,
|
|
130
|
+
):
|
|
131
|
+
datasets = sorted(
|
|
132
|
+
datasets,
|
|
133
|
+
key=lambda x: x.modification_time()
|
|
134
|
+
or datetime.datetime.fromtimestamp(0),
|
|
135
|
+
reverse=sort_type is SortType.REVERSE_MODIFICATION_TIME,
|
|
136
|
+
)
|
|
137
|
+
else:
|
|
138
|
+
raise ValueError("sort type not handled")
|
|
139
|
+
return datasets
|
|
140
|
+
|
|
141
|
+
def setDatasets(self, datasets):
|
|
142
|
+
assert isinstance(datasets, (tuple, list))
|
|
143
|
+
with qitem_model_resetter(self):
|
|
144
|
+
self._unsorted_datasets = datasets
|
|
145
|
+
# filter datasets
|
|
146
|
+
datasets = list(filter(self._match_dataset_patterns, datasets))
|
|
147
|
+
# sort datasets
|
|
148
|
+
datasets = self.sort_datasets(datasets=datasets, sort_type=self._sort_type)
|
|
149
|
+
|
|
150
|
+
self._sorted_datasets.clear()
|
|
151
|
+
for i_dataset, dataset in enumerate(datasets):
|
|
152
|
+
self._sorted_datasets[i_dataset] = dataset
|
|
153
|
+
|
|
154
|
+
def headerData(self, col, orientation, role):
|
|
155
|
+
if orientation == qt.Qt.Horizontal and role == qt.Qt.DisplayRole:
|
|
156
|
+
if col == 0:
|
|
157
|
+
return
|
|
158
|
+
else:
|
|
159
|
+
process_idx = col - 1
|
|
160
|
+
if process_idx < len(self._processes):
|
|
161
|
+
return self._processes[process_idx].name
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
def data(self, index, role):
|
|
165
|
+
if index.isValid() is False:
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
if role not in (qt.Qt.DisplayRole, qt.Qt.ToolTipRole, qt.Qt.BackgroundRole):
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
if index.column() == 0:
|
|
172
|
+
# if dataset name
|
|
173
|
+
if role == qt.Qt.DisplayRole:
|
|
174
|
+
return self._sorted_datasets[index.row()].short_description() or str(
|
|
175
|
+
self._sorted_datasets[index.row()]
|
|
176
|
+
)
|
|
177
|
+
elif role == qt.Qt.ToolTipRole:
|
|
178
|
+
return self._sorted_datasets[index.row()].long_description()
|
|
179
|
+
elif role == qt.Qt.BackgroundRole:
|
|
180
|
+
return qt.QColor("#dedede")
|
|
181
|
+
else:
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
dataset_short_name = self._sorted_datasets[index.row()]
|
|
185
|
+
process_name = self._processes[index.column() - 1]
|
|
186
|
+
dataset_process_state = ProcessManager().get_dataset_state(
|
|
187
|
+
dataset=dataset_short_name, process=process_name
|
|
188
|
+
)
|
|
189
|
+
dataset_process_details = ProcessManager().get_dataset_details(
|
|
190
|
+
dataset=dataset_short_name, process=process_name
|
|
191
|
+
)
|
|
192
|
+
if role == qt.Qt.BackgroundRole:
|
|
193
|
+
if dataset_process_state is None:
|
|
194
|
+
# if "unmet"
|
|
195
|
+
return qt.QColor("#ffffff")
|
|
196
|
+
else:
|
|
197
|
+
return _DATASET_STATE_BACKGROUND[dataset_process_state]
|
|
198
|
+
elif role == qt.Qt.DisplayRole:
|
|
199
|
+
if dataset_process_state is None:
|
|
200
|
+
return ""
|
|
201
|
+
else:
|
|
202
|
+
return dataset_process_state.value
|
|
203
|
+
if role == qt.Qt.ToolTipRole:
|
|
204
|
+
if dataset_process_details is None:
|
|
205
|
+
return ""
|
|
206
|
+
else:
|
|
207
|
+
return dataset_process_details
|
|
208
|
+
|
|
209
|
+
def _setSorting(self, sort_type: SortType):
|
|
210
|
+
sort_type = SortType(sort_type)
|
|
211
|
+
changed = self._sort_type is not sort_type
|
|
212
|
+
if changed:
|
|
213
|
+
self._sort_type = sort_type
|
|
214
|
+
self.setDatasets(datasets=self._unsorted_datasets)
|
|
215
|
+
self.layoutChanged.emit()
|