processview 1.5.1__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.
Files changed (51) hide show
  1. {processview-1.5.1 → processview-1.5.2}/PKG-INFO +1 -1
  2. processview-1.5.2/example/show_processmanagerwindow.py +118 -0
  3. {processview-1.5.1 → processview-1.5.2}/processview/core/sorting.py +1 -1
  4. processview-1.5.2/processview/gui/processmanager/ObservationTable.py +137 -0
  5. processview-1.5.2/processview/gui/processmanager/ProcessManager.py +83 -0
  6. processview-1.5.2/processview/gui/processmanager/ProcessManagerWidget.py +83 -0
  7. processview-1.5.2/processview/gui/processmanager/ProcessManagerWindow.py +17 -0
  8. processview-1.5.2/processview/gui/processmanager/_DatasetProcessModel.py +215 -0
  9. processview-1.5.2/processview/gui/processmanager/_FilterWidget.py +102 -0
  10. processview-1.5.2/processview/gui/processmanager/_OptionsWidget.py +36 -0
  11. processview-1.5.2/processview/gui/processmanager/_OrderingWidget.py +38 -0
  12. processview-1.5.2/processview/gui/processmanager/__init__.py +2 -0
  13. {processview-1.5.1 → processview-1.5.2}/processview/gui/processmanager.py +22 -21
  14. processview-1.5.2/processview/gui/utils/qitem_model_resetter.py +17 -0
  15. processview-1.5.2/processview/resources/gui/__init__.py +0 -0
  16. {processview-1.5.1 → processview-1.5.2}/processview/version.py +1 -1
  17. {processview-1.5.1 → processview-1.5.2}/processview.egg-info/PKG-INFO +1 -1
  18. {processview-1.5.1 → processview-1.5.2}/processview.egg-info/SOURCES.txt +12 -0
  19. {processview-1.5.1 → processview-1.5.2}/LICENSE +0 -0
  20. {processview-1.5.1 → processview-1.5.2}/README.md +0 -0
  21. {processview-1.5.1 → processview-1.5.2}/processview/__init__.py +0 -0
  22. {processview-1.5.1 → processview-1.5.2}/processview/core/__init__.py +0 -0
  23. {processview-1.5.1 → processview-1.5.2}/processview/core/dataset.py +0 -0
  24. {processview-1.5.1 → processview-1.5.2}/processview/core/helpers.py +0 -0
  25. {processview-1.5.1 → processview-1.5.2}/processview/core/manager/__init__.py +0 -0
  26. {processview-1.5.1 → processview-1.5.2}/processview/core/manager/manager.py +0 -0
  27. {processview-1.5.1 → processview-1.5.2}/processview/core/manager/test/__init__.py +0 -0
  28. {processview-1.5.1 → processview-1.5.2}/processview/core/manager/test/test_manager.py +0 -0
  29. {processview-1.5.1 → processview-1.5.2}/processview/core/setup.py +0 -0
  30. {processview-1.5.1 → processview-1.5.2}/processview/core/superviseprocess.py +0 -0
  31. {processview-1.5.1 → processview-1.5.2}/processview/core/test/__init__.py +0 -0
  32. {processview-1.5.1 → processview-1.5.2}/processview/gui/DropDownWidget.py +0 -0
  33. {processview-1.5.1 → processview-1.5.2}/processview/gui/__init__.py +0 -0
  34. {processview-1.5.1 → processview-1.5.2}/processview/gui/messagebox.py +0 -0
  35. {processview-1.5.1 → processview-1.5.2}/processview/gui/test/__init__.py +0 -0
  36. {processview-1.5.1 → processview-1.5.2}/processview/gui/test/test_process_manager.py +0 -0
  37. {processview-1.5.1/processview/resources → processview-1.5.2/processview/gui/utils}/__init__.py +0 -0
  38. {processview-1.5.1/processview/resources/gui → processview-1.5.2/processview/resources}/__init__.py +0 -0
  39. {processview-1.5.1 → processview-1.5.2}/processview/resources/gui/icons/advancement.png +0 -0
  40. {processview-1.5.1 → processview-1.5.2}/processview/resources/gui/icons/advancement.svg +0 -0
  41. {processview-1.5.1 → processview-1.5.2}/processview/resources/gui/icons/magnifying_glass.png +0 -0
  42. {processview-1.5.1 → processview-1.5.2}/processview/resources/gui/icons/magnifying_glass.svg +0 -0
  43. {processview-1.5.1 → processview-1.5.2}/processview/test/__init__.py +0 -0
  44. {processview-1.5.1 → processview-1.5.2}/processview/utils/__init__.py +0 -0
  45. {processview-1.5.1 → processview-1.5.2}/processview/utils/singleton.py +0 -0
  46. {processview-1.5.1 → processview-1.5.2}/processview.egg-info/dependency_links.txt +0 -0
  47. {processview-1.5.1 → processview-1.5.2}/processview.egg-info/requires.txt +0 -0
  48. {processview-1.5.1 → processview-1.5.2}/processview.egg-info/top_level.txt +0 -0
  49. {processview-1.5.1 → processview-1.5.2}/processview.egg-info/zip-safe +0 -0
  50. {processview-1.5.1 → processview-1.5.2}/setup.cfg +0 -0
  51. {processview-1.5.1 → processview-1.5.2}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: processview
