generate_workflow.py 18.8 KB
Newer Older
1
#!/usr/bin/python3
mmassaviol's avatar
mmassaviol committed
2
3
4
# This script will take a workflow yaml file and generate Snakefile, params file, docker recipe and copy all needed files (scripts)
# Accepts four parameters: workflow_yaml, output_dir, waw_dir, (local_config)
# Usage: ./generate_workflow.py ./workflows/my_workflow/my_workflow.yaml ./output_dir ./ ./local_configs/cluster_mbb_isem.yml
5
import os
6
7
import collections
import shutil
8
import sys
mmassaviol's avatar
mmassaviol committed
9
import re
10
import subprocess
11
12
13

from tools import *

14
15
16
17
18
19
20
21
22
WORKFLOW_YAML = collections.OrderedDict()
TOOLS_YAML = collections.OrderedDict()
BASE_CITATIONS_YAML = collections.OrderedDict()
BASE_VERSIONS_YAML = collections.OrderedDict()

STEPS = collections.OrderedDict()
TOOLS = list()

# Import all yaml files (workflow and tools)
23
def import_yaml_files(workflow_yaml, waw_dir):
24
25
    # Import workflow yaml
    global WORKFLOW_YAML
26
    WORKFLOW_YAML = read_yaml(workflow_yaml)
27
28
29
30
31
    
    # Import steps
    global STEPS
    for step in WORKFLOW_YAML["steps"]:
        STEPS[step["name"]] = dict()
mmassaviol's avatar
mmassaviol committed
32
        STEPS[step["name"]]["name"] = step["name"]
mmassaviol's avatar
mmassaviol committed
33
        STEPS[step["name"]]["title"] = step["title"]
34
35
36
37
38
39
40
41
        STEPS[step["name"]]["tools"] = step["tools"]
        STEPS[step["name"]]["default"] = step["default"]

        # Import every tools yaml
        global TOOLS
        global TOOLS_YAML
        for tool in step["tools"]:
            TOOLS.append(tool)
42
            TOOLS_YAML[tool] = read_yaml(os.path.join(waw_dir + "/tools/" + tool + "/" + tool + ".yaml"))
43
44
45
    
    # Import base yaml (versions and citations for base container)
    global BASE_CITATIONS_YAML
46
    BASE_CITATIONS_YAML = read_yaml(os.path.join(waw_dir+"/Docker_base/citations.yaml"))
47
    global BASE_VERSIONS_YAML
48
    BASE_VERSIONS_YAML = read_yaml(os.path.join(waw_dir+"/Docker_base/versions.yaml"))
49
50
51

def get_params_to_remove():
    # Get params to remove
52
    to_remove = list()
53
54
    if "params_equals" in WORKFLOW_YAML.keys():
        params_equals = WORKFLOW_YAML["params_equals"]
55
56
57
58
59
60
61
        for line in params_equals:
            if (len(line) == 1):
                to_remove.append(line["remove"])
            if (len(line) == 2):
                to_remove.append(line["param_B"])
    return to_remove

62
# Generate tool parameters with default values
63
def generate_tool_params(to_remove, waw_dir = "./"):
64
    TOOL_PARAMS = collections.OrderedDict()
65

66
67
    # Global workflow parameters
    for option in WORKFLOW_YAML["options"]:
mmassaviol's avatar
mmassaviol committed
68
        if (option["type"] == "input_file"):
69
70
            TOOL_PARAMS[option["name"]+"_select"] = "server"
        TOOL_PARAMS[option["name"]] = option["value"] if ("value" in option.keys()) else ""
mmassaviol's avatar
mmassaviol committed
71

72
73
    # input parameters
    if "input" in WORKFLOW_YAML:
74
75
76
77
        for raw_input in WORKFLOW_YAML["input"]:
               raw_inputs_yaml = read_yaml(os.path.join(waw_dir+"/raw_inputs/" + raw_input + ".yaml"))
               for option in raw_inputs_yaml["options"]:
                      TOOL_PARAMS[option["name"]] = option["value"] if ("value" in option.keys()) else ""
78
79
80
81
82
83
84

    # For each step
    for step_name, step_yaml in STEPS.items():
        TOOL_PARAMS[step_name] = step_yaml["default"]
        # For each tool that can be selected in a step
        for tool in step_yaml["tools"]:
            if "script" in TOOLS_YAML[tool]:
