19Machinery for reading, editing, and writing Condor DAG files.
21When running DAGs on Condor compute clusters, very often one will wish to
22re-run a portion of a DAG. This can be done by marking all jobs except the
23ones to be re-run as "DONE". Unfortunately the Condor software suite lacks
24an I/O library for reading and writing Condor DAG files, so there is no
25easy way to edit DAG files except by playing games sed, awk, or once-off
26Python or Perl scripts. That's where this module comes in. This module
27will read a DAG file into an in-ram representation that is easily edited,
28and allow the file to be written to disk again.
32>>> from gstlal import dagfile
33>>> dag = dagfile.DAG.parse(open("pipeline.dag"))
34>>> dag.write(open("pipeline.dag", "w"))
36Although it is possible to machine-generate an original DAG file using this
37module and write it to disk, this module does not provide the tools
38required to do any of the other tasks associated with pipeline
39construction. For example there is no facility here to generate or manage
40submit files, data files, or any other files that are associated with a
41full pipeline. Only the DAG file itself is considered here. For general
42pipeline construction see the pipeline module. The focus of this module is
43on editing existing DAG files.
45Developers should also consider doing any new pipeline development using
46DAX files as the fundamental workflow description, instead of DAGs. See
47http://pegasus.isi.edu for more information.
49A DAG file is loaded using the .parse() class method of the DAG class.
50This parses the file-like object passed to it and returns an instance of
51the DAG class representing the file's contents. Once loaded, the nodes in
52the DAG can all be found in the .nodes dictionary, whose keys are the node
53names and whose values are the corresponding node objects. Among each node
54object's attributes are sets .children and .parents containing references
55to the child and parent nodes (not their names) for each node. Note that
56every node must appear listed as a parent of each of its children, and vice
57versa. The other attributes of a DAG instance contain information about
58the DAG, for example the CONFIG file or the DOT file, and so on. All of
59the data for each node in the DAG, for example the node's VARS value, its
60initial working directory, and so on, can be found in the attributes of the
61nodes themselves. A DAG is written to a file using the .write() method of
80__all__ = [
"DAG",
"JOB",
"DATA",
"SPLICE",
"SUBDAG_EXTERNAL"]
94 Progress report wrapper. For internal use only.
101 def __iadd__(self, dn):
103 if self.
callback is not None and not self.
n % 7411:
114 Object providing a no-op .write() method to fake a file. For
117 def write(self, *args):
132 Representation of a JOB node in a Condor DAG. JOB objects have the
133 following attributes corresponding to information in the DAG file:
136 The name of the node in the DAG.
139 The name of the submit file for the JOB.
142 The initial working directory for the JOB. Set to None to
143 omit from DAG (job's working directory will be chosen by
147 Boolean indicating if the JOB is done or not. See
148 DAG.load_rescue() for more information.
151 Boolean indicating if the JOB is a no-op or not.
154 A dictionary of the name-->value pairs in the VARS line for
155 the JOB. Leave empty to omit VARS from DAG.
158 The number of retries for the job. Set to None to omit
161 .retry_unless_exit_value
162 The value of the UNLESS-EXIT suffix of the RETRY line.
163 Set to None to omit from DAG.
167 The PRIORITY value and CATEGORY name for the node in the
168 DAG. Set to None to omit from the DAG.
172 Sets of the parent and child nodes of JOB. The sets
173 contain references to the node objects, not their names.
179 The names and lists of arguments of the PRE and POST
180 scripts. Set to None to omit from DAG.
182 .abort_dag_on_abortexitvalue
183 .abort_dag_on_dagreturnvalue
184 The ABORT-DAG-ON abort exit value and DAG return value for
185 the JOB. Set to None to omit from DAG.
187 For more information about the function of these parameters, refer
188 to the Condor documentation.
192 def __init__(self, name, filename, directory = None, done = False, noop = False):
230 def write(self, f, progress = None):
232 Write the lines describing this node to the file-like
233 object f. The object must provide a .write() method.
235 If progress is not None, it will be incremented by 1 for
247 if progress
is not None:
252 f.write(
"PRIORITY %s %d\n" % (self.
name, self.
priority))
253 if progress
is not None:
258 f.write(
"CATEGORY %s %s\n" % (self.
name, self.
category))
259 if progress
is not None:
264 f.write(
"RETRY %s %d" % (self.
name, self.
retry))
268 if progress
is not None:
273 f.write(
"VARS %s" % self.
name)
274 for name, value
in sorted(self.
vars.items()):
276 f.write(
" %s=\"%s\"" % (name, value.replace(
"\\",
"\\\\").replace(
"\"",
"\\\"")))
278 if progress
is not None:
287 if progress
is not None:
296 if progress
is not None:
305 if progress
is not None:
312 Get the state of the node. One of 'wait', 'idle', 'run',
313 'abort', 'stop', 'success', 'fail'.
315 NOTE: this feature is not implemented at this time.
322 Representation of a Stork DATA node in a Condor DAG.
329 Representation of a SUBDAG EXTERNAL node in a Condor DAG.
331 keyword =
"SUBDAG EXTERNAL"
336 Representation of a SPLICE node in a Condor DAG.
348 Representation of the contents of a Condor DAG file.
350 BUGS: the semantics of the "+" special character in category names
351 is not understood. For now, it is an error for a node's category
352 to not be found verbatim in a MAXJOBS line. The "+" character is a
353 wildcard-like character used in the assignment of MAXJOBS values to
354 job categories in splices; see the Condor documentation for more
362 dotpat = re.compile(
r'^DOT\s+(?P<filename>\S+)(\s+(?P<options>.+))?', re.IGNORECASE)
363 jobpat = re.compile(
r'^JOB\s+(?P<name>\S+)\s+(?P<filename>\S+)(\s+DIR\s+(?P<directory>\S+))?(\s+(?P<noop>NOOP))?(\s+(?P<done>DONE))?', re.IGNORECASE)
364 datapat = re.compile(
r'^DATA\s+(?P<name>\S+)\s+(?P<filename>\S+)(\s+DIR\s+(?P<directory>\S+))?(\s+(?P<noop>NOOP))?(\s+(?P<done>DONE))?', re.IGNORECASE)
365 subdagpat = re.compile(
r'^SUBDAG\s+EXTERNAL\s+(?P<name>\S+)\s+(?P<filename>\S+)(\s+DIR\s+(?P<directory>\S+))?(\s+(?P<noop>NOOP))?(\s+(?P<done>DONE))?', re.IGNORECASE)
366 splicepat = re.compile(
r'^SPLICE\s+(?P<name>\S+)\s+(?P<filename>\S+)(\s+DIR\s+(?P<directory>\S+))?', re.IGNORECASE)
367 prioritypat = re.compile(
r'^PRIORITY\s+(?P<name>\S+)\s+(?P<value>\S+)', re.IGNORECASE)
368 categorypat = re.compile(
r'^CATEGORY\s+(?P<name>\S+)\s+(?P<category>\S+)', re.IGNORECASE)
369 retrypat = re.compile(
r'^RETRY\s+(?P<name>\S+)\s+(?P<retries>\S+)(\s+UNLESS-EXIT\s+(?P<retry_unless_exit_value>\S+))?', re.IGNORECASE)
370 varspat = re.compile(
r'^VARS\s+(?P<name>\S+)\s+(?P<vars>.+)', re.IGNORECASE)
371 varsvaluepat = re.compile(
r'(?P<name>\S+)\s*=\s*"(?P<value>.*?)(?<!\\)"', re.IGNORECASE)
372 scriptpat = re.compile(
r'^SCRIPT\s+(?P<type>(PRE)|(POST))\s(?P<name>\S+)\s+(?P<executable>\S+)(\s+(?P<arguments>.+))?', re.IGNORECASE)
373 abortdagonpat = re.compile(
r'^ABORT-DAG-ON\s+(?P<name>\S+)\s+(?P<exitvalue>\S+)(\s+RETURN\s+(?P<returnvalue>\S+))?', re.IGNORECASE)
374 arcpat = re.compile(
r'^PARENT\s+(?P<parents>.+?)\s+CHILD\s+(?P<children>.+)', re.IGNORECASE)
375 maxjobspat = re.compile(
r'^MAXJOBS\s+(?P<category>\S+)\s+(?P<value>\S+)', re.IGNORECASE)
376 configpat = re.compile(
r'^CONFIG\s+(?P<filename>\S+)', re.IGNORECASE)
377 nodestatuspat = re.compile(
r'^NODE_STATUS_FILE\s+(?P<filename>\S+)(\s+(?P<updatetime>\S+))?', re.IGNORECASE)
378 jobstatepat = re.compile(
r'^JOBSTATE_LOG\s+(?P<filename>\S+)', re.IGNORECASE)
384 donepat = re.compile(
r'^DONE\s+(?P<name>\S+)', re.IGNORECASE)
390 def __init__(self, nodes = {}, maxjobs = {}, config = None, dot = None, dotupdate = False, dotoverwrite = True, dotinclude = None, node_status_file = None, node_status_file_updatetime = None, jobstate_log = None):
392 The meanings of the keyword arguments are:
395 name --> JOB object mapping
397 category name --> integer max jobs value mapping. all
398 categories are listed, that is it is an error for a JOB
399 in the DAG to claim to be in a category that cannot be
400 found in this dictionary. categories that don't have a
401 MAXJOBS set for them use None as their max jobs value
409 booleans, defaults match Condor's
413 node_status_file_updatetime:
414 filename and update time or None for both
418 It is also possible to initialize a DAG object from another
419 DAG (-like) object with.
441 dag.nodes, dag.maxjobs, dag.config, dag.dot, dag.dotupdate, dag.dotoverwrite, dag.dotinclude, dag.node_status_file, dag.node_status_file_updatetime, dag.jobstate_log
442 except AttributeError:
450 self.
nodes = dict((name, copy.copy(node))
for name, node
in dag.nodes.items())
451 self.
maxjobs = dict(dag.maxjobs)
463 Rebuild the .nodes index. This is required if the names of
469 nodes = dict((node.name, node)
for node
in self.
nodes.values())
470 if len(nodes) != len(self.
nodes):
471 raise ValueError(
"node names are not unique")
473 self.
nodes.update(nodes)
478 Parse the file-like object f as a Condor DAG file. Return
479 a DAG object. The file object must be iterable, yielding
480 one line of text of the DAG file in each iteration.
482 If the progress argument is not None, it should be a
483 callable object. This object will be called periodically
484 and passed the f argument, the current line number, and a
485 boolean indicating if parsing is complete. The boolean is
486 always False until parsing is complete, then the callable
487 will be invoked one last time with the final line count and
488 the boolean set to True.
492 >>> def progress(f, n, done):
493 ... print("reading %s: %d lines\\r" % (f.name, n)),
497 >>> dag = DAG.parse(open("pipeline.dag"), progress = progress)
502 for n, line
in enumerate(f, start = 1):
507 if not line
or line.startswith(
"#"):
510 m = self.
jobpat.search(line)
512 if m.group(
"name")
in self.
nodes:
513 raise ValueError(
"line %d: duplicate JOB %s" % (n, m.group(
"name")))
514 self.
nodes[m.group(
"name")] =
JOB(m.group(
"name"), m.group(
"filename"), directory = m.group(
"directory")
and m.group(
"directory").strip(
"\""), done = bool(m.group(
"done")), noop = bool(m.group(
"noop")))
519 if m.group(
"name")
in self.
nodes:
520 raise ValueError(
"line %d: duplicate DATA %s" % (n, m.group(
"name")))
521 self.
nodes[m.group(
"name")] =
DATA(m.group(
"name"), m.group(
"filename"), directory = m.group(
"directory")
and m.group(
"directory").strip(
"\""), done = bool(m.group(
"done")), noop = bool(m.group(
"noop")))
526 if m.group(
"name")
in self.
nodes:
527 raise ValueError(
"line %d: duplicate SUBDAG EXTERNAL %s" % (n, m.group(
"name")))
528 self.
nodes[m.group(
"name")] =
SUBDAG_EXTERNAL(m.group(
"name"), m.group(
"filename"), directory = m.group(
"directory")
and m.group(
"directory").strip(
"\""), done = bool(m.group(
"done")), noop = bool(m.group(
"noop")))
533 if m.group(
"name")
in self.
nodes:
534 raise ValueError(
"line %d: duplicate SPLICE %s" % (n, m.group(
"name")))
535 self.
nodes[m.group(
"name")] =
SPLICE(m.group(
"name"), m.group(
"filename"), directory = m.group(
"directory")
and m.group(
"directory").strip(
"\""))
540 node = self.
nodes[m.group(
"name")]
542 for name, value
in self.
varsvaluepat.findall(m.group(
"vars")):
543 if name
in node.vars:
544 raise ValueError(
"line %d: multiple variable %s for %s %s" % (n, name, node.keyword, node.name))
546 node.vars[name] = value.replace(
"\\\\",
"\\").replace(
"\\\"",
"\"")
549 m = self.
arcpat.search(line)
551 parents = m.group(
"parents").strip().split()
552 children = m.group(
"children").strip().split()
553 arcs.extend((parent, child)
for parent
in parents
for child
in children)
558 node = self.
nodes[m.group(
"name")]
559 node.retry = int(m.group(
"retries"))
560 node.retry_unless_exit_value = m.group(
"retry_unless_exit_value")
565 node = self.
nodes[m.group(
"name")]
566 if m.group(
"type").upper() ==
"PRE":
567 if node.prescript
is not None:
568 raise ValueError(
"line %d: multiple SCRIPT PRE for %s %s" % (n, node.keyword, node.name))
569 node.prescript = m.group(
"executable")
570 if m.group(
"arguments")
is not None:
571 node.prescriptargs = m.group(
"arguments").split()
572 elif m.group(
"type").upper() ==
"POST":
573 if node.postscript
is not None:
574 raise ValueError(
"line %d: multiple SCRIPT POST for %s %s" % (n, node.keyword, node.name))
575 node.postscript = m.group(
"executable")
576 if m.group(
"arguments")
is not None:
577 node.postscriptargs = m.group(
"arguments").split()
584 node = self.
nodes[m.group(
"name")]
585 if node.priority
is not None:
586 raise ValueError(
"line %d: multiple PRIORITY for %s %s" % (n, node.keyword, node.name))
587 node.priority = int(m.group(
"value"))
592 self.
nodes[m.group(
"name")].category = m.group(
"category")
597 node = self.
nodes[m.group(
"name")]
598 if node.abort_dag_on_abortexitvalue
is not None:
599 raise ValueError(
"line %d: multiple ABORT-DAG-ON for %s %s" % (n, node.keyword, node.name))
600 node.abort_dag_on_abortexitvalue = int(m.group(
"exitvalue"))
601 if m.group(
"returnvalue")
is not None:
602 node.abort_dag_on_dagreturnvalue = int(m.group(
"returnvalue"))
607 if m.group(
"category")
in self.
maxjobs:
608 raise ValueError(
"line %d: multiple MAXJOBS for category %s" % (n, m.group(
"category")))
609 self.
maxjobs[m.group(
"category")] = int(m.group(
"value"))
612 m = self.
dotpat.search(line)
614 self.
dot = m.group(
"filename")
615 options = (m.group(
"options")
or "").split()
617 option = options.pop(0).upper()
618 if option ==
"UPDATE":
620 elif option ==
"DONT-UPDATE":
622 elif option ==
"OVERWRITE":
624 elif option ==
"DONT-OVERWRITE":
626 elif option ==
"INCLUDE":
630 raise ValueError(
"line %d: missing filename for INCLUDE option of DOT" % n)
632 raise ValueError(
"unrecognized option %s for DOT" % option)
635 m = self.
dotpat.search(line)
637 if self.
config is not None:
638 raise ValueError(
"line %d: multiple CONFIG lines in dag file" % n)
639 self.
config = m.group(
"filename")
645 raise ValueError(
"line %d: multiple NODE_STATUS_FILE lines in dag file" % n)
647 if m.group(updatetime)
is not None:
659 raise ValueError(
"line %d: invalid line in dag file: %s" % (n, line))
663 getnode = self.
nodes.__getitem__
664 for parent, child
in arcs:
665 parent = getnode(parent)
666 child = getnode(child)
667 parent.children.add(child)
668 child.parents.add(parent)
670 for node
in self.
nodes.values():
671 if node.category
is not None and node.category
not in self.
maxjobs:
672 self.
maxjobs[node.category] =
None
679 Construct a new DAG object containing only the nodes whose
680 names are in nodenames.
684 >>> names_to_rerun = set(["triggergen"])
685 >>> dag = DAG.select_nodes_by_name(dag, names_to_rerun | dag.get_all_parent_names(names_to_rerun))
687 NOTE: the new DAG object is given references to the node
688 (JOB, DATA, etc.) objects in the original DAG, not copies
689 of them. Therefore, editing the node objects, for example
690 modifying their parent or child sets, will affect both
691 DAGs. To obtain an independent DAG with its own node
692 objects, make a deepcopy of the object that is returned
693 (see the copy module in the Python standard library for
699 >>> dag = copy.deepcopy(DAG.select_nodes_by_name(dag, names_to_rerun | dag.get_all_parent_names(names_to_rerun)))
702 self.
nodes = dict((name, node)
for name, node
in dag.nodes.items()
if name
in nodenames)
703 self.
maxjobs = dict((category, dag.maxjobs[category])
for category
in set(node.category
for node
in self.
nodes.values()
if node.category
is not None))
708 Trace the DAG backward from the parents of the nodes whose
709 names are given to the head nodes, inclusively, and return
710 the set of the names of all nodes visited.
714 >>> all_parents = dag.get_all_parent_names(["triggergen"])
716 all_parent_names = set()
717 nodes_to_scan = set(self.
nodes[name]
for name
in names)
719 node = nodes_to_scan.pop()
720 nodes_to_scan |= node.parents
721 all_parent_names |= set(parent.name
for parent
in node.parents)
722 return all_parent_names
726 Trace the DAG forward from the children of the nodes whose
727 names are given to the leaf nodes, inclusively, and return
728 the set of the names of all nodes visited.
732 >>> all_children = dag.get_all_child_names(["triggergen"])
734 all_child_names = set()
735 nodes_to_scan = set(self.
nodes[name]
for name
in names)
737 node = nodes_to_scan.pop()
738 nodes_to_scan |= node.children
739 all_child_names |= set(child.name
for child
in node.children)
740 return all_child_names
744 Check all graph edges for validity. Checks that each of
745 every node's children lists that node as a parent, and vice
746 versa, and that all nodes listed in the parent and child
747 sets of all nodes are contained in this DAG. Raises
748 ValueError if a problem is found, otherwise returns None.
753 ... dag.check_edges()
754 ... except ValueError as e:
755 ... print("edges are broken: %s" % str(e))
757 ... print("all edges are OK")
760 nodes = set(self.
nodes.values())
762 for child
in node.children:
763 if node
not in child.parents:
764 raise ValueError(
"node %s is not a parent of its child %s" % (node.name, child.name))
765 if child
not in nodes:
766 raise ValueError(
"node %s has child %s that is not in DAG" % (node.name, child.name))
767 for parent
in node.parents:
768 if node
not in parent.children:
769 raise ValueError(
"node %s is not a child of its parent %s" % (node.name, parent.name))
770 if parent
not in nodes:
771 raise ValueError(
"node %s has parent %s that is not in DAG" % (node.name, parent.name))
775 Parse the file-like object f as a rescue DAG, using the
776 DONE lines therein to set the job states of this DAG.
778 In the past, rescue DAGs were full copies of the original
779 DAG with the word DONE added to the JOB lines of completed
780 jobs. In version 7.7.2 of Condor, the default format of
781 rescue DAGs was changed to a condensed format consisting of
782 only the names of completed jobs and the number of retries
783 remaining for incomplete jobs. Currently Condor still
784 supports the original rescue DAG format, but the user must
785 set the DAGMAN_WRITE_PARTIAL_RESCUE config variable to
786 false to obtain one. This module does not directly support
787 the new format, however this method allows a new-style
788 rescue DAG to be parsed to set the states of the jobs in a
789 DAG. This, in effect, converts a new-style rescue DAG to
790 an old-style rescue DAG, allowing the result to be
791 manipulated as before.
793 If the progress argument is not None, it should be a
794 callable object. This object will be called periodically
795 and passed the f argument, the current line number, and a
796 boolean indicating if parsing is complete. The boolean is
797 always False until parsing is complete, then the callable
798 will be invoked one last time with the final line count and
799 the boolean set to True.
802 for job
in self.
nodes.values():
806 for n, line
in enumerate(f):
813 if not line
or line.startswith(
"#"):
818 self.
nodes[m.group(
"name")].done =
True
823 node = self.
nodes[m.group(
"name")]
824 node.retry = int(m.group(
"retries"))
825 node.retry_unless_exit_value = m.group(
"retry_unless_exit_value")
828 raise ValueError(
"line %d: invalid line in rescue file: %s" % (n, line))
832 def write(self, f, progress = None, rescue = None):
834 Write the DAG to the file-like object f. The object must
835 provide a .write() method. In the special case that the
836 optional rescue argument is not None (see below) then f can
837 be set to None and no DAG file will be written (just the
838 rescue DAG will be written).
840 If the progress argument is not None, it should be a
841 callable object. This object will be called periodically
842 and passed the f argument, the current line number, and a
843 boolean indicating if writing is complete. The boolean is
844 always False until writing is complete, then the callable
845 will be invoked one last time with the final line count and
846 the boolean set to True.
850 >>> def progress(f, n, done):
851 ... print "writing %s: %d lines\\r" % (f.name, n),
855 >>> dag.write(open("pipeline.dag", "w"), progress = progress)
857 NOTE: when writing PARENT/CHILD graph edges, this method
858 will silently skip any node names that are not in this
859 DAG's graph. This is a convenience to simplify writing
860 DAGs constructed by the .select_nodes_by_name() class
861 method. If one wishes to check for broken parent/child
862 links before writing the DAG use the .check_edges() method.
864 If the optional rescue argument is not None, it must be a
865 file-like object providing a .write() method and the DONE
866 state of jobs will be written to this file instead of the
867 .dag (in the .dag all jobs will be marked not done).
871 >>> dag.write(open("pipeline.dag", "w"), rescue = open("pipeline.dag.rescue001", "w"))
873 NOTE: it is left as an exercise for the calling code to
874 ensure the name chosen for the rescue file is consistent
875 with the naming convention assumed by condor_dagman when it
883 if f
is None and rescue
is not None:
887 if self.
dot is not None:
888 f.write(
"DOT %s" % self.
dot)
892 f.write(
" DONT-OVERWRITE")
899 if self.
config is not None:
900 f.write(
"CONFIG %s\n" % self.
config)
917 if set(node.category
for node
in self.
nodes.values()
if node.category
is not None) - set(self.
maxjobs):
918 raise ValueError(
"no MAXJOBS statement(s) for node category(ies) %s" %
", ".join(sorted(set(node.category
for node
in self.
nodes.values()
if node.category
is not None) - set(self.
maxjobs))))
919 for name, value
in sorted(self.
maxjobs.items()):
920 if value
is not None:
921 f.write(
"MAXJOBS %s %d\n" % (name, value))
925 for name, node
in sorted(self.
nodes.items()):
926 if rescue
is not None:
928 rescue.write(
"DONE %s\n" % node.name)
932 node.write(f, progress = progress)
933 if rescue
is not None:
938 names = set(self.
nodes)
940 for name, node
in self.
nodes.items():
941 parents_of.setdefault(frozenset(child.name
for child
in node.children) & names, set()).add(node.name)
942 for children, parents
in parents_of.items():
944 f.write(
"PARENT %s CHILD %s\n" % (
" ".join(sorted(parents)),
" ".join(sorted(children))))
950 def dot_source(self, title = "DAG", rename = False, colour = "black", bgcolour = "#a3a3a3", statecolours = {'wait':
'yellow',
'idle':
'yellow',
'run':
'lightblue',
'abort':
'red',
'stop':
'red',
'success':
'green',
'fail':
'red'}):
952 Generator yielding a sequence of strings containing DOT
953 code to generate a visualization of the DAG graph. See
954 http://www.graphviz.org for more information.
956 title provides a title for the graph. If rename is True,
957 instead of using the names of the nodes for the node names
958 in the graph, numbers will be used instead. The numbers
959 are assigned to the nodes in alphabetical order by node
960 name. This might be required if the nodes have names that
961 are incompatible with the DOT syntax.
963 colour and bgcolour set the outline colour of the graph
964 nodes and the background colour for the graph respectively.
965 statecolours is a dictionary mapping node state (see the
966 .state attribute of the JOB class and its derivatives) to a
967 colour. Set statecolours to None to disable state-based
968 colouring of graph nodes.
973 >>> sys.stdout.writelines(dag.dot_source(statecolours = None))
975 BUGS: the JOB class does not implement the ability to
976 retrieve the job state at this time, therefore it is always
977 necessary to set statecolours to None. This might change
983 namemap = dict((name, str(n))
for n, name
in enumerate(sorted(self.
nodes), start = 1))
985 namemap = dict((name, name)
for name
in self.
nodes)
989 yield 'digraph "%s" {\nnode [color="%s", href="\\N"];\ngraph [bgcolor="%s"];\n' % (title, colour, bgcolour)
990 for node
in self.
nodes.values():
991 if statecolours
is not None:
992 yield '"%s"[color="%s"];\n' % (namemap[node.name], statecolours[node.state])
993 for child
in node.children:
994 yield '"%s" -> "%s";\n' % (namemap[node.name], namemap[child.name])
1005 def noopgen(dag, submit_filename):
1006 used = frozenset(name
for name
in dag.nodes
if name.startswith(
"NOOP"))
1007 for i
in itertools.count():
1013 filename = submit_filename,
1016 dag.nodes[noop.name] = noop
1018 noops = iter(noopgen(dag,
"noop.submit"))
1029 for name, node
in dag.nodes.items():
1030 parents_of.setdefault(frozenset(node.children), set()).add(node)
1035 for children, parents
in parents_of.items():
1036 if len(parents) < 3
or len(children) < 3
or len(parents) * len(children) < 25:
1040 noop.parents |= parents
1041 noop.children |= children
1042 for node
in parents:
1043 node.children.clear()
1044 node.children.add(noop)
1045 for node
in children:
1046 node.parents.clear()
1047 node.parents.add(noop)
select_nodes_by_name(cls, dag, nodenames)
parse(cls, f, progress=None)
get_all_child_names(self, names)
__init__(self, nodes={}, maxjobs={}, config=None, dot=None, dotupdate=False, dotoverwrite=True, dotinclude=None, node_status_file=None, node_status_file_updatetime=None, jobstate_log=None)
dot_source(self, title="DAG", rename=False, colour="black", bgcolour="#a3a3a3", statecolours={ 'wait':'yellow', 'idle':'yellow', 'run':'lightblue', 'abort':'red', 'stop':'red', 'success':'green', 'fail':'red'})
load_rescue(self, f, progress=None)
write(self, f, progress=None, rescue=None)
get_all_parent_names(self, names)
node_status_file_updatetime
abort_dag_on_abortexitvalue
write(self, f, progress=None)
abort_dag_on_dagreturnvalue