3
- Version: 1.5.1
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
@@ -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()
@@ -1,4 +1,4 @@
1
- from silx.utils.enum import Enum as _Enum
1
+ from enum import Enum as _Enum
2
2
 
3
3
 
4
4
  class SortType(_Enum):
@@ -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()
@@ -0,0 +1,102 @@
1
+ from __future__ import annotations
2
+
3
+ from silx.gui import qt
4
+ from silx.gui import icons
5
+
6
+
7
+ class FilterWidget(qt.QWidget):
8
+ """
9
+ Widget to define some filtering pattern on dataset and / or processes
10
+ """
11
+
12
+ sigProcessPatternEditingFinished = qt.Signal()
13
+ """signal emit when the process pattern editing finished"""
14
+
15
+ sigDatasetPatternEditingFinished = qt.Signal()
16
+ """signal emit when the dataset pattern editing finished"""
17
+
18
+ def __init__(self, parent=None, name=" filter", font_size=12, icon_size=20):
19
+ qt.QWidget.__init__(self, parent)
20
+ self.setLayout(qt.QGridLayout())
21
+ self.layout().setSpacing(2)
22
+ self.layout().setContentsMargins(0, 0, 0, 0)
23
+
24
+ font = self.font()
25
+ font.setPixelSize(font_size)
26
+ self.setFont(font)
27
+
28
+ icon = icons.getQPixmap("processview:gui/icons/magnifying_glass")
29
+ self._researchLabelIcon = qt.QLabel("", parent=self)
30
+ self._researchLabelIcon.setPixmap(icon)
31
+ self.layout().addWidget(qt.QLabel(name, self), 0, 1, 1, 1)
32
+ self.layout().addWidget(self._researchLabelIcon, 0, 2, 1, 1)
33
+
34
+ # filter by dataset id / name
35
+ self._datasetLabel = qt.QLabel("dataset", self)
36
+ self.layout().addWidget(self._datasetLabel, 1, 2, 1, 2)
37
+ self._datasetPatternLE = qt.QLineEdit("", self)
38
+ self.layout().addWidget(self._datasetPatternLE, 1, 4, 1, 1)
39
+ tooltip = (
40
+ "Provide one or several dataset name or pattern to only "
41
+ " display those datasets. Pattern should be separated by a "
42
+ "semi colon. Handle linux wild card. Example: "
43
+ "`pattern1; *suffix; prefix*`"
44
+ )
45
+ for widget in self._datasetLabel, self._datasetPatternLE:
46
+ widget.setToolTip(tooltip)
47
+ self._clearDatasetPatternPB = qt.QPushButton("clear", self)
48
+ self._clearDatasetPatternPB.setAutoDefault(True)
49
+ self.layout().addWidget(self._clearDatasetPatternPB, 1, 5, 1, 1)
50
+
51
+ # filter by process name
52
+ self._processLabel = qt.QLabel("process", self)
53
+ self.layout().addWidget(self._processLabel, 2, 2, 1, 2)
54
+ self._processPatternLE = qt.QLineEdit("", self)
55
+ self.layout().addWidget(self._processPatternLE, 2, 4, 1, 1)
56
+ tooltip = (
57
+ "Provide one or several process name or pattern to only "
58
+ " display those datasets. Pattern should be separated by a "
59
+ "semi colon. Handle linux wild card. Example: "
60
+ "`pattern1; *suffix; prefix*`"
61
+ )
62
+ for widget in self._processLabel, self._processPatternLE:
63
+ widget.setToolTip(tooltip)
64
+ self._clearProcessPatternPB = qt.QPushButton("clear", self)
65
+ self._clearProcessPatternPB.setAutoDefault(True)
66
+ self.layout().addWidget(self._clearProcessPatternPB, 2, 5, 1, 1)
67
+
68
+ # connect signal / slot
69
+ self._clearProcessPatternPB.released.connect(self._processPatternLE.clear)
70
+ self._clearProcessPatternPB.released.connect(
71
+ self._processPatternEditingFinished
72
+ )
73
+ self._clearDatasetPatternPB.released.connect(self._datasetPatternLE.clear)
74
+ self._clearDatasetPatternPB.released.connect(
75
+ self._datasetPatternEditingFinished
76
+ )
77
+ self._datasetPatternLE.editingFinished.connect(
78
+ self._datasetPatternEditingFinished
79
+ )
80
+ self._processPatternLE.editingFinished.connect(
81
+ self._processPatternEditingFinished
82
+ )
83
+
84
+ def getProcessPatterns(self) -> tuple:
85
+ patterns = self._processPatternLE.text().lstrip(" ").rstrip(" ")
86
+ if patterns == "":
87
+ return ("*",)
88
+ res = patterns.replace(" ", "")
89
+ return tuple(res.split(";"))
90
+
91
+ def getDatasetPatterns(self) -> tuple:
92
+ patterns = self._datasetPatternLE.text().lstrip(" ").rstrip(" ")
93
+ if patterns == "":
94
+ return ("*",)
95
+ res = patterns.replace(" ", "")
96
+ return tuple(res.split(";"))
97
+
98
+ def _datasetPatternEditingFinished(self, *args, **kwargs):
99
+ self.sigDatasetPatternEditingFinished.emit()
100
+
101
+ def _processPatternEditingFinished(self, *args, **kwargs):
102
+ self.sigProcessPatternEditingFinished.emit()
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ from silx.gui import qt
4
+
5
+ from ._OrderingWidget import _OrderingWidget
6
+ from ._FilterWidget import FilterWidget
7
+
8
+
9
+ class OptionsWidget(qt.QWidget):
10
+
11
+ def __init__(self, parent):
12
+ super().__init__(parent)
13
+ self.setLayout(qt.QGridLayout())
14
+
15
+ # dataset ordering
16
+ self._orderingWidget = _OrderingWidget(parent=self)
17
+ self.layout().setContentsMargins(2, 2, 2, 2)
18
+ self.layout().setSpacing(0)
19
+ self.layout().addWidget(self._orderingWidget, 2, 1, 3, 3)
20
+
21
+ # filtering
22
+ self._filterWidget = FilterWidget(parent=self)
23
+ self.layout().setContentsMargins(2, 2, 2, 2)
24
+ self.layout().setSpacing(0)
25
+ self.layout().addWidget(self._filterWidget, 4, 1, 3, 3)
26
+
27
+ @property
28
+ def filterWidget(self):
29
+ return self._filterWidget
30
+
31
+ # expose API
32
+ def getProcessPatterns(self):
33
+ return self._filterWidget.getProcessPatterns()
34
+
35
+ def getDatasetPatterns(self):
36
+ return self._filterWidget.getDatasetPatterns()
@@ -0,0 +1,38 @@
1
+ from __future__ import annotations
2
+
3
+ from silx.gui import qt
4
+ from processview.core.sorting import SortType, tooltips
5
+
6
+
7
+ class _OrderingWidget(qt.QWidget):
8
+ """Widget to let the user define the ordering of datasets"""
9
+
10
+ sigSortTypeChanged = qt.Signal(str)
11
+ """emit when sort type changed. Paramater is the name of the new sorting type"""
12
+
13
+ def __init__(self, parent=None, *args, **kwargs) -> None:
14
+ super().__init__(parent, *args, **kwargs)
15
+ self.setLayout(qt.QFormLayout())
16
+ self._sortingTypeCB = qt.QComboBox(self)
17
+ for sort_type in SortType:
18
+ self._sortingTypeCB.addItem(sort_type.value)
19
+ self._sortingTypeCB.setItemData(
20
+ self._sortingTypeCB.findText(sort_type.value),
21
+ tooltips[sort_type],
22
+ qt.Qt.ToolTipRole,
23
+ )
24
+ self.layout().addRow("sorting", self._sortingTypeCB)
25
+
26
+ # set up
27
+ self._sortingTypeCB.setCurrentIndex(
28
+ self._sortingTypeCB.findText(SortType.FIRST_APPEARANCE.value)
29
+ )
30
+
31
+ # connect signal / slot
32
+ self._sortingTypeCB.currentIndexChanged.connect(self._changed)
33
+
34
+ def _changed(self, *args, **kwargs):
35
+ self.sigSortTypeChanged.emit(self.getCurrentSort().value)
36
+
37
+ def getCurrentSort(self) -> SortType:
38
+ return SortType(self._sortingTypeCB.currentText())
@@ -0,0 +1,2 @@
1
+ from .ProcessManagerWindow import ProcessManagerWindow
2
+ from .ProcessManager import ProcessManager
@@ -13,9 +13,10 @@ from processview.core.manager import ProcessManager as _ProcessManager
13
13
  from processview.core.sorting import SortType, tooltips