mmassaviol's avatar
mmassaviol committed
85
                TOOL_PARAMS[step_name + "__" + TOOLS_YAML[tool]["id"] + "_script"] = "scripts/" + TOOLS_YAML[tool]["script"]
86
87
88
89
90
91
            # For each command (rule snakemake) in the tool
            for command in TOOLS_YAML[tool]["commands"]:
                TOOL_PARAMS[step_name + "__" + command["name"] + "_output_dir"] = step_name + "/" + command["output_dir"]
                TOOL_PARAMS[step_name + "__" + command["name"] + "_command"] = command["command"]
                # For each option in the command
                for option in command["options"]:
92
                    if step_name + "__" + option["name"] not in to_remove:
93
94
95
96
97
98
99
                        # Add parameter (and 'select parameter' in case of input_file)
                        if (option["type"] == "input_file"):
                            TOOL_PARAMS[step_name + "__" + option["name"]+"_select"] = "server"
                        TOOL_PARAMS[step_name + "__" + option["name"]] = option["value"] if ("value" in option.keys()) else ""
    return TOOL_PARAMS

# Generate parameters info (usefull for shiny app)
100
def generate_params_info(to_remove, waw_dir = "./"):
101
102
103
104
105
106
107
108
109
110
111
    PARAMS_INFO = collections.OrderedDict()

    for option in WORKFLOW_YAML["options"]:
        if (option["type"] == "input_file"):
            PARAMS_INFO[option["name"]+"_select"] = collections.OrderedDict()
            PARAMS_INFO[option["name"]+"_select"]["type"] = "select"
        PARAMS_INFO[option["name"]] = collections.OrderedDict()
        PARAMS_INFO[option["name"]]["type"] = option["type"]

    # input parameters
    if "input" in WORKFLOW_YAML:
112
113
114
115
116
        for raw_input in WORKFLOW_YAML["input"]:
            raw_inputs_yaml = read_yaml(os.path.join(waw_dir+"/raw_inputs/" + raw_input + ".yaml"))
            for option in raw_inputs_yaml["options"]:
                PARAMS_INFO[option["name"]] = collections.OrderedDict()
                PARAMS_INFO[option["name"]]["type"] = option["type"]
117
118
119
120
121
122
123
124
125

    # For each step
    for step_name, step_yaml in STEPS.items():
        # For each tool that can be selected in a step
        for tool in step_yaml["tools"]:
            # For each command (rule snakemake) in the tool
            for command in TOOLS_YAML[tool]["commands"]:
                # For each option in the command
                for option in command["options"]:
126
                    if step_name + "__" + option["name"] not in to_remove:
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
                        # Add parameter info (and 'select parameter' in case of input_file)
                        if (option["type"] == "input_file"):
                            PARAMS_INFO[step_name + "__" + option["name"] + "_select"] = collections.OrderedDict()
                            PARAMS_INFO[step_name + "__" + option["name"] + "_select"]["tool"] = tool
                            PARAMS_INFO[step_name + "__" + option["name"] + "_select"]["rule"] = step_name + "_" + command["name"]
                            PARAMS_INFO[step_name + "__" + option["name"] + "_select"]["type"] = "select"
                        PARAMS_INFO[step_name + "__" + option["name"]] = collections.OrderedDict()
                        PARAMS_INFO[step_name + "__" + option["name"]]["tool"] = tool
                        PARAMS_INFO[step_name + "__" + option["name"]]["rule"] = step_name + "_" + command["name"]
                        PARAMS_INFO[step_name + "__" + option["name"]]["type"] = option["type"]
                        PARAMS_INFO[step_name + "__" + option["name"]]["label"] = option["label"]

    return PARAMS_INFO

def generate_stop_cases():
    STOP_CASES = collections.OrderedDict()

    if "stop_cases" in WORKFLOW_YAML.keys():
        STOP_CASES = WORKFLOW_YAML["stop_cases"]

    return STOP_CASES

# Generate tools outputs
def generate_outputs():
    OUTPUTS = collections.OrderedDict()
    # For each step
    for step_name, step_yaml in STEPS.items():
        # For each tool that can be selected in a step
        for tool in step_yaml["tools"]:
            # For each command (rule snakemake) in the tool
