scipion-pyworkflow 3.7.0__py3-none-any.whl

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 (140) hide show
  1. pyworkflow/__init__.py +33 -0
  2. pyworkflow/apps/__init__.py +29 -0
  3. pyworkflow/apps/pw_manager.py +37 -0
  4. pyworkflow/apps/pw_plot.py +51 -0
  5. pyworkflow/apps/pw_project.py +113 -0
  6. pyworkflow/apps/pw_protocol_list.py +143 -0
  7. pyworkflow/apps/pw_protocol_run.py +51 -0
  8. pyworkflow/apps/pw_run_tests.py +267 -0
  9. pyworkflow/apps/pw_schedule_run.py +322 -0
  10. pyworkflow/apps/pw_sleep.py +37 -0
  11. pyworkflow/apps/pw_sync_data.py +439 -0
  12. pyworkflow/apps/pw_viewer.py +78 -0
  13. pyworkflow/config.py +536 -0
  14. pyworkflow/constants.py +212 -0
  15. pyworkflow/exceptions.py +18 -0
  16. pyworkflow/gui/__init__.py +36 -0
  17. pyworkflow/gui/browser.py +726 -0
  18. pyworkflow/gui/canvas.py +1190 -0
  19. pyworkflow/gui/dialog.py +976 -0
  20. pyworkflow/gui/form.py +2627 -0
  21. pyworkflow/gui/graph.py +247 -0
  22. pyworkflow/gui/graph_layout.py +271 -0
  23. pyworkflow/gui/gui.py +566 -0
  24. pyworkflow/gui/matplotlib_image.py +233 -0
  25. pyworkflow/gui/plotter.py +247 -0
  26. pyworkflow/gui/project/__init__.py +25 -0
  27. pyworkflow/gui/project/base.py +192 -0
  28. pyworkflow/gui/project/constants.py +139 -0
  29. pyworkflow/gui/project/labels.py +205 -0
  30. pyworkflow/gui/project/project.py +484 -0
  31. pyworkflow/gui/project/searchprotocol.py +154 -0
  32. pyworkflow/gui/project/searchrun.py +181 -0
  33. pyworkflow/gui/project/steps.py +166 -0
  34. pyworkflow/gui/project/utils.py +332 -0
  35. pyworkflow/gui/project/variables.py +179 -0
  36. pyworkflow/gui/project/viewdata.py +472 -0
  37. pyworkflow/gui/project/viewprojects.py +510 -0
  38. pyworkflow/gui/project/viewprotocols.py +2093 -0
  39. pyworkflow/gui/project/viewprotocols_extra.py +560 -0
  40. pyworkflow/gui/text.py +771 -0
  41. pyworkflow/gui/tooltip.py +185 -0
  42. pyworkflow/gui/tree.py +684 -0
  43. pyworkflow/gui/widgets.py +307 -0
  44. pyworkflow/mapper/__init__.py +26 -0
  45. pyworkflow/mapper/mapper.py +222 -0
  46. pyworkflow/mapper/sqlite.py +1578 -0
  47. pyworkflow/mapper/sqlite_db.py +145 -0
  48. pyworkflow/object.py +1512 -0
  49. pyworkflow/plugin.py +712 -0
  50. pyworkflow/project/__init__.py +31 -0
  51. pyworkflow/project/config.py +451 -0
  52. pyworkflow/project/manager.py +179 -0
  53. pyworkflow/project/project.py +1990 -0
  54. pyworkflow/project/scripts/clean_projects.py +77 -0
  55. pyworkflow/project/scripts/config.py +92 -0
  56. pyworkflow/project/scripts/create.py +77 -0
  57. pyworkflow/project/scripts/edit_workflow.py +90 -0
  58. pyworkflow/project/scripts/fix_links.py +39 -0
  59. pyworkflow/project/scripts/load.py +87 -0
  60. pyworkflow/project/scripts/refresh.py +83 -0
  61. pyworkflow/project/scripts/schedule.py +111 -0
  62. pyworkflow/project/scripts/stack2volume.py +41 -0
  63. pyworkflow/project/scripts/stop.py +81 -0
  64. pyworkflow/protocol/__init__.py +38 -0
  65. pyworkflow/protocol/bibtex.py +48 -0
  66. pyworkflow/protocol/constants.py +86 -0
  67. pyworkflow/protocol/executor.py +334 -0
  68. pyworkflow/protocol/hosts.py +313 -0
  69. pyworkflow/protocol/launch.py +270 -0
  70. pyworkflow/protocol/package.py +42 -0
  71. pyworkflow/protocol/params.py +744 -0
  72. pyworkflow/protocol/protocol.py +2554 -0
  73. pyworkflow/resources/Imagej.png +0 -0
  74. pyworkflow/resources/chimera.png +0 -0
  75. pyworkflow/resources/fa-exclamation-triangle_alert.png +0 -0
  76. pyworkflow/resources/fa-info-circle_alert.png +0 -0
  77. pyworkflow/resources/fa-search.png +0 -0
  78. pyworkflow/resources/fa-times-circle_alert.png +0 -0
  79. pyworkflow/resources/file_vol.png +0 -0
  80. pyworkflow/resources/loading.gif +0 -0
  81. pyworkflow/resources/no-image128.png +0 -0
  82. pyworkflow/resources/scipion_bn.png +0 -0
  83. pyworkflow/resources/scipion_icon.png +0 -0
  84. pyworkflow/resources/scipion_icon.svg +397 -0
  85. pyworkflow/resources/scipion_icon_proj.png +0 -0
  86. pyworkflow/resources/scipion_icon_projs.png +0 -0
  87. pyworkflow/resources/scipion_icon_prot.png +0 -0
  88. pyworkflow/resources/scipion_logo.png +0 -0
  89. pyworkflow/resources/scipion_logo_normal.png +0 -0
  90. pyworkflow/resources/scipion_logo_small.png +0 -0
  91. pyworkflow/resources/sprites.png +0 -0
  92. pyworkflow/resources/sprites.xcf +0 -0
  93. pyworkflow/resources/wait.gif +0 -0
  94. pyworkflow/template.py +322 -0
  95. pyworkflow/tests/__init__.py +29 -0
  96. pyworkflow/tests/test_utils.py +25 -0
  97. pyworkflow/tests/tests.py +341 -0
  98. pyworkflow/utils/__init__.py +38 -0
  99. pyworkflow/utils/dataset.py +414 -0
  100. pyworkflow/utils/echo.py +104 -0
  101. pyworkflow/utils/graph.py +196 -0
  102. pyworkflow/utils/log.py +284 -0
  103. pyworkflow/utils/path.py +527 -0
  104. pyworkflow/utils/process.py +132 -0
  105. pyworkflow/utils/profiler.py +92 -0
  106. pyworkflow/utils/progressbar.py +154 -0
  107. pyworkflow/utils/properties.py +627 -0
  108. pyworkflow/utils/reflection.py +129 -0
  109. pyworkflow/utils/utils.py +877 -0
  110. pyworkflow/utils/which.py +229 -0
  111. pyworkflow/viewer.py +328 -0
  112. pyworkflow/webservices/__init__.py +8 -0
  113. pyworkflow/webservices/config.py +11 -0
  114. pyworkflow/webservices/notifier.py +162 -0
  115. pyworkflow/webservices/repository.py +59 -0
  116. pyworkflow/webservices/workflowhub.py +74 -0
  117. pyworkflow/wizard.py +64 -0
  118. pyworkflowtests/__init__.py +51 -0
  119. pyworkflowtests/bibtex.py +51 -0
  120. pyworkflowtests/objects.py +830 -0
  121. pyworkflowtests/protocols.py +154 -0
  122. pyworkflowtests/tests/__init__.py +0 -0
  123. pyworkflowtests/tests/test_canvas.py +72 -0
  124. pyworkflowtests/tests/test_domain.py +45 -0
  125. pyworkflowtests/tests/test_logs.py +74 -0
  126. pyworkflowtests/tests/test_mappers.py +392 -0
  127. pyworkflowtests/tests/test_object.py +507 -0
  128. pyworkflowtests/tests/test_project.py +42 -0
  129. pyworkflowtests/tests/test_protocol_execution.py +72 -0
  130. pyworkflowtests/tests/test_protocol_export.py +78 -0
  131. pyworkflowtests/tests/test_protocol_output.py +158 -0
  132. pyworkflowtests/tests/test_streaming.py +47 -0
  133. pyworkflowtests/tests/test_utils.py +210 -0
  134. scipion_pyworkflow-3.7.0.dist-info/LICENSE.txt +674 -0
  135. scipion_pyworkflow-3.7.0.dist-info/METADATA +107 -0
  136. scipion_pyworkflow-3.7.0.dist-info/RECORD +140 -0
  137. scipion_pyworkflow-3.7.0.dist-info/WHEEL +5 -0
  138. scipion_pyworkflow-3.7.0.dist-info/dependency_links.txt +1 -0
  139. scipion_pyworkflow-3.7.0.dist-info/entry_points.txt +5 -0
  140. scipion_pyworkflow-3.7.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,2554 @@
