wrabbit 0.1.0__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 (44) hide show
  1. parser/__init__.py +9 -0
  2. parser/constants.py +154 -0
  3. parser/nextflow.py +438 -0
  4. parser/utils.py +330 -0
  5. sb_types/__init__.py +35 -0
  6. sb_types/port.py +2 -0
  7. sb_types/requirements.py +2 -0
  8. sb_types/sb_types.py +116 -0
  9. wrabbit/__init__.py +0 -0
  10. wrabbit/exceptions.py +64 -0
  11. wrabbit/parser/__init__.py +9 -0
  12. wrabbit/parser/constants.py +173 -0
  13. wrabbit/parser/js_templates/sample_sheet_generator.js +133 -0
  14. wrabbit/parser/js_templates/sample_sheet_switch.js +9 -0
  15. wrabbit/parser/nextflow.py +522 -0
  16. wrabbit/parser/utils.py +389 -0
  17. wrabbit/sb_types/__init__.py +36 -0
  18. wrabbit/sb_types/port.py +2 -0
  19. wrabbit/sb_types/requirements.py +2 -0
  20. wrabbit/sb_types/sb_types.py +116 -0
  21. wrabbit/specification/__init__.py +70 -0
  22. wrabbit/specification/binding.py +34 -0
  23. wrabbit/specification/hints.py +40 -0
  24. wrabbit/specification/listing.py +37 -0
  25. wrabbit/specification/node.py +444 -0
  26. wrabbit/specification/node_types.py +223 -0
  27. wrabbit/specification/port.py +106 -0
  28. wrabbit/specification/requirements.py +63 -0
  29. wrabbit/specification/sbg.py +33 -0
  30. wrabbit/specification/utils.py +0 -0
  31. wrabbit/version.py +1 -0
  32. wrabbit/wrapper/__init__.py +0 -0
  33. wrabbit/wrapper/constants.py +0 -0
  34. wrabbit/wrapper/utils.py +17 -0
  35. wrabbit/wrapper/wrapper.py +300 -0
  36. wrabbit-0.1.0.dist-info/LICENSE +201 -0
  37. wrabbit-0.1.0.dist-info/METADATA +58 -0
  38. wrabbit-0.1.0.dist-info/RECORD +44 -0
  39. wrabbit-0.1.0.dist-info/WHEEL +5 -0
  40. wrabbit-0.1.0.dist-info/top_level.txt +1 -0
  41. wrapper/__init__.py +0 -0
  42. wrapper/constants.py +0 -0
  43. wrapper/utils.py +0 -0
  44. wrapper/wrapper.py +235 -0
