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.
Files changed (57) hide show
  1. pyworkflow/config.py +131 -67
  2. pyworkflow/constants.py +2 -1
  3. pyworkflow/gui/browser.py +39 -5
  4. pyworkflow/gui/dialog.py +2 -0
  5. pyworkflow/gui/form.py +141 -52
  6. pyworkflow/gui/gui.py +8 -8
  7. pyworkflow/gui/project/project.py +6 -7
  8. pyworkflow/gui/project/searchprotocol.py +91 -7
  9. pyworkflow/gui/project/viewdata.py +1 -1
  10. pyworkflow/gui/project/viewprotocols.py +45 -22
  11. pyworkflow/gui/project/viewprotocols_extra.py +9 -6
  12. pyworkflow/gui/widgets.py +2 -2
  13. pyworkflow/mapper/sqlite.py +4 -4
  14. pyworkflow/plugin.py +93 -44
  15. pyworkflow/project/project.py +158 -70
  16. pyworkflow/project/usage.py +165 -0
  17. pyworkflow/protocol/executor.py +30 -18
  18. pyworkflow/protocol/hosts.py +9 -6
  19. pyworkflow/protocol/launch.py +15 -8
  20. pyworkflow/protocol/params.py +59 -19
  21. pyworkflow/protocol/protocol.py +124 -58
  22. pyworkflow/resources/showj/arrowDown.png +0 -0
  23. pyworkflow/resources/showj/arrowUp.png +0 -0
  24. pyworkflow/resources/showj/background_section.png +0 -0
  25. pyworkflow/resources/showj/colRowModeOff.png +0 -0
  26. pyworkflow/resources/showj/colRowModeOn.png +0 -0
  27. pyworkflow/resources/showj/delete.png +0 -0
  28. pyworkflow/resources/showj/doc_icon.png +0 -0
  29. pyworkflow/resources/showj/download_icon.png +0 -0
  30. pyworkflow/resources/showj/enabled_gallery.png +0 -0
  31. pyworkflow/resources/showj/galleryViewOff.png +0 -0
  32. pyworkflow/resources/showj/galleryViewOn.png +0 -0
  33. pyworkflow/resources/showj/goto.png +0 -0
  34. pyworkflow/resources/showj/menu.png +0 -0
  35. pyworkflow/resources/showj/separator.png +0 -0
  36. pyworkflow/resources/showj/tableViewOff.png +0 -0
  37. pyworkflow/resources/showj/tableViewOn.png +0 -0
  38. pyworkflow/resources/showj/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
  39. pyworkflow/resources/showj/ui-bg_glass_95_fef1ec_1x400.png +0 -0
  40. pyworkflow/resources/showj/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
  41. pyworkflow/resources/showj/volumeOff.png +0 -0
  42. pyworkflow/resources/showj/volumeOn.png +0 -0
  43. pyworkflow/utils/log.py +15 -6
  44. pyworkflow/utils/properties.py +78 -92
  45. pyworkflow/utils/utils.py +3 -2
  46. pyworkflow/viewer.py +23 -1
  47. pyworkflow/webservices/config.py +0 -3
  48. pyworkflow/webservices/notifier.py +24 -34
  49. pyworkflowtests/protocols.py +1 -3
  50. pyworkflowtests/tests/test_protocol_execution.py +4 -0
  51. {scipion_pyworkflow-3.10.6.dist-info → scipion_pyworkflow-3.11.1.dist-info}/METADATA +13 -27
  52. {scipion_pyworkflow-3.10.6.dist-info → scipion_pyworkflow-3.11.1.dist-info}/RECORD +56 -35
  53. {scipion_pyworkflow-3.10.6.dist-info → scipion_pyworkflow-3.11.1.dist-info}/WHEEL +1 -1
  54. scipion_pyworkflow-3.10.6.dist-info/dependency_links.txt +0 -1
  55. {scipion_pyworkflow-3.10.6.dist-info → scipion_pyworkflow-3.11.1.dist-info}/entry_points.txt +0 -0
  56. {scipion_pyworkflow-3.10.6.dist-info → scipion_pyworkflow-3.11.1.dist-info}/licenses/LICENSE.txt +0 -0
  57. {scipion_pyworkflow-3.10.6.dist-info → scipion_pyworkflow-3.11.1.dist-info}/top_level.txt +0 -0