1
+ # **************************************************************************
2
+ # *
3
+ # * Authors: J.M. De la Rosa Trevin (delarosatrevin@scilifelab.se) [1]
4
+ # *
5
+ # * [1] SciLifeLab, Stockholm University
6
+ # *
7
+ # * This program is free software: you can redistribute it and/or modify
8
+ # * it under the terms of the GNU General Public License as published by
9
+ # * the Free Software Foundation, either version 3 of the License, or
10
+ # * (at your option) any later version.
11
+ # *
12
+ # * This program is distributed in the hope that it will be useful,
13
+ # * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # * GNU General Public License for more details.
16
+ # *
17
+ # * You should have received a copy of the GNU General Public License
18
+ # * along with this program. If not, see <https://www.gnu.org/licenses/>.
19
+ # *
20
+ # * All comments concerning this program package may be sent to the
21
+ # * e-mail address 'scipion@cnb.csic.es'
22
+ # *
23
+ # **************************************************************************
24
+ """
25
+ This modules contains classes required for the workflow
26
+ execution and tracking like: Step and Protocol
27
+ """
28
+ import contextlib
29
+ import sys, os
30
+ import json
31
+ import threading
32
+ import time
33
+ from datetime import datetime
34
+
35
+ import pyworkflow as pw
36
+ from pyworkflow.exceptions import ValidationException, PyworkflowException
37
+ from pyworkflow.object import *
38
+ import pyworkflow.utils as pwutils
39
+ from pyworkflow.utils.log import LoggingConfigurator, getExtraLogInfo, STATUS, setDefaultLoggingContext
40
+ from .executor import StepExecutor, ThreadStepExecutor, QueueStepExecutor
41
+ from .constants import *
42
+ from .params import Form
43
+ from ..utils import getFileSize
44
+
45
+
46
+ import logging
47
+ # Get the root logger
48
+ logger = logging.getLogger(__name__)
49
+
50
+
51
+ class Step(Object):
52
+ """ Basic execution unit.
53
+ It should defines its Input, Output
54
+ and define a run method.
55
+ """
56
+
57
+ def __init__(self, **kwargs):
58
+ Object.__init__(self, **kwargs)
59
+ self._prerequisites = CsvList() # which steps needs to be done first
60
+ self.status = String()
61
+ self.initTime = String()
62
+ self.endTime = String()
63
+ self._error = String()
64
+ self.interactive = Boolean(False)
65
+ self._resultFiles = String()
66
+ self._index = None
67
+
68
+ def getIndex(self):
69
+ return self._index
70
+
71
+ def setIndex(self, newIndex):
72
+ self._index = newIndex
73
+
74
+ def getPrerequisites(self):
75
+ return self._prerequisites
76
+
77
+ def addPrerequisites(self, *newPrerequisites):
78
+ for p in newPrerequisites:
79
+ self._prerequisites.append(p)
80
+
81
+ def setPrerequisites(self, *newPrerequisites):
82
+ self._prerequisites.clear()
83
+ self.addPrerequisites(*newPrerequisites)
84
+
85
+ def _preconditions(self):
86
+ """ Check if the necessary conditions to
87
+ step execution are met"""
88
+ return self._validate() == []
89
+
90
+ def _postconditions(self):
91
+ """ Check if the step have done well its task
92
+ and accomplish its results"""
93
+ return True
94
+
95
+ def _run(self):
96
+ """ This is the function that will do the real job.
97
+ It should be override by sub-classes."""
98
+ pass
99
+
100
+ def setRunning(self):
101
+ """ The the state as STATE_RUNNING and
102
+ set the init and end times.
103
+ """
104
+ self.initTime.set(dt.datetime.now())
105
+ self.endTime.set(None)
106
+ self.status.set(STATUS_RUNNING)
107
+ self._error.set(None) # Clean previous error message
108
+
109
+ def getError(self):
110
+ return self._error
111
+
112
+ def getErrorMessage(self):
113
+ return self.getError().get('')
114
+
115
+ def setFailed(self, msg):
116
+ """ Set the run failed and store an error message. """
117
+ self._finalizeStep(STATUS_FAILED, msg=msg)
118
+
119
+ def setAborted(self):
120
+ """ Set the status to aborted and updates the endTime. """
121
+ self._finalizeStep(STATUS_ABORTED, "Aborted by user.")
122
+
123
+ def setFinished(self):
124
+ """ Set the status to finish updates the end time """
125
+ self._finalizeStep(STATUS_FINISHED)
126
+
127
+ def _finalizeStep(self, status, msg=None):
128
+ """ Closes the step, setting up the endTime and optionally an error message"""
129
+ self.endTime.set(dt.datetime.now())
130
+ if msg:
131
+ self._error.set(msg)
132
+ self.status.set(status)
133
+
134
+ def setSaved(self):
135
+ """ Set the status to saved and updated the endTime. """
136
+ self.initTime.set(None)
137
+ self.endTime.set(None)
138
+ self.status.set(STATUS_SAVED)
139
+ self._error.set(None) # Clean previous error message
140
+
141
+ def getStatus(self):
142
+ return self.status.get(STATUS_NEW)
143
+
144
+ def getElapsedTime(self, default=dt.timedelta()):
145
+ """ Return the time that took to run
146
+ (or the actual running time if still is running )
147
+ """
148
+ elapsed = default
149
+
150
+ if self.initTime.hasValue():
151
+ t1 = self.initTime.datetime()
152
+
153
+ if self.endTime.hasValue():
154
+ t2 = self.endTime.datetime()
155
+ else:
156
+ t2 = dt.datetime.now()
157
+
158
+ elapsed = t2 - t1
159
+
160
+ return elapsed
161
+
162
+ def setStatus(self, value):
163
+ return self.status.set(value)
164
+
165
+ def isNew(self):
166
+ return self.getStatus() == STATUS_NEW
167
+
168
+ def setInteractive(self, value):
169
+ return self.interactive.set(value)
170
+
171
+ def isActive(self):
172
+ return self.getStatus() in ACTIVE_STATUS
173
+
174
+ def isFinished(self):
175
+ return self.getStatus() == STATUS_FINISHED
176
+
177
+ def isRunning(self):
178
+ return self.getStatus() == STATUS_RUNNING
179
+
180
+ def isFailed(self):
181
+ return self.getStatus() == STATUS_FAILED
182
+
183
+ def isSaved(self):
184
+ return self.getStatus() == STATUS_SAVED
185
+
186
+ def isScheduled(self):
187
+ return self.getStatus() == STATUS_SCHEDULED
188
+
189
+ def isAborted(self):
190
+ return self.getStatus() == STATUS_ABORTED
191
+
192
+ def isLaunched(self):
193
+ return self.getStatus() == STATUS_LAUNCHED
194
+
195
+ def isInteractive(self):
196
+ return self.interactive.get()
197
+
198
+ def isWaiting(self):
199
+ return self.getStatus() == STATUS_WAITING
200
+
201
+ def run(self):
202
+ """ Do the job of this step"""
203
+ self.setRunning()
204
+ try:
205
+ self._run()
206
+ self.endTime.set(dt.datetime.now())
207
+ if self.status.get() == STATUS_RUNNING:
208
+ if self.isInteractive():
209
+ # If the Step is interactive, after run
210
+ # it will be waiting for use to mark it as DONE
211
+ status = STATUS_INTERACTIVE
212
+ else:
213
+ status = STATUS_FINISHED
214
+ self.status.set(status)
215
+
216
+ except PyworkflowException as e:
217
+ print(pwutils.redStr(str(e)))
218
+ self.setFailed(str(e))
219
+ except Exception as e:
220
+ self.setFailed(str(e))
221
+ import traceback
222
+ traceback.print_exc()
223
+ # raise #only in development
224
+ # finally:
225
+ # self.endTime.set(dt.datetime.now())
226
+
227
+
228
+ class FunctionStep(Step):
229
+ """ This is a Step wrapper around a normal function
230
+ This class will ease the insertion of Protocol function steps
231
+ through the function _insertFunctionStep"""
232
+
233
+ def __init__(self, func=None, funcName=None, *funcArgs, **kwargs):
234
+ """
235
+ Params:
236
+ func: the function that will be executed.
237
+ funcName: the name assigned to that function (will be stored)
238
+ *funcArgs: argument list passed to the function (serialized and stored)
239
+ **kwargs: extra parameters.
240
+ """
241
+ Step.__init__(self)
242
+ self._func = func # Function should be set before run
243
+ self._args = funcArgs
244
+ self.funcName = String(funcName)
245
+ self.argsStr = String(json.dumps(funcArgs, default=lambda x: None))
246
+ self.setInteractive(kwargs.get('interactive', False))
247
+ if kwargs.get('wait', False):
248
+ self.setStatus(STATUS_WAITING)
249
+
250
+ def _runFunc(self):
251
+ """ Return the possible result files after running the function. """
252
+ return self._func(*self._args)
253
+
254
+ def _run(self):
255
+ """ Run the function and check the result files if any. """
256
+ resultFiles = self._runFunc()
257
+ if isinstance(resultFiles, str):
258
+ resultFiles = [resultFiles]
259
+ if resultFiles and len(resultFiles):
260
+ missingFiles = pwutils.missingPaths(*resultFiles)
261
+ if len(missingFiles):
262
+ raise Exception('Missing filePaths: ' + ' '.join(missingFiles))
263
+ self._resultFiles.set(json.dumps(resultFiles))
264
+
265
+ def _postconditions(self):
266
+ """ This type of Step, will simply check
267
+ as postconditions that the result filePaths exists"""
268
+ if not self._resultFiles.hasValue():
269
+ return True
270
+ filePaths = json.loads(self._resultFiles.get())
271
+
272
+ return len(pwutils.missingPaths(*filePaths)) == 0
273
+
274
+ def __eq__(self, other):
275
+ """ Compare with other FunctionStep"""
276
+ return (self.funcName == other.funcName and
277
+ self.argsStr == other.argsStr)
278
+
279
+ def __ne__(self, other):
280
+ return not self.__eq__(other)
281
+
282
+ def __str__(self):
283
+ return self.funcName.get()
284
+
285
+
286
+ class RunJobStep(FunctionStep):
287
+ """ This Step will wrapper the commonly used function runJob
288
+ for launching specific programs with some parameters.
289
+ The runJob function should be provided by the protocol
290
+ when inserting a new RunJobStep"""
291
+
292
+ def __init__(self, runJobFunc=None, programName=None, arguments=None,
293
+ resultFiles=[], **kwargs):
294
+ FunctionStep.__init__(self, runJobFunc, 'runJob', programName,
295
+ arguments)
296
+ # Number of mpi and threads used to run the program
297
+ self.__runJob = runJobFunc # Store the current function to run the job
298
+ self.mpi = 1
299
+ self.threads = 1
300
+
301
+ def _runFunc(self):
302
+ """ Wrap around runJob function"""
303
+ # We know that:
304
+ # _func: is the runJob function
305
+ # _args[0]: is the program name
306
+ # _args[1]: is the arguments to the program
307
+ return self._func(None, self._args[0], self._args[1],
308
+ numberOfMpi=self.mpi, numberOfThreads=self.threads)
309
+ # TODO: Add the option to return resultFiles
310
+
311
+ def __str__(self):
312
+ return self._args[0] # return program name
313
+
314
+
315
+ class StepSet(Set):
316
+ """ Special type of Set for storing steps. """
317
+
318
+ def __init__(self, filename=None, prefix='',
319
+ mapperClass=None, **kwargs):
320
+ Set.__init__(self, filename, prefix, mapperClass, classesDict=globals(),
321
+ **kwargs)
322
+
323
+
324
+ class Protocol(Step):
325
+ """ The Protocol is a higher type of Step.
326
+ It also have the inputs, outputs and other Steps properties,
327
+ but contains a list of steps that are executed
328
+ """
329
+
330
+ # Version where protocol appeared first time
331
+ _lastUpdateVersion = pw.VERSION_1
332
+ _stepsCheckSecs = pw.Config.getStepsCheckSeconds()
333
+ # Protocol develop status: PROD, BETA, NEW
334
+ _devStatus = pw.PROD
335
+
336
+ """" Possible Outputs:
337
+ This is an optional but recommended attribute to fill.
338
+ It has to be an enum with names being the name of the output and value the class of the output:
339
+
340
+ class MyOutput(enum.Enum):
341
+ outputMicrographs = SetOfMicrographs
342
+ outputMicrographDW = SetOfMicrographs
343
+
344
+ When defining outputs you can, optionally, use this enum like:
345
+ self._defineOutputs(**{MyOutput.outputMicrographs.name, setOfMics})
346
+ It will help to keep output names consistently
347
+
348
+ Alternative an inline dictionary will work:
349
+ _possibleOutputs = {"outputMicrographs" : SetOfMicrographs}
350
+
351
+ For a more fine detailed/dynamic output based on parameters, you can overwrite the getter:
352
+ getPossibleOutputs() in your protocol.
353
+
354
+ """
355
+ _possibleOutputs = None
356
+
357
+ # Cache package and plugin
358
+ _package = None
359
+ _plugin = None
360
+
361
+ def __init__(self, **kwargs):
362
+ Step.__init__(self, **kwargs)
363
+ self._size = None
364
+ self._steps = [] # List of steps that will be executed
365
+ self._newSteps = False # Boolean to annotate when there are new steps added to the above list. And need persistence.
366
+ # All generated filePaths should be inside workingDir
367
+ self.workingDir = String(kwargs.get('workingDir', '.'))
368
+ self.mapper = kwargs.get('mapper', None)
369
+ self._inputs = []
370
+ self._outputs = CsvList()
371
+ # This flag will be used to annotate it output are already "migrated"
372
+ # and available in the _outputs list. Therefore iterating
373
+ self._useOutputList = Boolean(False)
374
+ # Expert level needs to be defined before parsing params
375
+ self.expertLevel = Integer(kwargs.get('expertLevel', LEVEL_NORMAL))
376
+ self._definition = Form(self)
377
+ self._defineParams(self._definition)
378
+ self._createVarsFromDefinition(**kwargs)
379
+ self._log = logger
380
+ self._buffer = '' # text buffer for reading log files
381
+ # Project to which the protocol belongs
382
+ self.__project = kwargs.get('project', None)
383
+ # Filename templates dict that will be used by _getFileName
384
+ self.__filenamesDict = {}
385
+
386
+ # This will be used at project load time to check if
387
+ # we need to update the protocol with the data from run.db
388
+ self.lastUpdateTimeStamp = String()
389
+
390
+ # For non-parallel protocols mpi=1 and threads=1
391
+ self.allowMpi = hasattr(self, 'numberOfMpi')
392
+ if not self.allowMpi:
393
+ self.numberOfMpi = Integer(1)
394
+
395
+ self.allowThreads = hasattr(self, 'numberOfThreads')
396
+
397
+ if not self.allowThreads:
398
+ self.numberOfThreads = Integer(1)
399
+
400
+ # Check if MPI or threads are passed in **kwargs, mainly used in tests
401
+ if 'numberOfMpi' in kwargs:
402
+ self.numberOfMpi.set(kwargs.get('numberOfMpi'))
403
+
404
+ if 'numberOfThreads' in kwargs:
405
+ self.numberOfThreads.set(kwargs.get('numberOfThreads'))
406
+
407
+ if not hasattr(self, 'hostName'):
408
+ self.hostName = String(kwargs.get('hostName', 'localhost'))
409
+
410
+ if not hasattr(self, 'hostFullName'):
411
+ self.hostFullName = String()
412
+
413
+ # Maybe this property can be inferred from the
414
+ # prerequisites of steps, but is easier to keep it
415
+ self.stepsExecutionMode = STEPS_SERIAL
416
+
417
+ # Run mode
418
+ self.runMode = Integer(kwargs.get('runMode', MODE_RESUME))
419
+ # Use queue system?
420
+ self._useQueue = Boolean(pw.Config.SCIPION_USE_QUEUE)
421
+ # Store a json string with queue name
422
+ # and queue parameters (only meaningful if _useQueue=True)
423
+ self._queueParams = String()
424
+ self.queueShown = False
425
+ self._jobId = CsvList() # Store queue job ids
426
+ self._pid = Integer()
427
+ self._stepsExecutor = None
428
+ self._stepsDone = Integer(0)
429
+ self._cpuTime = Integer(0)
430
+ self._numberOfSteps = Integer(0)
431
+ # For visualization
432
+ self.allowHeader = Boolean(True)
433
+ # Create an String variable to allow some protocol to precompute
434
+ # the summary message
435
+ self.summaryVar = String()
436
+ self.methodsVar = String()
437
+ # Create a variable to know if the protocol has expert params
438
+ self._hasExpert = None
439
+
440
+ # Store warnings here
441
+ self.summaryWarnings = []
442
+ # Get a lock for threading execution
443
+ self._lock = threading.RLock() # Recursive locks allows a thread to acquire lock on same object more
444
+ # than one time, thus avoiding deadlock situation. This fixed the concurrency problems we had before.
445
+ self.forceSchedule = Boolean(False)
446
+
447
+ def _storeAttributes(self, attrList, attrDict):
448
+ """ Store all attributes in attrDict as
449
+ attributes of self, also store the key in attrList.
450
+ """
451
+ for key, value in attrDict.items():
452
+ if key not in attrList:
453
+ attrList.append(key)
454
+ setattr(self, key, value)
455
+
456
+ def _defineInputs(self, **kwargs):
457
+ """ This function should be used to define
458
+ those attributes considered as Input.
459
+ """
460
+ self._storeAttributes(self._inputs, kwargs)
461
+
462
+ def _defineOutputs(self, **kwargs):
463
+ """ This function should be used to specify
464
+ expected outputs.
465
+ """
466
+ for k, v in kwargs.items():
467
+ if hasattr(self, k):
468
+ self._deleteChild(k, v)
469
+ self._insertChild(k, v)
470
+
471
+ # Store attributes in _output (this does not persist them!)
472
+ self._storeAttributes(self._outputs, kwargs)
473
+
474
+ # Persist outputs list
475
+ self._insertChild("_outputs", self._outputs)
476
+ self._useOutputList.set(True)
477
+ self._insertChild("_useOutputList", self._useOutputList)
478
+
479
+ def _closeOutputSet(self):
480
+ """Close all output set"""
481
+ for outputName, output in self.iterOutputAttributes():
482
+ if isinstance(output, Set) and output.isStreamOpen():
483
+ self.__tryUpdateOutputSet(outputName, output, state=Set.STREAM_CLOSED)
484
+
485
+ def _updateOutputSet(self, outputName, outputSet,
486
+ state=Set.STREAM_OPEN):
487
+ """ Use this function when updating an Stream output set.
488
+ """
489
+ self.__tryUpdateOutputSet(outputName, outputSet, state)
490
+
491
+ def __tryUpdateOutputSet(self, outputName, outputSet,
492
+ state=Set.STREAM_OPEN, tries=1, firstException=None):
493
+ try:
494
+ # Update the set with the streamState value (either OPEN or CLOSED)
495
+ outputSet.setStreamState(state)
496
+
497
+ if self.hasAttribute(outputName):
498
+ outputSet.write() # Write to commit changes
499
+ outputAttr = getattr(self, outputName)
500
+ # Copy the properties to the object contained in the protocol
501
+ # Default Set.copy ignores some attributes like size or mapperPath.
502
+ # In this case we want all to be copied
503
+ outputAttr.copy(outputSet, copyId=False, ignoreAttrs=[])
504
+ # Persist changes
505
+ self._store(outputAttr)
506
+ else:
507
+ # Here the defineOutputs function will call the write() method
508
+ self._defineOutputs(**{outputName: outputSet})
509
+ self._store(outputSet)
510
+ # Close set database to avoid locking it
511
+ outputSet.close()
512
+
513
+ except Exception as ex:
514
+
515
+ if tries > pw.Config.getUpdateSetAttempts():
516
+ raise BlockingIOError("Can't update %s (output) of %s after %s attempts. Reason: %s. "
517
+ "Concurrency, a non writable file system or a quota exceeded could be among the causes." %
518
+ (outputName, self,tries-1, ex)) from firstException
519
+ else:
520
+ logger.warning("Trying to update %s (output) of protocol %s, attempt=%d: %s " % (outputName, self, tries, ex))
521
+ time.sleep(pw.Config.getUpdateSetAttemptsWait())
522
+ self.__tryUpdateOutputSet(outputName, outputSet, state,
523
+ tries + 1, firstException= ex if tries==1 else firstException)
524
+
525
+ def hasExpert(self):
526
+ """ This function checks if the protocol has
527
+ any expert parameter"""
528
+ if self._hasExpert is None:
529
+ self._hasExpert = False
530
+ for paraName, param in self._definition.iterAllParams():
531
+ if param.isExpert():
532
+ self._hasExpert = True
533
+ break
534
+
535
+ return self._hasExpert
536
+
537
+ def getProject(self):
538
+ return self.__project
539
+
540
+ def setProject(self, project):
541
+ self.__project = project
542
+
543
+ @staticmethod
544
+ def hasDefinition(cls):
545
+ """ Check if the protocol has some definition.
546
+ This can help to detect "abstract" protocol that
547
+ only serve as base for other, not to be instantiated.
548
+ """
549
+ return hasattr(cls, '_definition')
550
+
551
+ @classmethod
552
+ def isNewDev(cls):
553
+ if cls._devStatus == pw.NEW:
554
+ return True
555
+
556
+ @classmethod
557
+ def isBeta(cls):
558
+ return cls._devStatus == pw.BETA
559
+
560
+ @classmethod
561
+ def isUpdated(cls):
562
+ return cls._devStatus == pw.UPDATED
563
+
564
+ def getDefinition(self):
565
+ """ Access the protocol definition. """
566
+ return self._definition
567
+
568
+ def getParam(self, paramName):
569
+ """ Return a _definition param give its name. """
570
+ return self._definition.getParam(paramName)
571
+
572
+ def getEnumText(self, paramName):
573
+ """ This function will retrieve the text value
574
+ of an enum parameter in the definition, taking the actual value in
575
+ the protocol.
576
+
577
+ :param paramName: the name of the enum param.
578
+
579
+ :returns: the string value corresponding to the enum choice.
580
+
581
+ """
582
+ index = getattr(self, paramName).get()
583
+ return self.getParam(paramName).choices[index]
584
+
585
+ def evalParamCondition(self, paramName):
586
+ """ Eval if the condition of paramName in _definition
587
+ is satisfied with the current values of the protocol attributes.
588
+ """
589
+ return self._definition.evalParamCondition(paramName)
590
+
591
+ def evalExpertLevel(self, paramName):
592
+ """ Return the expert level evaluation for a param with the given name.
593
+ """
594
+ return self.evalParamExpertLevel(self.getParam(paramName))
595
+
596
+ def evalParamExpertLevel(self, param):
597
+ """ Return True if the param has an expert level is less than
598
+ the one for the whole protocol.
599
+ """
600
+ return param.expertLevel.get() <= self.expertLevel.get()
601
+
602
+ def iterDefinitionAttributes(self):
603
+ """ Iterate over all the attributes from definition. """
604
+ for paramName, _ in self._definition.iterParams():
605
+ yield paramName, getattr(self, paramName)
606
+
607
+ def getDefinitionDict(self):
608
+ """ Similar to getObjDict, but only for those
609
+ params that are in the form.
610
+ This function is used for export protocols as json text file.
611
+ """
612
+ d = OrderedDict()
613
+ d['object.className'] = self.getClassName()
614
+ d['object.id'] = self.strId()
615
+ d['object.label'] = self.getObjLabel()
616
+ d['object.comment'] = self.getObjComment()
617
+ d['_useQueue'] = self._useQueue.getObjValue()
618
+ d['_prerequisites'] = self._prerequisites.getObjValue()
619
+
620
+ if self._queueParams:
621
+ d['_queueParams'] = self._queueParams.get()
622
+
623
+ od = self.getObjDict(includePointers=True)
624
+
625
+ for attrName in od:
626
+ if self.getParam(attrName) is not None:
627
+ d[attrName] = od[attrName]
628
+
629
+ return d
630
+
631
+ def processImportDict(self, importDict, importDir):
632
+ """
633
+ This function is used when we import a workflow from a json to process or
634
+ adjust the json data for reproducibility purposes e.g. resolve relative paths
635
+ Params:
636
+ importDict: Dict of the protocol that we got from the json
637
+ importDir: dir of the json we're importing
638
+ """
639
+ return importDict
640
+
641
+ def iterDefinitionSections(self):
642
+ """ Iterate over all the section of the definition. """
643
+ for section in self._definition.iterSections():
644
+ yield section
645
+
646
+ def iterInputAttributes(self):
647
+ """ Iterate over the main input parameters
648
+ of this protocol. Now the input are assumed to be these attribute
649
+ which are pointers and have no condition.
650
+ """
651
+ for key, attr in self.getAttributes():
652
+ if not isinstance(attr, Object):
653
+ raise Exception('Attribute %s have been overwritten to type %s '
654
+ % (key, type(attr)))
655
+ if isinstance(attr, PointerList) and attr.hasValue():
656
+ for item in attr:
657
+ # the same key is returned for all items inside the
658
+ # PointerList, this is used in viewprotocols.py
659
+ # to group them inside the same tree element
660
+ yield key, item
661
+ if attr.isPointer() and attr.hasValue():
662
+ yield key, attr
663
+
664
+ # Consider here scalars with pointers inside
665
+ elif isinstance(attr, Scalar) and attr.hasPointer():
666
+ # Scheduling was stale cause this Scalar with pointers where not returned
667
+ #if attr.get() is not None:
668
+ yield key, attr.getPointer()
669
+
670
+ def iterInputPointers(self):
671
+ """ This function is similar to iterInputAttributes, but it yields
672
+ all input Pointers, independently if they have value or not.
673
+ """
674
+ for key, attr in self.getAttributes():
675
+ if not isinstance(attr, Object):
676
+ raise Exception('Attribute %s have been overwritten to type %s '
677
+ % (key, type(attr)))
678
+ if isinstance(attr, PointerList) and attr.hasValue():
679
+ for item in attr:
680
+ # the same key is returned for all items inside the
681
+ # PointerList, this is used in viewprotocols.py
682
+ # to group them inside the same tree element
683
+ yield key, item
684
+ elif attr.isPointer():
685
+ yield key, attr
686
+
687
+ def getProtocolsToUpdate(self):
688
+ """
689
+ This function returns a list of protocols ids that need to update
690
+ their database to launch this protocol (this method is only used
691
+ when a WORKFLOW is restarted or continued).
692
+ Actions done here are:
693
+
694
+ #. Iterate over the main input Pointer of this protocol
695
+ (here, 3 different cases are analyzed):
696
+
697
+ A #. When the pointer points to a protocol
698
+
699
+ B #. When the pointer points to another object (INDIRECTLY).
700
+ The pointer has an _extended value (new parameters configuration
701
+ in the protocol)
702
+
703
+ C #. When the pointer points to another object (DIRECTLY).
704
+
705
+ - The pointer has not an _extended value (old parameters
706
+ configuration in the protocol)
707
+
708
+ #. The PROTOCOL to which the pointer points is determined and saved in
709
+ the list
710
+
711
+ #. If this pointer points to a set (case B and C):
712
+
713
+ - Iterate over the main attributes of the set
714
+ - if attribute is a pointer then we add the pointed protocol to the ids list
715
+ """
716
+ protocolIds = []
717
+ protocol = None
718
+ for key, attrInput in self.iterInputAttributes():
719
+ outputs = []
720
+ output = attrInput.get()
721
+ if isinstance(output, Protocol): # case A
722
+ protocol = output
723
+ for _, protOutput in protocol.iterOutputAttributes():
724
+ outputs.append(protOutput) # for case A store all the protocols outputs
725
+ else:
726
+ if attrInput.hasExtended(): # case B
727
+ protocol = attrInput.getObjValue()
728
+ else: # case C
729
+
730
+ if self.getProject() is not None:
731
+ protocol = self.getProject().getRunsGraph(refresh=True).getNode(str(output.getObjParentId())).run
732
+ else:
733
+ # This is a problem, since protocols coming from
734
+ # Pointers do not have the __project set.
735
+ # We do not have a clear way to get the protocol if
736
+ # we do not have the project object associated
737
+ # This case implies Direct Pointers to Sets
738
+ # (without extended): hopefully this will only be
739
+ # created from tests
740
+ logger.warning("Can't get %s info from %s."
741
+ " This could render unexpected results when "
742
+ "scheduling protocols. Value: %s" % (key, self, attrInput))
743
+ continue
744
+
745
+ if output is not None:
746
+ outputs.append(output)
747
+
748
+ # If there is output
749
+ if outputs:
750
+ # Iter over all the outputs
751
+ for output in outputs:
752
+ # For each output attribute: Looking for pointers like SetOfCoordinates.micrographs
753
+ for k, attr in output.getAttributes():
754
+ # If it's a pointer
755
+ if isinstance(attr, Pointer):
756
+ logger.debug("Pointer found in output: %s.%s (%s)" % (output, k, attr))
757
+ prot = attr.getObjValue()
758
+ if prot is not None:
759
+ if isinstance(prot, Protocol):
760
+ protocolIds.append(prot.getObjId())
761
+ else:
762
+ logger.warning(f"We have found that {output}.{key} points to {attr} "
763
+ f"and is a direct pointer. Direct pointers are less reliable "
764
+ f"in streaming scenarios. Developers should avoid them.")
765
+
766
+ protocolIds.append(protocol.getObjId())
767
+
768
+ return protocolIds
769
+
770
+ def getInputStatus(self):
771
+ """ Returns if any input pointer is not ready yet and if there is
772
+ any pointer to an open set
773
+ """
774
+ emptyPointers = False
775
+ openSetPointer = False
776
+ emptyInput = False
777
+
778
+ for paramName, attr in self.iterInputPointers():
779
+
780
+ param = self.getParam(paramName)
781
+ # Issue #1597: New data loaded with old code.
782
+ # If the input pointer is not a param:
783
+ # This could happen in backward incompatibility cases,
784
+ # Protocol has an attribute (inputPointer) but class does not define
785
+ # if in the define params.
786
+ if param is None:
787
+ print("%s attribute is not defined as parameter. "
788
+ "This could happen when loading new code with older "
789
+ "scipion versions." % paramName)
790
+ continue
791
+
792
+ condition = self.evalParamCondition(paramName)
793
+
794
+ obj = attr.get()
795
+ if isinstance(obj, Protocol) and obj.getStatus() == STATUS_SAVED: # the pointer points to a protocol
796
+ emptyPointers = True
797
+ if obj is None and attr.hasValue():
798
+ emptyPointers = True
799
+ if condition and obj is None and not param.allowsNull:
800
+ if not attr.hasValue():
801
+ emptyInput = True
802
+
803
+ if not self.worksInStreaming() and isinstance(obj, Set) and obj.isStreamOpen():
804
+ openSetPointer = True
805
+
806
+ return emptyInput, openSetPointer, emptyPointers
807
+
808
+ def iterOutputAttributes(self, outputClass=None, includePossible=False):
809
+ """ Iterate over the outputs produced by this protocol. """
810
+
811
+ iterator = self._iterOutputsNew if self._useOutputList else self._iterOutputsOld
812
+
813
+ hasOutput=False
814
+
815
+ # Iterate through actual outputs
816
+ for key, attr in iterator():
817
+ if outputClass is None or isinstance(attr, outputClass):
818
+ hasOutput = True
819
+ yield key, attr
820
+
821
+ # NOTE: This will only happen in case there is no actual output.
822
+ # There is no need to avoid duplication of actual output and possible output.
823
+ if includePossible and not hasOutput and self.getPossibleOutputs() is not None:
824
+ for possibleOutput in self.getPossibleOutputs():
825
+ if isinstance(possibleOutput, str):
826
+ yield possibleOutput, self._possibleOutputs[possibleOutput]
827
+ else:
828
+ yield possibleOutput.name, possibleOutput.value
829
+
830
+ def getPossibleOutputs(self):
831
+ return self._possibleOutputs
832
+
833
+ def _iterOutputsNew(self):
834
+ """ This methods iterates through a list where outputs have been
835
+ annotated"""
836
+
837
+ # Loop through the output list
838
+ for attrName in self._outputs:
839
+
840
+ # FIX: When deleting manually an output, specially for interactive protocols.
841
+ # The _outputs is properly deleted in projects.sqlite, not it's run.db remains.
842
+ # When the protocol is updated from run.db it brings the outputs that were deleted
843
+ if hasattr(self, attrName):
844
+ # Get it from the protocol
845
+ attr = getattr(self, attrName)
846
+
847
+ yield attrName, attr
848
+ else:
849
+ self._outputs.remove(attrName)
850
+
851
+ def _iterOutputsOld(self):
852
+ """ This method iterates assuming the old model: any EMObject attribute
853
+ is an output."""
854
+ # Iterate old Style:
855
+
856
+ try:
857
+ domain = self.getClassDomain()
858
+ except Exception as e:
859
+ print(e)
860
+ print("Protocol in workingdir ", self.getWorkingDir(), " is of an unknown class")
861
+ print("Maybe the class name has changed")
862
+ return "none", None
863
+
864
+ for key, attr in self.getAttributes():
865
+ if isinstance(attr, domain._objectClass):
866
+ yield key, attr
867
+ return
868
+
869
+ def isInStreaming(self):
870
+ # For the moment let's assume a protocol is in streaming
871
+ # if at least one of the output sets is in STREAM_OPEN state
872
+ for paramName, attr in self.iterOutputAttributes():
873
+ if isinstance(attr, Set):
874
+ if attr.isStreamOpen():
875
+ return True
876
+ return False
877
+
878
+ @classmethod
879
+ def worksInStreaming(cls):
880
+ # A protocol should work in streaming if it implements the stepCheck()
881
+ # Get the stepCheck method from the Protocol
882
+ baseStepCheck = Protocol._stepsCheck
883
+ ownStepCheck = cls._stepsCheck
884
+
885
+ return not pwutils.isSameFunction(baseStepCheck, ownStepCheck)
886
+
887
+ def allowsGpu(self):
888
+ """ Returns True if this protocol allows GPU computation. """
889
+ return self.hasAttribute(GPU_LIST)
890
+
891
+ def requiresGpu(self):
892
+ """ Return True if this protocol can only be executed in GPU. """
893
+ return self.allowsGpu() and not self.hasAttribute(USE_GPU)
894
+
895
+ def usesGpu(self):
896
+ return self.allowsGpu() and self.getAttributeValue(USE_GPU, True)
897
+
898
+ def getGpuList(self):
899
+ if not self.allowsGpu():
900
+ return []
901
+
902
+ return pwutils.getListFromRangeString(self.gpuList.get())
903
+
904
+ def getOutputsSize(self):
905
+ return sum(1 for _ in self.iterOutputAttributes())
906
+
907
+ def getOutputFiles(self):
908
+ """ Return the output files produced by this protocol.
909
+ This can be used in web to download results back.
910
+ """
911
+ # By default return the output file of each output attribute
912
+ s = set()
913
+
914
+ for _, attr in self.iterOutputAttributes():
915
+ s.update(attr.getFiles())
916
+
917
+ return s
918
+
919
+ def copyDefinitionAttributes(self, other):
920
+ """ Copy definition attributes to other protocol. """
921
+ for paramName, _ in self.iterDefinitionAttributes():
922
+ self.copyAttributes(other, paramName)
923
+
924
+ def _createVarsFromDefinition(self, **kwargs):
925
+ """ This function will setup the protocol instance variables
926
+ from the Protocol Class definition, taking into account
927
+ the variable type and default values.
928
+ """
929
+ if hasattr(self, '_definition'):
930
+ for paramName, param in self._definition.iterParams():
931
+ # Create the var with value coming from kwargs or from
932
+ # the default param definition
933
+ try:
934
+ value = kwargs.get(paramName, param.default.get())
935
+ var = param.paramClass(value=value)
936
+ setattr(self, paramName, var)
937
+ except Exception as e:
938
+ raise ValueError("Can't create parameter '%s' and set it to %s" %
939
+ (paramName, value)) from e
940
+ else:
941
+ print("FIXME: Protocol '%s' has not DEFINITION"
942
+ % self.getClassName())
943
+
944
+ def _getFileName(self, key, **kwargs):
945
+ """ This function will retrieve filenames given a key and some
946
+ keywords arguments. The __filenamesDict attribute should be
947
+ updated with templates that accept the given keys.
948
+ """
949
+ return self.__filenamesDict[key] % kwargs
950
+
951
+ def _updateFilenamesDict(self, fnDict):
952
+ """ Update the dictionary with templates that will be used
953
+ by the _getFileName function.
954
+ """
955
+ self.__filenamesDict.update(fnDict)
956
+
957
+ def _store(self, *objs):
958
+ """ Stores objects of the protocol using the mapper.
959
+ If not objects are passed, the whole protocol is stored.
960
+ """
961
+ if self.mapper is not None:
962
+ with self._lock: # _lock is now a Rlock object (recursive locks)
963
+ if len(objs) == 0:
964
+ self.mapper.store(self)
965
+ else:
966
+ for obj in objs:
967
+ self.mapper.store(obj)
968
+ self.mapper.commit()
969
+
970
+ def _insertChild(self, key, child):
971
+ """ Insert a new child not stored previously.
972
+ If stored previously, _store should be used.
973
+ The child will be set as self.key attribute
974
+ """
975
+ try:
976
+ setattr(self, key, child)
977
+ if self.hasObjId():
978
+ self.mapper.insertChild(self, key, child)
979
+ except Exception as ex:
980
+ print("Error with child '%s', value=%s, type=%s"
981
+ % (key, child, type(child)))
982
+ raise ex
983
+
984
+ def _deleteChild(self, key, child):
985
+ """ Delete a child from the mapper. """
986
+ self.mapper.delete(child)
987
+
988
+ def _insertAllSteps(self):
989
+ """ Define all the steps that will be executed. """
990
+ pass
991
+
992
+ def _defineParams(self, form):
993
+ """ Define the input parameters that will be used.
994
+ Params:
995
+ form: this is the form to be populated with sections and params.
996
+ """
997
+ pass
998
+
999
+ def __insertStep(self, step, **kwargs):
1000
+ """ Insert a new step in the list.
1001
+
1002
+ :param prerequisites: a single integer or a list with the steps index that need to be done
1003
+ previous to the current one."""
1004
+ prerequisites = kwargs.get('prerequisites', None)
1005
+
1006
+ if prerequisites is None:
1007
+ if len(self._steps):
1008
+ # By default add the previous step as prerequisite
1009
+ step.addPrerequisites(len(self._steps))
1010
+ else:
1011
+ # Allow passing just an id
1012
+ if not isinstance(prerequisites, list):
1013
+ prerequisites = [prerequisites]
1014
+
1015
+ step.addPrerequisites(*prerequisites)
1016
+
1017
+ self._steps.append(step)
1018
+ self._newSteps = True
1019
+ # Setup and return step index
1020
+ step.setIndex(len(self._steps))
1021
+
1022
+ return step.getIndex()
1023
+
1024
+ def setRunning(self):
1025
+ """ Do not reset the init time in RESUME_MODE"""
1026
+ previousStart = self.initTime.get()
1027
+ super().setRunning()
1028
+ if self.getRunMode() == MODE_RESUME and previousStart is not None:
1029
+ self.initTime.set(previousStart)
1030
+ else:
1031
+ self._cpuTime.set(0)
1032
+
1033
+ def setAborted(self):
1034
+ """ Abort the protocol, finalize the steps and close all open sets"""
1035
+ try:
1036
+ super().setAborted()
1037
+ self._updateSteps(lambda step: step.setAborted(), where="status='%s'" % STATUS_RUNNING)
1038
+ self._closeOutputSet()
1039
+ except Exception as e:
1040
+ print("An error occurred aborting the protocol (%s)" % e)
1041
+
1042
+ def setFailed(self, msg):
1043
+ """ Set the run failed and close all open sets. """
1044
+ super().setFailed(msg)
1045
+ self._closeOutputSet()
1046
+
1047
+ def _finalizeStep(self, status, msg=None):
1048
+ """ Closes the step and setting up the protocol process id """
1049
+ super()._finalizeStep(status, msg)
1050
+ self._pid.set(None)
1051
+
1052
+ def _updateSteps(self, updater, where="1"):
1053
+ """Set the status of all steps
1054
+ :parameter updater callback/lambda receiving a step and editing it inside
1055
+ :parameter where condition to filter the set with."""
1056
+ stepsSet = StepSet(filename=self.getStepsFile())
1057
+ for step in stepsSet.iterItems(where=where):
1058
+ updater(step)
1059
+ stepsSet.update(step)
1060
+ stepsSet.write()
1061
+ stepsSet.close() # Close the connection
1062
+
1063
+ def getPath(self, *paths):
1064
+ """ Same as _getPath but without underscore. """
1065
+ return self._getPath(*paths)
1066
+
1067
+ def _getPath(self, *paths):
1068
+ """ Return a path inside the workingDir. """
1069
+ return os.path.join(self.workingDir.get(), *paths)
1070
+
1071
+ def _getExtraPath(self, *paths):
1072
+ """ Return a path inside the extra folder. """
1073
+ return self._getPath("extra", *paths)
1074
+
1075
+ def _getTmpPath(self, *paths):
1076
+ """ Return a path inside the tmp folder. """
1077
+ return self._getPath("tmp", *paths)
1078
+
1079
+ def _getLogsPath(self, *paths):
1080
+ return self._getPath("logs", *paths)
1081
+
1082
+ def _getRelPath(self, *path):
1083
+ """ Return a relative path from the workingDir. """
1084
+ return os.path.relpath(self._getPath(*path), self.workingDir.get())
1085
+
1086
+ def _getRelPathExecutionDir(self, *path):
1087
+ """ Return a relative path from the projdir. """
1088
+ # TODO must be a bettis
1089
+ return os.path.relpath(
1090
+ self._getPath(*path),
1091
+ os.path.dirname(os.path.dirname(self.workingDir.get()))
1092
+ )
1093
+
1094
+ def _getBasePath(self, path):
1095
+ """ Take the basename of the path and get the path
1096
+ relative to working dir of the protocol.
1097
+ """
1098
+ return self._getPath(os.path.basename(path))
1099
+
1100
+ def _insertFunctionStep(self, func, *funcArgs, **kwargs):
1101
+ """
1102
+ Params:
1103
+ func: the function itself or, optionally, the name (string) of the function to be run in the Step.
1104
+ *funcArgs: the variable list of arguments to pass to the function.
1105
+ **kwargs: see __insertStep
1106
+ """
1107
+ if isinstance(func, str):
1108
+ # Get the function give its name
1109
+ func = getattr(self, func, None)
1110
+
1111
+ # Ensure the protocol instance have it and is callable
1112
+ if not func:
1113
+ raise Exception("Protocol._insertFunctionStep: '%s' function is "
1114
+ "not member of the protocol" % func)
1115
+ if not callable(func):
1116
+ raise Exception("Protocol._insertFunctionStep: '%s' is not callable"
1117
+ % func)
1118
+ step = FunctionStep(func, func.__name__, *funcArgs, **kwargs)
1119
+
1120
+ return self.__insertStep(step, **kwargs)
1121
+
1122
+ def _insertRunJobStep(self, progName, progArguments, resultFiles=[],
1123
+ **kwargs):
1124
+ """ Insert an Step that will simple call runJob function
1125
+ **args: see __insertStep
1126
+ """
1127
+ return self._insertFunctionStep('runJob', progName, progArguments,
1128
+ **kwargs)
1129
+
1130
+ def _insertCopyFileStep(self, sourceFile, targetFile, **kwargs):
1131
+ """ Shortcut function to insert an step for copying a file to a destiny. """
1132
+ step = FunctionStep(pwutils.copyFile, 'copyFile', sourceFile,
1133
+ targetFile,
1134
+ **kwargs)
1135
+ return self.__insertStep(step, **kwargs)
1136
+
1137
+ def _enterDir(self, path):
1138
+ """ Enter into a new directory path and store the current path.
1139
+ The current path will be used in _leaveDir, but nested _enterDir
1140
+ are not allowed since self._currentDir is overwritten.
1141
+ """
1142
+ self._currentDir = os.getcwd()
1143
+ os.chdir(path)
1144
+ if self._log:
1145
+ self._log.info("Entered into dir: cd '%s'" % path)
1146
+
1147
+ def _leaveDir(self):
1148
+ """ This method should be called after a call to _enterDir
1149
+ to return to the previous location.
1150
+ """
1151
+ os.chdir(self._currentDir)
1152
+ if self._log:
1153
+ self._log.info("Returned to dir: cd '%s'" % self._currentDir)
1154
+
1155
+ def _enterWorkingDir(self):
1156
+ """ Change to the protocol working dir. """
1157
+ self._enterDir(self.workingDir.get())
1158
+
1159
+ def _leaveWorkingDir(self):
1160
+ """ This function make sense to use in conjunction
1161
+ with _enterWorkingDir to go back to execution path.
1162
+ """
1163
+ self._leaveDir()
1164
+
1165
+ def continueFromInteractive(self):
1166
+ """ TODO: REMOVE this function.
1167
+ Check if there is an interactive step and set
1168
+ as finished, this is used now mainly in picking,
1169
+ but we should remove this since is weird for users.
1170
+ """
1171
+ if os.path.exists(self.getStepsFile()):
1172
+ stepsSet = StepSet(filename=self.getStepsFile())
1173
+ for step in stepsSet:
1174
+ if step.getStatus() == STATUS_INTERACTIVE:
1175
+ step.setStatus(STATUS_FINISHED)
1176
+ stepsSet.update(step)
1177
+ break
1178
+ stepsSet.write()
1179
+ stepsSet.close() # Close the connection
1180
+
1181
+ def loadSteps(self):
1182
+ """ Load the Steps stored in the steps.sqlite file.
1183
+ """
1184
+ prevSteps = []
1185
+
1186
+ if os.path.exists(self.getStepsFile()):
1187
+ stepsSet = StepSet(filename=self.getStepsFile())
1188
+ for step in stepsSet:
1189
+ prevSteps.append(step.clone())
1190
+ stepsSet.close() # Close the connection
1191
+ return prevSteps
1192
+
1193
+ def _insertPreviousSteps(self):
1194
+ """ Insert steps of previous execution.
1195
+ It can be used to track previous steps done for
1196
+ protocol that allow some kind of continue (such as ctf estimation).
1197
+ """
1198
+ for step in self.loadSteps():
1199
+ self.__insertStep(step)
1200
+
1201
+ def __findStartingStep(self):
1202
+ """ From a previous run, compare self._steps and self._prevSteps
1203
+ to find which steps we need to start at, skipping successful done
1204
+ and not changed steps. Steps that needs to be done, will be deleted
1205
+ from the previous run storage.
1206
+ """
1207
+ if self.runMode == MODE_RESTART:
1208
+ self._prevSteps = []
1209
+ return 0
1210
+
1211
+ self._prevSteps = self.loadSteps()
1212
+
1213
+ n = min(len(self._steps), len(self._prevSteps))
1214
+ self.debug("len(steps) %s len(prevSteps) %s "
1215
+ % (len(self._steps), len(self._prevSteps)))
1216
+
1217
+ for i in range(n):
1218
+ newStep = self._steps[i]
1219
+ oldStep = self._prevSteps[i]
1220
+ if (not oldStep.isFinished() or newStep != oldStep
1221
+ or not oldStep._postconditions()):
1222
+ if pw.Config.debugOn():
1223
+ self.info("Starting at step %d" % i)
1224
+ self.info(" Old step: %s, args: %s"
1225
+ % (oldStep.funcName, oldStep.argsStr))
1226
+ self.info(" New step: %s, args: %s"
1227
+ % (newStep.funcName, newStep.argsStr))
1228
+ self.info(" not oldStep.isFinished(): %s"
1229
+ % (not oldStep.isFinished()))
1230
+ self.info(" newStep != oldStep: %s"
1231
+ % (newStep != oldStep))
1232
+ self.info(" not oldStep._postconditions(): %s"
1233
+ % (not oldStep._postconditions()))
1234
+ return i
1235
+ newStep.copy(oldStep)
1236
+
1237
+ return n
1238
+
1239
+ def _storeSteps(self):
1240
+ """ Store the new steps list that can be retrieved
1241
+ in further execution of this protocol.
1242
+ """
1243
+ stepsFn = self.getStepsFile()
1244
+
1245
+ self._stepsSet = StepSet(filename=stepsFn)
1246
+ self._stepsSet.setStore(False)
1247
+ self._stepsSet.clear()
1248
+
1249
+ for step in self._steps:
1250
+ step.cleanObjId()
1251
+ self.setInteractive(self.isInteractive() or step.isInteractive())
1252
+ self._stepsSet.append(step)
1253
+
1254
+ self._stepsSet.write()
1255
+
1256
+ def __updateStep(self, step):
1257
+ """ Store a given step and write changes. """
1258
+ self._stepsSet.update(step)
1259
+ self._stepsSet.write()
1260
+
1261
+ def _stepStarted(self, step):
1262
+ """This function will be called whenever an step
1263
+ has started running.
1264
+ """
1265
+ self.info(pwutils.magentaStr("STARTED") + ": %s, step %d, time %s" %
1266
+ (step.funcName.get(), step._index, step.initTime.datetime()),
1267
+ extra=getExtraLogInfo("PROTOCOL", STATUS.START,
1268
+ project_name=self.getProject().getName(),
1269
+ prot_id=self.getObjId(),
1270
+ prot_name=self.getClassName(),
1271
+ step_id=step._index))
1272
+ self.__updateStep(step)
1273
+
1274
+ def _stepFinished(self, step):
1275
+ """This function will be called whenever an step
1276
+ has finished its run.
1277
+ """
1278
+ doContinue = True
1279
+ if step.isInteractive():
1280
+ doContinue = False
1281
+ elif step.isFailed():
1282
+ doContinue = False
1283
+ errorMsg = pwutils.redStr(
1284
+ "Protocol failed: " + step.getErrorMessage())
1285
+ self.setFailed(errorMsg)
1286
+ self.error(errorMsg)
1287
+ self.lastStatus = step.getStatus()
1288
+
1289
+ self.__updateStep(step)
1290
+ self._stepsDone.increment()
1291
+ self._cpuTime.set(self._cpuTime.get() + step.getElapsedTime().total_seconds())
1292
+ self._store(self._stepsDone, self._cpuTime)
1293
+
1294
+ self.info(pwutils.magentaStr(step.getStatus().upper()) + ": %s, step %d, time %s"
1295
+ % (step.funcName.get(), step._index, step.endTime.datetime()),
1296
+ extra=getExtraLogInfo("PROTOCOL",STATUS.STOP,
1297
+ project_name=self.getProject().getName(),
1298
+ prot_id=self.getObjId(),
1299
+ prot_name=self.getClassName(),
1300
+ step_id=step._index))
1301
+ if step.isFailed() and self.stepsExecutionMode == STEPS_PARALLEL:
1302
+ # In parallel mode the executor will exit to close
1303
+ # all working threads, so we need to close
1304
+ self._endRun()
1305
+ return doContinue
1306
+
1307
+ def _stepsCheck(self):
1308
+ pass
1309
+
1310
+ def _runSteps(self, startIndex):
1311
+ """ Run all steps defined in self._steps. """
1312
+ self._stepsDone.set(startIndex)
1313
+ self._numberOfSteps.set(len(self._steps))
1314
+ self.setRunning()
1315
+ # Keep the original value to set in sub-protocols
1316
+ self._originalRunMode = self.runMode.get()
1317
+ # Always set to resume, even if set to restart
1318
+ self.runMode.set(MODE_RESUME)
1319
+ self._store()
1320
+
1321
+ if startIndex == len(self._steps):
1322
+ self.lastStatus = STATUS_FINISHED
1323
+ self.info("All steps seem to be FINISHED, nothing to be done.")
1324
+ else:
1325
+ self.lastStatus = self.status.get()
1326
+ self._stepsExecutor.runSteps(self._steps,
1327
+ self._stepStarted,
1328
+ self._stepFinished,
1329
+ self._stepsCheck,
1330
+ self._stepsCheckSecs)
1331
+
1332
+ print("*** Last status is %s " % self.lastStatus)
1333
+ self.setStatus(self.lastStatus)
1334
+ self._store(self.status)
1335
+
1336
+ def __deleteOutputs(self):
1337
+ """ This function should only be used from RESTART.
1338
+ It will remove output attributes from mapper and object.
1339
+ """
1340
+ attributes = [a[0] for a in self.iterOutputAttributes()]
1341
+
1342
+ for attrName in attributes:
1343
+ attr = getattr(self, attrName)
1344
+ self.mapper.delete(attr)
1345
+ delattr(self, attrName)
1346
+
1347
+ self._outputs.clear()
1348
+ self.mapper.store(self._outputs)
1349
+
1350
+ def findAttributeName(self, attr2Find):
1351
+ for attrName, attr in self.iterOutputAttributes():
1352
+ if attr.getObjId() == attr2Find.getObjId():
1353
+ return attrName
1354
+ return None
1355
+
1356
+ def deleteOutput(self, output):
1357
+ attrName = self.findAttributeName(output)
1358
+ self.mapper.delete(output)
1359
+ delattr(self,attrName)
1360
+ if attrName in self._outputs:
1361
+ self._outputs.remove(attrName)
1362
+ self.mapper.store(self._outputs)
1363
+ self.mapper.commit()
1364
+
1365
+ def __copyRelations(self, other):
1366
+ """ This will copy relations from protocol other to self """
1367
+ pass
1368
+
1369
+ def copy(self, other, copyId=True, excludeInputs=False):
1370
+ """
1371
+ Copies its attributes into the passed protocol
1372
+
1373
+ :param other: protocol instance to copt the attributes to
1374
+ :param copyId: True (default) copies the identifier
1375
+ :param excludeInputs: False (default). If true input attributes are excluded
1376
+
1377
+ """
1378
+
1379
+ # Input attributes list
1380
+ inputAttributes = []
1381
+
1382
+ # If need to exclude input attributes
1383
+ if excludeInputs:
1384
+ # Get all the input attributes, to be ignored at copy():
1385
+ for key, attr in self.iterInputAttributes():
1386
+ inputAttributes.append(key)
1387
+
1388
+ copyDict = Object.copy(self, other, copyId, inputAttributes)
1389
+ self._store()
1390
+ self.mapper.deleteRelations(self)
1391
+
1392
+ for r in other.getRelations():
1393
+ rName = r['name']
1394
+ rCreator = r['parent_id']
1395
+ rParent = r[OBJECT_PARENT_ID]
1396
+ rChild = r['object_child_id']
1397
+ rParentExt = r['object_parent_extended']
1398
+ rChildExt = r['object_child_extended']
1399
+
1400
+ if rParent in copyDict:
1401
+ rParent = copyDict.get(rParent).getObjId()
1402
+
1403
+ if rChild in copyDict:
1404
+ rChild = copyDict.get(rChild).getObjId()
1405
+
1406
+ self.mapper.insertRelationData(rName, rCreator, rParent, rChild,
1407
+ rParentExt, rChildExt)
1408
+
1409
+ def getRelations(self):
1410
+ """ Return the relations created by this protocol. """
1411
+ return self.mapper.getRelationsByCreator(self)
1412
+
1413
+ def _defineRelation(self, relName, parentObj, childObj):
1414
+ """ Insert a new relation in the mapper using self as creator. """
1415
+ parentExt = None
1416
+ childExt = None
1417
+
1418
+ if parentObj.isPointer():
1419
+ parentExt = parentObj.getExtended()
1420
+ parentObj = parentObj.getObjValue()
1421
+
1422
+ if childObj.isPointer():
1423
+ childExt = childObj.getExtended()
1424
+ childObj = childObj.getObjValue()
1425
+
1426
+ self.mapper.insertRelation(relName, self, parentObj, childObj,
1427
+ parentExt, childExt)
1428
+
1429
+ def makePathsAndClean(self):
1430
+ """ Create the necessary path or clean
1431
+ if in RESTART mode.
1432
+ """
1433
+ # Clean working path if in RESTART mode
1434
+ if self.runMode == MODE_RESTART:
1435
+ self.cleanWorkingDir()
1436
+ self.__deleteOutputs()
1437
+ # Delete the relations created by this protocol
1438
+ # (delete this in both project and protocol db)
1439
+ self.mapper.deleteRelations(self)
1440
+ self.makeWorkingDir()
1441
+
1442
+ def cleanWorkingDir(self):
1443
+ """
1444
+ Delete all files and subdirectories related with the protocol
1445
+ """
1446
+ self.cleanTmp()
1447
+ pwutils.cleanPath(self._getPath())
1448
+
1449
+ def makeWorkingDir(self):
1450
+ # Create workingDir, logs and extra paths
1451
+ paths = [self._getPath(), self._getExtraPath(), self._getLogsPath()]
1452
+ pwutils.makePath(*paths)
1453
+ # Create scratch if SCIPION_SCRATCH environment variable exist.
1454
+ # In other case, tmp folder is created
1455
+ pwutils.makeTmpPath(self)
1456
+
1457
+ def cleanTmp(self):
1458
+ """ Delete all files and subdirectories under Tmp folder. """
1459
+ tmpFolder = self._getTmpPath()
1460
+
1461
+ if os.path.islink(tmpFolder):
1462
+ pwutils.cleanPath(os.path.realpath(tmpFolder))
1463
+ os.remove(tmpFolder)
1464
+ else:
1465
+ pwutils.cleanPath(tmpFolder)
1466
+
1467
+ self._cleanExtraFiles()
1468
+ def _cleanExtraFiles(self):
1469
+ """ This method will be called when the protocol finishes correctly.
1470
+ It is the responsibility of the protocols to implement this method to make extra cleanup
1471
+ of its folders, like iterations folder and files that are not needed when finished
1472
+ """
1473
+
1474
+ logger.info("Nothing to clean up")
1475
+ logger.debug('FOR DEVELOPERS: implement Protocol._cleanExtraFiles this protocol could'
1476
+ ' free up some space upon finishing.')
1477
+
1478
+ def _run(self):
1479
+ # Check that a proper Steps executor have been set
1480
+ if self._stepsExecutor is None:
1481
+ raise Exception('Protocol.run: Steps executor should be set before '
1482
+ 'running protocol')
1483
+ # Check the parameters are correct
1484
+ errors = self.validate()
1485
+ if len(errors):
1486
+ raise ValidationException(
1487
+ 'Protocol has validation errors:\n' + '\n'.join(errors))
1488
+
1489
+ self._insertAllSteps() # Define steps for execute later
1490
+ # Find at which step we need to start
1491
+ startIndex = self.__findStartingStep()
1492
+ self.info(" Starting at step: %d" % (startIndex + 1))
1493
+ self._storeSteps()
1494
+ self.info(" Running steps ")
1495
+ self._runSteps(startIndex)
1496
+
1497
+ def _getEnviron(self):
1498
+ """ This function should return an environ variable
1499
+ that will be used when running new programs.
1500
+ By default, the protocol will use the one defined
1501
+ in the package that it belongs or None.
1502
+ """
1503
+ return self.getClassPackage().Plugin.getEnviron()
1504
+
1505
+ def runJob(self, program, arguments, **kwargs):
1506
+ if self.stepsExecutionMode == STEPS_SERIAL:
1507
+ kwargs['numberOfMpi'] = kwargs.get('numberOfMpi',
1508
+ self.numberOfMpi.get())
1509
+ kwargs['numberOfThreads'] = kwargs.get('numberOfThreads',
1510
+ self.numberOfThreads.get())
1511
+ else:
1512
+ kwargs['numberOfMpi'] = kwargs.get('numberOfMpi', 1)
1513
+ kwargs['numberOfThreads'] = kwargs.get('numberOfThreads', 1)
1514
+ if 'env' not in kwargs:
1515
+ kwargs['env'] = self._getEnviron()
1516
+
1517
+ self._stepsExecutor.runJob(self._log, program, arguments, **kwargs)
1518
+
1519
+ def run(self):
1520
+ """ Before calling this method, the working dir for the protocol
1521
+ to run should exist.
1522
+ """
1523
+ try:
1524
+ action = "RUNNING" if self.runMode == MODE_RESTART else "RESUMING"
1525
+ self.info(pwutils.greenStr('%s PROTOCOL -----------------' % action))
1526
+ self.info("Protocol starts", extra=getExtraLogInfo("PROTOCOL", STATUS.START,
1527
+ project_name=self.getProject().getName(),
1528
+ prot_id=self.getObjId(),
1529
+ prot_name=self.getClassName()))
1530
+
1531
+ self.setHostFullName(pwutils.getHostFullName())
1532
+ self.info('Hostname: %s' % self.getHostFullName())
1533
+
1534
+ # Store the full machine name where the protocol is running
1535
+ # and also its PID
1536
+ if not self.useQueueForProtocol(): # Take as reference the pID
1537
+ self.setPid(os.getpid())
1538
+ self.info('PID: %s' % self.getPid())
1539
+ else: # Take as reference the jobID
1540
+ self.info('Executing through the queue system')
1541
+ self.info('JOBID: %s' % self.getJobIds())
1542
+
1543
+ self.info('pyworkflow: %s' % pw.__version__)
1544
+ plugin = self.getPlugin()
1545
+ self.info('plugin: %s - %s' % (plugin.getName(), plugin.getUrl()))
1546
+ package = self.getClassPackage()
1547
+ if hasattr(package, "__version__"):
1548
+ self.info('plugin v: %s%s' %(package.__version__, ' (devel)' if plugin.inDevelMode() else '(production)'))
1549
+ self.info('plugin binary v: %s' % plugin.getActiveVersion())
1550
+ self.info('currentDir: %s' % os.getcwd())
1551
+ self.info('workingDir: %s' % self.workingDir)
1552
+ self.info('runMode: %s' % MODE_CHOICES[self.runMode.get()])
1553
+ try:
1554
+ self.info(' MPI: %d' % self.numberOfMpi)
1555
+ self.info(' threads: %d' % self.numberOfThreads)
1556
+ except Exception as e:
1557
+ self.info(' * Cannot get information about MPI/threads (%s)' % e)
1558
+ # Something went wrong ans at this point status is launched. We mark it as failed.
1559
+ except Exception as e:
1560
+ print(e)
1561
+ self.setFailed(str(e))
1562
+ self._store(self.status, self.getError())
1563
+ self._endRun()
1564
+ return
1565
+
1566
+ Step.run(self)
1567
+ if self.isFailed():
1568
+ self._store()
1569
+ self._endRun()
1570
+
1571
+ def _endRun(self):
1572
+ """ Print some ending message and close some files. """
1573
+ # self._store()
1574
+ self._store(self.summaryVar)
1575
+ self._store(self.methodsVar)
1576
+ self._store(self.endTime)
1577
+
1578
+ if pwutils.envVarOn(pw.SCIPION_DEBUG_NOCLEAN):
1579
+ self.warning('Not cleaning temp folder since '
1580
+ '%s is set to True.' % pw.SCIPION_DEBUG_NOCLEAN)
1581
+ elif not self.isFailed():
1582
+ self.info('Cleaning temp folder....')
1583
+ self.cleanTmp()
1584
+
1585
+ self.info(pwutils.greenStr('------------------- PROTOCOL ' +
1586
+ self.getStatusMessage().upper()),
1587
+ extra=getExtraLogInfo("PROTOCOL",STATUS.STOP,
1588
+ project_name=self.getProject().getName(),
1589
+ prot_id=self.getObjId(),
1590
+ prot_name=self.getClassName()))
1591
+
1592
+
1593
+ def getLogPaths(self):
1594
+ return [self.getStdoutLog(),self.getStderrLog() , self.getScheduleLog()]
1595
+
1596
+ def getStdoutLog(self):
1597
+ return self._getLogsPath("run.stdout")
1598
+
1599
+ def getStderrLog(self):
1600
+ return self._getLogsPath('run.stderr')
1601
+
1602
+ def getScheduleLog(self):
1603
+ return self._getLogsPath('schedule.log')
1604
+
1605
+ def getSteps(self):
1606
+ """ Return the steps.sqlite file under logs directory. """
1607
+ return self._steps
1608
+
1609
+ def getStepsFile(self):
1610
+ """ Return the steps.sqlite file under logs directory. """
1611
+ return self._getLogsPath('steps.sqlite')
1612
+
1613
+
1614
+ def _addChunk(self, txt, fmt=None):
1615
+ """
1616
+ Add text txt to self._buffer, with format fmt.
1617
+ fmt can be a color (like 'red') or a link that looks like 'link:url'.
1618
+ """
1619
+ # Make the text html-safe first.
1620
+ for x, y in [('&', 'amp'), ('<', 'lt'), ('>', 'gt')]:
1621
+ txt = txt.replace(x, '&%s;' % y)
1622
+
1623
+ if fmt is None:
1624
+ self._buffer += txt
1625
+ elif fmt.startswith('link:'):
1626
+ url = fmt[len('link:'):]
1627
+ # Add the url in the TWiki style
1628
+ if url.startswith('http://'):
1629
+ self._buffer += '[[%s][%s]]' % (url, txt)
1630
+ # Web does not exist, webtools must find a solution for this case.
1631
+ # else:
1632
+ # from pyworkflow.web.pages import settings as django_settings
1633
+ # absolute_url = django_settings.ABSOLUTE_URL
1634
+ # self._buffer += '[[%s/get_log/?path=%s][%s]]' % (absolute_url,
1635
+ # url, txt)
1636
+ else:
1637
+ self._buffer += '<font color="%s">%s</font>' % (fmt, txt)
1638
+
1639
+ def getLogsAsStrings(self):
1640
+
1641
+ outputs = []
1642
+ for fname in self.getLogPaths():
1643
+ if pwutils.exists(fname):
1644
+ self._buffer = ''
1645
+ pwutils.renderTextFile(fname, self._addChunk)
1646
+ outputs.append(self._buffer)
1647
+ else:
1648
+ outputs.append('File "%s" does not exist' % fname)
1649
+ return outputs
1650
+
1651
+ def getLogsLastLines(self, lastLines=None, logFile=0):
1652
+ """
1653
+ Get the last(lastLines) lines of a log file.
1654
+
1655
+ :param lastLines, if None, will try 'PROT_LOGS_LAST_LINES' env variable, otherwise 20
1656
+ :param logFile: Log file to take the lines from, default = 0 (std.out). 1 for stdErr.
1657
+ """
1658
+ if not lastLines:
1659
+ lastLines = int(os.environ.get('PROT_LOGS_LAST_LINES', 20))
1660
+
1661
+ # Get stdout
1662
+ stdoutFn =self.getLogPaths()[logFile]
1663
+
1664
+ if not os.path.exists(stdoutFn):
1665
+ return []
1666
+
1667
+ with open(stdoutFn, 'r') as stdout:
1668
+
1669
+ iterlen = lambda it: sum(1 for _ in it)
1670
+ numLines = iterlen(stdout)
1671
+
1672
+ lastLines = min(lastLines, numLines)
1673
+ sk = numLines - lastLines
1674
+ sk = max(sk, 0)
1675
+
1676
+ stdout.seek(0, 0)
1677
+ output = [l.strip('\n') for k, l in enumerate(stdout)
1678
+ if k >= sk]
1679
+ return output
1680
+
1681
+ def warning(self, message, redirectStandard=True):
1682
+ self._log.warning(message)
1683
+
1684
+ def info(self, message, extra=None):
1685
+ self._log.info(message, extra= extra)
1686
+
1687
+ def error(self, message, redirectStandard=True):
1688
+ self._log.error(message)
1689
+
1690
+ def debug(self, message):
1691
+ self._log.debug(message)
1692
+
1693
+ def getWorkingDir(self):
1694
+ return self.workingDir.get()
1695
+
1696
+ def setWorkingDir(self, path):
1697
+ self.workingDir.set(path)
1698
+
1699
+ def setMapper(self, mapper):
1700
+ """ Set a new mapper for the protocol to persist state. """
1701
+ self.mapper = mapper
1702
+
1703
+ def getMapper(self):
1704
+ return self.mapper
1705
+
1706
+ def getDbPath(self):
1707
+ return self._getLogsPath('run.db')
1708
+
1709
+ def setStepsExecutor(self, executor=None):
1710
+ if executor is None:
1711
+ executor = StepExecutor(self.getHostConfig())
1712
+
1713
+ self._stepsExecutor = executor
1714
+ self._stepsExecutor.setProtocol(self) # executor needs the protocol to store the jobs Ids submitted to a queue
1715
+
1716
+ def getFiles(self):
1717
+ resultFiles = set()
1718
+ for paramName, _ in self.getDefinition().iterPointerParams():
1719
+ # Get all self attribute that are pointers
1720
+ attrPointer = getattr(self, paramName)
1721
+ obj = attrPointer.get() # Get object pointer by the attribute
1722
+ if hasattr(obj, 'getFiles'):
1723
+ resultFiles.update(obj.getFiles()) # Add files if any
1724
+ return resultFiles | pwutils.getFiles(self.workingDir.get())
1725
+
1726
+ def getHostName(self):
1727
+ """ Get the execution host name.
1728
+ This value is only the key of the host in the configuration file.
1729
+ """
1730
+ return self.hostName.get()
1731
+
1732
+ def setHostName(self, hostName):
1733
+ """ Set the execution host name (the host key in the config file) """
1734
+ self.hostName.set(hostName)
1735
+
1736
+ def getHostFullName(self):
1737
+ """ Return the full machine name where the protocol is running. """
1738
+ return self.hostFullName.get()
1739
+
1740
+ def setHostFullName(self, hostFullName):
1741
+ self.hostFullName.set(hostFullName)
1742
+
1743
+ def getHostConfig(self):
1744
+ """ Return the configuration host. """
1745
+ return self.hostConfig
1746
+
1747
+ def setHostConfig(self, config):
1748
+ self.hostConfig = config
1749
+ # Never store the host config as part of the protocol, it is kept
1750
+ # in the configuration information, the hostname is enough
1751
+ self.hostConfig.setStore(False)
1752
+
1753
+ def getJobIds(self):
1754
+ """ Return an iterable list of jobs Ids associated to a running protocol. """
1755
+ return self._jobId
1756
+
1757
+ def setJobId(self, jobId):
1758
+ " Reset this list to have the first active job "
1759
+ self._jobId.clear()
1760
+ self.appendJobId(jobId)
1761
+
1762
+ def setJobIds(self, jobIds):
1763
+ " Reset this list to have a list of active jobs "
1764
+ self._jobId = jobIds
1765
+
1766
+ def appendJobId(self, jobId):
1767
+ " Append active jobs to the list "
1768
+ self._jobId.append(jobId)
1769
+ def removeJobId(self, jobId):
1770
+ " Remove inactive jobs from the list "
1771
+ self._jobId.remove(jobId)
1772
+
1773
+ def getPid(self):
1774
+ return self._pid.get()
1775
+
1776
+ def setPid(self, pid):
1777
+ self._pid.set(pid)
1778
+
1779
+ def getRunName(self):
1780
+ runName = self.getObjLabel().strip()
1781
+ if not len(runName):
1782
+ runName = self.getDefaultRunName()
1783
+ return runName
1784
+
1785
+ def getDefaultRunName(self):
1786
+ return '%s.%s' % (self.getClassName(), self.strId())
1787
+
1788
+ @classmethod
1789
+ def getClassPackage(cls):
1790
+ """ Return the package module to which this protocol belongs.
1791
+ This function will only work, if for the given Domain, the
1792
+ method Domain.getProtocols() has been called once. After calling
1793
+ this method the protocol classes are registered with it Plugin
1794
+ and Domain info.
1795
+ """
1796
+ return cls._package
1797
+
1798
+ @classmethod
1799
+ def getClassPlugin(cls):
1800
+
1801
+ logger.warning("Deprecated on 04-2023. Use Protocol.getPlugin instead.")
1802
+ return cls.getPlugin()
1803
+
1804
+ @classmethod
1805
+ def getPlugin(cls):
1806
+ return cls._plugin
1807
+ @classmethod
1808
+ def getClassPackageName(cls):
1809
+ return cls.getClassPackage().__name__ if cls.getClassPackage() else "orphan"
1810
+
1811
+ @classmethod
1812
+ def getClassDomain(cls):
1813
+ """ Return the Domain class where this Protocol class is defined. """
1814
+ return pw.Config.getDomain()
1815
+
1816
+ @classmethod
1817
+ def getPluginLogoPath(cls):
1818
+ package = cls.getClassPackage()
1819
+ logo = getattr(package, '_logo', None)
1820
+ if logo:
1821
+ logoPath = (pw.findResource(logo) or
1822
+ os.path.join(os.path.abspath(os.path.dirname(package.__file__)), logo))
1823
+ else:
1824
+ logoPath = None
1825
+
1826
+ return logoPath
1827
+
1828
+ @classmethod
1829
+ def validatePackageVersion(cls, varName, errors):
1830
+ """
1831
+ Function to validate the package version specified in
1832
+ configuration file ~/.config/scipion/scipion.conf is among the available
1833
+ options and it is properly installed.
1834
+
1835
+ :param package: the package object (ej: eman2 or relion). Package should contain the
1836
+ following methods: getVersion(), getSupportedVersions()
1837
+ :param varName: the expected environment var containing the path (and version)
1838
+ :param errors: list of strings to add errors if found
1839
+
1840
+ """
1841
+ package = cls.getClassPackage()
1842
+ packageName = cls.getClassPackageName()
1843
+ varValue = package.Plugin.getVar(varName)
1844
+ versions = ','.join(package.Plugin.getSupportedVersions())
1845
+
1846
+ errorMsg = None
1847
+
1848
+ if not package.Plugin.getActiveVersion():
1849
+ errors.append("We could not detect *%s* version. " % packageName)
1850
+ errorMsg = "The path value should contains a valid version (%s)." % versions
1851
+ elif not os.path.exists(varValue):
1852
+ errors.append("Path of %s does not exists." % varName)
1853
+ errorMsg = "Check installed packages and versions with command:\n "
1854
+ errorMsg += "*scipion install --help*"
1855
+
1856
+ if errorMsg:
1857
+ errors.append("%s = %s" % (varName, varValue))
1858
+ errors.append(
1859
+ "Please, modify %s value in the configuration file:" % varName)
1860
+ errors.append("*~/.config/scipion/scipion.conf*")
1861
+ errors.append(errorMsg)
1862
+ errors.append("After fixed, you NEED TO RESTART THE PROJECT WINDOW")
1863
+
1864
+ @classmethod
1865
+ def getClassLabel(cls, prependPackageName=True):
1866
+ """ Return a more readable string representing the protocol class """
1867
+ label = cls.__dict__.get('_label', cls.__name__)
1868
+ if prependPackageName:
1869
+ try:
1870
+ label = "%s - %s" % (cls.getPlugin().getName(), label)
1871
+ except Exception as e:
1872
+ label = "%s -%s" % ("missing", label)
1873
+ logger.error("Couldn't get the plugin name for %s" % label, exc_info=e)
1874
+ return label
1875
+
1876
+ @classmethod
1877
+ def isDisabled(cls):
1878
+ """ Return True if this Protocol is disabled.
1879
+ Disabled protocols will not be offered in the available protocols."""
1880
+ return False
1881
+
1882
+ @classmethod
1883
+ def isBase(cls):
1884
+ """ Return True if this Protocol is a base class.
1885
+ Base classes should be marked with _label = None.
1886
+ """
1887
+ return cls.__dict__.get('_label', None) is None
1888
+
1889
+ def getSubmitDict(self):
1890
+ """ Return a dictionary with the necessary keys to
1891
+ launch the job to a queue system.
1892
+ """
1893
+ queueName, queueParams = self.getQueueParams()
1894
+ hc = self.getHostConfig()
1895
+
1896
+ script = self._getLogsPath(hc.getSubmitPrefix() + self.strId() + '.job')
1897
+ d = {'JOB_SCRIPT': script,
1898
+ 'JOB_LOGS': self._getLogsPath(hc.getSubmitPrefix() + self.strId()),
1899
+ 'JOB_NODEFILE': os.path.abspath(script.replace('.job', '.nodefile')),
1900
+ 'JOB_NAME': self.strId(),
1901
+ 'JOB_QUEUE': queueName,
1902
+ 'JOB_NODES': self.numberOfMpi.get(),
1903
+ 'JOB_THREADS': self.numberOfThreads.get(),
1904
+ 'JOB_CORES': self.numberOfMpi.get() * self.numberOfThreads.get(),
1905
+ 'JOB_HOURS': 72,
1906
+ 'GPU_COUNT': len(self.getGpuList()),
1907
+ 'QUEUE_FOR_JOBS': 'N'
1908
+ }
1909
+ d.update(queueParams)
1910
+ return d
1911
+
1912
+ def useQueue(self):
1913
+ """ Return True if the protocol should be launched through a queue. """
1914
+ return self._useQueue.get()
1915
+
1916
+ def useQueueForSteps(self):
1917
+ """ This function will return True if the protocol has been set
1918
+ to be launched through a queue by steps """
1919
+ return self.useQueue() and (self.getSubmitDict()["QUEUE_FOR_JOBS"] == "Y")
1920
+
1921
+ def useQueueForProtocol(self):
1922
+ """ This function will return True if the protocol has been set
1923
+ to be launched through a queue """
1924
+ return self.useQueue() and (self.getSubmitDict()["QUEUE_FOR_JOBS"] == "N")
1925
+
1926
+ def getQueueParams(self):
1927
+ if self._queueParams.hasValue():
1928
+ return json.loads(self._queueParams.get())
1929
+ else:
1930
+ return '', {}
1931
+
1932
+ def hasQueueParams(self):
1933
+ return self._queueParams.hasValue()
1934
+
1935
+ def setQueueParams(self, queueParams):
1936
+ self._queueParams.set(json.dumps(queueParams))
1937
+
1938
+ @property
1939
+ def numberOfSteps(self):
1940
+ return self._numberOfSteps.get(0)
1941
+
1942
+ @property
1943
+ def stepsDone(self):
1944
+ """ Return the number of steps executed. """
1945
+ return self._stepsDone.get(0)
1946
+
1947
+ @property
1948
+ def cpuTime(self):
1949
+ """ Return the sum of all durations of the finished steps"""
1950
+ return self._cpuTime.get()
1951
+
1952
+ def updateSteps(self):
1953
+ """ After the steps list is modified, this methods will update steps
1954
+ information. It will save the steps list and also the number of steps.
1955
+ """
1956
+ self._storeSteps()
1957
+ self._numberOfSteps.set(len(self._steps))
1958
+ self._store(self._numberOfSteps)
1959
+ self._newSteps = False
1960
+
1961
+ def getStatusMessage(self):
1962
+ """ Return the status string and if running the steps done.
1963
+ """
1964
+ msg = self.getStatus()
1965
+ if self.isRunning() or self.isAborted() or self.isFailed():
1966
+ msg += " (done %d/%d)" % (self.stepsDone, self.numberOfSteps)
1967
+
1968
+ return msg
1969
+
1970
+ def getRunMode(self):
1971
+ """ Return the mode of execution, either:
1972
+ MODE_RESTART or MODE_RESUME. """
1973
+ return self.runMode.get()
1974
+
1975
+ def hasSummaryWarnings(self):
1976
+ return len(self.summaryWarnings) != 0
1977
+
1978
+ def addSummaryWarning(self, warningDescription):
1979
+ """Appends the warningDescription param to the list of summaryWarnings.
1980
+ Will be printed in the protocol summary."""
1981
+ self.summaryWarnings.append(warningDescription)
1982
+ return self.summaryWarnings
1983
+
1984
+ def checkSummaryWarnings(self):
1985
+ """ Checks for warnings that we want to tell the user about by adding a
1986
+ warning sign to the run box and a description to the run summary.
1987
+ List of warnings checked:
1988
+ 1. If the folder for this protocol run exists.
1989
+ """
1990
+ if not self.isSaved() and not os.path.exists(self.workingDir.get()):
1991
+ self.addSummaryWarning("*Missing run data*: The directory for this "
1992
+ "run is missing, so it won't be possible to "
1993
+ "use its outputs in other protocols.")
1994
+
1995
+ def isContinued(self):
1996
+ """ Return if running in continue mode (MODE_RESUME). """
1997
+ return self.getRunMode() == MODE_RESUME
1998
+
1999
+ # Methods that should be implemented in subclasses
2000
+ def _validate(self):
2001
+ """ This function can be overwritten by subclasses.
2002
+ Used from the public validate function.
2003
+ """
2004
+ return []
2005
+
2006
+ @classmethod
2007
+ def getUrl(cls):
2008
+ return cls.getPlugin().getUrl(cls)
2009
+
2010
+ @classmethod
2011
+ def isInstalled(cls):
2012
+ # We a consider a protocol installed if there are not errors
2013
+ # from the _validateInstallation function
2014
+ return not cls.validateInstallation()
2015
+
2016
+ @classmethod
2017
+ def validateInstallation(cls):
2018
+ """ Check if the installation of this protocol is correct.
2019
+ By default, we will check if the protocols' package provide a
2020
+ validateInstallation function and use it.
2021
+ Returning an empty list means that the installation is correct
2022
+ and there are not errors. If some errors are found, a list with
2023
+ the error messages will be returned.
2024
+ """
2025
+ try:
2026
+ validateFunc = getattr(cls.getClassPackage().Plugin,
2027
+ 'validateInstallation', None)
2028
+
2029
+ return validateFunc() if validateFunc is not None else []
2030
+ except Exception as e:
2031
+ msg = str(e)
2032
+ msg += (" %s installation couldn't be validated. Possible cause "
2033
+ "could be a configuration issue. Try to run scipion "
2034
+ "config." % cls.__name__)
2035
+ print(msg)
2036
+ return [msg]
2037
+
2038
+ def validate(self):
2039
+ """ Check that input parameters are correct.
2040
+ Return a list with errors, if the list is empty, all was ok.
2041
+ """
2042
+ errors = []
2043
+ # Validate that all input pointer parameters have a value
2044
+ for paramName, param in self.getDefinition().iterParams():
2045
+ # Get all self attribute that are pointers
2046
+ attr = getattr(self, paramName)
2047
+ paramErrors = []
2048
+ condition = self.evalParamCondition(paramName)
2049
+ if attr.isPointer():
2050
+ obj = attr.get()
2051
+ if condition and obj is None and not param.allowsNull:
2052
+ paramErrors.append('cannot be EMPTY.')
2053
+ elif isinstance(attr, PointerList):
2054
+ # In this case allowsNull refers to not allowing empty items
2055
+ if not param.allowsNull:
2056
+ if len(attr) == 0:
2057
+ paramErrors.append('cannot be EMPTY.')
2058
+ # Consider empty pointers
2059
+ else:
2060
+ if any(pointer.get() is None for pointer in attr):
2061
+ paramErrors.append('Can not have EMPTY items.')
2062
+
2063
+ else:
2064
+ if condition:
2065
+ paramErrors = param.validate(attr.get())
2066
+ label = param.label.get()
2067
+ errors += ['*%s* %s' % (label, err) for err in paramErrors]
2068
+
2069
+ try:
2070
+ # Check that all ids specified in the 'Wait for' form entry
2071
+ # are valid protocol ids
2072
+ proj = self.getProject()
2073
+ for protId in self.getPrerequisites():
2074
+ try:
2075
+ prot = proj.getProtocol(int(protId))
2076
+ except Exception:
2077
+ prot = None
2078
+ if prot is None:
2079
+ errors.append('*%s* is not a valid protocol id.' % protId)
2080
+
2081
+ # Validate specific for the subclass
2082
+ installErrors = self.validateInstallation()
2083
+ if installErrors:
2084
+ errors += installErrors
2085
+ childErrors = self._validate()
2086
+ if childErrors:
2087
+ errors += childErrors
2088
+ except Exception:
2089
+ import urllib
2090
+ exceptionStr = pwutils.formatExceptionInfo()
2091
+ errors.append("Protocol validation failed. It usually happens because there are some "
2092
+ "input missing. Please check if the error message gives you any "
2093
+ "hint:\n{}".format(exceptionStr))
2094
+ return errors
2095
+
2096
+ def _warnings(self):
2097
+ """ Should be implemented in subclasses. See warning. """
2098
+ return []
2099
+
2100
+ def warnings(self):
2101
+ """ Return some message warnings that can be errors.
2102
+ User should approve to execute a protocol with warnings. """
2103
+ return self._warnings()
2104
+
2105
+ def _summary(self):
2106
+ """ Should be implemented in subclasses. See summary. """
2107
+ return ["No summary information."]
2108
+
2109
+ def summary(self):
2110
+ """ Return a summary message to provide some information to users. """
2111
+ try:
2112
+ baseSummary = self._summary() or ['No summary information.']
2113
+
2114
+ if isinstance(baseSummary, str):
2115
+ baseSummary = [baseSummary]
2116
+
2117
+ if not isinstance(baseSummary, list):
2118
+ raise Exception("Developers error: _summary() is not returning "
2119
+ "a list")
2120
+
2121
+ comments = self.getObjComment()
2122
+ if comments:
2123
+ baseSummary += ['', '*COMMENTS:* ', comments]
2124
+
2125
+ if self.getError().hasValue():
2126
+ baseSummary += ['', '*ERROR:*', self.getError().get()]
2127
+
2128
+ if self.summaryWarnings:
2129
+ baseSummary += ['', '*WARNINGS:*']
2130
+ baseSummary += self.summaryWarnings
2131
+
2132
+ except Exception as ex:
2133
+ baseSummary = [str(ex)]
2134
+
2135
+ return baseSummary
2136
+
2137
+ def getFileTag(self, fn):
2138
+ return "[[%s]]" % fn
2139
+
2140
+ def getObjectTag(self, objName):
2141
+ if isinstance(objName, str):
2142
+ obj = getattr(self, objName, None)
2143
+ else:
2144
+ obj = objName
2145
+
2146
+ if obj is None:
2147
+ return '*None*'
2148
+
2149
+ if obj.isPointer():
2150
+ obj = obj.get() # get the pointed object
2151
+ if obj is None:
2152
+ return '*None*'
2153
+
2154
+ return "[[sci-open:%s][%s]]" % (obj.getObjId(), obj.getNameId())
2155
+
2156
+ def _citations(self):
2157
+ """ Should be implemented in subclasses. See citations. """
2158
+ return getattr(self, "_references", [])
2159
+
2160
+ def __getPluginBibTex(self):
2161
+ """ Return the _bibtex from the package """
2162
+ return getattr(self.getClassPackage(), "_bibtex", {})
2163
+
2164
+ def _getCite(self, citeStr):
2165
+ bibtex = self.__getPluginBibTex()
2166
+ if citeStr in bibtex:
2167
+ text = self._getCiteText(bibtex[citeStr])
2168
+ else:
2169
+ text = "Reference with key *%s* not found." % citeStr
2170
+ return text
2171
+
2172
+ def _getCiteText(self, cite, useKeyLabel=False):
2173
+ try:
2174
+
2175
+ journal = cite.get("journal", cite.get("booktitle", ""))
2176
+ doi = cite.get("doi", "").strip()
2177
+ url = cite.get("url", "").strip()
2178
+ # Get the first author surname
2179
+ if useKeyLabel:
2180
+ label = cite['ID']
2181
+ else:
2182
+ label = cite['author'].split(' and ')[0].split(',')[0].strip()
2183
+ label += ' et al., %s, %s' % (journal, cite['year'])
2184
+ if len(doi) > 0:
2185
+ text = '[[%s][%s]] ' % (doi, label)
2186
+ elif len(url) > 0:
2187
+ text = '[[%s][%s]] ' % (url, label)
2188
+ else:
2189
+ text = label.strip()
2190
+ return text
2191
+
2192
+ except Exception as ex:
2193
+ print("Error with citation: " + label)
2194
+ print(ex)
2195
+ text = "Error with citation *%s*." % label
2196
+ return text
2197
+
2198
+ def __getCitations(self, citations):
2199
+ """ From the list of citations keys, obtains the full
2200
+ info from the package _bibtex dict.
2201
+ """
2202
+ bibtex = self.__getPluginBibTex()
2203
+ newCitations = []
2204
+ for c in citations:
2205
+ if c in bibtex:
2206
+ newCitations.append(self._getCiteText(bibtex[c]))
2207
+ else:
2208
+ newCitations.append(c)
2209
+ return newCitations
2210
+
2211
+ def __getCitationsDict(self, citationList, bibTexOutput=False):
2212
+ """ Return a dictionary with Cite keys and the citation links. """
2213
+ bibtex = self.__getPluginBibTex()
2214
+ od = OrderedDict()
2215
+ for c in citationList:
2216
+ if c in bibtex:
2217
+ if bibTexOutput:
2218
+ od[c] = bibtex[c]
2219
+ else:
2220
+ od[c] = self._getCiteText(bibtex[c])
2221
+ else:
2222
+ od[c] = c
2223
+
2224
+ return od
2225
+
2226
+ def getCitations(self, bibTexOutput=False):
2227
+ return self.__getCitationsDict(self._citations() or [],
2228
+ bibTexOutput=bibTexOutput)
2229
+
2230
+ def getPackageCitations(self, bibTexOutput=False):
2231
+ refs = getattr(self.getClassPackage(), "_references", [])
2232
+ return self.__getCitationsDict(refs, bibTexOutput=bibTexOutput)
2233
+
2234
+ def citations(self):
2235
+ """ Return a citation message to provide some information to users. """
2236
+ citations = list(self.getCitations().values())
2237
+ if citations:
2238
+ citations.insert(0, '*Protocol references:* ')
2239
+
2240
+ packageCitations = self.getPackageCitations().values()
2241
+ if packageCitations:
2242
+ citations.append('*Package references:*')
2243
+ citations += packageCitations
2244
+ if not citations:
2245
+ return ['No references provided']
2246
+ return citations
2247
+
2248
+ @classmethod
2249
+ def getHelpText(cls):
2250
+ """Get help text to show in the protocol help button"""
2251
+ helpText = cls.getDoc()
2252
+ # NOt used since getPlugin is always None
2253
+ # plugin = self.getPlugin()
2254
+ # if plugin:
2255
+ # pluginMetadata = plugin.metadata
2256
+ # helpText += "\n\nPlugin info:\n"
2257
+ # for key, value in pluginMetadata.iteritems():
2258
+ # helpText += "%s: \t%s\n" % (key, value)
2259
+ return helpText
2260
+
2261
+ def _methods(self):
2262
+ """ Should be implemented in subclasses. See methods. """
2263
+ return ["No methods information."]
2264
+
2265
+ def getParsedMethods(self):
2266
+ """ Get the _methods results and parse possible cites. """
2267
+ try:
2268
+ baseMethods = self._methods() or []
2269
+ bibtex = self.__getPluginBibTex()
2270
+ parsedMethods = []
2271
+ for m in baseMethods:
2272
+ for bibId, cite in bibtex.items():
2273
+ k = '[%s]' % bibId
2274
+ link = self._getCiteText(cite, useKeyLabel=True)
2275
+ m = m.replace(k, link)
2276
+ parsedMethods.append(m)
2277
+ except Exception as ex:
2278
+ parsedMethods = ['ERROR generating methods info: %s' % ex]
2279
+
2280
+ return parsedMethods
2281
+
2282
+ def methods(self):
2283
+ """ Return a description about methods about current protocol
2284
+ execution. """
2285
+ # TODO: Maybe store the methods and not computing all times??
2286
+ return self.getParsedMethods() + [''] + self.citations()
2287
+
2288
+ def runProtocol(self, protocol):
2289
+ """ Setup another protocol to be run from a workflow. """
2290
+ name = protocol.getClassName() + protocol.strId()
2291
+ # protocol.setName(name)
2292
+ protocol.setWorkingDir(self._getPath(name))
2293
+ protocol.setMapper(self.mapper)
2294
+ self.hostConfig.setStore(False)
2295
+ protocol.setHostConfig(self.getHostConfig())
2296
+ protocol.runMode.set(self._originalRunMode)
2297
+ protocol.makePathsAndClean()
2298
+ protocol.setStepsExecutor(self._stepsExecutor)
2299
+ protocol.run()
2300
+ self._store() # TODO: check if this is needed
2301
+
2302
+ def isChild(self):
2303
+ """ Return true if this protocol was invoked from a workflow
2304
+ (another protocol)"""
2305
+ return self.hasObjParentId()
2306
+
2307
+ def getStepsGraph(self, refresh=True):
2308
+ """ Build a graph taking into account the dependencies between
2309
+ steps. In streaming we might find first the createOutputStep (e.g 24)
2310
+ depending on 25"""
2311
+ from pyworkflow.utils.graph import Graph
2312
+ g = Graph(rootName='PROTOCOL')
2313
+ root = g.getRoot()
2314
+ root.label = 'Protocol'
2315
+
2316
+ steps = self.loadSteps()
2317
+ stepsDict = {str(i + 1): steps[i] for i in range(0, len(steps))}
2318
+ stepsDone = {}
2319
+
2320
+ def addStep(i, step):
2321
+
2322
+ # Exit if already done
2323
+ # This happens when, in streaming there is a child "before" a parent
2324
+ if i in stepsDone:
2325
+ return
2326
+
2327
+ index = step.getIndex() or i
2328
+ sid = str(index)
2329
+ n = g.createNode(sid)
2330
+ n.step = step
2331
+ stepsDone[i] = n
2332
+ if step.getPrerequisites().isEmpty():
2333
+ root.addChild(n)
2334
+ else:
2335
+ for p in step.getPrerequisites():
2336
+ # If prerequisite exists
2337
+ if p not in stepsDone:
2338
+ addStep(p, stepsDict[p])
2339
+ stepsDone[p].addChild(n)
2340
+
2341
+ for i, s in stepsDict.items():
2342
+ addStep(i, s)
2343
+ return g
2344
+
2345
+ def closeMappers(self):
2346
+ """ Close the mappers of all output Sets. """
2347
+ for _, attr in self.iterOutputAttributes(Set):
2348
+ attr.close()
2349
+
2350
+ def loadMappers(self):
2351
+ """ Open mapper connections from previous closed outputs. """
2352
+ for _, attr in self.iterOutputAttributes(Set):
2353
+ attr.load()
2354
+
2355
+ def allowsDelete(self, obj):
2356
+ return False
2357
+
2358
+ def legacyCheck(self):
2359
+ """ Hook defined to run some compatibility checks
2360
+ before display the protocol.
2361
+ """
2362
+ pass
2363
+
2364
+ def getSize(self):
2365
+ """ Returns the size of the folder corresponding to this protocol"""
2366
+ if not self._size:
2367
+ self._size = getFileSize(self.getPath())
2368
+
2369
+ return self._size
2370
+
2371
+ def cleanExecutionAttributes(self):
2372
+ """ Clean all the executions attributes """
2373
+ self.setPid(0)
2374
+ self._jobId.clear()
2375
+ self._stepsDone.set(0)
2376
+
2377
+ class LegacyProtocol(Protocol):
2378
+ """ Special subclass of Protocol to be used when a protocol class
2379
+ is not found. It means that have been removed or it is in another
2380
+ development branch. In such, we will use the LegacyProtocol to
2381
+ simply store the parameters and inputs/outputs."""
2382
+
2383
+ def __str__(self):
2384
+ return self.getObjLabel()
2385
+
2386
+ # overload getClassDomain because legacy protocols
2387
+ # do not have a package associated to it
2388
+ @classmethod
2389
+ def getClassDomain(cls):
2390
+ return pw.Config.getDomain()
2391
+
2392
+
2393
+ # ---------- Helper functions related to Protocols --------------------
2394
+
2395
+ def runProtocolMain(projectPath, protDbPath, protId):
2396
+ """
2397
+ Main entry point when a protocol will be executed.
2398
+ This function should be called when::
2399
+
2400
+ scipion runprotocol ...
2401
+
2402
+ :param projectPath: the absolute path to the project directory.
2403
+ :param protDbPath: path to protocol db relative to projectPath
2404
+ :param protId: id of the protocol object in db.
2405
+
2406
+ """
2407
+
2408
+ # Enter to the project directory and load protocol from db
2409
+ protocol = getProtocolFromDb(projectPath, protDbPath, protId, chdir=True)
2410
+
2411
+ setDefaultLoggingContext(protId, protocol.getProject().getShortName())
2412
+
2413
+ hostConfig = protocol.getHostConfig()
2414
+
2415
+ # Create the steps executor
2416
+ executor = None
2417
+ nThreads = max(protocol.numberOfThreads.get(), 1)
2418
+
2419
+ if protocol.stepsExecutionMode == STEPS_PARALLEL and nThreads > 1:
2420
+ if protocol.useQueueForSteps():
2421
+ executor = QueueStepExecutor(hostConfig,
2422
+ protocol.getSubmitDict(),
2423
+ nThreads - 1,
2424
+ gpuList=protocol.getGpuList())
2425
+ else:
2426
+ executor = ThreadStepExecutor(hostConfig, nThreads - 1,
2427
+ gpuList=protocol.getGpuList())
2428
+
2429
+ if executor is None and protocol.useQueueForSteps():
2430
+ executor = QueueStepExecutor(hostConfig, protocol.getSubmitDict(), 1,
2431
+ gpuList=protocol.getGpuList())
2432
+
2433
+ if executor is None:
2434
+ executor = StepExecutor(hostConfig,
2435
+ gpuList=protocol.getGpuList())
2436
+
2437
+ protocol.setStepsExecutor(executor)
2438
+ # Finally run the protocol
2439
+ protocol.run()
2440
+
2441
+
2442
+ def getProtocolFromDb(projectPath, protDbPath, protId, chdir=False):
2443
+ """ Retrieve the Protocol object from a given .sqlite file
2444
+ and the protocol id.
2445
+ """
2446
+
2447
+ if not os.path.exists(projectPath):
2448
+ raise Exception("ERROR: project path '%s' does not exist. "
2449
+ % projectPath)
2450
+
2451
+ fullDbPath = os.path.join(projectPath, protDbPath)
2452
+
2453
+ if not os.path.exists(fullDbPath):
2454
+ raise Exception("ERROR: protocol database '%s' does not exist. "
2455
+ % fullDbPath)
2456
+
2457
+ # We need this import here because from Project is imported
2458
+ # all from protocol indirectly, so if move this to the top
2459
+ # we get an import error
2460
+ from pyworkflow.project import Project
2461
+ project = Project(pw.Config.getDomain(), projectPath)
2462
+ project.load(dbPath=os.path.join(projectPath, protDbPath), chdir=chdir,
2463
+ loadAllConfig=False)
2464
+ protocol = project.getProtocol(protId)
2465
+ return protocol
2466
+
2467
+
2468
+ def getUpdatedProtocol(protocol):
2469
+ """ Retrieve the updated protocol and close db connections
2470
+ """
2471
+ prot2 = getProtocolFromDb(protocol.getProject().path,
2472
+ protocol.getDbPath(),
2473
+ protocol.getObjId())
2474
+ # Close DB connections
2475
+ prot2.getProject().closeMapper()
2476
+ prot2.closeMappers()
2477
+ return prot2
2478
+
2479
+
2480
+ def isProtocolUpToDate(protocol):
2481
+ """ Check timestamps between protocol lastModificationDate and the
2482
+ corresponding runs.db timestamp"""
2483
+ if protocol is None:
2484
+ return True
2485
+
2486
+ if protocol.lastUpdateTimeStamp.get(None) is None:
2487
+ return False
2488
+
2489
+ protTS = protocol.lastUpdateTimeStamp.datetime()
2490
+
2491
+ if protTS is None:
2492
+ return False
2493
+
2494
+ dbTS = pwutils.getFileLastModificationDate(protocol.getDbPath())
2495
+
2496
+ if not (protTS and dbTS):
2497
+ logger.info("Can't compare if protocol is up to date: "
2498
+ "Protocol %s, protocol time stamp: %s, %s timeStamp: %s"
2499
+ % (protocol, protTS, protocol, dbTS))
2500
+ else:
2501
+ return protTS >= dbTS
2502
+
2503
+
2504
+ class ProtImportBase(Protocol):
2505
+ """ Base Import protocol"""
2506
+
2507
+ class ProtStreamingBase(Protocol):
2508
+ """ Base protocol to implement streaming protocols.
2509
+ stepsGeneratorStep should be implemented (see its description) and output
2510
+ should be created at the end of the processing Steps created by the stepsGeneratorStep.
2511
+ To avoid concurrency error, when creating the output, do it in a with self._lock: block.
2512
+ Minimum number of threads is 3 and should run in parallel mode.
2513
+ """
2514
+
2515
+ def __init__(self, **kwargs):
2516
+
2517
+ super().__init__()
2518
+ self.stepsExecutionMode = STEPS_PARALLEL
2519
+ def _insertAllSteps(self):
2520
+ # Insert the step that generates the steps
2521
+ self._insertFunctionStep(self.resumableStepGeneratorStep, str(datetime.now()))
2522
+
2523
+ def resumableStepGeneratorStep(self, ts):
2524
+ """ This allow to resume protocols. ts is the time stamp so this stap is alway different form previous exceution"""
2525
+ self.stepsGeneratorStep()
2526
+
2527
+
2528
+ def _stepsCheck(self):
2529
+
2530
+ # Just store steps created in checkNewInputStep
2531
+ if self._newSteps:
2532
+ self.updateSteps()
2533
+
2534
+ def stepsGeneratorStep(self):
2535
+ """
2536
+ This step should be implemented by any streaming protocol.
2537
+ It should check its input and when ready conditions are met
2538
+ call the self._insertFunctionStep method.
2539
+
2540
+ :return: None
2541
+ """
2542
+ pass
2543
+
2544
+ def _validateThreads(self, messages:list):
2545
+
2546
+ if self.numberOfThreads.get() < 2:
2547
+ messages.append("At least 2 threads are needed for running this protocol. "
2548
+ "1 for the 'stepsGenerator step' and one more for the actual processing" )
2549
+ def _validate(self):
2550
+ """ If you want to implement a validate method do it but call _validateThreads or validate threads value."""
2551
+ errors = []
2552
+ self._validateThreads(errors)
2553
+
2554
+ return errors