mmassaviol's avatar
mmassaviol committed
157
            OUTPUTS[step_name + "__" + tool] = dict()
158
            for command in TOOLS_YAML[tool]["commands"]:
mmassaviol's avatar
mmassaviol committed
159
                OUTPUTS[step_name + "__" + tool][step_name + "__" + command["name"]] = command["outputs"]
160
161
162
163
164
165
166
167
168
169
170
171
    return OUTPUTS

# Generate multiqc (for multiqc config file generation)
def generate_multiqc():
    MULTIQC = collections.OrderedDict()

    for tool_name, tool_yaml in TOOLS_YAML.items():
        MULTIQC[tool_name] = tool_yaml["multiqc"]

    return MULTIQC

# Generate citations for all tools and dependencies
172
def generate_citations(output_dir):
173
174
175
176
177
178
179
180
181
182
    CITATIONS = collections.OrderedDict()

    CITATIONS["base_tools"] = BASE_CITATIONS_YAML["citations"]
    for tool_name, tool_yaml in TOOLS_YAML.items():
        if "citations" in tool_yaml:
            CITATIONS[tool_name] = tool_yaml["citations"]
    
    return CITATIONS

# Generate versions of all tools and dependencies
183
def generate_versions(output_dir):
184
185
186
187
188
189
190
191
192
193
194
195
    VERSIONS = collections.OrderedDict()

    VERSIONS["base_tools"] = BASE_VERSIONS_YAML["versions"]
    for tool_name, tool_yaml in TOOLS_YAML.items():
        if "version" in tool_yaml:
            VERSIONS[tool_name] = tool_yaml["version"]

    return VERSIONS

# Generate list of prepare report scripts
def generate_prepare_report_scripts():
    PREPARE_REPORT_SCRIPTS = dict()
196
197
198
199
200
201
202
203
204
205
206
207
208
    scripts_list = list()
    # For each step
    for step_name, step_yaml in STEPS.items():
        # For each tool that can be selected in a step
        for tool in step_yaml["tools"]:
            if "prepare_report_script" in TOOLS_YAML[tool]:
                res = dict()
                res["tool"] = tool
                res["step"] = step_name
                res["script"] = TOOLS_YAML[tool]["prepare_report_script"]
                PREPARE_REPORT_SCRIPTS[step_name + "__" + tool] = res
                scripts_list.append(step_name + "__" + TOOLS_YAML[tool]["prepare_report_script"])
    PREPARE_REPORT_SCRIPTS["SCRIPTS"] = scripts_list
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
    return PREPARE_REPORT_SCRIPTS

# Generate list of prepare report scripts
def generate_tools_scripts():
    TOOLS_SCRIPTS = dict()
    for tool_yaml in TOOLS_YAML.values():
        if "script" in tool_yaml.keys():
            TOOLS_SCRIPTS[tool_yaml["id"]] =tool_yaml["script"]

    return TOOLS_SCRIPTS

# Generate list of prepare report outputs
def generate_prepare_report_outputs():
    PREPARE_REPORT_OUTPUTS = collections.OrderedDict()
    # For each step
    for step_name, step_yaml in STEPS.items():
        # For each tool that can be selected in a step
        for tool in step_yaml["tools"]:
            if "prepare_report_outputs" in TOOLS_YAML[tool]:
                PREPARE_REPORT_OUTPUTS[step_name + "__" + tool] = TOOLS_YAML[tool]["prepare_report_outputs"]

    return PREPARE_REPORT_OUTPUTS

232
def generate_snake_rule(step_name,tool_name, waw_dir = "./"):
233
234
    RULE = "\n"
    
235
    with open(os.path.join(waw_dir + "/tools/" + tool_name + "/" + tool_name + ".rule.snakefile"), "r") as rule:
236
        RULE += rule.read()
mmassaviol's avatar
mmassaviol committed
237

238
    RULE += "\n"
239

mmassaviol's avatar
mmassaviol committed
240
    RULE = RULE.replace("<step_name>",step_name)
241

242
    return RULE
243

244
# Generate Snakefile
245
def generate_snakefile(workflow_yaml, waw_dir = "./"):
246
247
    SNAKEFILE = ""
    RULES = ""
248