14
14
  from processview.core.superviseprocess import SuperviseProcess
15
15
  from processview.gui.DropDownWidget import DropDownWidget
16
- from silx.gui import icons
16
+ from processview.gui import icons as icons
17
17
  from processview.gui.messagebox import MessageBox
18
18
  from processview.utils import docstring
19
+ from processview.gui.utils.qitem_model_resetter import qitem_model_resetter
19
20
 
20
21
  _DATASET_STATE_BACKGROUND = {
21
22
  DatasetState.ON_GOING: qt.QColor("#839684"), # light blue
@@ -302,12 +303,12 @@ class _DatasetProcessModel(qt.QAbstractTableModel):
302
303
  except Exception:
303
304
  pass
304
305
 
305
- self.setDatasets(remaining_datasets)
306
- self.endResetModel()
306
+ with qitem_model_resetter(self):
307
+ self.setDatasets(remaining_datasets)
307
308
 
308
309
  def clear(self):
309
- self._processes = OrderedDict()
310
- self.endResetModel()
310
+ with qitem_model_resetter(self):
311
+ self._processes = OrderedDict()
311
312
 
312
313
  def rowCount(self, parent=None):
313
314
  return len(self._unsorted_datasets)
@@ -317,11 +318,11 @@ class _DatasetProcessModel(qt.QAbstractTableModel):
317
318
 
318
319
  def setProcesses(self, processes):
319
320
  assert isinstance(processes, (tuple, list))
320
- self._processes = {}
321
- processes = list(filter(self._match_process_patterns, processes))
322
- for i_process, process in enumerate(processes):
323
- self._processes[i_process] = process
324
- self.endResetModel()
321
+ with qitem_model_resetter(self):
322
+ self._processes = {}
323
+ processes = list(filter(self._match_process_patterns, processes))
324
+ for i_process, process in enumerate(processes):
325
+ self._processes[i_process] = process
325
326
 
326
327
  @staticmethod
327
328
  def sort_datasets(datasets: list, sort_type):
@@ -357,16 +358,16 @@ class _DatasetProcessModel(qt.QAbstractTableModel):
357
358
 
358
359
  def setDatasets(self, datasets):
359
360
  assert isinstance(datasets, (tuple, list))
360
- self._unsorted_datasets = datasets
361
- # filter datasets
362
- datasets = list(filter(self._match_dataset_patterns, datasets))
363
- # sort datasets
364
- datasets = self.sort_datasets(datasets=datasets, sort_type=self._sort_type)
361
+ with qitem_model_resetter(self):
362
+ self._unsorted_datasets = datasets
363
+ # filter datasets
364
+ datasets = list(filter(self._match_dataset_patterns, datasets))
365
+ # sort datasets
366
+ datasets = self.sort_datasets(datasets=datasets, sort_type=self._sort_type)
365
367
 
366
- self._sorted_datasets.clear()
367
- for i_dataset, dataset in enumerate(datasets):
368
- self._sorted_datasets[i_dataset] = dataset
369
- self.endResetModel()
368
+ self._sorted_datasets.clear()
369
+ for i_dataset, dataset in enumerate(datasets):
370
+ self._sorted_datasets[i_dataset] = dataset
370
371
 
371
372
  def headerData(self, col, orientation, role):
372
373
  if orientation == qt.Qt.Horizontal and role == qt.Qt.DisplayRole:
@@ -483,9 +484,9 @@ class _FilterWidget(qt.QWidget):
483
484
  font.setPixelSize(font_size)
484
485
  self.setFont(font)
485
486
 
486
- icon = icons.getQPixmap("processview:gui/icons/magnifying_glass")
487
+ icon = icons.getQIcon("magnifying_glass")
487
488
  self._researchLabelIcon = qt.QLabel("", parent=self)
488
- self._researchLabelIcon.setPixmap(icon)
489
+ self._researchLabelIcon.setPixmap(icon.pixmap(icon_size, state=qt.QIcon.On))
489
490
  self.layout().addWidget(qt.QLabel(name, self), 0, 1, 1, 1)
490
491
  self.layout().addWidget(self._researchLabelIcon, 0, 2, 1, 1)
491
492
 
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ from silx.gui import qt
4
+ from contextlib import contextmanager
5
+
6
+
7
+ @contextmanager
8
+ def qitem_model_resetter(model: qt.QAbstractItemModel):
9
+ """Context manager for resetting a QAbstractItemModel.
10
+
11
+ Make sure `beginResetModel()` is emit at the beginning and endResetModel() at the end.
12
+ """
13
+ model.beginResetModel()
14
+ try:
15
+ yield
16
+ finally:
17
+ model.endResetModel()
@@ -77,7 +77,7 @@ RELEASE_LEVEL_VALUE = {
77
77
 
78
78
  MAJOR = 1
79
79
  MINOR = 5
80
- MICRO = 1
80
+ MICRO = 2
81
81
  RELEV = "final" # <16
82
82
  SERIAL = 0 # <16
83
83
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: processview
3
- Version: 1.5.1
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
@@ -2,6 +2,7 @@ LICENSE
2
2
  README.md
3
3
  setup.cfg
4
4
  setup.py
5
+ example/show_processmanagerwindow.py
5
6
  processview/__init__.py
6
7
  processview/version.py
7
8
  processview.egg-info/PKG-INFO
@@ -25,8 +26,19 @@ processview/gui/DropDownWidget.py
25
26
  processview/gui/__init__.py
26
27
  processview/gui/messagebox.py
27
28
  processview/gui/processmanager.py
29
+ processview/gui/processmanager/ObservationTable.py
30
+ processview/gui/processmanager/ProcessManager.py
31
+ processview/gui/processmanager/ProcessManagerWidget.py
32
+ processview/gui/processmanager/ProcessManagerWindow.py
33
+ processview/gui/processmanager/_DatasetProcessModel.py
34
+ processview/gui/processmanager/_FilterWidget.py
35
+ processview/gui/processmanager/_OptionsWidget.py
36
+ processview/gui/processmanager/_OrderingWidget.py
37
+ processview/gui/processmanager/__init__.py
28
38
  processview/gui/test/__init__.py
29
39
  processview/gui/test/test_process_manager.py
40
+ processview/gui/utils/__init__.py
41
+ processview/gui/utils/qitem_model_resetter.py
30
42
  processview/resources/__init__.py
31
43
  processview/resources/gui/__init__.py
32
44
  processview/resources/gui/icons/advancement.png
File without changes
File without changes
File without changes
File without changes