parser/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ import os
2
+
3
+ PACKAGE_PATH = os.path.abspath(os.path.dirname(__file__))
4
+ JS_PATH = os.path.join(PACKAGE_PATH, 'js_templates')
5
+
6
+
7
+ def read_js_template(file_name):
8
+ with open(os.path.join(JS_PATH, file_name), 'r') as f:
9
+ return f.read()
parser/constants.py ADDED
@@ -0,0 +1,154 @@
1
+ from enum import Enum
2
+ from wrabbit.parser import read_js_template
3
+
4
+
5
+ from wrabbit.specification import (
6
+ FileType,
7
+ DirectoryType,
8
+ ArrayType,
9
+ )
10
+
11
+ # ############################## Generic Bits ############################### #
12
+ PACKAGE_SIZE_LIMIT = 100 * 1024 * 1024 # 100 MB
13
+ REMOVE_INPUT_KEY = "REMOVE_THIS_KEY"
14
+
15
+
16
+ # keep track of what extensions are applicable for processing
17
+ class EXTENSIONS:
18
+ yaml = 'yaml'
19
+ yml = 'yml'
20
+ json = 'json'
21
+ cwl = 'cwl'
22
+
23
+ yaml_all = [yaml, yml, cwl]
24
+ json_all = [json]
25
+ all_ = [yaml, yml, json, cwl]
26
+
27
+
28
+ # ############################ CWL Standard Bits ############################ #
29
+ # A generic SB input array of files that should be available on the
30
+ # instance but are not explicitly provided to the execution as wdl params.
31
+ SAMPLE_SHEET_FUNCTION = read_js_template("sample_sheet_generator.js")
32
+ SAMPLE_SHEET_SWITCH = read_js_template("sample_sheet_switch.js")
33
+ SB_SAMPLES_SCHEMA_DEFAULT_NAME = "samplesheet.csv"
34
+
35
+ GENERIC_FILE_ARRAY_INPUT = {
36
+ "id": "auxiliary_files",
37
+ "type": ArrayType(items=[FileType()], optional=True),
38
+ "label": "Auxiliary files",
39
+ "doc": "List of files not added as explicit workflow inputs but "
40
+ "required for workflow execution."
41
+ }
42
+
43
+ SAMPLE_SHEET_FILE_ARRAY_INPUT = {
44
+ "id": "file_input",
45
+ "type": ArrayType(items=[FileType()], optional=True),
46
+ "label": "Input files",
47
+ "doc": "List of files that will be used to autogenerate the sample sheet "
48
+ "that is required for workflow execution."
49
+ }
50
+
51
+ GENERIC_NF_OUTPUT_DIRECTORY = {
52
+ "id": "nf_workdir",
53
+ "type": DirectoryType(optional=True),
54
+ "label": "Work Directory",
55
+ "doc": "This is a template output. "
56
+ "Please change glob to directories specified in "
57
+ "publishDir in the workflow.",
58
+ "outputBinding": {
59
+ "glob": "work"
60
+ }
61
+ }
62
+
63
+ GENERIC_WDL_OUTPUT_DIRECTORY = {
64
+ "id": "output_txt",
65
+ "doc": "This is a template output. "
66
+ "Please modify to collect final outputs using "
67
+ "glob inside the working directory.",
68
+ "type": ArrayType(items=[FileType()], optional=False),
69
+ "outputBinding": {
70
+ "glob": "*.txt"
71
+ }
72
+ }
73
+
74
+ # Requirements for sb wrapper
75
+ INLINE_JS_REQUIREMENT = {
76
+ 'class': "InlineJavascriptRequirement"
77
+ }
78
+ LOAD_LISTING_REQUIREMENT = {
79
+ 'class': "LoadListingRequirement"
80
+ }
81
+ AUX_FILES_REQUIREMENT = {
82
+ "class": "InitialWorkDirRequirement",
83
+ "listing": [
84
+ "$(inputs.auxiliary_files)"
85
+ ]
86
+ }
87
+
88
+ # Legacy - Delete after updating wdl.py
89
+ WRAPPER_REQUIREMENTS = [
90
+ INLINE_JS_REQUIREMENT,
91
+ AUX_FILES_REQUIREMENT
92
+ ]
93
+
94
+
95
+ def sample_sheet(
96
+ file_name, sample_sheet_input, format_, input_source, header, rows,
97
+ defaults, group_by):
98
+ basename = ".".join(file_name.split(".")[:-1])
99
+ ext = file_name.split(".")[-1]
100
+ new_name = f"{basename}.new.{ext}"
101
+
102
+ return {
103
+ "class": "InitialWorkDirRequirement",
104
+ "listing": [
105
+ {
106
+ "entryname": f"${{ return {sample_sheet_input} ? {sample_sheet_input}.nameroot + '.new' + {sample_sheet_input}.nameext : '{file_name}' }}",
107
+ "entry": SAMPLE_SHEET_FUNCTION.format_map(locals()),
108
+ "writable": False
109
+ }
110
+ ]
111
+ }
112
+
113
+
114
+ # ############################## Nextflow Bits ############################## #
115
+ # Keys that should be skipped when parsing nextflow tower yaml file
116
+
117
+ NF_CONFIG_DEFAULT_NAME = 'nextflow.config'
118
+ NF_SCHEMA_DEFAULT_NAME = 'nextflow_schema.json'
119
+ SB_SCHEMA_DEFAULT_NAME = 'sb_nextflow_schema'
120
+ README_DEFAULT_NAME = 'README.md'
121
+
122
+ # Mappings of nextflow input fields to SB input fields
123
+ # nextflow_key: cwl_key mapping
124
+ NF_TO_CWL_PORT_MAP = {
125
+ 'default': 'sbg:toolDefaultValue',
126
+ 'description': 'label',
127
+ 'help_text': 'doc',
128
+ 'mimetype': 'format',
129
+ 'fa_icon': 'sbg:icon',
130
+ 'pattern': 'sbg:pattern',
131
+ 'hidden': 'sbg:hidden',
132
+ }
133
+
134
+ # Mappings of nextflow definition fields to SB category fields
135
+ # nextflow_key: cwl_key mapping
136
+ NF_TO_CWL_CATEGORY_MAP = {
137
+ 'title': 'sbg:title',
138
+ 'description': 'sbg:doc',
139
+ 'fa_icon': 'sbg:icon',
140
+ }
141
+
142
+ # What keys to skip from the tower.yml file
143
+ SKIP_NEXTFLOW_TOWER_KEYS = [
144
+ 'tower',
145
+ 'mail',
146
+ ]
147
+
148
+
149
+ class ExecMode(Enum):
150
+ single = 'single-instance'
151
+ multi = 'multi-instance'
152
+
153
+ def __str__(self):
154
+ return self.value
parser/nextflow.py ADDED
@@ -0,0 +1,438 @@
1
+ from wrabbit.parser.utils import (
2
+ get_config_files,
3
+ get_nf_schema,
4
+ parse_config_file,
5
+ create_profile_enum,
6
+ nf_to_sb_input_mapper,
7
+ get_tower_yml,
8
+ get_dict_depth,
9
+ get_docs_file,
10
+ )
11
+
12
+ from wrabbit.parser import (
13
+ sample_sheet,
14
+ SB_SCHEMA_DEFAULT_NAME,
15
+ EXTENSIONS,
16
+ NF_TO_CWL_CATEGORY_MAP,
17
+ GENERIC_FILE_ARRAY_INPUT,
18
+ AUX_FILES_REQUIREMENT,
19
+ INLINE_JS_REQUIREMENT,
20
+ GENERIC_NF_OUTPUT_DIRECTORY,
21
+ SAMPLE_SHEET_FILE_ARRAY_INPUT,
22
+ SAMPLE_SHEET_SWITCH,
23
+ LOAD_LISTING_REQUIREMENT,
24
+ SKIP_NEXTFLOW_TOWER_KEYS,
25
+ )
26
+
27
+ from wrabbit.specification import (
28
+ RecordType,
29
+ FileType,
30
+ DirectoryType,
31
+ ArrayType,
32
+ )
33
+
34
+ import yaml
35
+ import re
36
+ import os
37
+ import json
38
+
39
+ from wrabbit.wrapper.wrapper import SbWrapper
40
+
41
+
42
+ class NextflowParser:
43
+ def __init__(self, workflow_path, sb_doc=None):
44
+ self.sb_wrapper = SbWrapper()
45
+ self.workflow_path = workflow_path
46
+
47
+ # Locate nextflow files in the package if possible
48
+ self.nf_config_files = get_config_files(self.workflow_path) or []
49
+ self.nf_schema_path = get_nf_schema(self.workflow_path)
50
+ self.readme_path = get_docs_file(self.workflow_path)
51
+
52
+ self.sb_doc = sb_doc
53
+
54
+ def generate_sb_inputs(self):
55
+ """
56
+ Generate SB inputs schema
57
+ """
58
+
59
+ # ## Add profiles to the input ## #
60
+
61
+ profiles = dict()
62
+
63
+ for path in self.nf_config_files:
64
+ profiles.update(parse_config_file(path))
65
+
66
+ profiles_choices = sorted(list(set(profiles.keys())))
67
+
68
+ if profiles:
69
+ self.sb_wrapper.safe_add_input(
70
+ create_profile_enum(profiles_choices)
71
+ )
72
+
73
+ # Optional inputs due to profiles
74
+ # optional_inputs = []
75
+ # for profile_id, profile_contents in profiles.items():
76
+ # for key in profile_contents.keys():
77
+ # if 'params.' in key:
78
+ # input_ = key.rsplit('params.', 0)
79
+ # optional_inputs.extend(input_)
80
+ # optional_inputs = set(optional_inputs)
81
+
82
+ # ## Add inputs ## #
83
+ if self.nf_schema_path:
84
+ with open(self.nf_schema_path, 'r') as f:
85
+ nf_schema = yaml.safe_load(f)
86
+
87
+ for p_key, p_value in nf_schema.get('properties', {}).items():
88
+ self.sb_wrapper.safe_add_input(
89
+ nf_to_sb_input_mapper(p_key, p_value))
90
+ for def_name, definition in nf_schema.get(
91
+ 'definitions', {}).items():
92
+ # Nextflow inputs schema contains multiple definitions where
93
+ # each definition contains multiple properties
94
+ category = dict()
95
+
96
+ for nf_field, sb_field in NF_TO_CWL_CATEGORY_MAP.items():
97
+ if nf_field in definition:
98
+ category[sb_field] = definition[nf_field]
99
+
100
+ input_category = 'Inputs'
101
+ if 'title' in definition:
102
+ input_category = category['sbg:title']
103
+
104
+ for port_id, port_data in definition['properties'].items():
105
+ req = False
106
+ # if port_id in definition.get('required', []) and \
107
+ # port_id not in optional_inputs:
108
+ # req = True
109
+
110
+ self.sb_wrapper.safe_add_input(nf_to_sb_input_mapper(
111
+ port_id,
112
+ port_data,
113
+ category=input_category,
114
+ required=req,
115
+ ))
116
+
117
+ # Add the generic file array input - auxiliary files
118
+ self.sb_wrapper.safe_add_input(GENERIC_FILE_ARRAY_INPUT)
119
+ self.sb_wrapper.add_requirement(AUX_FILES_REQUIREMENT)
120
+ self.sb_wrapper.add_requirement(INLINE_JS_REQUIREMENT)
121
+
122
+ def generate_sb_outputs(self):
123
+ """
124
+ Generate SB output schema
125
+ """
126
+ if get_tower_yml(self.workflow_path):
127
+ for output in self.parse_output_yml(
128
+ open(get_tower_yml(self.workflow_path))
129
+ ):
130
+ self.sb_wrapper.safe_add_output(output)
131
+
132
+ # if the only output is reports, or there are no outputs, add generic
133
+ if len(self.sb_wrapper.outputs) == 0 or \
134
+ (len(self.sb_wrapper.outputs) == 1 and
135
+ self.sb_wrapper.outputs[0]['id'] == 'reports'):
136
+ self.sb_wrapper.safe_add_output(GENERIC_NF_OUTPUT_DIRECTORY)
137
+
138
+ def generate_sb_app(
139
+ self, sb_entrypoint='main.nf',
140
+ executor_version=None, sb_package_id=None, execution_mode=None,
141
+ sample_sheet_schema=None,
142
+ ):
143
+ """
144
+ Generate an SB app for a nextflow workflow, OR edit the one created and
145
+ defined by the user
146
+ """
147
+
148
+ self.sb_wrapper.cwl_version = 'None'
149
+ self.sb_wrapper.class_ = 'nextflow'
150
+
151
+ self.generate_sb_inputs()
152
+ self.generate_sb_outputs()
153
+
154
+ if sample_sheet_schema:
155
+ self.parse_sample_sheet_schema(open(sample_sheet_schema))
156
+
157
+ self.sb_wrapper.set_app_content(
158
+ code_package=sb_package_id,
159
+ entrypoint=sb_entrypoint,
160
+ executor_version=executor_version,
161
+ )
162
+
163
+ if execution_mode:
164
+ self.sb_wrapper.add_hint({
165
+ 'class': 'sbg:NextflowExecutionMode',
166
+ 'value': execution_mode
167
+ })
168
+
169
+ if self.sb_doc:
170
+ self.sb_wrapper.add_docs(self.sb_doc)
171
+ elif self.readme_path:
172
+ with open(self.readme_path, 'r') as docs:
173
+ self.sb_wrapper.add_docs(docs.read())
174
+
175
+ def parse_sample_sheet_schema(self, path):
176
+ """
177
+ Example sample sheet:
178
+ sample_sheet_input: input_sample_sheet # taken from app wrapper
179
+ sample_sheet_name: samplesheet.csv
180
+ header:
181
+ - SampleID
182
+ - Fastq1
183
+ - Fastq2
184
+ rows:
185
+ - sample_id
186
+ - path
187
+ - path
188
+ defaults:
189
+ - NA
190
+ - NA
191
+ - NA
192
+ group_by: sample_id
193
+ format_: csv
194
+
195
+ """
196
+ schema = yaml.safe_load(path)
197
+
198
+ sample_sheet_input = schema.get('sample_sheet_input')
199
+ sample_sheet_name = schema.get('sample_sheet_name', 'samplesheet')
200
+ header = schema.get('header', 'null')
201
+
202
+ # fix rows
203
+ rows = schema.get('rows')
204
+ for i, r in enumerate(rows):
205
+ if "." not in r:
206
+ if r == 'path':
207
+ n = 0
208
+ new_r = f'files[{n}].path'
209
+ while new_r in rows:
210
+ n += 1
211
+ new_r = f'files[{n}].path'
212
+ rows[i] = new_r
213
+ else:
214
+ rows[i] = f'files[0].metadata.{r}'
215
+
216
+ defaults = schema.get('defaults', 'null')
217
+
218
+ # fix group by
219
+ group_by = schema.get('group_by')
220
+ if type(group_by) is str:
221
+ group_by = [group_by]
222
+ for i, gb in enumerate(group_by):
223
+
224
+ if "." not in gb:
225
+ if gb in ['file', 'none']:
226
+ group_by[i] = 'file.path'
227
+ else:
228
+ group_by[i] = f'file.metadata.{gb}'
229
+
230
+ format_ = schema.get('format_', None)
231
+
232
+ if format_ and not sample_sheet_name.endswith(format_):
233
+ sample_sheet_name += f".{format_}".lower()
234
+
235
+ if not format_ and not sample_sheet_name.endswith(['.tsv', '.csv']):
236
+ raise Exception('Sample sheet format could not be identified. '
237
+ 'Please specify one of "tsv" or "csv" in the '
238
+ 'sample sheet schema file.')
239
+
240
+ if not format_ and sample_sheet_name.endswith(['.tsv', '.csv']):
241
+ format_ = sample_sheet_name.split('.').pop().lower()
242
+
243
+ if format_.lower() not in ['tsv', 'csv']:
244
+ raise Exception(f'Unrecognized sample sheet format "{format_}".')
245
+
246
+ # Step 1:
247
+ # add a new input to the pipeline
248
+ # - new input must not clash with other inputs by ID
249
+ # Ensure that the new input is unique
250
+
251
+ # Create the sample sheet file array input
252
+ file_input = self.sb_wrapper.safe_add_input(
253
+ SAMPLE_SHEET_FILE_ARRAY_INPUT
254
+ )
255
+ file_input_id = file_input.get('id')
256
+
257
+ # Step 2:
258
+ # add argument for sample sheet
259
+ # - requires: sample sheet input (sample_sheet_input),
260
+ # file input (ss_file_input)
261
+ # - if the sample sheet is provided on input,
262
+ # do not generate a new ss
263
+ input_changes = {
264
+ 'id': sample_sheet_input,
265
+ 'loadContents': True
266
+ }
267
+
268
+ prefix = self.sb_wrapper.get_input(
269
+ sample_sheet_input
270
+ )['inputBinding']['prefix']
271
+
272
+ self.sb_wrapper.remove_input(sample_sheet_input)
273
+ self.sb_wrapper.add_input(input_changes)
274
+ self.sb_wrapper.add_argument(
275
+ {
276
+ "prefix": prefix,
277
+ "shellQuote": False,
278
+ "valueFrom": SAMPLE_SHEET_SWITCH.format(
279
+ file_input=f"inputs.{file_input_id}",
280
+ sample_sheet=f"inputs.{sample_sheet_input}",
281
+ sample_sheet_name=sample_sheet_name,
282
+ )
283
+ }
284
+ )
285
+
286
+ # Step 3:
287
+ # add file requirement
288
+ # - requires: sample sheet schema
289
+ # - add InitialWorkDirRequirement if there are none
290
+ # - if there are, append the entry to listing
291
+ ss = sample_sheet(
292
+ file_name=sample_sheet_name,
293
+ sample_sheet_input=f"inputs.{sample_sheet_input}",
294
+ format_=format_,
295
+ input_source=f"inputs.{file_input_id}",
296
+ header=header,
297
+ rows=rows,
298
+ defaults=defaults,
299
+ group_by=group_by,
300
+ )
301
+
302
+ self.sb_wrapper.add_requirement(ss)
303
+ self.sb_wrapper.add_requirement(INLINE_JS_REQUIREMENT)
304
+ self.sb_wrapper.add_requirement(LOAD_LISTING_REQUIREMENT)
305
+
306
+ def make_output_type(self, key, output_dict, is_record=False):
307
+ """
308
+ This creates an output of specific type based on information provided
309
+ through output_dict.
310
+
311
+ :param key:
312
+ :param output_dict:
313
+ :param is_record:
314
+ :return:
315
+ """
316
+
317
+ converted_cwl_output = dict()
318
+
319
+ file_pattern = re.compile(r'.*\.(\w+)$')
320
+ folder_pattern = re.compile(r'[^.]+$')
321
+ id_key = 'id'
322
+
323
+ if is_record:
324
+ id_key = 'name'
325
+
326
+ name = key
327
+ if 'display' in output_dict:
328
+ name = output_dict['display']
329
+
330
+ clean_id = re.sub(r'[^a-zA-Z0-9_]', "", name.replace(
331
+ " ", "_")).lower()
332
+
333
+ # Case 1: Output is a Record-type
334
+ if get_dict_depth(output_dict) > 0:
335
+ # this is a record, go through the dict_ recursively
336
+ fields = [self.make_output_type(key, val, is_record=True)
337
+ for key, val in output_dict.items()]
338
+
339
+ used_field_ids = set()
340
+
341
+ for field in fields:
342
+ base_field_id = field.get('name', 'Output')
343
+
344
+ # Since name fields can be the same for multiple inputs,
345
+ # correct the name if it has already been used.
346
+ chk_id = base_field_id
347
+ i = 1
348
+ if chk_id in used_field_ids:
349
+ chk_id = f"{base_field_id}_{i}"
350
+ i += 1
351
+ used_field_ids.add(chk_id)
352
+
353
+ field['name'] = chk_id
354
+
355
+ converted_cwl_output = {
356
+ id_key: clean_id,
357
+ "label": name,
358
+ "type": RecordType(fields=fields, name=clean_id, optional=True)
359
+ }
360
+
361
+ # Case 2: Output is a File type
362
+ elif re.fullmatch(file_pattern, key):
363
+ # create a list of files output
364
+ converted_cwl_output = {
365
+ id_key: clean_id,
366
+ "label": name,
367
+ "type": ArrayType(items=[FileType()], optional=True),
368
+ "outputBinding": {
369
+ "glob": key
370
+ }
371
+ }
372
+
373
+ # Case 3: Output is a folder type
374
+ elif re.fullmatch(folder_pattern, key):
375
+ # create a list of directories output
376
+ converted_cwl_output = {
377
+ id_key: clean_id,
378
+ "label": name,
379
+ "type": DirectoryType(optional=True),
380
+ "outputBinding": {
381
+ "glob": key,
382
+ "loadListing": "deep_listing"
383
+ }
384
+ }
385
+ return converted_cwl_output
386
+
387
+ def parse_output_yml(self, yml_file):
388
+ """
389
+ Extracts output information from a YAML file, usually in tower.yml
390
+ format.
391
+
392
+ :param yml_file: path to YAML file.
393
+ :return: list of outputs in CWL format.
394
+ """
395
+ outputs = list()
396
+ yml_schema = yaml.safe_load(yml_file)
397
+
398
+ for key, value in yml_schema.items():
399
+ # Tower yml file can use "tower" key in the yml file to designate
400
+ # some configurations tower uses. Since these are not output
401
+ # definitions, we skip these.
402
+ if key in SKIP_NEXTFLOW_TOWER_KEYS:
403
+ continue
404
+
405
+ outputs.append(
406
+ self.make_output_type(key, value)
407
+ )
408
+
409
+ return outputs
410
+
411
+ def dump_sb_wrapper(self, out_format=EXTENSIONS.yaml):
412
+ """
413
+ Dump SB wrapper for nextflow workflow to a file
414
+ """
415
+ print('Writing sb nextflow schema file...')
416
+ basename = SB_SCHEMA_DEFAULT_NAME
417
+ counter = 0
418
+ sb_wrapper_path = os.path.join(
419
+ self.workflow_path,
420
+ f'{basename}.{out_format}'
421
+ )
422
+
423
+ while os.path.exists(sb_wrapper_path):
424
+ counter += 1
425
+ sb_wrapper_path = os.path.join(
426
+ self.workflow_path,
427
+ f'{basename}.{counter}.{out_format}'
428
+ )
429
+
430
+ print(f"Schema written to file <{sb_wrapper_path}>")
431
+
432
+ if out_format in EXTENSIONS.yaml_all:
433
+ with open(sb_wrapper_path, 'w') as f:
434
+ yaml.dump(self.sb_wrapper.dump(), f, indent=4, sort_keys=True)
435
+ elif out_format in EXTENSIONS.json_all:
436
+ with open(sb_wrapper_path, 'w') as f:
437
+ json.dump(self.sb_wrapper.dump(), f, indent=4, sort_keys=True)
438
+