mmassaviol's avatar
mmassaviol committed
249
250
251
252
253
    # import tools
    SNAKEFILE += "from tools import *\n"

    # import raw_input function
    if "input" in WORKFLOW_YAML:
254
255
       for raw_input in WORKFLOW_YAML["input"]:
             SNAKEFILE += "from "+ raw_input +" import "+ raw_input+"\n"
mmassaviol's avatar
mmassaviol committed
256
257
258
259

    # configure workdir (snakemake -d)
    SNAKEFILE += "workdir: config['params']['results_dir']\n"

260
    # Open template
mmassaviol's avatar
mmassaviol committed
261

262
263
    with open(workflow_yaml.replace(".yaml",".snakefile"), "r") as template:
        SNAKEFILE += template.read()
264

mmassaviol's avatar
mmassaviol committed
265
    # import wildcards and imports
266
    with open(os.path.join(waw_dir + "/workflows/global_imports.py"), "r") as imports:
mmassaviol's avatar
mmassaviol committed
267
268
269
        GLOBAL_IMPORTS = imports.read()
        SNAKEFILE = SNAKEFILE.replace("{import global_imports}",GLOBAL_IMPORTS)

270
    # Global functions
271
    with open(os.path.join(waw_dir + "/workflows/global_functions.py")) as global_functions:
272
273
        GLOBAL_FUNCTIONS = global_functions.read()
        SNAKEFILE = SNAKEFILE.replace("{import global_functions}",GLOBAL_FUNCTIONS)
274

275
    # Global rules
276
    with open(os.path.join(waw_dir + "/workflows/global_rules.snakefile")) as global_rules:
277
278
279
280
281
        GLOBAL_RULES = global_rules.read()
        SNAKEFILE = SNAKEFILE.replace("{import global_rules}",GLOBAL_RULES)
    
    for step_name, step_yaml in STEPS.items():
        for tool in step_yaml["tools"]:
282
            RULES += generate_snake_rule(step_name,tool,waw_dir)
mmassaviol's avatar
mmassaviol committed
283

284
    SNAKEFILE = SNAKEFILE.replace("{import rules}", RULES)
285
    
286
287
288
289
290
291
292
    #####
    # Replace params
    if "params_equals" in WORKFLOW_YAML.keys():
        for line in WORKFLOW_YAML["params_equals"]:
            if (len(line) == 2):
                SNAKEFILE = SNAKEFILE.replace("config[\""+line["param_B"]+"\"]","config[\""+line["param_A"]+"\"]")
    #####
293

294
    return SNAKEFILE
mmassaviol's avatar
mmassaviol committed
295

296
# Generate Dockerfile
297
298
def generate_dockerfile(output_dir, local_config, waw_dir = "./"):
    workflow_name = WORKFLOW_YAML["name"]
299
300
    DOCKERFILE = ""
    # Open template
301
    with open(waw_dir+"/Dockerfile.template", "r") as template:
302
303
        DOCKERFILE = template.read()
    DOCKERFILE += "\n"
304

305
    # COPY files if data in workflow yaml
306
    files = ""
307
308
    if "data" in WORKFLOW_YAML.keys():
        for data in WORKFLOW_YAML["data"]:
309
            file = os.path.normpath(waw_dir + "/workflows/" + workflow_name + "/data/" + data["name"])
mmassaviol's avatar
mmassaviol committed
310
            files += "COPY ./files/data/" + data["name"] + " /" + data["name"] + "\n"
311
            # Copy files
312
            copy_dir(file,output_dir+'/files/data/'+data["name"])
313
314
315
316
317
318
319
320

    # COPY files if data in tools yaml
    # And import install commands
    tools_installs = collections.OrderedDict()
    for tool_name, tool_yaml in TOOLS_YAML.items():
        tools_installs.update(tool_yaml["install"])
        if "data" in tool_yaml.keys():
            for data in tool_yaml["data"]:
mmassaviol's avatar
mmassaviol committed
321
                file = os.path.normpath(waw_dir + "/tools/" + tool_name + "/data/" + data["name"])
mmassaviol's avatar
mmassaviol committed
322
                files += "COPY ./files/data/" + data["name"] + " /" + data["name"] + "\n"
323
                # Copy files
jlopez's avatar
Update    
jlopez committed
324
                shutil.copytree(file, output_dir+workflow_name + '/files/data/' + data["name"])
