scipion-pyworkflow 3.10.6__py3-none-any.whl → 3.11.1__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.
- pyworkflow/config.py +131 -67
- pyworkflow/constants.py +2 -1
- pyworkflow/gui/browser.py +39 -5
- pyworkflow/gui/dialog.py +2 -0
- pyworkflow/gui/form.py +141 -52
- pyworkflow/gui/gui.py +8 -8
- pyworkflow/gui/project/project.py +6 -7
- pyworkflow/gui/project/searchprotocol.py +91 -7
- pyworkflow/gui/project/viewdata.py +1 -1
- pyworkflow/gui/project/viewprotocols.py +45 -22
- pyworkflow/gui/project/viewprotocols_extra.py +9 -6
- pyworkflow/gui/widgets.py +2 -2
- pyworkflow/mapper/sqlite.py +4 -4
- pyworkflow/plugin.py +93 -44
- pyworkflow/project/project.py +158 -70
- pyworkflow/project/usage.py +165 -0
- pyworkflow/protocol/executor.py +30 -18
- pyworkflow/protocol/hosts.py +9 -6
- pyworkflow/protocol/launch.py +15 -8
- pyworkflow/protocol/params.py +59 -19
- pyworkflow/protocol/protocol.py +124 -58
- pyworkflow/resources/showj/arrowDown.png +0 -0
- pyworkflow/resources/showj/arrowUp.png +0 -0
- pyworkflow/resources/showj/background_section.png +0 -0
- pyworkflow/resources/showj/colRowModeOff.png +0 -0
- pyworkflow/resources/showj/colRowModeOn.png +0 -0
- pyworkflow/resources/showj/delete.png +0 -0
- pyworkflow/resources/showj/doc_icon.png +0 -0
- pyworkflow/resources/showj/download_icon.png +0 -0
- pyworkflow/resources/showj/enabled_gallery.png +0 -0
- pyworkflow/resources/showj/galleryViewOff.png +0 -0
- pyworkflow/resources/showj/galleryViewOn.png +0 -0
- pyworkflow/resources/showj/goto.png +0 -0
- pyworkflow/resources/showj/menu.png +0 -0
- pyworkflow/resources/showj/separator.png +0 -0
- pyworkflow/resources/showj/tableViewOff.png +0 -0
- pyworkflow/resources/showj/tableViewOn.png +0 -0
- pyworkflow/resources/showj/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
- pyworkflow/resources/showj/ui-bg_glass_95_fef1ec_1x400.png +0 -0
- pyworkflow/resources/showj/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
- pyworkflow/resources/showj/volumeOff.png +0 -0
- pyworkflow/resources/showj/volumeOn.png +0 -0
- pyworkflow/utils/log.py +15 -6
- pyworkflow/utils/properties.py +78 -92
- pyworkflow/utils/utils.py +3 -2
- pyworkflow/viewer.py +23 -1
- pyworkflow/webservices/config.py +0 -3
- pyworkflow/webservices/notifier.py +24 -34
- pyworkflowtests/protocols.py +1 -3
- pyworkflowtests/tests/test_protocol_execution.py +4 -0
- {scipion_pyworkflow-3.10.6.dist-info → scipion_pyworkflow-3.11.1.dist-info}/METADATA +13 -27
- {scipion_pyworkflow-3.10.6.dist-info → scipion_pyworkflow-3.11.1.dist-info}/RECORD +56 -35
- {scipion_pyworkflow-3.10.6.dist-info → scipion_pyworkflow-3.11.1.dist-info}/WHEEL +1 -1
- scipion_pyworkflow-3.10.6.dist-info/dependency_links.txt +0 -1
- {scipion_pyworkflow-3.10.6.dist-info → scipion_pyworkflow-3.11.1.dist-info}/entry_points.txt +0 -0
- {scipion_pyworkflow-3.10.6.dist-info → scipion_pyworkflow-3.11.1.dist-info}/licenses/LICENSE.txt +0 -0
- {scipion_pyworkflow-3.10.6.dist-info → scipion_pyworkflow-3.11.1.dist-info}/top_level.txt +0 -0
pyworkflow/project/project.py
CHANGED
@@ -26,6 +26,7 @@
|
|
26
26
|
# **************************************************************************
|
27
27
|
import logging
|
28
28
|
|
29
|
+
from .usage import ScipionWorkflow
|
29
30
|
from ..protocol.launch import _checkJobStatus
|
30
31
|
|
31
32
|
ROOT_NODE_NAME = "PROJECT"
|
@@ -48,7 +49,7 @@ from pyworkflow.mapper import SqliteMapper
|
|
48
49
|
from pyworkflow.protocol.constants import (MODE_RESTART, MODE_RESUME,
|
49
50
|
STATUS_INTERACTIVE, ACTIVE_STATUS,
|
50
51
|
UNKNOWN_JOBID, INITIAL_SLEEP_TIME, STATUS_FINISHED)
|
51
|
-
from pyworkflow.protocol.protocol import Protocol
|
52
|
+
from pyworkflow.protocol.protocol import Protocol, LegacyProtocol
|
52
53
|
|
53
54
|
from . import config
|
54
55
|
|
@@ -587,7 +588,7 @@ class Project(object):
|
|
587
588
|
self._continueWorkflow(errorsList,workflowProtocolList)
|
588
589
|
return errorsList
|
589
590
|
|
590
|
-
def launchProtocol(self, protocol, wait=False, scheduled=False,
|
591
|
+
def launchProtocol(self, protocol:Protocol, wait=False, scheduled=False,
|
591
592
|
force=False):
|
592
593
|
""" In this function the action of launching a protocol
|
593
594
|
will be initiated. Actions done here are:
|
@@ -633,13 +634,15 @@ class Project(object):
|
|
633
634
|
self.mapper.deleteRelations(self)
|
634
635
|
# Clean and persist execution attributes; otherwise, this would retain old job IDs and PIDs.
|
635
636
|
protocol.cleanExecutionAttributes()
|
636
|
-
protocol._store(protocol._jobId)
|
637
|
+
protocol._store(protocol._jobId, protocol._pid)
|
637
638
|
|
638
639
|
self.mapper.commit()
|
639
640
|
|
640
641
|
# NOTE: now we are simply copying the entire project db, this can be
|
641
642
|
# changed later to only create a subset of the db need for the run
|
642
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()))
|
643
646
|
|
644
647
|
# Launch the protocol; depending on the case, either the pId or the jobId will be set in this call
|
645
648
|
pwprot.launch(protocol, wait)
|
@@ -683,8 +686,14 @@ class Project(object):
|
|
683
686
|
self.mapper.store(protocol)
|
684
687
|
self.mapper.commit()
|
685
688
|
|
686
|
-
def _updateProtocol(self, protocol: Protocol, tries=0, checkPid=False
|
687
|
-
|
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
|
688
697
|
|
689
698
|
# If this is read only exit
|
690
699
|
if self.openedAsReadOnly():
|
@@ -692,67 +701,88 @@ class Project(object):
|
|
692
701
|
|
693
702
|
try:
|
694
703
|
|
695
|
-
#
|
696
|
-
#
|
697
|
-
|
698
|
-
label = protocol.getObjLabel()
|
699
|
-
comment = protocol.getObjComment()
|
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())
|
700
707
|
|
701
|
-
|
702
|
-
|
703
|
-
if pwprot.isProtocolUpToDate(protocol):
|
708
|
+
# If the protocol database has changes ....
|
709
|
+
if not pwprot.isProtocolUpToDate(protocol):
|
704
710
|
|
705
|
-
|
706
|
-
self.checkIsAlive(protocol)
|
707
|
-
return pw.NOT_UPDATED_UNNECESSARY
|
711
|
+
logger.debug("Protocol %s outdated. Updating it now." % protocol.getRunName())
|
708
712
|
|
713
|
+
updated = pw.PROTOCOL_UPDATED
|
709
714
|
|
710
|
-
|
711
|
-
|
712
|
-
|
713
|
-
|
714
|
-
|
715
|
-
|
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!!
|
716
721
|
|
717
|
-
|
718
|
-
|
722
|
+
if project is None:
|
723
|
+
logger.warning("Protocol %s hasn't the project associated when updating it." % label)
|
719
724
|
|
720
|
-
|
721
|
-
|
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())
|
722
730
|
|
723
|
-
|
724
|
-
|
731
|
+
# Capture the db timestamp before loading.
|
732
|
+
lastUpdateTime = pwutils.getFileLastModificationDate(protocol.getDbPath())
|
725
733
|
|
726
|
-
|
727
|
-
|
728
|
-
for attr in localOutputs:
|
729
|
-
if attr not in protocol._outputs:
|
730
|
-
protocol._outputs.append(attr)
|
734
|
+
# Copy is only working for db restored objects
|
735
|
+
protocol.setMapper(self.mapper)
|
731
736
|
|
732
|
-
|
733
|
-
|
734
|
-
|
735
|
-
# In this case the jobid is obtained by the GUI and the job id should be preserved.
|
736
|
-
protocol.setJobIds(jobId)
|
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)
|
737
740
|
|
738
|
-
|
739
|
-
|
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)
|
740
746
|
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
|
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)
|
746
752
|
|
747
|
-
|
748
|
-
|
749
|
-
|
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()
|
750
771
|
|
751
|
-
self.mapper.store(protocol)
|
752
772
|
|
753
|
-
#
|
754
|
-
|
755
|
-
|
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)
|
756
786
|
|
757
787
|
except Exception as ex:
|
758
788
|
if tries == 3: # 3 tries have been failed
|
@@ -766,19 +796,34 @@ class Project(object):
|
|
766
796
|
pass
|
767
797
|
return pw.NOT_UPDATED_ERROR
|
768
798
|
else:
|
769
|
-
logger.warning("Couldn't update protocol %s
|
770
|
-
% (protocol.
|
799
|
+
logger.warning("Couldn't update protocol %s from it's own database. ERROR: %s, attempt=%d"
|
800
|
+
% (protocol.getRunName(), ex, tries))
|
771
801
|
time.sleep(0.5)
|
772
|
-
self._updateProtocol(protocol, tries + 1)
|
802
|
+
return self._updateProtocol(protocol, tries + 1)
|
773
803
|
|
774
|
-
return
|
804
|
+
return updated
|
775
805
|
|
776
806
|
def checkIsAlive(self, protocol):
|
777
|
-
""" Check if a protocol is alive based on its jobid or pid
|
778
|
-
|
779
|
-
|
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)
|
780
821
|
else:
|
781
|
-
self.checkPid(protocol)
|
822
|
+
alive = self.checkPid(protocol)
|
823
|
+
|
824
|
+
if alive:
|
825
|
+
logger.debug("Protocol %s is alive." % protocol.getRunName())
|
826
|
+
return alive
|
782
827
|
|
783
828
|
def stopProtocol(self, protocol):
|
784
829
|
""" Stop a running protocol """
|
@@ -908,7 +953,7 @@ class Project(object):
|
|
908
953
|
break
|
909
954
|
# If it is a class already: "possibleOutput" case. In this case attr is the class and not
|
910
955
|
# an instance of c. In this special case
|
911
|
-
elif possibleOutput and attr
|
956
|
+
elif possibleOutput and issubclass(attr, c):
|
912
957
|
match = True
|
913
958
|
cancelConditionEval = True
|
914
959
|
|
@@ -1239,6 +1284,39 @@ class Project(object):
|
|
1239
1284
|
|
1240
1285
|
return result
|
1241
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
|
+
|
1242
1320
|
def getProtocolsDict(self, protocols=None, namesOnly=False):
|
1243
1321
|
""" Creates a dict with the information of the given protocols.
|
1244
1322
|
|
@@ -1567,6 +1645,7 @@ class Project(object):
|
|
1567
1645
|
for r in self.runs:
|
1568
1646
|
|
1569
1647
|
self._setProtocolMapper(r)
|
1648
|
+
r.setProject(self)
|
1570
1649
|
|
1571
1650
|
# Check for run warnings
|
1572
1651
|
r.checkSummaryWarnings()
|
@@ -1611,39 +1690,45 @@ class Project(object):
|
|
1611
1690
|
""" Check if a running protocol is still alive or not.
|
1612
1691
|
The check will only be done for protocols that have not been sent
|
1613
1692
|
to a queue system.
|
1693
|
+
|
1694
|
+
:returns True if pid is alive or irrelevant
|
1614
1695
|
"""
|
1615
1696
|
from pyworkflow.protocol.launch import _runsLocally
|
1616
1697
|
pid = protocol.getPid()
|
1617
1698
|
|
1618
1699
|
if pid == 0:
|
1619
|
-
return
|
1700
|
+
return True
|
1620
1701
|
|
1621
1702
|
# Include running and scheduling ones
|
1622
1703
|
# Exclude interactive protocols
|
1623
1704
|
# NOTE: This may be happening even with successfully finished protocols
|
1624
1705
|
# which PID is gone.
|
1625
|
-
if (protocol.isActive() and not protocol.isInteractive()
|
1706
|
+
if (protocol.isActive() and not protocol.isInteractive()
|
1626
1707
|
and not pwutils.isProcessAlive(pid)):
|
1627
1708
|
protocol.setFailed("Process %s not found running on the machine. "
|
1628
1709
|
"It probably has died or been killed without "
|
1629
1710
|
"reporting the status to Scipion. Logs might "
|
1630
1711
|
"have information about what happened to this "
|
1631
1712
|
"process." % pid)
|
1713
|
+
return False
|
1714
|
+
|
1715
|
+
return True
|
1632
1716
|
|
1633
1717
|
def checkJobId(self, protocol):
|
1634
1718
|
""" Check if a running protocol is still alive or not.
|
1635
1719
|
The check will only be done for protocols that have been sent
|
1636
1720
|
to a queue system.
|
1637
|
-
"""
|
1638
1721
|
|
1722
|
+
:returns True if job is still alive or irrelevant
|
1723
|
+
"""
|
1639
1724
|
if len(protocol.getJobIds()) == 0:
|
1640
|
-
|
1641
|
-
|
1725
|
+
logger.warning("Checking if protocol alive in the queue but JOB ID is empty. Considering it dead.")
|
1726
|
+
return False
|
1642
1727
|
jobid = protocol.getJobIds()[0]
|
1643
1728
|
hostConfig = protocol.getHostConfig()
|
1644
1729
|
|
1645
1730
|
if jobid == UNKNOWN_JOBID:
|
1646
|
-
return
|
1731
|
+
return True
|
1647
1732
|
|
1648
1733
|
# Include running and scheduling ones
|
1649
1734
|
# Exclude interactive protocols
|
@@ -1654,12 +1739,15 @@ class Project(object):
|
|
1654
1739
|
jobStatus = _checkJobStatus(hostConfig, jobid)
|
1655
1740
|
|
1656
1741
|
if jobStatus == STATUS_FINISHED:
|
1657
|
-
protocol.setFailed("
|
1658
|
-
"It probably has died or been killed without "
|
1742
|
+
protocol.setFailed("JOB ID %s not found running on the queue engine. "
|
1743
|
+
"It probably has timeout, died or been killed without "
|
1659
1744
|
"reporting the status to Scipion. Logs might "
|
1660
1745
|
"have information about what happened to this "
|
1661
|
-
"
|
1746
|
+
"JOB ID." % jobid)
|
1747
|
+
|
1748
|
+
return False
|
1662
1749
|
|
1750
|
+
return True
|
1663
1751
|
def iterSubclasses(self, classesName, objectFilter=None):
|
1664
1752
|
""" Retrieve all objects from the project that are instances
|
1665
1753
|
of any of the classes in classesName list.
|
@@ -0,0 +1,165 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
# -*- coding: utf-8 -*-
|
3
|
+
# **************************************************************************
|
4
|
+
# *
|
5
|
+
# * Authors: Pablo Conesa [1]
|
6
|
+
# *
|
7
|
+
# * [1] Biocomputing unit, CNB-CSIC
|
8
|
+
# *
|
9
|
+
# * This program is free software: you can redistribute it and/or modify
|
10
|
+
# * it under the terms of the GNU General Public License as published by
|
11
|
+
# * the Free Software Foundation, either version 3 of the License, or
|
12
|
+
# * (at your option) any later version.
|
13
|
+
# *
|
14
|
+
# * This program is distributed in the hope that it will be useful,
|
15
|
+
# * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
16
|
+
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
17
|
+
# * GNU General Public License for more details.
|
18
|
+
# *
|
19
|
+
# * You should have received a copy of the GNU General Public License
|
20
|
+
# * along with this program. If not, see <https://www.gnu.org/licenses/>.
|
21
|
+
# *
|
22
|
+
# * All comments concerning this program package may be sent to the
|
23
|
+
# * e-mail address 'scipion@cnb.csic.es'
|
24
|
+
# *
|
25
|
+
# **************************************************************************
|
26
|
+
import json
|
27
|
+
import logging
|
28
|
+
logger = logging.getLogger(__name__)
|
29
|
+
|
30
|
+
from urllib.request import urlopen
|
31
|
+
from pyworkflow import Config
|
32
|
+
|
33
|
+
|
34
|
+
# Module to have Models for reporting usage data to the scipion.i2pc.es site.
|
35
|
+
class ProtStat:
|
36
|
+
""" Class to store the usage part of a reported ScipionWorkflow"""
|
37
|
+
def __init__(self, count=0, nextProtsDict=None):
|
38
|
+
self._count = count
|
39
|
+
self._nextProts = nextProtsDict or dict()
|
40
|
+
|
41
|
+
def getCount(self):
|
42
|
+
return self._count
|
43
|
+
def addUsage(self, count=1):
|
44
|
+
self._count += count
|
45
|
+
def addCountToNextProtocol(self, nextProt, count=1):
|
46
|
+
self._nextProts[nextProt] = self._nextProts.get(nextProt, 0) + count
|
47
|
+
|
48
|
+
def toJSON(self):
|
49
|
+
jsonStr = "[%s,{%s}]"
|
50
|
+
nextProtS = ""
|
51
|
+
|
52
|
+
if len(self._nextProts):
|
53
|
+
nextProtA = []
|
54
|
+
for protName, count in self._nextProts.items():
|
55
|
+
nextProtA.append('"%s":%d' % (protName, count))
|
56
|
+
|
57
|
+
nextProtS= ",".join(nextProtA)
|
58
|
+
|
59
|
+
jsonStr = jsonStr % (self._count, nextProtS)
|
60
|
+
|
61
|
+
return jsonStr
|
62
|
+
|
63
|
+
def __repr__(self):
|
64
|
+
return self.toJSON()
|
65
|
+
|
66
|
+
class ScipionWorkflow:
|
67
|
+
""" Class to serialize and deserialize what is reported from scipion.
|
68
|
+
Example: {"ProtA":
|
69
|
+
[2, {
|
70
|
+
"ProtB":2,
|
71
|
+
"ProtC":3,
|
72
|
+
...
|
73
|
+
}
|
74
|
+
], ...
|
75
|
+
}
|
76
|
+
"""
|
77
|
+
|
78
|
+
def __init__(self, jsonStr=None):
|
79
|
+
""" Instantiate this class optionally with a JSON string serialized from this class
|
80
|
+
(what is sent by Scipion to this web service)."""
|
81
|
+
|
82
|
+
self._prots = dict()
|
83
|
+
if jsonStr is not None:
|
84
|
+
self.deserialize(jsonStr)
|
85
|
+
|
86
|
+
def getProtStats(self):
|
87
|
+
return self._prots
|
88
|
+
|
89
|
+
def deserialize(self, jsonStr):
|
90
|
+
""" Deserialize a JSONString serialized by this class with the toJSON method"""
|
91
|
+
jsonObj = json.loads(jsonStr)
|
92
|
+
|
93
|
+
if isinstance(jsonObj, dict):
|
94
|
+
self.deserializeV2(jsonObj)
|
95
|
+
else:
|
96
|
+
self.deserializeList(jsonObj)
|
97
|
+
|
98
|
+
def deserializeV2(self, jsonObj):
|
99
|
+
""" Deserializes v2 usage stats: something like {"ProtA": [2,{..}],...} """
|
100
|
+
for key, value in jsonObj.items():
|
101
|
+
# Value should be something like [2,{..}]
|
102
|
+
count = value[0]
|
103
|
+
nextProtDict = value[1]
|
104
|
+
nextProt = ProtStat(count, nextProtDict)
|
105
|
+
self._prots[key] = nextProt
|
106
|
+
|
107
|
+
def deserializeList(self, jsonObj):
|
108
|
+
""" Deserializes old data: a list of protocol names repeated: ["ProtA","ProtA", "ProtB", ...] """
|
109
|
+
|
110
|
+
for protName in jsonObj:
|
111
|
+
self.addCount(protName)
|
112
|
+
|
113
|
+
def addCount(self, protName):
|
114
|
+
""" Adds one to the count of a protocol"""
|
115
|
+
|
116
|
+
protStat = self.getProtStat(protName)
|
117
|
+
|
118
|
+
protStat.addUsage()
|
119
|
+
|
120
|
+
def getProtStat(self, protName):
|
121
|
+
|
122
|
+
protStat = self._prots.get(protName, ProtStat())
|
123
|
+
if protName not in self._prots:
|
124
|
+
self._prots[protName] = protStat
|
125
|
+
|
126
|
+
return protStat
|
127
|
+
|
128
|
+
def addCountToNextProtocol(self, protName, nextProtName):
|
129
|
+
protStat = self.getProtStat(protName)
|
130
|
+
protStat.addCountToNextProtocol(nextProtName)
|
131
|
+
|
132
|
+
def getCount(self):
|
133
|
+
""" Returns the number of protocols in the workflow"""
|
134
|
+
count = 0
|
135
|
+
for ps in self._prots.values():
|
136
|
+
count += ps._count
|
137
|
+
return count
|
138
|
+
|
139
|
+
def toJSON(self):
|
140
|
+
""" Returns a valid JSON string"""
|
141
|
+
if len(self._prots) == 0:
|
142
|
+
return "{}"
|
143
|
+
else:
|
144
|
+
jsonStr="{"
|
145
|
+
for protName, protStat in self._prots.items():
|
146
|
+
|
147
|
+
jsonStr += '"%s":%s,' % (protName, protStat.toJSON())
|
148
|
+
|
149
|
+
jsonStr = jsonStr[:-1] + "}"
|
150
|
+
|
151
|
+
return jsonStr
|
152
|
+
|
153
|
+
def __repr__(self):
|
154
|
+
return self.toJSON()
|
155
|
+
|
156
|
+
def getNextProtocolSuggestions(protocol):
|
157
|
+
""" Returns the suggestions from the Scipion website for the next protocols to the protocol passed"""
|
158
|
+
|
159
|
+
try:
|
160
|
+
url = Config.SCIPION_STATS_SUGGESTION % protocol # protocol.getClassName()
|
161
|
+
results = json.loads(urlopen(url).read().decode('utf-8'))
|
162
|
+
return results
|
163
|
+
except Exception as e:
|
164
|
+
logger.error("Suggestions system not available", exc_info=e)
|
165
|
+
return []
|
pyworkflow/protocol/executor.py
CHANGED
@@ -60,10 +60,7 @@ class StepExecutor:
|
|
60
60
|
""" Set protocol to append active jobs to its jobIds. """
|
61
61
|
self.protocol = protocol
|
62
62
|
|
63
|
-
def
|
64
|
-
return {PLUGIN_MODULE_VAR: self.protocol.getPlugin().getName()}
|
65
|
-
|
66
|
-
def runJob(self, log, programName, params,
|
63
|
+
def runJob(self, log, programName, params,
|
67
64
|
numberOfMpi=1, numberOfThreads=1,
|
68
65
|
env=None, cwd=None, executable=None):
|
69
66
|
""" This function is a wrapper around runJob,
|
@@ -161,6 +158,9 @@ class StepThread(threading.Thread):
|
|
161
158
|
self.step = step
|
162
159
|
self.lock = lock
|
163
160
|
|
161
|
+
def needsGPU(self):
|
162
|
+
return self.step.needsGPU()
|
163
|
+
|
164
164
|
def run(self):
|
165
165
|
error = None
|
166
166
|
try:
|
@@ -255,24 +255,35 @@ class ThreadStepExecutor(StepExecutor):
|
|
255
255
|
newGPUList.append(gpuid)
|
256
256
|
return newGPUList
|
257
257
|
|
258
|
+
def getCurrentStepThread(self) -> StepThread:
|
259
|
+
|
260
|
+
return threading.current_thread()
|
261
|
+
|
258
262
|
def getGpuList(self):
|
259
263
|
""" Return the GPU list assigned to current thread
|
260
264
|
or empty list if not using GPUs. """
|
261
265
|
|
262
266
|
# If the node id has assigned gpus?
|
263
|
-
|
264
|
-
if nodeId in self.gpuDict:
|
265
|
-
gpus = self.gpuDict.get(nodeId)
|
266
|
-
logger.info("Reusing GPUs (%s) slot for %s" % (gpus, nodeId))
|
267
|
-
return gpus
|
268
|
-
else:
|
267
|
+
stepThread = self.getCurrentStepThread()
|
269
268
|
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
269
|
+
# If the step does not need the gpu
|
270
|
+
if not stepThread.needsGPU():
|
271
|
+
# return an empty list
|
272
|
+
return []
|
273
|
+
else:
|
274
|
+
nodeId = stepThread.thId
|
275
|
+
if nodeId in self.gpuDict:
|
276
|
+
gpus = self.gpuDict.get(nodeId)
|
277
|
+
logger.info("Reusing GPUs (%s) slot for %s" % (gpus, nodeId))
|
275
278
|
return gpus
|
279
|
+
else:
|
280
|
+
|
281
|
+
gpus = self.getFreeGpuSlot(nodeId)
|
282
|
+
if gpus is None:
|
283
|
+
logger.warning("Step on node %s is requesting GPUs but there isn't any available. Review configuration of threads/GPUs. Returning an empty list." % nodeId)
|
284
|
+
return []
|
285
|
+
else:
|
286
|
+
return gpus
|
276
287
|
def getFreeGpuSlot(self, stepId=None):
|
277
288
|
""" Returns a free gpu slot available or None. If node is passed it also reserves it for that node
|
278
289
|
|
@@ -336,7 +347,7 @@ class ThreadStepExecutor(StepExecutor):
|
|
336
347
|
runningSteps = {} # currently running step in each node ({node: step})
|
337
348
|
freeNodes = list(range(1, self.numberOfProcs+1)) # available nodes to send jobs
|
338
349
|
logger.info("Execution threads: %s" % freeNodes)
|
339
|
-
logger.info("Running steps using %s threads. 1 thread is used for this main
|
350
|
+
logger.info("Running steps using %s threads. 1 thread is used for this main process." % self.numberOfProcs)
|
340
351
|
|
341
352
|
while True:
|
342
353
|
# See which of the runningSteps are not really running anymore.
|
@@ -452,8 +463,9 @@ class QueueStepExecutor(ThreadStepExecutor):
|
|
452
463
|
self.protocol._store(self.protocol._jobId)
|
453
464
|
|
454
465
|
if (jobid is None) or (jobid == UNKNOWN_JOBID):
|
455
|
-
|
456
|
-
|
466
|
+
errorMsg = "Failed to submit to queue. JOBID is not valid. There's probably an error interacting with the queue: %s" % error
|
467
|
+
logger.info(errorMsg)
|
468
|
+
raise Exception(errorMsg)
|
457
469
|
|
458
470
|
status = cts.STATUS_RUNNING
|
459
471
|
wait = 3
|
pyworkflow/protocol/hosts.py
CHANGED
@@ -187,13 +187,16 @@ class HostConfig(Object):
|
|
187
187
|
return default
|
188
188
|
|
189
189
|
def getDict(var):
|
190
|
-
|
190
|
+
try:
|
191
|
+
od = OrderedDict()
|
191
192
|
|
192
|
-
|
193
|
-
|
194
|
-
|
193
|
+
if cp.has_option(hostName, var):
|
194
|
+
for key, value in json.loads(get(var)).items():
|
195
|
+
od[key] = value
|
195
196
|
|
196
|
-
|
197
|
+
return od
|
198
|
+
except Exception as e:
|
199
|
+
raise AttributeError("There is a parsing error in the '%s' variable: %s" % (var, str(e)))
|
197
200
|
|
198
201
|
host.setScipionHome(get(pw.SCIPION_HOME_VAR, pw.Config.SCIPION_HOME))
|
199
202
|
host.setScipionConfig(pw.Config.SCIPION_CONFIG)
|
@@ -207,9 +210,9 @@ class HostConfig(Object):
|
|
207
210
|
|
208
211
|
# If the NAME is not provided or empty
|
209
212
|
# do no try to parse the rest of Queue parameters
|
213
|
+
hostQueue.submitPrefix.set(get('SUBMIT_PREFIX', ''))
|
210
214
|
if hostQueue.hasName():
|
211
215
|
hostQueue.setMandatory(get('MANDATORY', 0))
|
212
|
-
hostQueue.submitPrefix.set(get('SUBMIT_PREFIX', ''))
|
213
216
|
hostQueue.submitCommand.set(get('SUBMIT_COMMAND'))
|
214
217
|
hostQueue.submitTemplate.set(get('SUBMIT_TEMPLATE'))
|
215
218
|
hostQueue.cancelCommand.set(get('CANCEL_COMMAND'))
|