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.
- parser/__init__.py +9 -0
- parser/constants.py +154 -0
- parser/nextflow.py +438 -0
- parser/utils.py +330 -0
- sb_types/__init__.py +35 -0
- sb_types/port.py +2 -0
- sb_types/requirements.py +2 -0
- sb_types/sb_types.py +116 -0
- wrabbit/__init__.py +0 -0
- wrabbit/exceptions.py +64 -0
- wrabbit/parser/__init__.py +9 -0
- wrabbit/parser/constants.py +173 -0
- wrabbit/parser/js_templates/sample_sheet_generator.js +133 -0
- wrabbit/parser/js_templates/sample_sheet_switch.js +9 -0
- wrabbit/parser/nextflow.py +522 -0
- wrabbit/parser/utils.py +389 -0
- wrabbit/sb_types/__init__.py +36 -0
- wrabbit/sb_types/port.py +2 -0
- wrabbit/sb_types/requirements.py +2 -0
- wrabbit/sb_types/sb_types.py +116 -0
- wrabbit/specification/__init__.py +70 -0
- wrabbit/specification/binding.py +34 -0
- wrabbit/specification/hints.py +40 -0
- wrabbit/specification/listing.py +37 -0
- wrabbit/specification/node.py +444 -0
- wrabbit/specification/node_types.py +223 -0
- wrabbit/specification/port.py +106 -0
- wrabbit/specification/requirements.py +63 -0
- wrabbit/specification/sbg.py +33 -0
- wrabbit/specification/utils.py +0 -0
- wrabbit/version.py +1 -0
- wrabbit/wrapper/__init__.py +0 -0
- wrabbit/wrapper/constants.py +0 -0
- wrabbit/wrapper/utils.py +17 -0
- wrabbit/wrapper/wrapper.py +300 -0
- wrabbit-0.1.0.dist-info/LICENSE +201 -0
- wrabbit-0.1.0.dist-info/METADATA +58 -0
- wrabbit-0.1.0.dist-info/RECORD +44 -0
- wrabbit-0.1.0.dist-info/WHEEL +5 -0
- wrabbit-0.1.0.dist-info/top_level.txt +1 -0
- wrapper/__init__.py +0 -0
- wrapper/constants.py +0 -0
- wrapper/utils.py +0 -0
- wrapper/wrapper.py +235 -0
parser/__init__.py
ADDED
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
|
+
|