@@ -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
- skipUpdatedProtocols=True):
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
- # Backup the values of 'jobId', 'label' and 'comment'
696
- # to be restored after the .copy
697
- jobId = protocol.getJobIds().clone() # Use clone to prevent this variable from being overwritten or cleared in the latter .copy() call
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
- if skipUpdatedProtocols:
702
- # If we are already updated, comparing timestamps
703
- if pwprot.isProtocolUpToDate(protocol):
708
+ # If the protocol database has changes ....
709
+ if not pwprot.isProtocolUpToDate(protocol):
704
710
 
705
- # Always check for the status of the process (queue job or pid)
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
- # If the protocol database has ....
711
- # Comparing date will not work unless we have a reliable
712
- # lastModificationDate of a protocol in the project.sqlite
713
- prot2 = pwprot.getProtocolFromDb(self.path,
714
- protocol.getDbPath(),
715
- protocol.getObjId())
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
- # Capture the db timestamp before loading.
718
- lastUpdateTime = pwutils.getFileLastModificationDate(protocol.getDbPath())
722
+ if project is None:
723
+ logger.warning("Protocol %s hasn't the project associated when updating it." % label)
719
724
 
720
- # Copy is only working for db restored objects
721
- protocol.setMapper(self.mapper)
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
- localOutputs = list(protocol._outputs)
724
- protocol.copy(prot2, copyId=False, excludeInputs=True)
731
+ # Capture the db timestamp before loading.
732
+ lastUpdateTime = pwutils.getFileLastModificationDate(protocol.getDbPath())
725
733
 
726
- # merge outputs: This is necessary when outputs are added from the GUI
727
- # e.g.: adding coordinates from analyze result and protocol is active (interactive).
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
- # Restore backup values
733
- if protocol.useQueueForProtocol() and jobId: # If jobId not empty then restore value as the db is empty
734
- # Case for direct protocol launch from the GUI. Without passing through a scheduling process.
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
- # In case of scheduling a protocol, the jobid is obtained during the "scheduling job"
739
- # and it is written in the rub.db. Therefore, it should be taken from there.
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
- protocol.setObjLabel(label)
742
- protocol.setObjComment(comment)
743
- # Use the run.db timestamp instead of the system TS to prevent
744
- # possible inconsistencies.
745
- protocol.lastUpdateTimeStamp.set(lastUpdateTime)
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
- # Check pid at the end, once updated
748
- if checkPid:
749
- self.checkIsAlive(protocol)
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
- # Close DB connections
754
- prot2.getProject().closeMapper()
755
- prot2.closeMappers()
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(jobId=%s) from it's own database. ERROR: %s, attempt=%d"
770
- % (protocol.getObjName(), jobId, ex, tries))
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 pw.PROTOCOL_UPDATED
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
- if protocol.getPid() == 0:
779
- self.checkJobId(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)
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 == c:
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() and _runsLocally(protocol)
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
- return
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("Process %s not found running on the machine. "
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
- "process." % jobid)
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 []
@@ -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 getRunContext(self):
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
- nodeId = threading.current_thread().thId
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
- gpus = self.getFreeGpuSlot(nodeId)
271
- if gpus is None:
272
- 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)
273
- return []
274
- else:
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 proccess." % self.numberOfProcs)
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
- logger.info("jobId is none therefore we set it to fail")
456
- raise Exception("Failed to submit to queue.")
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
@@ -187,13 +187,16 @@ class HostConfig(Object):
187
187
  return default
188
188
 
189
189
  def getDict(var):
190
- od = OrderedDict()
190
+ try:
191
+ od = OrderedDict()
191
192
 
192
- if cp.has_option(hostName, var):
193
- for key, value in json.loads(get(var)).items():
194
- od[key] = value
193
+ if cp.has_option(hostName, var):
194
+ for key, value in json.loads(get(var)).items():
195
+ od[key] = value
195
196
 
196
- return od
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'))