Skip to content

Commit cbc33f3

Browse files
cwlviewer like dot (#1316)
1 parent defd882 commit cbc33f3

File tree

10 files changed

+391
-14
lines changed

10 files changed

+391
-14
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,5 @@ response.txt
5252
test.txt
5353
time.txt
5454
value
55+
56+
.python-version

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ include cwltool/extensions-v1.1.yml
4545
include cwltool/jshint/jshint_wrapper.js
4646
include cwltool/jshint/jshint.js
4747
include cwltool/hello.simg
48+
include cwltool/rdfqueries/*.sparql
4849
prune cwltool/schemas/v1.0/salad/typeshed
4950
prune cwltool/schemas/v1.0/salad/schema_salad/tests
5051
prune cwltool/schemas/v1.1.0-dev1/salad/typeshed

cwltool/cwlrdf.py

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from schema_salad.ref_resolver import ContextType
99

1010
from .process import Process
11+
from .cwlviewer import CWLViewer
1112

1213

1314
def gather(tool: Process, ctx: ContextType) -> Graph:
@@ -193,17 +194,8 @@ def printdot(
193194
wf: Process,
194195
ctx: ContextType,
195196
stdout: Union[TextIO, StreamWriter],
196-
include_parameters: bool = False,
197197
) -> None:
198-
g = gather(wf, ctx)
199-
200-
stdout.write("digraph {")
201-
202-
# g.namespace_manager.qname(predicate)
203-
204-
if include_parameters:
205-
dot_with_parameters(g, stdout)
206-
else:
207-
dot_without_parameters(g, stdout)
208-
209-
stdout.write("}")
198+
cwl_viewer = CWLViewer(printrdf(wf, ctx, 'n3')) # type: CWLViewer
199+
stdout.write(
200+
cwl_viewer.dot()
201+
)

cwltool/cwlviewer.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
"""Visualize a CWL workflow."""
2+
import rdflib
3+
from urllib.parse import urlparse
4+
import os
5+
import pydot # type: ignore
6+
7+
8+
class CWLViewer:
9+
"""Produce similar images with the https://github.com/common-workflow-language/cwlviewer."""
10+
11+
_queries_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'rdfqueries')
12+
_get_inner_edges_query_path = os.path.join(_queries_dir, 'get_inner_edges.sparql')
13+
_get_input_edges_query_path = os.path.join(_queries_dir, 'get_input_edges.sparql')
14+
_get_output_edges_query_path = os.path.join(_queries_dir, 'get_output_edges.sparql')
15+
_get_root_query_path = os.path.join(_queries_dir, 'get_root.sparql')
16+
17+
def __init__(
18+
self,
19+
rdf_description # type: str
20+
):
21+
"""Create a viewer object based on the rdf description of the workflow."""
22+
self._dot_graph = CWLViewer._init_dot_graph() # type: pydot.Graph
23+
self._rdf_graph = self._load_cwl_graph(rdf_description) # type: rdflib.graph.Graph
24+
self._root_graph_uri = self._get_root_graph_uri() # type: str
25+
self._set_inner_edges()
26+
self._set_input_edges()
27+
self._set_output_edges()
28+
29+
def _load_cwl_graph(
30+
self,
31+
rdf_description # type: str
32+
) -> rdflib.graph.Graph:
33+
rdf_graph = rdflib.Graph()
34+
rdf_graph.parse(data=rdf_description, format='n3')
35+
return rdf_graph
36+
37+
def _set_inner_edges(self) -> None:
38+
with open(self._get_inner_edges_query_path) as f:
39+
get_inner_edges_query = f.read()
40+
inner_edges = self._rdf_graph.query(get_inner_edges_query, initBindings={'root_graph': self._root_graph_uri})
41+
for inner_edge_row in inner_edges:
42+
source_label = inner_edge_row['source_label'] \
43+
if inner_edge_row['source_label'] is not None \
44+
else urlparse(inner_edge_row['source_step']).fragment
45+
n = pydot.Node(
46+
'',
47+
fillcolor='lightgoldenrodyellow', style="filled",
48+
label=source_label,
49+
shape='record'
50+
)
51+
n.set_name(str(inner_edge_row['source_step']))
52+
self._dot_graph.add_node(n)
53+
target_label = inner_edge_row['target_label'] \
54+
if inner_edge_row['target_label'] is not None \
55+
else urlparse(inner_edge_row['target_step']).fragment
56+
n = pydot.Node(
57+
'',
58+
fillcolor='lightgoldenrodyellow', style="filled",
59+
label=target_label,
60+
shape='record'
61+
)
62+
n.set_name(str(inner_edge_row['target_step']))
63+
self._dot_graph.add_node(n)
64+
self._dot_graph.add_edge(pydot.Edge(str(inner_edge_row['source_step']), str(inner_edge_row['target_step'])))
65+
66+
def _set_input_edges(self) -> None:
67+
with open(self._get_input_edges_query_path) as f:
68+
get_input_edges_query = f.read()
69+
inputs_subgraph = pydot.Subgraph(graph_name="cluster_inputs")
70+
self._dot_graph.add_subgraph(inputs_subgraph)
71+
inputs_subgraph.set_rank("same")
72+
inputs_subgraph.create_attribute_methods(['style'])
73+
inputs_subgraph.set_style("dashed")
74+
inputs_subgraph.set_label("Workflow Inputs")
75+
76+
input_edges = self._rdf_graph.query(get_input_edges_query, initBindings={'root_graph': self._root_graph_uri})
77+
for input_row in input_edges:
78+
n = pydot.Node(
79+
'',
80+
fillcolor="#94DDF4",
81+
style="filled",
82+
label=urlparse(input_row['input']).fragment,
83+
shape='record'
84+
)
85+
n.set_name(str(input_row['input']))
86+
inputs_subgraph.add_node(n)
87+
self._dot_graph.add_edge(pydot.Edge(str(input_row['input']), str(input_row['step'])))
88+
89+
def _set_output_edges(self) -> None:
90+
with open(self._get_output_edges_query_path) as f:
91+
get_output_edges = f.read()
92+
outputs_graph = pydot.Subgraph(graph_name="cluster_outputs")
93+
self._dot_graph.add_subgraph(outputs_graph)
94+
outputs_graph.set_rank("same")
95+
outputs_graph.create_attribute_methods(['style'])
96+
outputs_graph.set_style("dashed")
97+
outputs_graph.set_label("Workflow Outputs")
98+
outputs_graph.set_labelloc("b")
99+
output_edges = self._rdf_graph.query(get_output_edges, initBindings={'root_graph': self._root_graph_uri})
100+
for output_edge_row in output_edges:
101+
n = pydot.Node('',
102+
fillcolor="#94DDF4",
103+
style="filled",
104+
label=urlparse(output_edge_row['output']).fragment,
105+
shape='record'
106+
)
107+
n.set_name(str(output_edge_row['output']))
108+
outputs_graph.add_node(n)
109+
self._dot_graph.add_edge(pydot.Edge(output_edge_row['step'], output_edge_row['output']))
110+
111+
def _get_root_graph_uri(self) -> rdflib.URIRef:
112+
with open(self._get_root_query_path) as f:
113+
get_root_query = f.read()
114+
root = list(self._rdf_graph.query(get_root_query, ))
115+
if len(root) != 1:
116+
raise RuntimeError("Cannot identify root workflow! Notice that only Workflows can be visualized")
117+
118+
workflow = root[0]['workflow'] # type: rdflib.URIRef
119+
return workflow
120+
121+
@classmethod
122+
def _init_dot_graph(cls) -> pydot.Graph:
123+
graph = pydot.Graph(graph_type='digraph', simplify=False)
124+
graph.set_bgcolor("#eeeeee")
125+
graph.set_clusterrank("local")
126+
graph.set_labelloc("bottom")
127+
graph.set_labelloc("bottom")
128+
graph.set_labeljust("right")
129+
130+
return graph
131+
132+
def get_dot_graph(self) -> pydot.Graph:
133+
"""Get the dot graph object."""
134+
return self._dot_graph
135+
136+
def dot(self) -> str:
137+
"""Get the graph as graphviz."""
138+
return str(self._dot_graph.to_string())
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
PREFIX ArraySchema: <https://w3id.org/cwl/salad#ArraySchema/>
2+
PREFIX CommandLineBinding: <https://w3id.org/cwl/cwl#CommandLineBinding/>
3+
PREFIX CommandLineTool: <https://w3id.org/cwl/cwl#CommandLineTool/>
4+
PREFIX CommandOutputBinding: <https://w3id.org/cwl/cwl#CommandOutputBinding/>
5+
PREFIX Directory: <https://w3id.org/cwl/cwl#Directory/>
6+
PREFIX Dirent: <https://w3id.org/cwl/cwl#Dirent/>
7+
PREFIX DockerRequirement: <https://w3id.org/cwl/cwl#DockerRequirement/>
8+
PREFIX EnumSchema: <https://w3id.org/cwl/salad#EnumSchema/>
9+
PREFIX EnvVarRequirement: <https://w3id.org/cwl/cwl#EnvVarRequirement/>
10+
PREFIX EnvironmentDef: <https://w3id.org/cwl/cwl#EnvironmentDef/>
11+
PREFIX ExpressionTool: <https://w3id.org/cwl/cwl#ExpressionTool/>
12+
PREFIX File: <https://w3id.org/cwl/cwl#File/>
13+
PREFIX InlineJavascriptRequirement: <https://w3id.org/cwl/cwl#InlineJavascriptRequirement/>
14+
PREFIX LinkMergeMethod: <https://w3id.org/cwl/cwl#LinkMergeMethod/>
15+
PREFIX Parameter: <https://w3id.org/cwl/cwl#Parameter/>
16+
PREFIX RecordSchema: <https://w3id.org/cwl/salad#RecordSchema/>
17+
PREFIX ResourceRequirement: <https://w3id.org/cwl/cwl#ResourceRequirement/>
18+
PREFIX ScatterMethod: <https://w3id.org/cwl/cwl#ScatterMethod/>
19+
PREFIX SchemaDefRequirement: <https://w3id.org/cwl/cwl#SchemaDefRequirement/>
20+
PREFIX SoftwarePackage: <https://w3id.org/cwl/cwl#SoftwarePackage/>
21+
PREFIX SoftwareRequirement: <https://w3id.org/cwl/cwl#SoftwareRequirement/>
22+
PREFIX Workflow: <https://w3id.org/cwl/cwl#Workflow/>
23+
PREFIX cwl: <https://w3id.org/cwl/cwl#>
24+
PREFIX dct: <http://purl.org/dc/terms/>
25+
PREFIX ns1: <http://commonwl.org/cwltool#>
26+
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
27+
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
28+
PREFIX sld: <https://w3id.org/cwl/salad#>
29+
PREFIX xml: <http://www.w3.org/XML/1998/namespace>
30+
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
31+
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
32+
33+
# GET EDGES
34+
SELECT DISTINCT ?source_label ?target_label ?source_step ?target_step ?target_ref_input ?output_ref ?workflow
35+
WHERE {
36+
?workflow Workflow:steps ?step .
37+
{
38+
?step cwl:in ?target_ref_input .
39+
?target_ref_input cwl:source ?output_ref .
40+
?source_step cwl:out ?output_ref .
41+
?source_step cwl:run ?source_step_node .
42+
?target_step cwl:in ?target_ref_input .
43+
?target_step cwl:run ?target_step_node .
44+
OPTIONAL {?source_step_node rdfs:label ?source_label} .
45+
OPTIONAL {?target_step_node rdfs:label ?target_label} .
46+
} .
47+
# root_graph is binded in python
48+
FILTER(?workflow = ?root_graph) .
49+
}
50+
ORDER BY ?source_node ?target_node
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
PREFIX ArraySchema: <https://w3id.org/cwl/salad#ArraySchema/>
2+
PREFIX CommandLineBinding: <https://w3id.org/cwl/cwl#CommandLineBinding/>
3+
PREFIX CommandLineTool: <https://w3id.org/cwl/cwl#CommandLineTool/>
4+
PREFIX CommandOutputBinding: <https://w3id.org/cwl/cwl#CommandOutputBinding/>
5+
PREFIX Directory: <https://w3id.org/cwl/cwl#Directory/>
6+
PREFIX Dirent: <https://w3id.org/cwl/cwl#Dirent/>
7+
PREFIX DockerRequirement: <https://w3id.org/cwl/cwl#DockerRequirement/>
8+
PREFIX EnumSchema: <https://w3id.org/cwl/salad#EnumSchema/>
9+
PREFIX EnvVarRequirement: <https://w3id.org/cwl/cwl#EnvVarRequirement/>
10+
PREFIX EnvironmentDef: <https://w3id.org/cwl/cwl#EnvironmentDef/>
11+
PREFIX ExpressionTool: <https://w3id.org/cwl/cwl#ExpressionTool/>
12+
PREFIX File: <https://w3id.org/cwl/cwl#File/>
13+
PREFIX InlineJavascriptRequirement: <https://w3id.org/cwl/cwl#InlineJavascriptRequirement/>
14+
PREFIX LinkMergeMethod: <https://w3id.org/cwl/cwl#LinkMergeMethod/>
15+
PREFIX Parameter: <https://w3id.org/cwl/cwl#Parameter/>
16+
PREFIX RecordSchema: <https://w3id.org/cwl/salad#RecordSchema/>
17+
PREFIX ResourceRequirement: <https://w3id.org/cwl/cwl#ResourceRequirement/>
18+
PREFIX ScatterMethod: <https://w3id.org/cwl/cwl#ScatterMethod/>
19+
PREFIX SchemaDefRequirement: <https://w3id.org/cwl/cwl#SchemaDefRequirement/>
20+
PREFIX SoftwarePackage: <https://w3id.org/cwl/cwl#SoftwarePackage/>
21+
PREFIX SoftwareRequirement: <https://w3id.org/cwl/cwl#SoftwareRequirement/>
22+
PREFIX Workflow: <https://w3id.org/cwl/cwl#Workflow/>
23+
PREFIX cwl: <https://w3id.org/cwl/cwl#>
24+
PREFIX dct: <http://purl.org/dc/terms/>
25+
PREFIX ns1: <http://commonwl.org/cwltool#>
26+
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
27+
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
28+
PREFIX sld: <https://w3id.org/cwl/salad#>
29+
PREFIX xml: <http://www.w3.org/XML/1998/namespace>
30+
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
31+
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
32+
33+
# GET INPUT
34+
SELECT ?input ?step
35+
WHERE {
36+
?workflow cwl:inputs ?input .
37+
?steps_input_port cwl:source ?input .
38+
?step cwl:in ?steps_input_port .
39+
# root_graph is binded in python
40+
FILTER(?workflow = ?root_graph) .
41+
}
42+
ORDER BY ?source_node ?target_node
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
PREFIX ArraySchema: <https://w3id.org/cwl/salad#ArraySchema/>
2+
PREFIX CommandLineBinding: <https://w3id.org/cwl/cwl#CommandLineBinding/>
3+
PREFIX CommandLineTool: <https://w3id.org/cwl/cwl#CommandLineTool/>
4+
PREFIX CommandOutputBinding: <https://w3id.org/cwl/cwl#CommandOutputBinding/>
5+
PREFIX Directory: <https://w3id.org/cwl/cwl#Directory/>
6+
PREFIX Dirent: <https://w3id.org/cwl/cwl#Dirent/>
7+
PREFIX DockerRequirement: <https://w3id.org/cwl/cwl#DockerRequirement/>
8+
PREFIX EnumSchema: <https://w3id.org/cwl/salad#EnumSchema/>
9+
PREFIX EnvVarRequirement: <https://w3id.org/cwl/cwl#EnvVarRequirement/>
10+
PREFIX EnvironmentDef: <https://w3id.org/cwl/cwl#EnvironmentDef/>
11+
PREFIX ExpressionTool: <https://w3id.org/cwl/cwl#ExpressionTool/>
12+
PREFIX File: <https://w3id.org/cwl/cwl#File/>
13+
PREFIX InlineJavascriptRequirement: <https://w3id.org/cwl/cwl#InlineJavascriptRequirement/>
14+
PREFIX LinkMergeMethod: <https://w3id.org/cwl/cwl#LinkMergeMethod/>
15+
PREFIX Parameter: <https://w3id.org/cwl/cwl#Parameter/>
16+
PREFIX RecordSchema: <https://w3id.org/cwl/salad#RecordSchema/>
17+
PREFIX ResourceRequirement: <https://w3id.org/cwl/cwl#ResourceRequirement/>
18+
PREFIX ScatterMethod: <https://w3id.org/cwl/cwl#ScatterMethod/>
19+
PREFIX SchemaDefRequirement: <https://w3id.org/cwl/cwl#SchemaDefRequirement/>
20+
PREFIX SoftwarePackage: <https://w3id.org/cwl/cwl#SoftwarePackage/>
21+
PREFIX SoftwareRequirement: <https://w3id.org/cwl/cwl#SoftwareRequirement/>
22+
PREFIX Workflow: <https://w3id.org/cwl/cwl#Workflow/>
23+
PREFIX cwl: <https://w3id.org/cwl/cwl#>
24+
PREFIX dct: <http://purl.org/dc/terms/>
25+
PREFIX ns1: <http://commonwl.org/cwltool#>
26+
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
27+
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
28+
PREFIX sld: <https://w3id.org/cwl/salad#>
29+
PREFIX xml: <http://www.w3.org/XML/1998/namespace>
30+
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
31+
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
32+
33+
# GET OUTPUT
34+
SELECT ?output ?step
35+
WHERE {
36+
?workflow cwl:outputs ?output .
37+
?output cwl:outputSource ?out.
38+
?step cwl:out ?out .
39+
# root_graph is binded in python
40+
FILTER(?workflow = ?root_graph) .
41+
}
42+
ORDER BY ?source_node ?target_node

cwltool/rdfqueries/get_root.sparql

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
PREFIX ArraySchema: <https://w3id.org/cwl/salad#ArraySchema/>
2+
PREFIX CommandLineBinding: <https://w3id.org/cwl/cwl#CommandLineBinding/>
3+
PREFIX CommandLineTool: <https://w3id.org/cwl/cwl#CommandLineTool/>
4+
PREFIX CommandOutputBinding: <https://w3id.org/cwl/cwl#CommandOutputBinding/>
5+
PREFIX Directory: <https://w3id.org/cwl/cwl#Directory/>
6+
PREFIX Dirent: <https://w3id.org/cwl/cwl#Dirent/>
7+
PREFIX DockerRequirement: <https://w3id.org/cwl/cwl#DockerRequirement/>
8+
PREFIX EnumSchema: <https://w3id.org/cwl/salad#EnumSchema/>
9+
PREFIX EnvVarRequirement: <https://w3id.org/cwl/cwl#EnvVarRequirement/>
10+
PREFIX EnvironmentDef: <https://w3id.org/cwl/cwl#EnvironmentDef/>
11+
PREFIX ExpressionTool: <https://w3id.org/cwl/cwl#ExpressionTool/>
12+
PREFIX File: <https://w3id.org/cwl/cwl#File/>
13+
PREFIX InlineJavascriptRequirement: <https://w3id.org/cwl/cwl#InlineJavascriptRequirement/>
14+
PREFIX LinkMergeMethod: <https://w3id.org/cwl/cwl#LinkMergeMethod/>
15+
PREFIX Parameter: <https://w3id.org/cwl/cwl#Parameter/>
16+
PREFIX RecordSchema: <https://w3id.org/cwl/salad#RecordSchema/>
17+
PREFIX ResourceRequirement: <https://w3id.org/cwl/cwl#ResourceRequirement/>
18+
PREFIX ScatterMethod: <https://w3id.org/cwl/cwl#ScatterMethod/>
19+
PREFIX SchemaDefRequirement: <https://w3id.org/cwl/cwl#SchemaDefRequirement/>
20+
PREFIX SoftwarePackage: <https://w3id.org/cwl/cwl#SoftwarePackage/>
21+
PREFIX SoftwareRequirement: <https://w3id.org/cwl/cwl#SoftwareRequirement/>
22+
PREFIX Workflow: <https://w3id.org/cwl/cwl#Workflow/>
23+
PREFIX cwl: <https://w3id.org/cwl/cwl#>
24+
PREFIX dct: <http://purl.org/dc/terms/>
25+
PREFIX ns1: <http://commonwl.org/cwltool#>
26+
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
27+
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
28+
PREFIX sld: <https://w3id.org/cwl/salad#>
29+
PREFIX xml: <http://www.w3.org/XML/1998/namespace>
30+
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
31+
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
32+
33+
SELECT DISTINCT ?workflow
34+
WHERE {
35+
{
36+
SELECT ?workflow
37+
WHERE {
38+
?workflow Workflow:steps ?step .
39+
}
40+
}
41+
MINUS
42+
{
43+
SELECT ?workflow
44+
WHERE {
45+
?root Workflow:steps ?step .
46+
?step cwl:run ?workflow .
47+
}
48+
}
49+
}
50+
ORDER BY ?workflow

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@
104104
"bagit >= 1.6.4",
105105
"typing-extensions",
106106
"coloredlogs",
107+
'pydot >= 1.4.1',
107108
],
108109
extras_require={
109110
':python_version<"3.6"': ["typing >= 3.5.3"],

0 commit comments

Comments
 (0)