scipion-pyworkflow 3.11.0__py3-none-any.whl → 3.11.2__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 (104) hide show
  1. pyworkflow/apps/__init__.py +29 -0
  2. pyworkflow/apps/pw_manager.py +37 -0
  3. pyworkflow/apps/pw_plot.py +51 -0
  4. pyworkflow/apps/pw_project.py +130 -0
  5. pyworkflow/apps/pw_protocol_list.py +143 -0
  6. pyworkflow/apps/pw_protocol_run.py +51 -0
  7. pyworkflow/apps/pw_run_tests.py +268 -0
  8. pyworkflow/apps/pw_schedule_run.py +322 -0
  9. pyworkflow/apps/pw_sleep.py +37 -0
  10. pyworkflow/apps/pw_sync_data.py +440 -0
  11. pyworkflow/apps/pw_viewer.py +78 -0
  12. pyworkflow/constants.py +1 -1
  13. pyworkflow/gui/__init__.py +36 -0
  14. pyworkflow/gui/browser.py +768 -0
  15. pyworkflow/gui/canvas.py +1190 -0
  16. pyworkflow/gui/dialog.py +981 -0
  17. pyworkflow/gui/form.py +2727 -0
  18. pyworkflow/gui/graph.py +247 -0
  19. pyworkflow/gui/graph_layout.py +271 -0
  20. pyworkflow/gui/gui.py +571 -0
  21. pyworkflow/gui/matplotlib_image.py +233 -0
  22. pyworkflow/gui/plotter.py +247 -0
  23. pyworkflow/gui/project/__init__.py +25 -0
  24. pyworkflow/gui/project/base.py +193 -0
  25. pyworkflow/gui/project/constants.py +139 -0
  26. pyworkflow/gui/project/labels.py +205 -0
  27. pyworkflow/gui/project/project.py +491 -0
  28. pyworkflow/gui/project/searchprotocol.py +240 -0
  29. pyworkflow/gui/project/searchrun.py +181 -0
  30. pyworkflow/gui/project/steps.py +171 -0
  31. pyworkflow/gui/project/utils.py +332 -0
  32. pyworkflow/gui/project/variables.py +179 -0
  33. pyworkflow/gui/project/viewdata.py +472 -0
  34. pyworkflow/gui/project/viewprojects.py +519 -0
  35. pyworkflow/gui/project/viewprotocols.py +2141 -0
  36. pyworkflow/gui/project/viewprotocols_extra.py +562 -0
  37. pyworkflow/gui/text.py +774 -0
  38. pyworkflow/gui/tooltip.py +185 -0
  39. pyworkflow/gui/tree.py +684 -0
  40. pyworkflow/gui/widgets.py +307 -0
  41. pyworkflow/mapper/__init__.py +26 -0
  42. pyworkflow/mapper/mapper.py +226 -0
  43. pyworkflow/mapper/sqlite.py +1583 -0
  44. pyworkflow/mapper/sqlite_db.py +145 -0
  45. pyworkflow/object.py +1 -0
  46. pyworkflow/plugin.py +4 -4
  47. pyworkflow/project/__init__.py +31 -0
  48. pyworkflow/project/config.py +454 -0
  49. pyworkflow/project/manager.py +180 -0
  50. pyworkflow/project/project.py +2095 -0
  51. pyworkflow/project/usage.py +165 -0
  52. pyworkflow/protocol/__init__.py +38 -0
  53. pyworkflow/protocol/bibtex.py +48 -0
  54. pyworkflow/protocol/constants.py +87 -0
  55. pyworkflow/protocol/executor.py +515 -0
  56. pyworkflow/protocol/hosts.py +318 -0
  57. pyworkflow/protocol/launch.py +277 -0
  58. pyworkflow/protocol/package.py +42 -0
  59. pyworkflow/protocol/params.py +781 -0
  60. pyworkflow/protocol/protocol.py +2712 -0
  61. pyworkflow/resources/protlabels.xcf +0 -0
  62. pyworkflow/resources/sprites.png +0 -0
  63. pyworkflow/resources/sprites.xcf +0 -0
  64. pyworkflow/template.py +1 -1
  65. pyworkflow/tests/__init__.py +29 -0
  66. pyworkflow/tests/test_utils.py +25 -0
  67. pyworkflow/tests/tests.py +342 -0
  68. pyworkflow/utils/__init__.py +38 -0
  69. pyworkflow/utils/dataset.py +414 -0
  70. pyworkflow/utils/echo.py +104 -0
  71. pyworkflow/utils/graph.py +169 -0
  72. pyworkflow/utils/log.py +293 -0
  73. pyworkflow/utils/path.py +528 -0
  74. pyworkflow/utils/process.py +154 -0
  75. pyworkflow/utils/profiler.py +92 -0
  76. pyworkflow/utils/progressbar.py +154 -0
  77. pyworkflow/utils/properties.py +618 -0
  78. pyworkflow/utils/reflection.py +129 -0
  79. pyworkflow/utils/utils.py +880 -0
  80. pyworkflow/utils/which.py +229 -0
  81. pyworkflow/webservices/__init__.py +8 -0
  82. pyworkflow/webservices/config.py +8 -0
  83. pyworkflow/webservices/notifier.py +152 -0
  84. pyworkflow/webservices/repository.py +59 -0
  85. pyworkflow/webservices/workflowhub.py +86 -0
  86. pyworkflowtests/tests/__init__.py +0 -0
  87. pyworkflowtests/tests/test_canvas.py +72 -0
  88. pyworkflowtests/tests/test_domain.py +45 -0
  89. pyworkflowtests/tests/test_logs.py +74 -0
  90. pyworkflowtests/tests/test_mappers.py +392 -0
  91. pyworkflowtests/tests/test_object.py +507 -0
  92. pyworkflowtests/tests/test_project.py +42 -0
  93. pyworkflowtests/tests/test_protocol_execution.py +146 -0
  94. pyworkflowtests/tests/test_protocol_export.py +78 -0
  95. pyworkflowtests/tests/test_protocol_output.py +158 -0
  96. pyworkflowtests/tests/test_streaming.py +47 -0
  97. pyworkflowtests/tests/test_utils.py +210 -0
  98. {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.2.dist-info}/METADATA +2 -2
  99. scipion_pyworkflow-3.11.2.dist-info/RECORD +162 -0
  100. scipion_pyworkflow-3.11.0.dist-info/RECORD +0 -71
  101. {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.2.dist-info}/WHEEL +0 -0
  102. {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.2.dist-info}/entry_points.txt +0 -0
  103. {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.2.dist-info}/licenses/LICENSE.txt +0 -0
  104. {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,2095 @@
1
+ #!/usr/bin/env python
2
+ # **************************************************************************
3
+ # *
4
+ # * Authors: J.M. De la Rosa Trevin (delarosatrevin@scilifelab.se) [1]
5
+ # *
6
+ # * [1] SciLifeLab, Stockholm University
7
+ # *
8
+ # * This program is free software; you can redistribute it and/or modify
9
+ # * it under the terms of the GNU General Public License as published by
10
+ # * the Free Software Foundation; either version 3 of the License, or
11
+ # * (at your option) any later version.
12
+ # *
13
+ # * This program is distributed in the hope that it will be useful,
14
+ # * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
+ # * GNU General Public License for more details.
17
+ # *
18
+ # * You should have received a copy of the GNU General Public License
19
+ # * along with this program; if not, write to the Free Software
20
+ # * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
21
+ # * 02111-1307 USA
22
+ # *
23
+ # * All comments concerning this program package may be sent to the
24
+ # * e-mail address 'scipion@cnb.csic.es'
25
+ # *
26
+ # **************************************************************************
27
+ import logging
28
+
29
+ from .usage import ScipionWorkflow
30
+ from ..protocol.launch import _checkJobStatus
31
+
32
+ ROOT_NODE_NAME = "PROJECT"
33
+ logger = logging.getLogger(__name__)
34
+ from pyworkflow.utils.log import LoggingConfigurator
35
+ import datetime as dt
36
+ import json
37
+ import os
38
+ import re
39
+ import time
40
+ import traceback
41
+ from collections import OrderedDict
42
+
43
+ import pyworkflow as pw
44
+ from pyworkflow.constants import PROJECT_DBNAME, PROJECT_SETTINGS
45
+ import pyworkflow.object as pwobj
46
+ import pyworkflow.protocol as pwprot
47
+ import pyworkflow.utils as pwutils
48
+ from pyworkflow.mapper import SqliteMapper
49
+ from pyworkflow.protocol.constants import (MODE_RESTART, MODE_RESUME,
50
+ STATUS_INTERACTIVE, ACTIVE_STATUS,
51
+ UNKNOWN_JOBID, INITIAL_SLEEP_TIME, STATUS_FINISHED)
52
+ from pyworkflow.protocol.protocol import Protocol, LegacyProtocol
53
+
54
+ from . import config
55
+
56
+
57
+ OBJECT_PARENT_ID = pwobj.OBJECT_PARENT_ID
58
+ PROJECT_LOGS = 'Logs'
59
+ PROJECT_RUNS = 'Runs'
60
+ PROJECT_TMP = 'Tmp'
61
+ PROJECT_UPLOAD = 'Uploads'
62
+ PROJECT_CONFIG = '.config'
63
+ PROJECT_CREATION_TIME = 'CreationTime'
64
+
65
+ # Regex to get numbering suffix and automatically propose runName
66
+ REGEX_NUMBER_ENDING = re.compile(r'(?P<prefix>.+)(?P<number>\(\d*\))\s*$')
67
+ REGEX_NUMBER_ENDING_CP = re.compile(r'(?P<prefix>.+\s\(copy)(?P<number>.*)\)\s*$')
68
+
69
+
70
+ class Project(object):
71
+ """This class will handle all information
72
+ related with a Project"""
73
+
74
+ @classmethod
75
+ def getDbName(cls):
76
+ """ Return the name of the database file of projects. """
77
+ return PROJECT_DBNAME
78
+
79
+ def __init__(self, domain, path):
80
+ """
81
+ Create a new Project instance.
82
+ :param domain: The application domain from where to get objects and
83
+ protocols.
84
+ :param path: Path where the project will be created/loaded
85
+ """
86
+ self._domain = domain
87
+ self.name = path
88
+ self.shortName = os.path.basename(path)
89
+ self.path = os.path.abspath(path)
90
+ self._isLink = os.path.islink(path)
91
+ self._isInReadOnlyFolder = False
92
+ self.pathList = [] # Store all related paths
93
+ self.dbPath = self.__addPath(PROJECT_DBNAME)
94
+ self.logsPath = self.__addPath(PROJECT_LOGS)
95
+ self.runsPath = self.__addPath(PROJECT_RUNS)
96
+ self.tmpPath = self.__addPath(PROJECT_TMP)
97
+ self.uploadPath = self.__addPath(PROJECT_UPLOAD)
98
+ self.settingsPath = self.__addPath(PROJECT_SETTINGS)
99
+ self.configPath = self.__addPath(PROJECT_CONFIG)
100
+ self.runs = None
101
+ self._runsGraph = None
102
+ self._transformGraph = None
103
+ self._sourceGraph = None
104
+ self.address = ''
105
+ self.port = pwutils.getFreePort()
106
+ self.mapper = None
107
+ self.settings:config.ProjectSettings = None
108
+ # Host configuration
109
+ self._hosts = None
110
+
111
+ # Creation time should be stored in project.sqlite when the project
112
+ # is created and then loaded with other properties from the database
113
+ self._creationTime = None
114
+
115
+ # Time stamp with the last run has been updated
116
+ self._lastRunTime = None
117
+
118
+ def getObjId(self):
119
+ """ Return the unique id assigned to this project. """
120
+ return os.path.basename(self.path)
121
+
122
+ def __addPath(self, *paths):
123
+ """Store a path needed for the project"""
124
+ p = self.getPath(*paths)
125
+ self.pathList.append(p)
126
+ return p
127
+
128
+ def getPath(self, *paths):
129
+ """Return path from the project root"""
130
+ if paths:
131
+ return os.path.join(*paths) # Why this is relative!!
132
+ else:
133
+ return self.path
134
+
135
+ def isLink(self):
136
+ """Returns if the project path is a link to another folder."""
137
+ return self._isLink
138
+
139
+ def getDbPath(self):
140
+ """ Return the path to the sqlite db. """
141
+ return self.dbPath
142
+
143
+ def getDbLastModificationDate(self):
144
+ """ Return the last modification date of the database """
145
+ pwutils.getFileLastModificationDate(self.getDbPath())
146
+
147
+ def getCreationTime(self):
148
+ """ Return the time when the project was created. """
149
+ # In project.create method, the first object inserted
150
+ # in the mapper should be the creation time
151
+ return self._creationTime.datetime()
152
+
153
+
154
+ def getComment(self):
155
+ """ Returns the project comment. Stored as CreationTime comment."""
156
+ return self._creationTime.getObjComment()
157
+
158
+ def setComment(self, newComment):
159
+ """ Sets the project comment """
160
+ self._creationTime.setObjComment(newComment)
161
+
162
+ def getSettingsCreationTime(self):
163
+ return self.settings.getCreationTime()
164
+
165
+ def getElapsedTime(self):
166
+ """ Returns the time elapsed from the creation to the last
167
+ execution time. """
168
+ if self._creationTime and self._lastRunTime:
169
+ creationTs = self.getCreationTime()
170
+ lastRunTs = self._lastRunTime.datetime()
171
+ return lastRunTs - creationTs
172
+ return None
173
+
174
+ def getLeftTime(self):
175
+ lifeTime = self.settings.getLifeTime()
176
+ if lifeTime:
177
+ td = dt.timedelta(hours=lifeTime)
178
+ return td - self.getElapsedTime()
179
+ else:
180
+ return None
181
+
182
+ def setDbPath(self, dbPath):
183
+ """ Set the project db path.
184
+ This function is used when running a protocol where
185
+ a project is loaded but using the protocol own sqlite file.
186
+ """
187
+ # First remove from pathList the old dbPath
188
+ self.pathList.remove(self.dbPath)
189
+ self.dbPath = os.path.abspath(dbPath)
190
+ self.pathList.append(self.dbPath)
191
+
192
+ def getName(self):
193
+ return self.name
194
+
195
+ def getDomain(self):
196
+ return self._domain
197
+
198
+ # TODO: maybe it has more sense to use this behaviour
199
+ # for just getName function...
200
+ def getShortName(self):
201
+ return self.shortName
202
+
203
+ def getTmpPath(self, *paths):
204
+ return self.getPath(PROJECT_TMP, *paths)
205
+
206
+ def getLogPath(self, *paths):
207
+ return self.getPath(PROJECT_LOGS, *paths)
208
+
209
+ def getProjectLog(self):
210
+ return os.path.join(self.path,self.getLogPath("project.log")) # For some reason getLogsPath is relative!
211
+
212
+ def getSettings(self):
213
+ return self.settings
214
+
215
+ def saveSettings(self):
216
+ # Read only mode
217
+ if not self.openedAsReadOnly():
218
+ self.settings.write()
219
+
220
+ def createSettings(self, runsView=1, readOnly=False):
221
+ self.settings = config.ProjectSettings()
222
+ self.settings.setRunsView(runsView)
223
+ self.settings.setReadOnly(readOnly)
224
+ self.settings.write(self.settingsPath)
225
+ return self.settings
226
+
227
+ def createMapper(self, sqliteFn):
228
+ """ Create a new SqliteMapper object and pass as classes dict
229
+ all globals and update with data and protocols from em.
230
+ """
231
+ classesDict = pwobj.Dict(default=pwprot.LegacyProtocol)
232
+ classesDict.update(self._domain.getMapperDict())
233
+ classesDict.update(config.__dict__)
234
+ return SqliteMapper(sqliteFn, classesDict)
235
+
236
+ def load(self, dbPath=None, hostsConf=None, protocolsConf=None, chdir=True,
237
+ loadAllConfig=True):
238
+ """
239
+ Load project data, configuration and settings.
240
+
241
+ :param dbPath: the path to the project database.
242
+ If None, use the project.sqlite in the project folder.
243
+ :param hostsConf: where to read the host configuration.
244
+ If None, check if exists in .config/hosts.conf
245
+ or read from ~/.config/scipion/hosts.conf
246
+ :param protocolsConf: Not used
247
+ :param chdir: If True, os.cwd will be set to project's path.
248
+ :param loadAllConfig: If True, settings from settings.sqlite will also be loaded
249
+
250
+ """
251
+
252
+ if not os.path.exists(self.path):
253
+ raise Exception("Cannot load project, path doesn't exist: %s"
254
+ % self.path)
255
+
256
+ # If folder is read only, flag it and warn about it.
257
+ if not os.access(self.path, os.W_OK):
258
+ self._isInReadOnlyFolder = True
259
+ logger.warning("Project \"%s\": you don't have write permissions "
260
+ "for project folder. Loading asd READ-ONLY." % self.shortName)
261
+
262
+ if chdir:
263
+ os.chdir(self.path) # Before doing nothing go to project dir
264
+
265
+ try:
266
+ self._loadDb(dbPath)
267
+ self._loadHosts(hostsConf)
268
+
269
+ if loadAllConfig:
270
+
271
+ # FIXME: Handle settings argument here
272
+
273
+ # It is possible that settings does not exists if
274
+ # we are loading a project after a Project.setDbName,
275
+ # used when running protocols
276
+ settingsPath = os.path.join(self.path, self.settingsPath)
277
+
278
+ logger.debug("settingsPath: %s" % settingsPath)
279
+
280
+ if os.path.exists(settingsPath):
281
+ self.settings = config.ProjectSettings.load(settingsPath)
282
+ else:
283
+ logger.info("settings is None")
284
+ self.settings = None
285
+
286
+ self._loadCreationTime()
287
+
288
+ # Catch DB not found exception (when loading a project from a folder
289
+ # without project.sqlite
290
+ except MissingProjectDbException as noDBe:
291
+ # Raise it at before: This is a critical error and should be raised
292
+ raise noDBe
293
+
294
+ # Catch any less severe exception..to allow at least open the project.
295
+ # except Exception as e:
296
+ # logger.info("ERROR: Project %s load failed.\n"
297
+ # " Message: %s\n" % (self.path, e))
298
+
299
+ def configureLogging(self):
300
+ LoggingConfigurator.setUpGUILogging(self.getProjectLog())
301
+ def _loadCreationTime(self):
302
+ # Load creation time, it should be in project.sqlite or
303
+ # in some old projects it is found in settings.sqlite
304
+
305
+ creationTime = self.mapper.selectBy(name=PROJECT_CREATION_TIME)
306
+
307
+ if creationTime: # CreationTime was found in project.sqlite
308
+ ctStr = creationTime[0] # This is our String type instance
309
+
310
+ # We store it in mem as datetime
311
+ self._creationTime = ctStr
312
+
313
+ else:
314
+
315
+ # If connected to project.sqlite and not any or the run.db
316
+ if self.path.endswith(PROJECT_DBNAME):
317
+ # We should read the creation time from settings.sqlite and
318
+ # update the CreationTime in the project.sqlite
319
+ self._creationTime = pwobj.String(self.getSettingsCreationTime())
320
+ self._storeCreationTime()
321
+
322
+ # ---- Helper functions to load different pieces of a project
323
+ def _loadDb(self, dbPath):
324
+ """ Load the mapper from the sqlite file in dbPath. """
325
+ if dbPath is not None:
326
+ self.setDbPath(dbPath)
327
+
328
+ absDbPath = os.path.join(self.path, self.dbPath)
329
+ if not os.path.exists(absDbPath):
330
+ raise MissingProjectDbException(
331
+ "Project database not found at '%s'" % absDbPath)
332
+ self.mapper = self.createMapper(absDbPath)
333
+
334
+ def closeMapper(self):
335
+ if self.mapper is not None:
336
+ self.mapper.close()
337
+ self.mapper = None
338
+
339
+ def getLocalConfigHosts(self):
340
+ """ Return the local file where the project will try to
341
+ read the hosts configuration. """
342
+ return self.getPath(PROJECT_CONFIG, pw.Config.SCIPION_HOSTS)
343
+
344
+ def _loadHosts(self, hosts):
345
+ """ Loads hosts configuration from hosts file. """
346
+ # If the host file is not passed as argument...
347
+ configHosts = pw.Config.SCIPION_HOSTS
348
+ projHosts = self.getLocalConfigHosts()
349
+
350
+ if hosts is None:
351
+ # Try first to read it from the project file .config./hosts.conf
352
+ if os.path.exists(projHosts):
353
+ hostsFile = projHosts
354
+ else:
355
+ localDir = os.path.dirname(pw.Config.SCIPION_LOCAL_CONFIG)
356
+ hostsFile = os.path.join(localDir, configHosts)
357
+ else:
358
+ pwutils.copyFile(hosts, projHosts)
359
+ hostsFile = hosts
360
+
361
+ self._hosts = pwprot.HostConfig.load(hostsFile)
362
+
363
+ def getHostNames(self):
364
+ """ Return the list of host name in the project. """
365
+ return list(self._hosts.keys())
366
+
367
+ def getHostConfig(self, hostName):
368
+ if hostName in self._hosts:
369
+ hostKey = hostName
370
+ else:
371
+ hostKey = self.getHostNames()[0]
372
+ logger.warning("Protocol host '%s' not found." % hostName)
373
+ logger.warning(" Using '%s' instead." % hostKey)
374
+
375
+ return self._hosts[hostKey]
376
+
377
+ def getProtocolView(self):
378
+ """ Returns de view selected in the tree when it was persisted"""
379
+ return self.settings.getProtocolView()
380
+
381
+ def create(self, runsView=1, readOnly=False, hostsConf=None,
382
+ protocolsConf=None, comment=None):
383
+ """Prepare all required paths and files to create a new project.
384
+
385
+ :param runsView: default view to associate the project with
386
+ :param readOnly: If True, project will be loaded as read only.
387
+ :param hostsConf: Path to the host.conf to be used when executing protocols
388
+ :param protocolsConf: Not used.
389
+ """
390
+ # Create project path if not exists
391
+ pwutils.path.makePath(self.path)
392
+ os.chdir(self.path) # Before doing nothing go to project dir
393
+ self._cleanData()
394
+ logger.info("Creating project at %s" % os.path.abspath(self.dbPath))
395
+ # Create db through the mapper
396
+ self.mapper = self.createMapper(self.dbPath)
397
+ # Store creation time
398
+ self._creationTime = pwobj.String(dt.datetime.now())
399
+ self.setComment(comment)
400
+ self._storeCreationTime()
401
+ # Load settings from .conf files and write .sqlite
402
+ self.settings = self.createSettings(runsView=runsView,
403
+ readOnly=readOnly)
404
+ # Create other paths inside project
405
+ for p in self.pathList:
406
+ pwutils.path.makePath(p)
407
+
408
+ self._loadHosts(hostsConf)
409
+
410
+ def _storeCreationTime(self, new=True):
411
+ """ Store the creation time in the project db. """
412
+ # Store creation time
413
+ self._creationTime.setName(PROJECT_CREATION_TIME)
414
+ self.mapper.store(self._creationTime)
415
+ self.mapper.commit()
416
+
417
+ def _cleanData(self):
418
+ """Clean all project data"""
419
+ pwutils.path.cleanPath(*self.pathList)
420
+
421
+ def _continueWorkflow(self, errorsList, continuedProtList=None):
422
+ """
423
+ This function continue a workflow from a selected protocol.
424
+ The previous results are preserved.
425
+ Actions done here are:
426
+ 1. if the protocol list exists (for each protocol)
427
+ 1.1 if the protocol is not an interactive protocol
428
+ 1.1.1. If the protocol is in streaming (CONTINUE ACTION):
429
+ - 'dataStreaming' parameter if the protocol is an import
430
+ protocol
431
+ - check if the __stepsCheck function exist and it's not
432
+ the same implementation of the base class
433
+ (worksInStreaming function)
434
+ 1.1.1.1 Open the protocol sets, store and save them in
435
+ the database
436
+ 1.1.1.2 Change the protocol status (SAVED)
437
+ 1.1.1.3 Schedule the protocol
438
+ Else Restart the workflow from that point (RESTART ACTION) if
439
+ at least one protocol in streaming has been launched
440
+ """
441
+ if continuedProtList is not None:
442
+ for protocol, level in continuedProtList.values():
443
+ if not protocol.isInteractive():
444
+ if protocol.isScheduled():
445
+ continue
446
+
447
+ # streaming ...
448
+ if protocol.worksInStreaming() and not protocol.isSaved():
449
+ attrSet = [attr for name, attr in
450
+ protocol.iterOutputAttributes(pwprot.Set)]
451
+ try:
452
+ if attrSet:
453
+ # Open output sets..
454
+ for attr in attrSet:
455
+ attr.setStreamState(attr.STREAM_OPEN)
456
+ attr.write()
457
+ attr.close()
458
+ protocol.setStatus(pwprot.STATUS_SAVED)
459
+ protocol._updateSteps(lambda step: step.setStatus(pwprot.STATUS_SAVED))
460
+ protocol.setMapper(self.createMapper(protocol.getDbPath()))
461
+ protocol._store()
462
+ self._storeProtocol(protocol)
463
+ self.scheduleProtocol(protocol,
464
+ initialSleepTime=level*INITIAL_SLEEP_TIME)
465
+ except Exception as ex:
466
+ errorsList.append("Error trying to launch the "
467
+ "protocol: %s\nERROR: %s\n" %
468
+ (protocol.getObjLabel(), ex))
469
+ break
470
+ else:
471
+ if level != 0:
472
+ # Not in streaming and not the first protocol.
473
+ if protocol.isActive():
474
+ self.stopProtocol(protocol)
475
+ self._restartWorkflow(errorsList,{protocol.getObjId(): (protocol, level)})
476
+
477
+ else: # First protocol not in streaming
478
+ if not protocol.isActive():
479
+ self.scheduleProtocol(protocol)
480
+
481
+
482
+
483
+ def _restartWorkflow(self, errorsList, restartedProtList=None):
484
+ """
485
+ This function restart a workflow from a selected protocol.
486
+ All previous results will be deleted
487
+ Actions done here are:
488
+ 1. Set the protocol run mode (RESTART). All previous results will be
489
+ deleted
490
+ 2. Schedule the protocol if not is an interactive protocol
491
+ 3. For each of the dependents protocols, repeat from step 1
492
+ """
493
+ if restartedProtList is not None:
494
+ for protocol, level in restartedProtList.values():
495
+ if not protocol.isInteractive():
496
+ try:
497
+ if protocol.isScheduled():
498
+ continue
499
+ elif protocol.isActive():
500
+ self.stopProtocol(protocol)
501
+ protocol.runMode.set(MODE_RESTART)
502
+ self.scheduleProtocol(protocol,
503
+ initialSleepTime=level*INITIAL_SLEEP_TIME)
504
+ except Exception as ex:
505
+ errorsList.append("Error trying to restart a protocol: %s"
506
+ "\nERROR: %s\n" % (protocol.getObjLabel(),
507
+ ex))
508
+ break
509
+ else:
510
+ protocol.setStatus(pwprot.STATUS_SAVED)
511
+ self._storeProtocol(protocol)
512
+ protocol.runMode.set(MODE_RESTART)
513
+ self._setupProtocol(protocol)
514
+ protocol.makePathsAndClean() # Create working dir if necessary
515
+ # Delete the relations created by this protocol
516
+ self.mapper.deleteRelations(self)
517
+ self.mapper.commit()
518
+ self.mapper.store(protocol)
519
+ self.mapper.commit()
520
+
521
+ def _fixProtParamsConfiguration(self, protocol=None):
522
+ """
523
+ This function fix:
524
+ 1. The old parameters configuration in the protocols.
525
+ Now, dependent protocols have a pointer to the parent protocol, and
526
+ the extended parameter has a parent output value
527
+ """
528
+ # Take the old configuration attributes and fix the pointer
529
+ oldStylePointerList = [item for key, item in
530
+ protocol.iterInputAttributes()
531
+ if not isinstance(item.getObjValue(),
532
+ pwprot.Protocol)]
533
+ if oldStylePointerList:
534
+ # Fix the protocol parameters
535
+ for pointer in oldStylePointerList:
536
+ auxPointer = pointer.getObjValue()
537
+ pointer.set(self.getRunsGraph().getNode(str(pointer.get().getObjParentId())).run)
538
+ pointer.setExtended(auxPointer.getLastName())
539
+ protocol._store()
540
+ self._storeProtocol(protocol)
541
+ self._updateProtocol(protocol)
542
+ self.mapper.commit()
543
+
544
+ def stopWorkFlow(self, activeProtList):
545
+ """
546
+ This function can stop a workflow from a selected protocol
547
+ :param initialProtocol: selected protocol
548
+ """
549
+ errorProtList = []
550
+ for protocol in activeProtList.values():
551
+ try:
552
+ self.stopProtocol(protocol)
553
+ except Exception:
554
+ errorProtList.append(protocol)
555
+ return errorProtList
556
+
557
+ def resetWorkFlow(self, workflowProtocolList):
558
+ """
559
+ This function can reset a workflow from a selected protocol
560
+ :param initialProtocol: selected protocol
561
+ """
562
+ errorProtList = []
563
+ if workflowProtocolList:
564
+ for protocol, level in workflowProtocolList.values():
565
+ if protocol.getStatus() != pwprot.STATUS_SAVED:
566
+ try:
567
+ self.resetProtocol(protocol)
568
+ except Exception:
569
+ errorProtList.append(protocol)
570
+ return errorProtList
571
+
572
+ def launchWorkflow(self, workflowProtocolList, mode=MODE_RESUME):
573
+ """
574
+ This function can launch a workflow from a selected protocol in two
575
+ modes depending on the 'mode' value (RESTART, CONTINUE)
576
+ Actions done here are:
577
+
578
+ 1. Check if the workflow has active protocols.
579
+ 2. Fix the workflow if is not properly configured
580
+ 3. Restart or Continue a workflow starting from the protocol depending
581
+ on the 'mode' value
582
+
583
+ """
584
+ errorsList = []
585
+ if mode == MODE_RESTART:
586
+ self._restartWorkflow(errorsList, workflowProtocolList)
587
+ else:
588
+ self._continueWorkflow(errorsList,workflowProtocolList)
589
+ return errorsList
590
+
591
+ def launchProtocol(self, protocol:Protocol, wait=False, scheduled=False,
592
+ force=False):
593
+ """ In this function the action of launching a protocol
594
+ will be initiated. Actions done here are:
595
+
596
+ 1. Store the protocol and assign name and working dir
597
+ 2. Create the working dir and also the protocol independent db
598
+ 3. Call the launch method in protocol.job to handle submission:
599
+ mpi, thread, queue.
600
+
601
+ If the protocol has some prerequisites (other protocols that
602
+ needs to be finished first), it will be scheduled.
603
+
604
+ :param protocol: Protocol instance to launch
605
+ :param wait: Optional. If true, this method
606
+ will wait until execution is finished. Used in tests.
607
+ :param scheduled: Optional. If true, run.db and paths
608
+ already exist and are preserved.
609
+ :param force: Optional. If true, launch is forced, regardless
610
+ latter dependent executions. Used when restarting many protocols a once.
611
+
612
+ """
613
+ if protocol.getPrerequisites() and not scheduled:
614
+ return self.scheduleProtocol(protocol)
615
+
616
+ isRestart = protocol.getRunMode() == MODE_RESTART
617
+
618
+ if not force:
619
+ if (not protocol.isInteractive() and not protocol.isInStreaming()) or isRestart:
620
+ self._checkModificationAllowed([protocol],
621
+ 'Cannot RE-LAUNCH protocol')
622
+
623
+ protocol.setStatus(pwprot.STATUS_LAUNCHED)
624
+ self._setupProtocol(protocol)
625
+
626
+ # Prepare a separate db for this run if not from schedule jobs
627
+ # Scheduled protocols will load the project db from the run.db file,
628
+ # so there is no need to copy the database
629
+
630
+ if not scheduled:
631
+ protocol.makePathsAndClean() # Create working dir if necessary
632
+ # Delete the relations created by this protocol
633
+ if isRestart:
634
+ self.mapper.deleteRelations(self)
635
+ # Clean and persist execution attributes; otherwise, this would retain old job IDs and PIDs.
636
+ protocol.cleanExecutionAttributes()
637
+ protocol._store(protocol._jobId, protocol._pid)
638
+
639
+ self.mapper.commit()
640
+
641
+ # NOTE: now we are simply copying the entire project db, this can be
642
+ # changed later to only create a subset of the db need for the run
643
+ pwutils.path.copyFile(self.dbPath, protocol.getDbPath())
644
+ # Update the lastUpdateTimeStamp so later PID obtained in launch is not "remove" with run.db data.
645
+ protocol.lastUpdateTimeStamp.set(pwutils.getFileLastModificationDate(protocol.getDbPath()))
646
+
647
+ # Launch the protocol; depending on the case, either the pId or the jobId will be set in this call
648
+ pwprot.launch(protocol, wait)
649
+
650
+ # Commit changes
651
+ if wait: # This is only useful for launching tests...
652
+ self._updateProtocol(protocol)
653
+ else:
654
+ self.mapper.store(protocol)
655
+ self.mapper.commit()
656
+
657
+ def scheduleProtocol(self, protocol, prerequisites=[], initialSleepTime=0):
658
+ """ Schedule a new protocol that will run when the input data
659
+ is available and the prerequisites are finished.
660
+
661
+ :param protocol: the protocol that will be scheduled.
662
+ :param prerequisites: a list with protocols ids that the scheduled
663
+ protocol will wait for.
664
+ :param initialSleepTime: number of seconds to wait before
665
+ checking input's availability
666
+
667
+ """
668
+ isRestart = protocol.getRunMode() == MODE_RESTART
669
+
670
+ protocol.setStatus(pwprot.STATUS_SCHEDULED)
671
+ protocol.addPrerequisites(*prerequisites)
672
+
673
+ self._setupProtocol(protocol)
674
+ protocol.makePathsAndClean() # Create working dir if necessary
675
+ # Delete the relations created by this protocol if any
676
+ if isRestart:
677
+ self.mapper.deleteRelations(self)
678
+ self.mapper.commit()
679
+
680
+ # Prepare a separate db for this run
681
+ # NOTE: now we are simply copying the entire project db, this can be
682
+ # changed later to only create a subset of the db need for the run
683
+ pwutils.path.copyFile(self.dbPath, protocol.getDbPath())
684
+ # Launch the protocol, the jobId should be set after this call
685
+ pwprot.schedule(protocol, initialSleepTime=initialSleepTime)
686
+ self.mapper.store(protocol)
687
+ self.mapper.commit()
688
+
689
+ def _updateProtocol(self, protocol: Protocol, tries=0, checkPid=False):
690
+ """ Update the protocol passed taking the data from its run.db.
691
+ It also checks if the protocol is alive base on its PID of JOBIDS """
692
+ # NOTE: when this method fails recurrently....we are setting the protocol to failed and
693
+ # therefore closing its outputs. This, in streaming scenarios triggers a false closing to protocols
694
+ # while actual protocol is still alive but
695
+
696
+ updated = pw.NOT_UPDATED_UNNECESSARY
697
+
698
+ # If this is read only exit
699
+ if self.openedAsReadOnly():
700
+ return pw.NOT_UPDATED_READ_ONLY
701
+
702
+ try:
703
+
704
+ # IMPORTANT: the protocol after some iterations of this ends up without the project!
705
+ # This is a problem if we want tu use protocol.useQueueForJobs that uses project info!
706
+ # print("PROJECT: %s" % protocol.getProject())
707
+
708
+ # If the protocol database has changes ....
709
+ if not pwprot.isProtocolUpToDate(protocol):
710
+
711
+ logger.debug("Protocol %s outdated. Updating it now." % protocol.getRunName())
712
+
713
+ updated = pw.PROTOCOL_UPDATED
714
+
715
+ # Backup the values of 'jobId', 'label' and 'comment'
716
+ # to be restored after the .copy
717
+ jobId = protocol.getJobIds().clone() # Use clone to prevent this variable from being overwritten or cleared in the latter .copy() call
718
+ label = protocol.getObjLabel()
719
+ comment = protocol.getObjComment()
720
+ project = protocol.getProject() # The later protocol.copy(prot2, copyId=False, excludeInputs=True) cleans the project!!
721
+
722
+ if project is None:
723
+ logger.warning("Protocol %s hasn't the project associated when updating it." % label)
724
+
725
+ # Comparing date will not work unless we have a reliable
726
+ # lastModificationDate of a protocol in the project.sqlite
727
+ prot2 = pwprot.getProtocolFromDb(self.path,
728
+ protocol.getDbPath(),
729
+ protocol.getObjId())
730
+
731
+ # Capture the db timestamp before loading.
732
+ lastUpdateTime = pwutils.getFileLastModificationDate(protocol.getDbPath())
733
+
734
+ # Copy is only working for db restored objects
735
+ protocol.setMapper(self.mapper)
736
+
737
+ localOutputs = list(protocol._outputs)
738
+ protocol.copy(prot2, copyId=False, excludeInputs=True) # This cleans protocol._project cause getProtocolFromDb does not bring the project
739
+ protocol.setProject(project)
740
+
741
+ # merge outputs: This is necessary when outputs are added from the GUI
742
+ # e.g.: adding coordinates from analyze result and protocol is active (interactive).
743
+ for attr in localOutputs:
744
+ if attr not in protocol._outputs:
745
+ protocol._outputs.append(attr)
746
+
747
+ # Restore backup values
748
+ if protocol.useQueueForProtocol() and jobId: # If jobId not empty then restore value as the db is empty
749
+ # Case for direct protocol launch from the GUI. Without passing through a scheduling process.
750
+ # In this case the jobid is obtained by the GUI and the job id should be preserved.
751
+ protocol.setJobIds(jobId)
752
+
753
+ # In case of scheduling a protocol, the jobid is obtained during the "scheduling job"
754
+ # and it is written in the rub.db. Therefore, it should be taken from there.
755
+
756
+ # Restore values edited in the GUI
757
+ protocol.setObjLabel(label)
758
+ protocol.setObjComment(comment)
759
+ # Use the run.db timestamp instead of the system TS to prevent
760
+ # possible inconsistencies.
761
+ protocol.lastUpdateTimeStamp.set(lastUpdateTime)
762
+
763
+ # # Check pid at the end, once updated. It may have brought new pids? Job ids? or process died and pid
764
+ # # pid and job ids were reset and status set to failed, so it does not make sense to check pids
765
+ # if checkPid and protocol.isActive():
766
+ # self.checkIsAlive(protocol)
767
+
768
+ # Close DB connections to rundb
769
+ prot2.getProject().closeMapper()
770
+ prot2.closeMappers()
771
+
772
+
773
+ # If protocol is still active
774
+ if protocol.isActive():
775
+ # If it is still alive, and hasn't been updated from run db
776
+ # NOTE: checkIsAlive may have changed the protocol status,in case the process ware killed
777
+ # So we need to persist those changes.
778
+ if not self.checkIsAlive(protocol):
779
+
780
+ updated = pw.PROTOCOL_UPDATED
781
+
782
+
783
+ if updated == pw.PROTOCOL_UPDATED:
784
+ # We store changes, either after updating the protocol with data from run-db or because it died
785
+ self.mapper.store(protocol)
786
+
787
+ except Exception as ex:
788
+ if tries == 3: # 3 tries have been failed
789
+ traceback.print_exc()
790
+ # If any problem happens, the protocol will be marked
791
+ # with a FAILED status
792
+ try:
793
+ protocol.setFailed(str(ex))
794
+ self.mapper.store(protocol)
795
+ except Exception:
796
+ pass
797
+ return pw.NOT_UPDATED_ERROR
798
+ else:
799
+ logger.warning("Couldn't update protocol %s from it's own database. ERROR: %s, attempt=%d"
800
+ % (protocol.getRunName(), ex, tries))
801
+ time.sleep(0.5)
802
+ return self._updateProtocol(protocol, tries + 1)
803
+
804
+ return updated
805
+
806
+ def checkIsAlive(self, protocol):
807
+ """ Check if a protocol is alive based on its jobid (queue engines) or pid
808
+ :param protocol: protocol to check its status
809
+ :returns True if it is alive
810
+ """
811
+ # For some reason pid ends up with a None...
812
+ pid = protocol.getPid()
813
+
814
+ if pid is None:
815
+ logger.info("Protocol's %s pid is None and is active... this should not happen. Checking its job id: %s" % (protocol.getRunName(), protocol.getJobIds()))
816
+ pid = 0
817
+
818
+ alive = False
819
+ if pid == 0:
820
+ alive = self.checkJobId(protocol)
821
+ else:
822
+ alive = self.checkPid(protocol)
823
+
824
+ if alive:
825
+ logger.debug("Protocol %s is alive." % protocol.getRunName())
826
+ return alive
827
+
828
+ def stopProtocol(self, protocol):
829
+ """ Stop a running protocol """
830
+ try:
831
+ if protocol.getStatus() in ACTIVE_STATUS:
832
+ self._updateProtocol(protocol) # update protocol to have the latest rub.db values
833
+ pwprot.stop(protocol)
834
+ except Exception as e:
835
+ logger.error("Couldn't stop the protocol: %s" % e)
836
+ raise
837
+ finally:
838
+ protocol.setAborted()
839
+ protocol.setMapper(self.createMapper(protocol.getDbPath()))
840
+ protocol._store()
841
+ self._storeProtocol(protocol)
842
+ protocol.getMapper().close()
843
+
844
+ def resetProtocol(self, protocol):
845
+ """ Stop a running protocol """
846
+ try:
847
+ if protocol.getStatus() in ACTIVE_STATUS:
848
+ pwprot.stop(protocol)
849
+ except Exception:
850
+ raise
851
+ finally:
852
+ protocol.setSaved()
853
+ protocol.runMode.set(MODE_RESTART)
854
+ protocol.makePathsAndClean() # Create working dir if necessary
855
+ # Clean jobIds, Pid and StepsDone;
856
+ protocol.cleanExecutionAttributes() # otherwise, this would retain old executions info
857
+ protocol._store()
858
+
859
+ def continueProtocol(self, protocol):
860
+ """ This function should be called
861
+ to mark a protocol that have an interactive step
862
+ waiting for approval that can continue
863
+ """
864
+ protocol.continueFromInteractive()
865
+ self.launchProtocol(protocol)
866
+
867
+ def __protocolInList(self, prot, protocols):
868
+ """ Check if a protocol is in a list comparing the ids. """
869
+ for p in protocols:
870
+ if p.getObjId() == prot.getObjId():
871
+ return True
872
+ return False
873
+
874
+ def __validDependency(self, prot, child, protocols):
875
+ """ Check if the given child is a true dependency of the protocol
876
+ in order to avoid any modification.
877
+ """
878
+ return (not self.__protocolInList(child, protocols) and
879
+ not child.isSaved() and not child.isScheduled())
880
+
881
+ def _getProtocolsDependencies(self, protocols):
882
+ error = ''
883
+ runsGraph = self.getRunsGraph()
884
+ for prot in protocols:
885
+ node = runsGraph.getNode(prot.strId())
886
+ if node:
887
+ childs = [node.run for node in node.getChildren() if
888
+ self.__validDependency(prot, node.run, protocols)]
889
+ if childs:
890
+ deps = [' ' + c.getRunName() for c in childs]
891
+ error += '\n *%s* is referenced from:\n - ' % prot.getRunName()
892
+ error += '\n - '.join(deps)
893
+ return error
894
+
895
+ def _getProtocolDescendents(self, protocol):
896
+ """Getting the descendents protocols from a given one"""
897
+ runsGraph = self.getRunsGraph()
898
+ visitedNodes = dict()
899
+ node = runsGraph.getNode(protocol.strId())
900
+ if node is None:
901
+ return visitedNodes
902
+
903
+ visitedNodes[int(node.getName())] = node
904
+
905
+ def getDescendents(rootNode):
906
+ for child in rootNode.getChildren():
907
+ if int(child.getName()) not in visitedNodes:
908
+ visitedNodes[int(child.getName())] = child
909
+ getDescendents(child)
910
+
911
+ getDescendents(node)
912
+ return visitedNodes
913
+
914
+ def getProtocolCompatibleOutputs(self, protocol, classes, condition):
915
+ """Getting the outputs compatible with an object type. The outputs of the child protocols are excluded. """
916
+ objects = []
917
+ maxNum = 200
918
+ protocolDescendents = self._getProtocolDescendents(protocol)
919
+ runs = self.getRuns(refresh=False)
920
+
921
+ for prot in runs:
922
+ # Make sure we don't include previous output of the same
923
+ # and other descendent protocols
924
+ if prot.getObjId() not in protocolDescendents:
925
+ # Check if the protocol itself is one of the desired classes
926
+ if any(issubclass(prot.getClass(), c) for c in classes):
927
+ p = pwobj.Pointer(prot)
928
+ objects.append(p)
929
+
930
+ try:
931
+ # paramName and attr must be set to None
932
+ # Otherwise, if a protocol has failed and the corresponding output object of type XX does not exist
933
+ # any other protocol that uses objects of type XX as input will not be able to choose then using
934
+ # the magnifier glass (object selector of type XX)
935
+ paramName = None
936
+ attr = None
937
+ for paramName, attr in prot.iterOutputAttributes(includePossible=True):
938
+ def _checkParam(paramName, attr):
939
+ # If attr is a subclasses of any desired one, add it to the list
940
+ # we should also check if there is a condition, the object
941
+ # must comply with the condition
942
+ p = None
943
+
944
+ match = False
945
+ cancelConditionEval = False
946
+ possibleOutput = isinstance(attr, type)
947
+
948
+ # Go through all compatible Classes coming from in pointerClass string
949
+ for c in classes:
950
+ # If attr is an instance
951
+ if isinstance(attr, c):
952
+ match = True
953
+ break
954
+ # If it is a class already: "possibleOutput" case. In this case attr is the class and not
955
+ # an instance of c. In this special case
956
+ elif possibleOutput and issubclass(attr, c):
957
+ match = True
958
+ cancelConditionEval = True
959
+
960
+ # If attr matches the class
961
+ if match:
962
+ if cancelConditionEval or not condition or attr.evalCondition(condition):
963
+ p = pwobj.Pointer(prot, extended=paramName)
964
+ p._allowsSelection = True
965
+ objects.append(p)
966
+ return
967
+
968
+ # JMRT: For all sets, we don't want to include the
969
+ # subitems here for performance reasons (e.g. SetOfParticles)
970
+ # Thus, a Set class can define EXPOSE_ITEMS = True
971
+ # to enable the inclusion of its items here
972
+ if getattr(attr, 'EXPOSE_ITEMS', False) and not possibleOutput:
973
+ # If the ITEM type match any of the desired classes
974
+ # we will add some elements from the set
975
+ if (attr.ITEM_TYPE is not None and
976
+ any(issubclass(attr.ITEM_TYPE, c) for c in classes)):
977
+ if p is None: # This means the set have not be added
978
+ p = pwobj.Pointer(prot, extended=paramName)
979
+ p._allowsSelection = False
980
+ objects.append(p)
981
+ # Add each item on the set to the list of objects
982
+ try:
983
+ for i, item in enumerate(attr):
984
+ if i == maxNum: # Only load up to NUM particles
985
+ break
986
+ pi = pwobj.Pointer(prot, extended=paramName)
987
+ pi.addExtended(item.getObjId())
988
+ pi._parentObject = p
989
+ objects.append(pi)
990
+ except Exception as ex:
991
+ print("Error loading items from:")
992
+ print(" protocol: %s, attribute: %s" % (prot.getRunName(), paramName))
993
+ print(" dbfile: ", os.path.join(self.getPath(), attr.getFileName()))
994
+ print(ex)
995
+
996
+ _checkParam(paramName, attr)
997
+ # The following is a dirty fix for the RCT case where there
998
+ # are inner output, maybe we should consider extend this for
999
+ # in a more general manner
1000
+ for subParam in ['_untilted', '_tilted']:
1001
+ if hasattr(attr, subParam):
1002
+ _checkParam('%s.%s' % (paramName, subParam),
1003
+ getattr(attr, subParam))
1004
+ except Exception as e:
1005
+ print("Cannot read attributes for %s (%s)" % (prot.getClass(), e))
1006
+
1007
+ return objects
1008
+
1009
+ def _checkProtocolsDependencies(self, protocols, msg):
1010
+ """ Check if the protocols have dependencies.
1011
+ This method is used before delete or save protocols to be sure
1012
+ it is not referenced from other runs. (an Exception is raised)
1013
+ Params:
1014
+ protocols: protocol list to be analyzed.
1015
+ msg: String message to be prefixed to Exception error.
1016
+ """
1017
+ # Check if the protocol have any dependencies
1018
+ error = self._getProtocolsDependencies(protocols)
1019
+ if error:
1020
+ raise ModificationNotAllowedException(msg + error)
1021
+
1022
+ def _checkModificationAllowed(self, protocols, msg):
1023
+ """ Check if any modification operation is allowed for
1024
+ this group of protocols.
1025
+ """
1026
+ if self.openedAsReadOnly():
1027
+ raise Exception(msg + " Running in READ-ONLY mode.")
1028
+
1029
+ self._checkProtocolsDependencies(protocols, msg)
1030
+
1031
+ def _getSubworkflow(self, protocol, fixProtParam=True, getStopped=True):
1032
+ """
1033
+ This function get the workflow from "protocol" and determine the
1034
+ protocol level into the graph. Also, checks if there are active
1035
+ protocols excluding interactive protocols.
1036
+ :param protocol from where to start the subworkflow (included)
1037
+ :param fixProtParam fix the old parameters configuration in the protocols
1038
+ :param getStopped takes into account protocols that aren't stopped
1039
+ """
1040
+ affectedProtocols = {}
1041
+ affectedProtocolsActive = {}
1042
+ auxProtList = []
1043
+ # store the protocol and your level into the workflow
1044
+ affectedProtocols[protocol.getObjId()] = [protocol, 0]
1045
+ auxProtList.append([protocol.getObjId(), 0])
1046
+ runGraph = self.getRunsGraph()
1047
+
1048
+ while auxProtList:
1049
+ protId, level = auxProtList.pop(0)
1050
+ protocol = runGraph.getNode(str(protId)).run
1051
+
1052
+ # Increase the level for the children
1053
+ level = level + 1
1054
+
1055
+ if fixProtParam:
1056
+ self._fixProtParamsConfiguration(protocol)
1057
+
1058
+ if not getStopped and protocol.isActive():
1059
+ affectedProtocolsActive[protocol.getObjId()] = protocol
1060
+ elif not protocol.getObjId() in affectedProtocolsActive.keys() and getStopped and \
1061
+ not protocol.isSaved() and protocol.getStatus() != STATUS_INTERACTIVE:
1062
+ affectedProtocolsActive[protocol.getObjId()] = protocol
1063
+
1064
+ node = runGraph.getNode(protocol.strId())
1065
+ dependencies = [node.run for node in node.getChildren()]
1066
+ for dep in dependencies:
1067
+ if not dep.getObjId() in auxProtList:
1068
+ auxProtList.append([dep.getObjId(), level])
1069
+
1070
+ if not dep.getObjId() in affectedProtocols.keys():
1071
+ affectedProtocols[dep.getObjId()] = [dep, level]
1072
+ elif level > affectedProtocols[dep.getObjId()][1]:
1073
+ affectedProtocols[dep.getObjId()][1] = level
1074
+
1075
+ return affectedProtocols, affectedProtocolsActive
1076
+
1077
+ def deleteProtocol(self, *protocols):
1078
+ self._checkModificationAllowed(protocols, 'Cannot DELETE protocols')
1079
+
1080
+ for prot in protocols:
1081
+ # Delete the relations created by this protocol
1082
+ self.mapper.deleteRelations(prot)
1083
+ # Delete from protocol from database
1084
+ self.mapper.delete(prot)
1085
+ wd = prot.workingDir.get()
1086
+
1087
+ if wd.startswith(PROJECT_RUNS):
1088
+ prot.cleanWorkingDir()
1089
+ else:
1090
+ logger.info("Can't delete protocol %s. Its workingDir %s does not starts with %s " % (prot, wd, PROJECT_RUNS))
1091
+
1092
+ self.mapper.commit()
1093
+
1094
+ def deleteProtocolOutput(self, protocol, output):
1095
+ """ Delete a given object from the project.
1096
+ Usually to clean up some outputs.
1097
+ """
1098
+ node = self.getRunsGraph().getNode(protocol.strId())
1099
+ deps = []
1100
+
1101
+ for node in node.getChildren():
1102
+ for _, inputObj in node.run.iterInputAttributes():
1103
+ value = inputObj.get()
1104
+ if (value is not None and
1105
+ value.getObjId() == output.getObjId() and
1106
+ not node.run.isSaved()):
1107
+ deps.append(node.run)
1108
+
1109
+ if deps:
1110
+ error = 'Cannot DELETE Object, it is referenced from:'
1111
+ for d in deps:
1112
+ error += '\n - %s' % d.getRunName()
1113
+ raise Exception(error)
1114
+ else:
1115
+ protocol.deleteOutput(output)
1116
+ pwutils.path.copyFile(self.dbPath, protocol.getDbPath())
1117
+
1118
+ def __setProtocolLabel(self, newProt):
1119
+ """ Set a readable label to a newly created protocol.
1120
+ We will try to find another existing protocol with the default label
1121
+ and then use an incremental labeling in parenthesis (<number>++)
1122
+ """
1123
+ defaultLabel = newProt.getClassLabel()
1124
+ maxSuffix = 0
1125
+
1126
+ for prot in self.getRuns(iterate=True, refresh=False):
1127
+ otherProtLabel = prot.getObjLabel()
1128
+ m = REGEX_NUMBER_ENDING.match(otherProtLabel)
1129
+ if m and m.groupdict()['prefix'].strip() == defaultLabel:
1130
+ stringSuffix = m.groupdict()['number'].strip('(').strip(')')
1131
+ try:
1132
+ maxSuffix = max(int(stringSuffix), maxSuffix)
1133
+ except:
1134
+ logger.error("Couldn't set protocol's label. %s" % stringSuffix)
1135
+ elif otherProtLabel == defaultLabel: # When only we have the prefix,
1136
+ maxSuffix = max(1, maxSuffix) # this REGEX don't match.
1137
+
1138
+ if maxSuffix:
1139
+ protLabel = '%s (%d)' % (defaultLabel, maxSuffix+1)
1140
+ else:
1141
+ protLabel = defaultLabel
1142
+
1143
+ newProt.setObjLabel(protLabel)
1144
+
1145
+ def newProtocol(self, protocolClass, **kwargs):
1146
+ """ Create a new protocol from a given class. """
1147
+ newProt = protocolClass(project=self, **kwargs)
1148
+ # Only set a default label to the protocol if is was not
1149
+ # set through the kwargs
1150
+ if not newProt.getObjLabel():
1151
+ self.__setProtocolLabel(newProt)
1152
+
1153
+ newProt.setMapper(self.mapper)
1154
+ newProt.setProject(self)
1155
+
1156
+ return newProt
1157
+
1158
+ def __getIOMatches(self, node, childNode):
1159
+ """ Check if some output of node is used as input in childNode.
1160
+ Return the list of attribute names that matches.
1161
+ Used from self.copyProtocol
1162
+ """
1163
+ matches = []
1164
+ for iKey, iAttr in childNode.run.iterInputAttributes():
1165
+ # As this point iAttr should be always a Pointer that
1166
+ # points to the output of other protocol
1167
+ if iAttr.getObjValue() is node.run:
1168
+ oKey = iAttr.getExtended()
1169
+ matches.append((oKey, iKey))
1170
+ else:
1171
+ for oKey, oAttr in node.run.iterOutputAttributes():
1172
+ # If node output is "real" and iAttr is still just a pointer
1173
+ # the iAttr.get() will return None
1174
+ pointed = iAttr.get()
1175
+ if pointed is not None and oAttr.getObjId() == pointed.getObjId():
1176
+ matches.append((oKey, iKey))
1177
+
1178
+ return matches
1179
+
1180
+ def __cloneProtocol(self, protocol):
1181
+ """ Make a copy of the protocol parameters, not outputs.
1182
+ We will label the new protocol with the same name adding the
1183
+ parenthesis as follow -> (copy) -> (copy 2) -> (copy 3)
1184
+ """
1185
+ newProt = self.newProtocol(protocol.getClass())
1186
+ oldProtName = protocol.getRunName()
1187
+ maxSuffix = 0
1188
+
1189
+ # if '(copy...' suffix is not in the old name, we add it in the new name
1190
+ # and setting the newnumber
1191
+ mOld = REGEX_NUMBER_ENDING_CP.match(oldProtName)
1192
+ if mOld:
1193
+ newProtPrefix = mOld.groupdict()['prefix']
1194
+ if mOld.groupdict()['number'] == '':
1195
+ oldNumber = 1
1196
+ else:
1197
+ oldNumber = int(mOld.groupdict()['number'])
1198
+ else:
1199
+ newProtPrefix = oldProtName + ' (copy'
1200
+ oldNumber = 0
1201
+ newNumber = oldNumber + 1
1202
+
1203
+ # looking for "<old name> (copy" prefixes in the project and
1204
+ # setting the newNumber as the maximum+1
1205
+ for prot in self.getRuns(iterate=True, refresh=False):
1206
+ otherProtLabel = prot.getObjLabel()
1207
+ mOther = REGEX_NUMBER_ENDING_CP.match(otherProtLabel)
1208
+ if mOther and mOther.groupdict()['prefix'] == newProtPrefix:
1209
+ stringSuffix = mOther.groupdict()['number']
1210
+ if stringSuffix == '':
1211
+ stringSuffix = 1
1212
+ maxSuffix = max(maxSuffix, int(stringSuffix))
1213
+ if newNumber <= maxSuffix:
1214
+ newNumber = maxSuffix + 1
1215
+
1216
+ # building the new name
1217
+ if newNumber == 1:
1218
+ newProtLabel = newProtPrefix + ')'
1219
+ else:
1220
+ newProtLabel = '%s %d)' % (newProtPrefix, newNumber)
1221
+
1222
+ newProt.setObjLabel(newProtLabel)
1223
+ newProt.copyDefinitionAttributes(protocol)
1224
+ newProt.copyAttributes(protocol, 'hostName', '_useQueue', '_queueParams')
1225
+ newProt.runMode.set(MODE_RESTART)
1226
+ newProt.cleanExecutionAttributes() # Clean jobIds and Pid; otherwise, this would retain old job IDs and PIDs.
1227
+
1228
+ return newProt
1229
+
1230
+ def copyProtocol(self, protocol):
1231
+ """ Make a copy of the protocol,
1232
+ Return a new instance with copied values. """
1233
+ result = None
1234
+
1235
+ if isinstance(protocol, pwprot.Protocol):
1236
+ result = self.__cloneProtocol(protocol)
1237
+
1238
+ elif isinstance(protocol, list):
1239
+ # Handle the copy of a list of protocols
1240
+ # for this case we need to update the references of input/outputs
1241
+ newDict = {}
1242
+
1243
+ for prot in protocol:
1244
+ newProt = self.__cloneProtocol(prot)
1245
+ newDict[prot.getObjId()] = newProt
1246
+ self.saveProtocol(newProt)
1247
+
1248
+ g = self.getRunsGraph()
1249
+
1250
+ for prot in protocol:
1251
+ node = g.getNode(prot.strId())
1252
+ newProt = newDict[prot.getObjId()]
1253
+
1254
+ for childNode in node.getChildren():
1255
+ newChildProt = newDict.get(childNode.run.getObjId(), None)
1256
+
1257
+ if newChildProt:
1258
+ # Get the matches between outputs/inputs of
1259
+ # node and childNode
1260
+ matches = self.__getIOMatches(node, childNode)
1261
+ # For each match, set the pointer and the extend
1262
+ # attribute to reproduce the dependencies in the
1263
+ # new workflow
1264
+ for oKey, iKey in matches:
1265
+ childPointer = getattr(newChildProt, iKey)
1266
+
1267
+ # Scalar with pointer case: If is a scalar with a pointer
1268
+ if isinstance(childPointer, pwobj.Scalar) and childPointer.hasPointer():
1269
+ # In this case childPointer becomes the contained Pointer
1270
+ childPointer = childPointer.getPointer()
1271
+
1272
+ elif isinstance(childPointer, pwobj.PointerList):
1273
+ for p in childPointer:
1274
+ if p.getObjValue().getObjId() == prot.getObjId():
1275
+ childPointer = p
1276
+ childPointer.set(newProt)
1277
+ childPointer.setExtended(oKey)
1278
+ self.mapper.store(newChildProt)
1279
+
1280
+ self.mapper.commit()
1281
+ else:
1282
+ raise Exception("Project.copyProtocol: invalid input protocol ' "
1283
+ "'type '%s'." % type(protocol))
1284
+
1285
+ return result
1286
+
1287
+ def getProjectUsage(self) -> ScipionWorkflow:
1288
+ """ returns usage class ScipionWorkflow populated with project data
1289
+ """
1290
+ protocols = self.getRuns()
1291
+
1292
+ # Handle the copy of a list of protocols
1293
+ # for this case we need to update the references of input/outputs
1294
+ sw = ScipionWorkflow()
1295
+ g = self.getRunsGraph()
1296
+
1297
+ for prot in protocols:
1298
+
1299
+ if not isinstance(prot, LegacyProtocol):
1300
+ # Add a count for the protocol
1301
+ protName = prot.getClassName()
1302
+ sw.addCount(protName)
1303
+
1304
+ # Add next protocols count
1305
+ node = g.getNode(prot.strId())
1306
+
1307
+ for childNode in node.getChildren():
1308
+ prot = childNode.run
1309
+ if not isinstance(prot, LegacyProtocol):
1310
+ nextProtName = prot.getClassName()
1311
+ sw.addCountToNextProtocol(protName, nextProtName)
1312
+
1313
+ # Special case: First protocols, those without parent. Import protocols mainly.
1314
+ # All protocols, even the firs ones have a parent. For the fisrt ones the parent is "PROJECT" node that is the only root one.
1315
+ if node.getParent().isRoot():
1316
+ sw.addCountToNextProtocol(str(None), protName)
1317
+
1318
+ return sw
1319
+
1320
+ def getProtocolsDict(self, protocols=None, namesOnly=False):
1321
+ """ Creates a dict with the information of the given protocols.
1322
+
1323
+ :param protocols: list of protocols or None to include all.
1324
+ :param namesOnly: the output list will contain only the protocol names.
1325
+
1326
+ """
1327
+ protocols = protocols or self.getRuns()
1328
+
1329
+ # If the nameOnly, we will simply return a json list with their names
1330
+ if namesOnly:
1331
+ return {i: prot.getClassName() for i, prot in enumerate(protocols)}
1332
+
1333
+ # Handle the copy of a list of protocols
1334
+ # for this case we need to update the references of input/outputs
1335
+ newDict = OrderedDict()
1336
+
1337
+ for prot in protocols:
1338
+ newDict[prot.getObjId()] = prot.getDefinitionDict()
1339
+
1340
+ g = self.getRunsGraph()
1341
+
1342
+ for prot in protocols:
1343
+ protId = prot.getObjId()
1344
+ node = g.getNode(prot.strId())
1345
+
1346
+ for childNode in node.getChildren():
1347
+ childId = childNode.run.getObjId()
1348
+ childProt = childNode.run
1349
+ if childId in newDict:
1350
+ childDict = newDict[childId]
1351
+ # Get the matches between outputs/inputs of
1352
+ # node and childNode
1353
+ matches = self.__getIOMatches(node, childNode)
1354
+ for oKey, iKey in matches:
1355
+ inputAttr = getattr(childProt, iKey)
1356
+ if isinstance(inputAttr, pwobj.PointerList):
1357
+ childDict[iKey] = [p.getUniqueId() for p in
1358
+ inputAttr]
1359
+ else:
1360
+ childDict[iKey] = '%s.%s' % (
1361
+ protId, oKey) # equivalent to pointer.getUniqueId
1362
+
1363
+ return newDict
1364
+
1365
+ def getProtocolsJson(self, protocols=None, namesOnly=False):
1366
+ """
1367
+ Wraps getProtocolsDict to get a json string
1368
+
1369
+ :param protocols: list of protocols or None to include all.
1370
+ :param namesOnly: the output list will contain only the protocol names.
1371
+
1372
+ """
1373
+ newDict = self.getProtocolsDict(protocols=protocols, namesOnly=namesOnly)
1374
+ return json.dumps(list(newDict.values()),
1375
+ indent=4, separators=(',', ': '))
1376
+
1377
+ def exportProtocols(self, protocols, filename):
1378
+ """ Create a text json file with the info
1379
+ to import the workflow into another project.
1380
+ This method is very similar to copyProtocol
1381
+
1382
+ :param protocols: a list of protocols to export.
1383
+ :param filename: the filename where to write the workflow.
1384
+
1385
+ """
1386
+ jsonStr = self.getProtocolsJson(protocols)
1387
+ f = open(filename, 'w')
1388
+ f.write(jsonStr)
1389
+ f.close()
1390
+
1391
+ def loadProtocols(self, filename=None, jsonStr=None):
1392
+ """ Load protocols generated in the same format as self.exportProtocols.
1393
+
1394
+ :param filename: the path of the file where to read the workflow.
1395
+ :param jsonStr:
1396
+
1397
+ Note: either filename or jsonStr should be not None.
1398
+
1399
+ """
1400
+ importDir = None
1401
+ if filename:
1402
+ with open(filename) as f:
1403
+ importDir = os.path.dirname(filename)
1404
+ protocolsList = json.load(f)
1405
+
1406
+ elif jsonStr:
1407
+ protocolsList = json.loads(jsonStr)
1408
+ else:
1409
+ logger.error("Invalid call to loadProtocols. Either filename or jsonStr has to be passed.")
1410
+ return
1411
+
1412
+ emProtocols = self._domain.getProtocols()
1413
+ newDict = OrderedDict()
1414
+
1415
+ # First iteration: create all protocols and setup parameters
1416
+ for i, protDict in enumerate(protocolsList):
1417
+ protClassName = protDict['object.className']
1418
+ protId = protDict['object.id']
1419
+ protClass = emProtocols.get(protClassName, None)
1420
+
1421
+ if protClass is None:
1422
+ logger.error("Protocol with class name '%s' not found. Are you missing its plugin?." % protClassName)
1423
+ else:
1424
+ protLabel = protDict.get('object.label', None)
1425
+ prot = self.newProtocol(protClass,
1426
+ objLabel=protLabel,
1427
+ objComment=protDict.get('object.comment', None))
1428
+ protocolsList[i] = prot.processImportDict(protDict, importDir) if importDir else protDict
1429
+
1430
+ prot._useQueue.set(protDict.get('_useQueue', pw.Config.SCIPION_USE_QUEUE))
1431
+ prot._queueParams.set(protDict.get('_queueParams', None))
1432
+ prot._prerequisites.set(protDict.get('_prerequisites', None))
1433
+ prot.forceSchedule.set(protDict.get('forceSchedule', False))
1434
+ newDict[protId] = prot
1435
+ # This saves the protocol JUST with the common attributes. Is it necessary?
1436
+ # Actually, if after this the is an error, the protocol appears.
1437
+ self.saveProtocol(prot)
1438
+
1439
+ # Second iteration: update pointers values
1440
+ def _setPointer(pointer, value):
1441
+ # Properly setup the pointer value checking if the
1442
+ # id is already present in the dictionary
1443
+ # Value to pointers could be None: Partial workflows
1444
+ if value:
1445
+ parts = value.split('.')
1446
+
1447
+ protId = parts[0]
1448
+ # Try to get the protocol holding the input form the dictionary
1449
+ target = newDict.get(protId, None)
1450
+
1451
+ if target is None:
1452
+ # Try to use existing protocol in the project
1453
+ logger.info("Protocol identifier (%s) not self contained. Looking for it in the project." % protId)
1454
+
1455
+ try:
1456
+ target = self.getProtocol(int(protId), fromRuns=True)
1457
+ except:
1458
+ # Not a protocol..
1459
+ logger.info("%s is not a protocol identifier. Probably a direct pointer created by tests. This case is not considered." % protId)
1460
+
1461
+ if target:
1462
+ logger.info("Linking %s to existing protocol in the project: %s" % (prot, target))
1463
+
1464
+ pointer.set(target)
1465
+ if not pointer.pointsNone():
1466
+ pointer.setExtendedParts(parts[1:])
1467
+
1468
+ def _setPrerequisites(prot):
1469
+ prerequisites = prot.getPrerequisites()
1470
+ if prerequisites:
1471
+ newPrerequisites = []
1472
+ for prerequisite in prerequisites:
1473
+ if prerequisite in newDict:
1474
+ newProtId = newDict[prerequisite].getObjId()
1475
+ newPrerequisites.append(newProtId)
1476
+ else:
1477
+ logger.info('"Wait for" id %s missing: ignored.' % prerequisite)
1478
+ prot._prerequisites.set(newPrerequisites)
1479
+
1480
+ for protDict in protocolsList:
1481
+ protId = protDict['object.id']
1482
+
1483
+ if protId in newDict:
1484
+ prot = newDict[protId]
1485
+ _setPrerequisites(prot)
1486
+ for paramName, attr in prot.iterDefinitionAttributes():
1487
+ if paramName in protDict:
1488
+ # If the attribute is a pointer, we should look
1489
+ # if the id is already in the dictionary and
1490
+ # set the extended property
1491
+ if attr.isPointer():
1492
+ _setPointer(attr, protDict[paramName])
1493
+ # This case is similar to Pointer, but the values
1494
+ # is a list and we will setup a pointer for each value
1495
+ elif isinstance(attr, pwobj.PointerList):
1496
+ attribute = protDict[paramName]
1497
+ if attribute is None:
1498
+ continue
1499
+ for value in attribute:
1500
+ p = pwobj.Pointer()
1501
+ _setPointer(p, value)
1502
+ attr.append(p)
1503
+ # For "normal" parameters we just set the string value
1504
+ else:
1505
+ try:
1506
+ attr.set(protDict[paramName])
1507
+ # Case for Scalars with pointers. So far this will work for Numbers. With Strings (still there are no current examples)
1508
+ # We will need something different to test if the value look like a pointer: regex? ####.text
1509
+ except ValueError as e:
1510
+ newPointer = pwobj.Pointer()
1511
+ _setPointer(newPointer, protDict[paramName])
1512
+ attr.setPointer(newPointer)
1513
+
1514
+ self.mapper.store(prot)
1515
+
1516
+ self.mapper.commit()
1517
+
1518
+ return newDict
1519
+
1520
+ def saveProtocol(self, protocol):
1521
+ self._checkModificationAllowed([protocol], 'Cannot SAVE protocol')
1522
+
1523
+ if (protocol.isRunning() or protocol.isFinished()
1524
+ or protocol.isLaunched()):
1525
+ raise ModificationNotAllowedException('Cannot SAVE a protocol that is %s. '
1526
+ 'Copy it instead.' % protocol.getStatus())
1527
+
1528
+ protocol.setStatus(pwprot.STATUS_SAVED)
1529
+ if protocol.hasObjId():
1530
+ self._storeProtocol(protocol)
1531
+ else:
1532
+ self._setupProtocol(protocol)
1533
+
1534
+ def getProtocolFromRuns(self, protId):
1535
+ """ Returns the protocol with the id=protId from the runs list (memory) or None"""
1536
+ if self.runs:
1537
+ for run in self.runs:
1538
+ if run.getObjId() == protId:
1539
+ return run
1540
+
1541
+ return None
1542
+
1543
+ def getProtocol(self, protId, fromRuns=False):
1544
+ """ Returns the protocol with the id=protId or raises an Exception
1545
+
1546
+ :param protId: integer with an existing protocol identifier
1547
+ :param fromRuns: If true, it tries to get it from the runs list (memory) avoiding querying the db."""
1548
+
1549
+ protocol = self.getProtocolFromRuns(protId) if fromRuns else None
1550
+
1551
+ if protocol is None:
1552
+ protocol = self.mapper.selectById(protId)
1553
+
1554
+ if not isinstance(protocol, pwprot.Protocol):
1555
+ raise Exception('>>> ERROR: Invalid protocol id: %d' % protId)
1556
+
1557
+ self._setProtocolMapper(protocol)
1558
+
1559
+ return protocol
1560
+
1561
+ # FIXME: this function just return if a given object exists, not
1562
+ # if it is a protocol, so it is incorrect judging by the name
1563
+ # Moreover, a more consistent name (comparing to similar methods)
1564
+ # would be: hasProtocol
1565
+ def doesProtocolExists(self, protId):
1566
+ return self.mapper.exists(protId)
1567
+
1568
+ def getProtocolsByClass(self, className):
1569
+ return self.mapper.selectByClass(className)
1570
+
1571
+ def getObject(self, objId):
1572
+ """ Retrieve an object from the db given its id. """
1573
+ return self.mapper.selectById(objId)
1574
+
1575
+ def _setHostConfig(self, protocol):
1576
+ """ Set the appropriate host config to the protocol
1577
+ give its value of 'hostname'
1578
+ """
1579
+ hostName = protocol.getHostName()
1580
+ hostConfig = self.getHostConfig(hostName)
1581
+ protocol.setHostConfig(hostConfig)
1582
+
1583
+ def _storeProtocol(self, protocol):
1584
+ # Read only mode
1585
+ if not self.openedAsReadOnly():
1586
+ self.mapper.store(protocol)
1587
+ self.mapper.commit()
1588
+
1589
+ def _setProtocolMapper(self, protocol):
1590
+ """ Set the project and mapper to the protocol. """
1591
+
1592
+ # Tolerate loading errors. For support.
1593
+ # When only having the sqlite, sometime there are exceptions here
1594
+ # due to the absence of a set.
1595
+ from pyworkflow.mapper.sqlite import SqliteFlatMapperException
1596
+ try:
1597
+
1598
+ protocol.setProject(self)
1599
+ protocol.setMapper(self.mapper)
1600
+ self._setHostConfig(protocol)
1601
+
1602
+ except SqliteFlatMapperException:
1603
+ protocol.addSummaryWarning(
1604
+ "*Protocol loading problem*: A set related to this "
1605
+ "protocol couldn't be loaded.")
1606
+
1607
+ def _setupProtocol(self, protocol):
1608
+ """Insert a new protocol instance in the database"""
1609
+
1610
+ # Read only mode
1611
+ if not self.openedAsReadOnly():
1612
+ self._storeProtocol(protocol) # Store first to get a proper id
1613
+ # Set important properties of the protocol
1614
+ workingDir = self.getProtWorkingDir(protocol)
1615
+ self._setProtocolMapper(protocol)
1616
+
1617
+ protocol.setWorkingDir(self.getPath(PROJECT_RUNS, workingDir))
1618
+ # Update with changes
1619
+ self._storeProtocol(protocol)
1620
+
1621
+ @staticmethod
1622
+ def getProtWorkingDir(protocol):
1623
+ """
1624
+ Return the protocol working directory
1625
+ """
1626
+ return "%06d_%s" % (protocol.getObjId(), protocol.getClassName())
1627
+
1628
+ def getRuns(self, iterate=False, refresh=True, checkPids=False):
1629
+ """ Return the existing protocol runs in the project.
1630
+ """
1631
+ if self.runs is None or refresh:
1632
+ # Close db open connections to db files
1633
+ if self.runs is not None:
1634
+ for r in self.runs:
1635
+ r.closeMappers()
1636
+
1637
+ # Use new selectAll Batch
1638
+ # self.runs = self.mapper.selectAll(iterate=False,
1639
+ # objectFilter=lambda o: isinstance(o, pwprot.Protocol))
1640
+ self.runs = self.mapper.selectAllBatch(objectFilter=lambda o: isinstance(o, pwprot.Protocol))
1641
+
1642
+ # Invalidate _runsGraph because the runs are updated
1643
+ self._runsGraph = None
1644
+
1645
+ for r in self.runs:
1646
+
1647
+ self._setProtocolMapper(r)
1648
+ r.setProject(self)
1649
+
1650
+ # Check for run warnings
1651
+ r.checkSummaryWarnings()
1652
+
1653
+ # Update nodes that are running and were not invoked
1654
+ # by other protocols
1655
+ if r.isActive():
1656
+ if not r.isChild():
1657
+ self._updateProtocol(r, checkPid=checkPids)
1658
+
1659
+ self._annotateLastRunTime(r.endTime)
1660
+
1661
+ self.mapper.commit()
1662
+
1663
+ return self.runs
1664
+
1665
+ def _annotateLastRunTime(self, protLastTS):
1666
+ """ Sets _lastRunTime for the project if it is after current _lastRunTime"""
1667
+ try:
1668
+ if protLastTS is None:
1669
+ return
1670
+
1671
+ if self._lastRunTime is None:
1672
+ self._lastRunTime = protLastTS
1673
+ elif self._lastRunTime.datetime() < protLastTS.datetime():
1674
+ self._lastRunTime = protLastTS
1675
+ except Exception as e:
1676
+ return
1677
+
1678
+ def needRefresh(self):
1679
+ """ True if any run is active and its timestamp is older than its
1680
+ corresponding runs.db
1681
+ NOTE: If an external script changes the DB this will fail. It uses
1682
+ only in memory objects."""
1683
+ for run in self.runs:
1684
+ if run.isActive():
1685
+ if not pwprot.isProtocolUpToDate(run):
1686
+ return True
1687
+ return False
1688
+
1689
+ def checkPid(self, protocol):
1690
+ """ Check if a running protocol is still alive or not.
1691
+ The check will only be done for protocols that have not been sent
1692
+ to a queue system.
1693
+
1694
+ :returns True if pid is alive or irrelevant
1695
+ """
1696
+ from pyworkflow.protocol.launch import _runsLocally
1697
+ pid = protocol.getPid()
1698
+
1699
+ if pid == 0:
1700
+ return True
1701
+
1702
+ # Include running and scheduling ones
1703
+ # Exclude interactive protocols
1704
+ # NOTE: This may be happening even with successfully finished protocols
1705
+ # which PID is gone.
1706
+ if (protocol.isActive() and not protocol.isInteractive()
1707
+ and not pwutils.isProcessAlive(pid)):
1708
+ protocol.setFailed("Process %s not found running on the machine. "
1709
+ "It probably has died or been killed without "
1710
+ "reporting the status to Scipion. Logs might "
1711
+ "have information about what happened to this "
1712
+ "process." % pid)
1713
+ return False
1714
+
1715
+ return True
1716
+
1717
+ def checkJobId(self, protocol):
1718
+ """ Check if a running protocol is still alive or not.
1719
+ The check will only be done for protocols that have been sent
1720
+ to a queue system.
1721
+
1722
+ :returns True if job is still alive or irrelevant
1723
+ """
1724
+ if len(protocol.getJobIds()) == 0:
1725
+ logger.warning("Checking if protocol alive in the queue but JOB ID is empty. Considering it dead.")
1726
+ return False
1727
+ jobid = protocol.getJobIds()[0]
1728
+ hostConfig = protocol.getHostConfig()
1729
+
1730
+ if jobid == UNKNOWN_JOBID:
1731
+ return True
1732
+
1733
+ # Include running and scheduling ones
1734
+ # Exclude interactive protocols
1735
+ # NOTE: This may be happening even with successfully finished protocols
1736
+ # which PID is gone.
1737
+ if protocol.isActive() and not protocol.isInteractive():
1738
+
1739
+ jobStatus = _checkJobStatus(hostConfig, jobid)
1740
+
1741
+ if jobStatus == STATUS_FINISHED:
1742
+ protocol.setFailed("JOB ID %s not found running on the queue engine. "
1743
+ "It probably has timeout, died or been killed without "
1744
+ "reporting the status to Scipion. Logs might "
1745
+ "have information about what happened to this "
1746
+ "JOB ID." % jobid)
1747
+
1748
+ return False
1749
+
1750
+ return True
1751
+ def iterSubclasses(self, classesName, objectFilter=None):
1752
+ """ Retrieve all objects from the project that are instances
1753
+ of any of the classes in classesName list.
1754
+ Params:
1755
+ classesName: String with commas separated values of classes name.
1756
+ objectFilter: a filter function to discard some of the retrieved
1757
+ objects."""
1758
+ for objClass in classesName.split(","):
1759
+ for obj in self.mapper.selectByClass(objClass.strip(), iterate=True,
1760
+ objectFilter=objectFilter):
1761
+ yield obj
1762
+
1763
+ def getRunsGraph(self, refresh=False, checkPids=False):
1764
+ """ Build a graph taking into account the dependencies between
1765
+ different runs, ie. which outputs serves as inputs of other protocols.
1766
+ """
1767
+
1768
+ if refresh or self._runsGraph is None:
1769
+ runs = [r for r in self.getRuns(refresh=refresh, checkPids=checkPids)
1770
+ if not r.isChild()]
1771
+ self._runsGraph = self.getGraphFromRuns(runs)
1772
+
1773
+ return self._runsGraph
1774
+
1775
+ def getGraphFromRuns(self, runs):
1776
+ """
1777
+ This function will build a dependencies graph from a set
1778
+ of given runs.
1779
+
1780
+ :param runs: The input runs to build the graph
1781
+ :return: The graph taking into account run dependencies
1782
+
1783
+ """
1784
+ outputDict = {} # Store the output dict
1785
+ g = pwutils.Graph(rootName=ROOT_NODE_NAME)
1786
+
1787
+ for r in runs:
1788
+ n = g.createNode(r.strId())
1789
+ n.run = r
1790
+
1791
+ # Legacy protocols do not have a plugin!!
1792
+ develTxt = ''
1793
+ plugin = r.getPlugin()
1794
+ if plugin and plugin.inDevelMode():
1795
+ develTxt = '* '
1796
+
1797
+ n.setLabel('%s%s' % (develTxt, r.getRunName()))
1798
+ outputDict[r.getObjId()] = n
1799
+ for _, attr in r.iterOutputAttributes():
1800
+ # mark this output as produced by r
1801
+ if attr is None:
1802
+ logger.warning("Output attribute %s of %s is None" % (_, r))
1803
+ else:
1804
+ outputDict[attr.getObjId()] = n
1805
+
1806
+ def _checkInputAttr(node, pointed):
1807
+ """ Check if an attr is registered as output"""
1808
+ if pointed is not None:
1809
+ pointedId = pointed.getObjId()
1810
+
1811
+ if pointedId in outputDict:
1812
+ parentNode = outputDict[pointedId]
1813
+ if parentNode is node:
1814
+ logger.warning("WARNING: Found a cyclic dependence from node %s to itself, probably a bug. " % pointedId)
1815
+ else:
1816
+ parentNode.addChild(node)
1817
+ if os.environ.get('CHECK_CYCLIC_REDUNDANCY') and self._checkCyclicRedundancy(parentNode, node):
1818
+ conflictiveNodes = set()
1819
+ for child in node.getChildren():
1820
+ if node in child._parents:
1821
+ child._parents.remove(node)
1822
+ conflictiveNodes.add(child)
1823
+ logger.warning("WARNING: Found a cyclic dependence from node %s to %s, probably a bug. "
1824
+ % (node.getLabel() + '(' + node.getName() + ')',
1825
+ child.getLabel() + '(' + child.getName() + ')'))
1826
+
1827
+ for conflictNode in conflictiveNodes:
1828
+ node._children.remove(conflictNode)
1829
+
1830
+ return False
1831
+ return True
1832
+ return False
1833
+
1834
+ for r in runs:
1835
+ node = g.getNode(r.strId())
1836
+ for _, attr in r.iterInputAttributes():
1837
+ if attr.hasValue():
1838
+ pointed = attr.getObjValue()
1839
+ # Only checking pointed object and its parent, if more
1840
+ # levels we need to go up to get the correct dependencies
1841
+ if not _checkInputAttr(node, pointed):
1842
+ parent = self.mapper.getParent(pointed)
1843
+ _checkInputAttr(node, parent)
1844
+ rootNode = g.getRoot()
1845
+ rootNode.run = None
1846
+ rootNode.label = ROOT_NODE_NAME
1847
+
1848
+ for n in g.getNodes():
1849
+ if n.isRoot() and n is not rootNode:
1850
+ rootNode.addChild(n)
1851
+ return g
1852
+
1853
+ @staticmethod
1854
+ def _checkCyclicRedundancy(parent, child):
1855
+ visitedNodes = set()
1856
+ recursionStack = set()
1857
+
1858
+ def depthFirstSearch(node):
1859
+ visitedNodes.add(node)
1860
+ recursionStack.add(node)
1861
+ for child in node.getChildren():
1862
+ if child not in visitedNodes:
1863
+ if depthFirstSearch(child):
1864
+ return True
1865
+ elif child in recursionStack and child != parent:
1866
+ return True
1867
+
1868
+ recursionStack.remove(node)
1869
+ return False
1870
+
1871
+ return depthFirstSearch(child)
1872
+
1873
+
1874
+ def _getRelationGraph(self, relation=pwobj.RELATION_SOURCE, refresh=False):
1875
+ """ Retrieve objects produced as outputs and
1876
+ make a graph taking into account the SOURCE relation. """
1877
+ relations = self.mapper.getRelationsByName(relation)
1878
+ g = pwutils.Graph(rootName=ROOT_NODE_NAME)
1879
+ root = g.getRoot()
1880
+ root.pointer = None
1881
+ runs = self.getRuns(refresh=refresh)
1882
+
1883
+ for r in runs:
1884
+ for paramName, attr in r.iterOutputAttributes():
1885
+ p = pwobj.Pointer(r, extended=paramName)
1886
+ node = g.createNode(p.getUniqueId(), attr.getNameId())
1887
+ node.pointer = p
1888
+ # The following alias if for backward compatibility
1889
+ p2 = pwobj.Pointer(attr)
1890
+ g.aliasNode(node, p2.getUniqueId())
1891
+
1892
+ for rel in relations:
1893
+ pObj = self.getObject(rel[OBJECT_PARENT_ID])
1894
+
1895
+ # Duplicated ...
1896
+ if pObj is None:
1897
+ logger.warning("Relation seems to point to a deleted object. "
1898
+ "%s: %s" % (OBJECT_PARENT_ID, rel[OBJECT_PARENT_ID]))
1899
+ continue
1900
+
1901
+ pExt = rel['object_parent_extended']
1902
+ pp = pwobj.Pointer(pObj, extended=pExt)
1903
+
1904
+ if pObj is None or pp.get() is None:
1905
+ logger.error("project._getRelationGraph: pointer to parent is "
1906
+ "None. IGNORING IT.\n")
1907
+ for key in rel.keys():
1908
+ logger.info("%s: %s" % (key, rel[key]))
1909
+
1910
+ continue
1911
+
1912
+ pid = pp.getUniqueId()
1913
+ parent = g.getNode(pid)
1914
+
1915
+ while not parent and pp.hasExtended():
1916
+ pp.removeExtended()
1917
+ parent = g.getNode(pp.getUniqueId())
1918
+
1919
+ if not parent:
1920
+ logger.error("project._getRelationGraph: parent Node "
1921
+ "is None: %s" % pid)
1922
+ else:
1923
+ cObj = self.getObject(rel['object_child_id'])
1924
+ cExt = rel['object_child_extended']
1925
+
1926
+ if cObj is not None:
1927
+ if cObj.isPointer():
1928
+ cp = cObj
1929
+ if cExt:
1930
+ cp.setExtended(cExt)
1931
+ else:
1932
+ cp = pwobj.Pointer(cObj, extended=cExt)
1933
+ child = g.getNode(cp.getUniqueId())
1934
+
1935
+ if not child:
1936
+ logger.error("project._getRelationGraph: child Node "
1937
+ "is None: %s." % cp.getUniqueId())
1938
+ logger.error(" parent: %s" % pid)
1939
+ else:
1940
+ parent.addChild(child)
1941
+ else:
1942
+ logger.error("project._getRelationGraph: child Obj "
1943
+ "is None, id: %s " % rel['object_child_id'])
1944
+ logger.error(" parent: %s" % pid)
1945
+
1946
+ for n in g.getNodes():
1947
+ if n.isRoot() and n is not root:
1948
+ root.addChild(n)
1949
+
1950
+ return g
1951
+
1952
+ def getSourceChilds(self, obj):
1953
+ """ Return all the objects have used obj
1954
+ as a source.
1955
+ """
1956
+ return self.mapper.getRelationChilds(pwobj.RELATION_SOURCE, obj)
1957
+
1958
+ def getSourceParents(self, obj):
1959
+ """ Return all the objects that are SOURCE of this object.
1960
+ """
1961
+ return self.mapper.getRelationParents(pwobj.RELATION_SOURCE, obj)
1962
+
1963
+ def getTransformGraph(self, refresh=False):
1964
+ """ Get the graph from the TRANSFORM relation. """
1965
+ if refresh or not self._transformGraph:
1966
+ self._transformGraph = self._getRelationGraph(pwobj.RELATION_TRANSFORM,
1967
+ refresh)
1968
+
1969
+ return self._transformGraph
1970
+
1971
+ def getSourceGraph(self, refresh=False):
1972
+ """ Get the graph from the SOURCE relation. """
1973
+ if refresh or not self._sourceGraph:
1974
+ self._sourceGraph = self._getRelationGraph(pwobj.RELATION_SOURCE,
1975
+ refresh)
1976
+
1977
+ return self._sourceGraph
1978
+
1979
+ def getRelatedObjects(self, relation, obj, direction=pwobj.RELATION_CHILDS,
1980
+ refresh=False):
1981
+ """ Get all objects related to obj by a give relation.
1982
+
1983
+ :param relation: the relation name to search for.
1984
+ :param obj: object from which the relation will be search,
1985
+ actually not only this, but all other objects connected
1986
+ to this one by the pwobj.RELATION_TRANSFORM.
1987
+ :parameter direction: Not used
1988
+ :param refresh: If True, cached objects will be refreshed
1989
+
1990
+ """
1991
+
1992
+ graph = self.getTransformGraph(refresh)
1993
+ relations = self.mapper.getRelationsByName(relation)
1994
+ connection = self._getConnectedObjects(obj, graph)
1995
+
1996
+ objects = []
1997
+ objectsDict = {}
1998
+
1999
+ for rel in relations:
2000
+ pObj = self.getObject(rel[OBJECT_PARENT_ID])
2001
+
2002
+ if pObj is None:
2003
+ logger.warning("Relation seems to point to a deleted object. "
2004
+ "%s: %s" % (OBJECT_PARENT_ID, rel[OBJECT_PARENT_ID]))
2005
+ continue
2006
+ pExt = rel['object_parent_extended']
2007
+ pp = pwobj.Pointer(pObj, extended=pExt)
2008
+
2009
+ if pp.getUniqueId() in connection:
2010
+ cObj = self.getObject(rel['object_child_id'])
2011
+ cExt = rel['object_child_extended']
2012
+ cp = pwobj.Pointer(cObj, extended=cExt)
2013
+ if cp.hasValue() and cp.getUniqueId() not in objectsDict:
2014
+ objects.append(cp)
2015
+ objectsDict[cp.getUniqueId()] = True
2016
+
2017
+ return objects
2018
+
2019
+ def _getConnectedObjects(self, obj, graph):
2020
+ """ Given a TRANSFORM graph, return the elements that
2021
+ are connected to an object, either children, ancestors or siblings.
2022
+ """
2023
+ n = graph.getNode(obj.strId())
2024
+ # Get the oldest ancestor of a node, before reaching the root node
2025
+ while n is not None and not n.getParent().isRoot():
2026
+ n = n.getParent()
2027
+
2028
+ connection = {}
2029
+
2030
+ if n is not None:
2031
+ # Iterate recursively all descendants
2032
+ for node in n.iterChildren():
2033
+ connection[node.pointer.getUniqueId()] = True
2034
+ # Add also
2035
+ connection[node.pointer.get().strId()] = True
2036
+
2037
+ return connection
2038
+
2039
+ def isReadOnly(self):
2040
+ if getattr(self, 'settings', None) is None:
2041
+ return False
2042
+
2043
+ return self.settings.getReadOnly()
2044
+
2045
+ def isInReadOnlyFolder(self):
2046
+ return self._isInReadOnlyFolder
2047
+
2048
+ def openedAsReadOnly(self):
2049
+ return self.isReadOnly() or self.isInReadOnlyFolder()
2050
+
2051
+ def setReadOnly(self, value):
2052
+ self.settings.setReadOnly(value)
2053
+
2054
+ def fixLinks(self, searchDir):
2055
+ logger.info(f"Fixing links for project {self.getShortName()}. Searching in: {searchDir}")
2056
+ runs = self.getRuns()
2057
+
2058
+ counter = 0
2059
+ for prot in runs:
2060
+ if prot.getClassName().startswith("ProtImport"):
2061
+ runName = prot.getRunName()
2062
+ logger.info(f"Found protocol {runName}")
2063
+ for f in prot.getOutputFiles():
2064
+ if ':' in f:
2065
+ f = f.split(':')[0]
2066
+
2067
+ if not os.path.exists(f):
2068
+ logger.info(f"\tMissing link: {f}")
2069
+
2070
+ if os.path.islink(f):
2071
+ sourceFile = os.path.realpath(f)
2072
+ newFile = pwutils.findFileRecursive(os.path.basename(sourceFile),
2073
+ searchDir)
2074
+ if newFile:
2075
+ counter += 1
2076
+ logger.info(f"\t\tCreating link: {f} -> {newFile}")
2077
+ pwutils.createAbsLink(newFile, f)
2078
+
2079
+ logger.info(f"Fixed {counter} broken links")
2080
+
2081
+ @staticmethod
2082
+ def cleanProjectName(projectName):
2083
+ """ Cleans a project name to avoid common errors
2084
+ Use it whenever you want to get the final project name pyworkflow will end up.
2085
+ Spaces will be replaced by _ """
2086
+
2087
+ return re.sub(r"[^\w\d\-\_]", "-", projectName)
2088
+
2089
+
2090
+ class MissingProjectDbException(Exception):
2091
+ pass
2092
+
2093
+
2094
+ class ModificationNotAllowedException(Exception):
2095
+ pass