325

326
327
    files += "COPY files /workflow\n"
    files += "COPY sagApp /sagApp\n\n"
mmassaviol's avatar
mmassaviol committed
328

329
330
331
    for value in tools_installs.values():
        tool_install="RUN "
        tool_path=""
332
        for line in value:
333
            if "ENV" in line:
mmassaviol's avatar
mmassaviol committed
334
                tool_path+= line + "\n"
335
336
337
338
339
340
341
342
            else:
                tool_install += line + " \\\n && "
        tool_install = tool_install[:-7] # remove trailing '\n && '
        tool_install += "\n\n"

        if (tool_path!=""):
            DOCKERFILE += tool_path    
        DOCKERFILE += tool_install
mmassaviol's avatar
mmassaviol committed
343

344
    # Local Config
345
346
    if local_config != "default":
        local_config_data = read_yaml(local_config)
347
348
349
350
351
352
353
        if "docker" in local_config_data.keys():
            DOCKERFILE += '\n'.join(local_config_data["docker"]["env"])
            DOCKERFILE += '\n\n'
            DOCKERFILE += '\n'.join(local_config_data["docker"]["run"])
    DOCKERFILE += "\n\n"
    DOCKERFILE += "EXPOSE 3838\n"
    DOCKERFILE += "CMD [\"Rscript\", \"-e\", \"setwd('/sagApp/'); shiny::runApp('/sagApp/app.R',port=3838 , host='0.0.0.0')\"]\n"
354

355
356
357
358
    # Ajout des copy après les installs
    DOCKERFILE += "\n\n"
    DOCKERFILE += "FROM alltools\n\n"
    DOCKERFILE += files
mmassaviol's avatar
mmassaviol committed
359
    
360
    return DOCKERFILE
mmassaviol's avatar
mmassaviol committed
361

362
# Generate params yaml with all parameters and informations
363
def generate_params_yaml(workflow_name, to_remove, waw_dir = "./"):
364
    PARAMS = collections.OrderedDict()
mmassaviol's avatar
mmassaviol committed
365

366
    PARAMS["pipeline"] = workflow_name
367
    PARAMS["params"] = generate_tool_params(to_remove, waw_dir)
mmassaviol's avatar
mmassaviol committed
368
    PARAMS["steps"] = list(STEPS.values())
369
    PARAMS["params_info"] = generate_params_info(to_remove, waw_dir)
370
    PARAMS["prepare_report_scripts"] = list(generate_prepare_report_scripts()["SCRIPTS"])
371
372
373
374
    PARAMS["prepare_report_outputs"] = generate_prepare_report_outputs()
    PARAMS["outputs"] = generate_outputs()
    PARAMS["multiqc"] = generate_multiqc()
    PARAMS["stop_cases"] = generate_stop_cases()
mmassaviol's avatar
mmassaviol committed
375

376
    return PARAMS
mmassaviol's avatar
mmassaviol committed
377

378
379
380
381
382
383
def generate_pipeline_files(workflow_yaml, output_dir, waw_dir, local_config="default"):

    import_yaml_files(workflow_yaml, waw_dir)

    workflow_name = WORKFLOW_YAML["name"]

384
    # Create output directory if needed
385
386
387
388
    if not os.path.isdir(output_dir):
        os.mkdir(output_dir)
    if not os.path.isdir(output_dir+"/files"):
        os.mkdir(output_dir+"/files")
mmassaviol's avatar
mmassaviol committed
389

390
    ### Generate all output files
mmassaviol's avatar
mmassaviol committed
391

392
    write_yaml(output_dir + "/files/citations.yaml", generate_citations(output_dir))
mmassaviol's avatar
mmassaviol committed
393

394
    write_yaml(output_dir + "/files/versions.yaml", generate_versions(output_dir))
395
    
396
397
    with open(output_dir + "/files/Snakefile", "w") as out:
        out.write(generate_snakefile(workflow_yaml, waw_dir))
mmassaviol's avatar
mmassaviol committed
398

399
    with open(output_dir + "/Dockerfile", "w") as out:
400
        out.write(generate_dockerfile(output_dir, local_config, waw_dir))
mmassaviol's avatar
mmassaviol committed
401

402
    write_yaml(output_dir + "/files/params.total.yml", generate_params_yaml(workflow_name, get_params_to_remove(), waw_dir))
