21
21
import re
22
22
import tempfile
23
23
from pathlib import Path
24
- from typing import Any , Dict , Optional , Tuple , Union
24
+ from typing import Any , Dict , List , Optional , Tuple , Union , cast
25
25
from uuid import uuid4
26
26
27
27
import cwl_utils .parser .cwl_v1_2 as cwl
28
28
29
29
from renku .core import errors
30
30
from renku .core .plugin import hookimpl
31
31
from renku .core .plugin .provider import RENKU_ENV_PREFIX
32
- from renku .core .util .yaml import write_yaml
32
+ from renku .core .util .yaml import dumps_yaml , write_yaml
33
33
from renku .core .workflow .concrete_execution_graph import ExecutionGraph
34
34
from renku .domain_model .workflow .composite_plan import CompositePlan
35
35
from renku .domain_model .workflow .converters import IWorkflowConverter
@@ -94,38 +94,80 @@ def workflow_format(self):
94
94
95
95
@hookimpl
96
96
def workflow_convert (
97
- self , workflow : Union [CompositePlan , Plan ], basedir : Path , output : Optional [Path ], output_format : Optional [str ]
98
- ):
99
- """Converts the specified workflow to CWL format."""
97
+ self ,
98
+ workflow : Union [CompositePlan , Plan ],
99
+ basedir : Path ,
100
+ output : Optional [Path ],
101
+ output_format : Optional [str ],
102
+ resolve_paths : Optional [bool ],
103
+ nest_workflows : Optional [bool ],
104
+ ) -> str :
105
+ """Converts the specified workflow to CWL format.
106
+
107
+ Args:
108
+ worflow(Union[CompositePlan, Plan]): The plan or composite plan to be converted to cwl.
109
+ basedir(Path): The path of the base location used as a prefix for all workflow input and outputs.
110
+ output(Optional[Path]): The file where the CWL specification should be saved,
111
+ if None then no file is created.
112
+ output_format(Optional[str]): Not used. Only YAML is generated, regardless of what is provided.
113
+ resolve_paths(Optional[bool]): Whether to make all paths absolute and resolve all symlinks,
114
+ True by default.
115
+ nest_workflows(Optional[bool]): Whether nested CWL workflows should be used or each sub-workflow should be
116
+ a separate file, False by default.
117
+
118
+ Returns:
119
+ The contents of the CWL workflow as string. If nested workflows are used then only the parent
120
+ specification is returned.
121
+ """
100
122
filename = None
123
+
124
+ if resolve_paths is None :
125
+ resolve_paths = True
126
+
101
127
if output :
102
128
if output .is_dir ():
103
129
tmpdir = output
130
+ filename = None
104
131
else :
105
132
tmpdir = output .parent
106
133
filename = output
107
134
else :
108
135
tmpdir = Path (tempfile .mkdtemp ())
109
136
137
+ cwl_workflow : Union [cwl .Workflow , CommandLineTool ]
110
138
if isinstance (workflow , CompositePlan ):
111
- path = CWLExporter ._convert_composite (
112
- workflow , tmpdir , basedir , filename = filename , output_format = output_format
113
- )
139
+ cwl_workflow = CWLExporter ._convert_composite (workflow , basedir , resolve_paths = resolve_paths )
140
+ if nest_workflows :
141
+ # INFO: There is only one parent workflow with all children embedded in it
142
+ if cwl_workflow .requirements is None :
143
+ cwl_workflow .requirements = []
144
+ cwl_workflow .requirements .append (cwl .SubworkflowFeatureRequirement ())
145
+ else :
146
+ # INFO: The parent composite worfklow references other workflow files,
147
+ # write the child workflows in separate files and reference them in parent
148
+ for step in cast (List [WorkflowStep ], cwl_workflow .steps ):
149
+ step_filename = Path (f"{ uuid4 ()} .cwl" )
150
+ step_path = (tmpdir / step_filename ).resolve ()
151
+ write_yaml (step_path , step .run .save ())
152
+ step .run = str (step_path )
153
+ if filename is None :
154
+ filename = Path (f"parent_{ uuid4 ()} .cwl" )
114
155
else :
115
- _ , path = CWLExporter ._convert_step (
116
- workflow , tmpdir , basedir , filename = filename , output_format = output_format
117
- )
156
+ cwl_workflow = CWLExporter ._convert_step (workflow , basedir , resolve_paths = resolve_paths )
157
+ if filename is None :
158
+ filename = Path ( f" { uuid4 () } .cwl" )
118
159
119
- return path .read_text ()
160
+ cwl_workflow_dict : Dict [str , Any ] = cwl_workflow .save ()
161
+ path = (tmpdir / filename ).resolve ()
162
+ write_yaml (path , cwl_workflow_dict )
163
+ return dumps_yaml (cwl_workflow_dict )
120
164
121
165
@staticmethod
122
166
def _sanitize_id (id ):
123
167
return re .sub (r"/|-" , "_" , id )
124
168
125
169
@staticmethod
126
- def _convert_composite (
127
- workflow : CompositePlan , tmpdir : Path , basedir : Path , filename : Optional [Path ], output_format : Optional [str ]
128
- ):
170
+ def _convert_composite (workflow : CompositePlan , basedir : Path , resolve_paths : bool ) -> cwl .Workflow :
129
171
"""Converts a composite plan to a CWL file."""
130
172
inputs : Dict [str , str ] = {}
131
173
arguments = {}
@@ -145,10 +187,8 @@ def _convert_composite(
145
187
import networkx as nx
146
188
147
189
for i , wf in enumerate (nx .topological_sort (graph .workflow_graph )):
148
- cwl_workflow , path = CWLExporter ._convert_step (
149
- workflow = wf , tmpdir = tmpdir , basedir = basedir , filename = None , output_format = output_format
150
- )
151
- step = WorkflowStep (in_ = [], out = [], run = str (path ), id = "step_{}" .format (i ))
190
+ step_clitool = CWLExporter ._convert_step (workflow = wf , basedir = basedir , resolve_paths = resolve_paths )
191
+ step = WorkflowStep (in_ = [], out = [], run = step_clitool , id = "step_{}" .format (i ))
152
192
153
193
for input in wf .inputs :
154
194
input_path = input .actual_value
@@ -192,11 +232,17 @@ def _convert_composite(
192
232
# check types of paths and add as top level inputs/outputs
193
233
for path , id_ in inputs .items ():
194
234
type_ = "Directory" if os .path .isdir (path ) else "File"
235
+ location = Path (path )
236
+ if resolve_paths :
237
+ location = location .resolve ()
238
+ location_str = str (location .as_uri ())
239
+ else :
240
+ location_str = str (location )
195
241
workflow_object .inputs .append (
196
242
cwl .WorkflowInputParameter (
197
243
id = id_ ,
198
244
type = type_ ,
199
- default = {"location" : Path ( path ). resolve (). as_uri () , "class" : type_ },
245
+ default = {"location" : location_str , "class" : type_ },
200
246
)
201
247
)
202
248
@@ -211,19 +257,12 @@ def _convert_composite(
211
257
id = "output_{}" .format (index ), outputSource = "{}/{}" .format (step_id , id_ ), type = type_
212
258
)
213
259
)
214
- if filename is None :
215
- filename = Path ("parent_{}.cwl" .format (uuid4 ()))
216
260
217
- output = workflow_object .save ()
218
- path = (tmpdir / filename ).resolve ()
219
- write_yaml (path , output )
220
- return path
261
+ return workflow_object
221
262
222
263
@staticmethod
223
- def _convert_step (
224
- workflow : Plan , tmpdir : Path , basedir : Path , filename : Optional [Path ], output_format : Optional [str ]
225
- ):
226
- """Converts a single workflow step to a CWL file."""
264
+ def _convert_step (workflow : Plan , basedir : Path , resolve_paths : bool ) -> CommandLineTool :
265
+ """Converts a single workflow step to a CWL CommandLineTool."""
227
266
stdin , stdout , stderr = None , None , None
228
267
229
268
inputs = list (workflow .inputs )
@@ -276,7 +315,7 @@ def _convert_step(
276
315
tool_object .inputs .append (arg )
277
316
278
317
for input_ in inputs :
279
- tool_input = CWLExporter ._convert_input (input_ , basedir )
318
+ tool_input = CWLExporter ._convert_input (input_ , basedir , resolve_paths = resolve_paths )
280
319
281
320
workdir_req .listing .append (
282
321
cwl .Dirent (entry = "$(inputs.{})" .format (tool_input .id ), entryname = input_ .actual_value , writable = False )
@@ -299,12 +338,18 @@ def _convert_step(
299
338
workdir_req .listing .append (
300
339
cwl .Dirent (entry = "$(inputs.input_renku_metadata)" , entryname = ".renku" , writable = False )
301
340
)
341
+ location = basedir / ".renku"
342
+ if resolve_paths :
343
+ location = location .resolve ()
344
+ location_str = location .as_uri ()
345
+ else :
346
+ location_str = str (location )
302
347
tool_object .inputs .append (
303
348
cwl .CommandInputParameter (
304
349
id = "input_renku_metadata" ,
305
350
type = "Directory" ,
306
351
inputBinding = None ,
307
- default = {"location" : ( basedir / ".renku" ). resolve (). as_uri () , "class" : "Directory" },
352
+ default = {"location" : location_str , "class" : "Directory" },
308
353
)
309
354
)
310
355
@@ -315,12 +360,7 @@ def _convert_step(
315
360
if environment_variables :
316
361
tool_object .requirements .append (cwl .EnvVarRequirement (environment_variables )) # type: ignore
317
362
318
- output = tool_object .save ()
319
- if filename is None :
320
- filename = Path ("{}.cwl" .format (uuid4 ()))
321
- path = (tmpdir / filename ).resolve ()
322
- write_yaml (path , output )
323
- return output , path
363
+ return tool_object
324
364
325
365
@staticmethod
326
366
def _convert_parameter (parameter : CommandParameter ):
@@ -347,7 +387,7 @@ def _convert_parameter(parameter: CommandParameter):
347
387
)
348
388
349
389
@staticmethod
350
- def _convert_input (input : CommandInput , basedir : Path ):
390
+ def _convert_input (input : CommandInput , basedir : Path , resolve_paths : bool ):
351
391
"""Converts an input to a CWL input."""
352
392
type_ = (
353
393
"Directory"
@@ -371,13 +411,19 @@ def _convert_input(input: CommandInput, basedir: Path):
371
411
prefix = prefix [:- 1 ]
372
412
separate = True
373
413
414
+ location = basedir / input .actual_value
415
+ if resolve_paths :
416
+ location = location .resolve ()
417
+ location_str = location .as_uri ()
418
+ else :
419
+ location_str = str (location )
374
420
return cwl .CommandInputParameter (
375
421
id = sanitized_id ,
376
422
type = type_ ,
377
423
inputBinding = cwl .CommandLineBinding (position = position , prefix = prefix , separate = separate )
378
424
if position or prefix
379
425
else None ,
380
- default = {"location" : ( basedir / input . actual_value ). resolve (). as_uri () , "class" : type_ },
426
+ default = {"location" : location_str , "class" : type_ },
381
427
)
382
428
383
429
@staticmethod
0 commit comments