jlopez's avatar
Update    
jlopez committed
403
    ###1
mmassaviol's avatar
mmassaviol committed
404

405
    ### Copy scripts and other files
mmassaviol's avatar
mmassaviol committed
406

mmassaviol's avatar
mmassaviol committed
407
    NB_SCRIPTS = len(list(generate_prepare_report_scripts()["SCRIPTS"])) + len(list(generate_tools_scripts()))
mmassaviol's avatar
mmassaviol committed
408

409
410
    if not os.path.isdir(output_dir + "/files/scripts"):
        os.mkdir(output_dir + "/files/scripts")
411
    if (NB_SCRIPTS==0):
412
        with open(output_dir + "/files/scripts/.gitignore",'w') as gitignore:
mmassaviol's avatar
mmassaviol committed
413
            to_write ="""# gitignore to force creation of scripts dir
mmassaviol's avatar
mmassaviol committed
414
415
416
417
!.gitignore
"""
            gitignore.write(to_write)
    else:
mmassaviol's avatar
mmassaviol committed
418

419
420
421
422
423
        for name, script_dict in generate_prepare_report_scripts().items():
            if name!= "SCRIPTS":
                tool = script_dict["tool"]
                script = script_dict["script"]
                step = script_dict["step"]
424
                returned_value = subprocess.call("sed 's/<step_name>/"+step+"/g' " + os.path.join(waw_dir + "/tools/" + tool + "/" + script) + " > " + output_dir + "/files/scripts/" + step + "__" + script, shell=True)
425

426
        for tool, script in generate_tools_scripts().items():
427
            shutil.copy(os.path.join(waw_dir+"/tools/" + tool + "/" + script), output_dir+ "/files/scripts")
428

429
430
    shutil.copy(os.path.join(waw_dir+"/generate_multiqc_config.py"), output_dir+ "/files")
    shutil.copy(os.path.join(waw_dir+"/tools.py"), output_dir + "/files")
khalid's avatar
khalid committed
431
    shutil.copy(os.path.join(waw_dir+"/mbb.png"), output_dir + "/files")
mmassaviol's avatar
mmassaviol committed
432

433
    if "input" in WORKFLOW_YAML:
434
435
        for raw_input in WORKFLOW_YAML["input"]:
            shutil.copy(os.path.join(waw_dir+"/raw_inputs/"+raw_input+".py"), output_dir+"/files")
436

mmassaviol's avatar
mmassaviol committed
437
438
439
    if (local_config != "default"):
        local_config_data = read_yaml(local_config)
        if ("qsub" in local_config_data.keys()):
440
            with open(output_dir+"/waw_workflow.qsub", 'w') as qsub_file:
mmassaviol's avatar
mmassaviol committed
441
442
443
                qsub_file.writelines([ a.replace("{workflow_name}",WORKFLOW_YAML["docker_name"]) +'\n' for a in local_config_data["qsub"]] )

    # deploys scripts
444
445
    for script in os.listdir(os.path.join(waw_dir+"/deploys")):
        with open(os.path.join(waw_dir+"/deploys/"+script),"r") as infile:
mmassaviol's avatar
mmassaviol committed
446
            lines = infile.readlines()
447
            with open(output_dir+"/"+script,"w") as outfile:
mmassaviol's avatar
mmassaviol committed
448
449
450
                outfile.writelines([ a.replace("{workflow_name}",WORKFLOW_YAML["docker_name"]) for a in lines] )


451
    ###
mmassaviol's avatar
mmassaviol committed
452

453
def main():
mmassaviol's avatar
mmassaviol committed
454
    #print(len(sys.argv))
jlopez's avatar
Update    
jlopez committed
455
456
    if len(sys.argv) == 5:
        generate_pipeline_files(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4])
457
    elif len(sys.argv) == 4:
458
459
        generate_pipeline_files(sys.argv[1], sys.argv[2], sys.argv[3])
    else:
460
461
        exit("""Need at least three parameters: workflow_yaml, output_dir, waw_dir, (local_config)
Usage: ./generate_workflow.py workflow_yaml output_dir waw_dir (local_config)""")
462

463
464
if __name__ == "__main__":
    # execute only if run as a script
mmassaviol's avatar
mmassaviol committed
465
    main()