eva-sub-cli 0.4.4__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.
- eva_sub_cli/__init__.py +27 -0
- eva_sub_cli/_version.py +21 -0
- eva_sub_cli/auth.py +106 -0
- eva_sub_cli/date_utils.py +32 -0
- eva_sub_cli/etc/EVA_Submission_template.xlsx +0 -0
- eva_sub_cli/etc/eva-biosamples.json +174 -0
- eva_sub_cli/etc/eva_logo.png +0 -0
- eva_sub_cli/etc/eva_schema.json +369 -0
- eva_sub_cli/etc/spreadsheet2json_conf.yaml +135 -0
- eva_sub_cli/exceptions/__init__.py +0 -0
- eva_sub_cli/exceptions/invalid_file_type_exception.py +4 -0
- eva_sub_cli/exceptions/submission_not_found_exception.py +4 -0
- eva_sub_cli/exceptions/submission_status_exception.py +5 -0
- eva_sub_cli/executables/__init__.py +0 -0
- eva_sub_cli/executables/check_fasta_insdc.py +159 -0
- eva_sub_cli/executables/check_metadata_semantics.py +18 -0
- eva_sub_cli/executables/cli.py +115 -0
- eva_sub_cli/executables/samples_checker.py +144 -0
- eva_sub_cli/executables/trim_down.py +78 -0
- eva_sub_cli/executables/xlsx2json.py +335 -0
- eva_sub_cli/file_utils.py +117 -0
- eva_sub_cli/jinja_templates/fasta_check.html +86 -0
- eva_sub_cli/jinja_templates/file_validation.html +93 -0
- eva_sub_cli/jinja_templates/html_report.html +142 -0
- eva_sub_cli/jinja_templates/metadata_validation.html +52 -0
- eva_sub_cli/jinja_templates/project_details.html +37 -0
- eva_sub_cli/jinja_templates/sample_name_check.html +60 -0
- eva_sub_cli/jinja_templates/shallow_validation.html +29 -0
- eva_sub_cli/metadata_utils.py +46 -0
- eva_sub_cli/nextflow/validation.nf +303 -0
- eva_sub_cli/orchestrator.py +233 -0
- eva_sub_cli/report.py +40 -0
- eva_sub_cli/semantic_metadata.py +243 -0
- eva_sub_cli/submission_ws.py +61 -0
- eva_sub_cli/submit.py +111 -0
- eva_sub_cli/validators/__init__.py +0 -0
- eva_sub_cli/validators/docker_validator.py +205 -0
- eva_sub_cli/validators/native_validator.py +68 -0
- eva_sub_cli/validators/validation_results_parsers.py +210 -0
- eva_sub_cli/validators/validator.py +510 -0
- eva_sub_cli-0.4.4.dist-info/METADATA +164 -0
- eva_sub_cli-0.4.4.dist-info/RECORD +46 -0
- eva_sub_cli-0.4.4.dist-info/WHEEL +5 -0
- eva_sub_cli-0.4.4.dist-info/entry_points.txt +7 -0
- eva_sub_cli-0.4.4.dist-info/licenses/LICENSE +201 -0
- eva_sub_cli-0.4.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
import csv
|
|
3
|
+
import datetime
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
from functools import lru_cache, cached_property
|
|
8
|
+
|
|
9
|
+
import yaml
|
|
10
|
+
from ebi_eva_common_pyutils.command_utils import run_command_with_output
|
|
11
|
+
from ebi_eva_common_pyutils.config import WritableConfig
|
|
12
|
+
|
|
13
|
+
from eva_sub_cli import ETC_DIR, SUB_CLI_CONFIG_FILE, __version__
|
|
14
|
+
from eva_sub_cli.file_utils import backup_file_or_directory, resolve_single_file_path
|
|
15
|
+
from eva_sub_cli.report import generate_html_report
|
|
16
|
+
from ebi_eva_common_pyutils.logger import logging_config, AppLogger
|
|
17
|
+
|
|
18
|
+
from eva_sub_cli.validators.validation_results_parsers import parse_assembly_check_log, parse_assembly_check_report, \
|
|
19
|
+
parse_biovalidator_validation_results, convert_metadata_sheet, convert_metadata_row, convert_metadata_attribute, \
|
|
20
|
+
parse_vcf_check_report, parse_metadata_property
|
|
21
|
+
|
|
22
|
+
VALIDATION_OUTPUT_DIR = "validation_output"
|
|
23
|
+
VALIDATION_RESULTS_KEY = 'validation_results'
|
|
24
|
+
VALIDATION_RESULTS_FILE = 'validation_results.yaml'
|
|
25
|
+
READY_FOR_SUBMISSION_TO_EVA = 'ready_for_submission_to_eva'
|
|
26
|
+
|
|
27
|
+
logger = logging_config.get_logger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Validator(AppLogger):
|
|
31
|
+
|
|
32
|
+
def __init__(self, mapping_file, submission_dir, project_title=None, metadata_json=None, metadata_xlsx=None,
|
|
33
|
+
shallow_validation=False, submission_config: WritableConfig = None):
|
|
34
|
+
# validator write to the validation output directory
|
|
35
|
+
# If the submission_config is not set it will also be written to the VALIDATION_OUTPUT_DIR
|
|
36
|
+
self.submission_dir = submission_dir
|
|
37
|
+
self.output_dir = os.path.join(submission_dir, VALIDATION_OUTPUT_DIR)
|
|
38
|
+
self.validation_result_file = os.path.join(submission_dir, VALIDATION_RESULTS_FILE)
|
|
39
|
+
self.mapping_file = mapping_file
|
|
40
|
+
vcf_files, fasta_files = self._find_vcf_and_fasta_files()
|
|
41
|
+
self.vcf_files = vcf_files
|
|
42
|
+
self.fasta_files = fasta_files
|
|
43
|
+
self.results = {'shallow_validation': {'requested': shallow_validation}}
|
|
44
|
+
self.project_title = project_title
|
|
45
|
+
self.validation_date = datetime.datetime.now()
|
|
46
|
+
self.metadata_json = metadata_json
|
|
47
|
+
self.metadata_xlsx = metadata_xlsx
|
|
48
|
+
self.shallow_validation = shallow_validation
|
|
49
|
+
if submission_config:
|
|
50
|
+
self.sub_config = submission_config
|
|
51
|
+
else:
|
|
52
|
+
config_file = os.path.join(submission_dir, SUB_CLI_CONFIG_FILE)
|
|
53
|
+
self.sub_config = WritableConfig(config_file, version=__version__)
|
|
54
|
+
|
|
55
|
+
def __enter__(self):
|
|
56
|
+
return self
|
|
57
|
+
|
|
58
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
59
|
+
self.sub_config.backup()
|
|
60
|
+
self.sub_config.write()
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def metadata_json_post_validation(self):
|
|
64
|
+
if self.metadata_json:
|
|
65
|
+
return self.metadata_json
|
|
66
|
+
return resolve_single_file_path(os.path.join(self.output_dir, 'metadata.json'))
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
def _run_quiet_command(command_description, command, **kwargs):
|
|
70
|
+
return run_command_with_output(command_description, command, stdout_log_level=logging.DEBUG,
|
|
71
|
+
stderr_log_level=logging.DEBUG, **kwargs)
|
|
72
|
+
|
|
73
|
+
def _find_vcf_and_fasta_files(self):
|
|
74
|
+
vcf_files = []
|
|
75
|
+
fasta_files = []
|
|
76
|
+
with open(self.mapping_file) as open_file:
|
|
77
|
+
reader = csv.DictReader(open_file, delimiter=',')
|
|
78
|
+
for row in reader:
|
|
79
|
+
vcf_files.append(row['vcf'])
|
|
80
|
+
fasta_files.append(row['fasta'])
|
|
81
|
+
return vcf_files, fasta_files
|
|
82
|
+
|
|
83
|
+
def validate_and_report(self):
|
|
84
|
+
self.info('Start validation')
|
|
85
|
+
self.validate()
|
|
86
|
+
self.info('Create report')
|
|
87
|
+
self.report()
|
|
88
|
+
|
|
89
|
+
def validate(self):
|
|
90
|
+
self.set_up_output_dir()
|
|
91
|
+
self.verify_files_present()
|
|
92
|
+
self._validate()
|
|
93
|
+
self.clean_up_output_dir()
|
|
94
|
+
self._collect_validation_workflow_results()
|
|
95
|
+
self._assess_validation_results()
|
|
96
|
+
self._save_validation_results()
|
|
97
|
+
|
|
98
|
+
def report(self):
|
|
99
|
+
self.create_reports()
|
|
100
|
+
|
|
101
|
+
def _validate(self):
|
|
102
|
+
raise NotImplementedError
|
|
103
|
+
|
|
104
|
+
def set_up_output_dir(self):
|
|
105
|
+
if os.path.exists(self.output_dir):
|
|
106
|
+
backup_file_or_directory(self.output_dir, max_backups=9)
|
|
107
|
+
os.makedirs(self.output_dir, exist_ok=True)
|
|
108
|
+
|
|
109
|
+
def clean_up_output_dir(self):
|
|
110
|
+
# Move intermediate validation outputs into a subdir except metadata.json
|
|
111
|
+
subdir = os.path.join(self.output_dir, 'other_validations')
|
|
112
|
+
os.mkdir(subdir)
|
|
113
|
+
for file_name in os.listdir(self.output_dir):
|
|
114
|
+
if file_name == 'metadata.json':
|
|
115
|
+
continue
|
|
116
|
+
file_path = os.path.join(self.output_dir, file_name)
|
|
117
|
+
if os.path.isfile(file_path):
|
|
118
|
+
os.rename(file_path, os.path.join(subdir, file_name))
|
|
119
|
+
|
|
120
|
+
@staticmethod
|
|
121
|
+
def _validation_file_path_for(file_path):
|
|
122
|
+
return file_path
|
|
123
|
+
|
|
124
|
+
def verify_files_present(self):
|
|
125
|
+
# verify mapping file exists
|
|
126
|
+
if not os.path.exists(self.mapping_file):
|
|
127
|
+
raise FileNotFoundError(f'Mapping file {self.mapping_file} not found')
|
|
128
|
+
|
|
129
|
+
# verify all files mentioned in metadata files exist
|
|
130
|
+
files_missing, missing_files_list = self.check_if_file_missing()
|
|
131
|
+
if files_missing:
|
|
132
|
+
raise FileNotFoundError(f"some files (vcf/fasta) mentioned in metadata file could not be found. "
|
|
133
|
+
f"Missing files list {missing_files_list}")
|
|
134
|
+
|
|
135
|
+
def check_if_file_missing(self):
|
|
136
|
+
files_missing = False
|
|
137
|
+
missing_files_list = []
|
|
138
|
+
with open(self.mapping_file) as open_file:
|
|
139
|
+
reader = csv.DictReader(open_file, delimiter=',')
|
|
140
|
+
for row in reader:
|
|
141
|
+
if not os.path.exists(row['vcf']):
|
|
142
|
+
files_missing = True
|
|
143
|
+
missing_files_list.append(row['vcf'])
|
|
144
|
+
if not os.path.exists(row['fasta']):
|
|
145
|
+
files_missing = True
|
|
146
|
+
missing_files_list.append(row['fasta'])
|
|
147
|
+
# Assembly report is optional but should exist if it is set.
|
|
148
|
+
if row.get('report') and not os.path.exists(row['report']):
|
|
149
|
+
files_missing = True
|
|
150
|
+
missing_files_list.append(row['report'])
|
|
151
|
+
return files_missing, missing_files_list
|
|
152
|
+
|
|
153
|
+
def verify_ready_for_submission_to_eva(self):
|
|
154
|
+
""" Checks if all the validation are passed """
|
|
155
|
+
return all((
|
|
156
|
+
all((value.get('pass', False) is True for key, value in self.results.items() if
|
|
157
|
+
key in ['vcf_check', 'assembly_check', 'fasta_check', 'sample_check', 'metadata_check'])),
|
|
158
|
+
any((
|
|
159
|
+
self.results['shallow_validation']['requested'] is False,
|
|
160
|
+
self.results['shallow_validation'].get('required', True) is False
|
|
161
|
+
))
|
|
162
|
+
))
|
|
163
|
+
|
|
164
|
+
def _collect_validation_workflow_results(self):
|
|
165
|
+
# Collect information from the output and summarise in the config
|
|
166
|
+
if self.shallow_validation:
|
|
167
|
+
self._collect_trim_down_metrics()
|
|
168
|
+
self._collect_vcf_check_results()
|
|
169
|
+
self._collect_assembly_check_results()
|
|
170
|
+
self._load_sample_check_results()
|
|
171
|
+
self._load_fasta_check_results()
|
|
172
|
+
self._collect_metadata_results()
|
|
173
|
+
|
|
174
|
+
def _assess_validation_results(self):
|
|
175
|
+
"""
|
|
176
|
+
Assess if the validation results are meeting expectations and marks them as "PASS: true" or "PASS: false"
|
|
177
|
+
It assumes all validation have been parsed already.
|
|
178
|
+
"""
|
|
179
|
+
# vcf_check result
|
|
180
|
+
vcf_check_result = all((vcf_check.get('critical_count', 1) == 0
|
|
181
|
+
for vcf_name, vcf_check in self.results.get('vcf_check', {}).items()))
|
|
182
|
+
self.results['vcf_check']['pass'] = vcf_check_result
|
|
183
|
+
|
|
184
|
+
# assembly_check result
|
|
185
|
+
asm_nb_mismatch_result = all((asm_check.get('nb_mismatch', 1) == 0
|
|
186
|
+
for vcf_name, asm_check in self.results.get('assembly_check', {}).items()))
|
|
187
|
+
asm_nb_error_result = all((asm_check.get('nb_error', 1) == 0
|
|
188
|
+
for vcf_name, asm_check in self.results.get('assembly_check', {}).items()))
|
|
189
|
+
self.results['assembly_check']['pass'] = asm_nb_mismatch_result and asm_nb_error_result
|
|
190
|
+
|
|
191
|
+
# fasta_check result
|
|
192
|
+
fasta_check_result = all((fa_file_check.get('all_insdc', False) is True
|
|
193
|
+
for fa_file, fa_file_check in self.results.get('fasta_check', {}).items()))
|
|
194
|
+
self.results['fasta_check']['pass'] = fasta_check_result
|
|
195
|
+
|
|
196
|
+
# sample check result
|
|
197
|
+
self.results['sample_check']['pass'] = self.results.get('sample_check', {}).get('overall_differences',
|
|
198
|
+
True) is False
|
|
199
|
+
|
|
200
|
+
# metadata check result
|
|
201
|
+
metadata_xlsx_result = len(self.results.get('metadata_check', {}).get('spreadsheet_errors', []) or []) == 0
|
|
202
|
+
metadata_json_result = len(self.results.get('metadata_check', {}).get('json_errors', []) or []) == 0
|
|
203
|
+
self.results['metadata_check']['pass'] = metadata_xlsx_result and metadata_json_result
|
|
204
|
+
|
|
205
|
+
# update config based on the validation results
|
|
206
|
+
self.sub_config.set(READY_FOR_SUBMISSION_TO_EVA, value=self.verify_ready_for_submission_to_eva())
|
|
207
|
+
|
|
208
|
+
def _save_validation_results(self):
|
|
209
|
+
with open(self.validation_result_file, 'w') as val_res_file:
|
|
210
|
+
yaml.safe_dump(self.results, val_res_file)
|
|
211
|
+
|
|
212
|
+
self.debug(f"saved validation result in {self.validation_result_file}")
|
|
213
|
+
|
|
214
|
+
@lru_cache
|
|
215
|
+
def _vcf_check_log(self, vcf_name):
|
|
216
|
+
return resolve_single_file_path(
|
|
217
|
+
os.path.join(self.output_dir, 'vcf_format', vcf_name + '.vcf_format.log')
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
@lru_cache
|
|
221
|
+
def _vcf_check_text_report(self, vcf_name):
|
|
222
|
+
return resolve_single_file_path(
|
|
223
|
+
os.path.join(self.output_dir, 'vcf_format', vcf_name + '.*.txt')
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
@lru_cache
|
|
227
|
+
def _vcf_check_db_report(self, vcf_name):
|
|
228
|
+
return resolve_single_file_path(
|
|
229
|
+
os.path.join(self.output_dir, 'vcf_format', vcf_name + '.*.db')
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
@lru_cache
|
|
233
|
+
def _assembly_check_log(self, vcf_name):
|
|
234
|
+
return resolve_single_file_path(
|
|
235
|
+
os.path.join(self.output_dir, 'assembly_check', vcf_name + '.assembly_check.log')
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
@lru_cache
|
|
239
|
+
def _assembly_check_text_report(self, vcf_name):
|
|
240
|
+
return resolve_single_file_path(
|
|
241
|
+
os.path.join(self.output_dir, 'assembly_check', vcf_name + '*text_assembly_report*')
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
@cached_property
|
|
245
|
+
def _sample_check_yaml(self):
|
|
246
|
+
return resolve_single_file_path(os.path.join(self.output_dir, 'other_validations', 'sample_checker.yml'))
|
|
247
|
+
|
|
248
|
+
def _collect_vcf_check_results(self,):
|
|
249
|
+
# detect output files for vcf check
|
|
250
|
+
self.results['vcf_check'] = {}
|
|
251
|
+
for vcf_file in self.vcf_files:
|
|
252
|
+
vcf_name = os.path.basename(vcf_file)
|
|
253
|
+
|
|
254
|
+
vcf_check_log = self._vcf_check_log(vcf_name)
|
|
255
|
+
vcf_check_text_report = self._vcf_check_text_report(vcf_name)
|
|
256
|
+
|
|
257
|
+
if vcf_check_log and vcf_check_text_report:
|
|
258
|
+
valid, warning_count, error_count, critical_count, error_list, critical_list = parse_vcf_check_report(vcf_check_text_report)
|
|
259
|
+
else:
|
|
260
|
+
valid, warning_count, error_count, critical_count, error_list, critical_list = (False, 0, 0, 1, [], ['Process failed'])
|
|
261
|
+
self.results['vcf_check'][vcf_name] = {
|
|
262
|
+
'report_path': vcf_check_text_report,
|
|
263
|
+
'valid': valid,
|
|
264
|
+
'error_list': error_list,
|
|
265
|
+
'error_count': error_count,
|
|
266
|
+
'warning_count': warning_count,
|
|
267
|
+
'critical_count': critical_count,
|
|
268
|
+
'critical_list': critical_list
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
def _collect_assembly_check_results(self):
|
|
272
|
+
# detect output files for assembly check
|
|
273
|
+
self.results['assembly_check'] = {}
|
|
274
|
+
for vcf_file in self.vcf_files:
|
|
275
|
+
vcf_name = os.path.basename(vcf_file)
|
|
276
|
+
|
|
277
|
+
assembly_check_log = self._assembly_check_log(vcf_name)
|
|
278
|
+
assembly_check_text_report = self._assembly_check_text_report(vcf_name)
|
|
279
|
+
|
|
280
|
+
if assembly_check_log and assembly_check_text_report:
|
|
281
|
+
error_list_from_log, nb_error_from_log, match, total = \
|
|
282
|
+
parse_assembly_check_log(assembly_check_log)
|
|
283
|
+
mismatch_list, nb_mismatch, error_list_from_report, nb_error_from_report = \
|
|
284
|
+
parse_assembly_check_report(assembly_check_text_report)
|
|
285
|
+
nb_error = nb_error_from_log + nb_error_from_report
|
|
286
|
+
error_list = error_list_from_log + error_list_from_report
|
|
287
|
+
else:
|
|
288
|
+
error_list, mismatch_list, nb_mismatch, nb_error, match, total = (['Process failed'], [], 0, 1, 0, 0)
|
|
289
|
+
self.results['assembly_check'][vcf_name] = {
|
|
290
|
+
'report_path': assembly_check_text_report,
|
|
291
|
+
'error_list': error_list,
|
|
292
|
+
'mismatch_list': mismatch_list,
|
|
293
|
+
'nb_mismatch': nb_mismatch,
|
|
294
|
+
'nb_error': nb_error,
|
|
295
|
+
'match': match,
|
|
296
|
+
'total': total
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
def _load_fasta_check_results(self):
|
|
300
|
+
for fasta_file in self.fasta_files:
|
|
301
|
+
fasta_file_name = os.path.basename(fasta_file)
|
|
302
|
+
fasta_check = resolve_single_file_path(os.path.join(self.output_dir, 'other_validations',
|
|
303
|
+
f'{fasta_file_name}_check.yml'))
|
|
304
|
+
self.results['fasta_check'] = {}
|
|
305
|
+
if not fasta_check:
|
|
306
|
+
continue
|
|
307
|
+
with open(fasta_check) as open_yaml:
|
|
308
|
+
self.results['fasta_check'][fasta_file_name] = yaml.safe_load(open_yaml)
|
|
309
|
+
|
|
310
|
+
def _load_sample_check_results(self):
|
|
311
|
+
self.results['sample_check'] = {}
|
|
312
|
+
if not self._sample_check_yaml:
|
|
313
|
+
return
|
|
314
|
+
with open(self._sample_check_yaml) as open_yaml:
|
|
315
|
+
self.results['sample_check'] = yaml.safe_load(open_yaml)
|
|
316
|
+
self.results['sample_check']['report_path'] = self._sample_check_yaml
|
|
317
|
+
|
|
318
|
+
def _collect_metadata_results(self):
|
|
319
|
+
self.results['metadata_check'] = {}
|
|
320
|
+
self._load_spreadsheet_conversion_errors()
|
|
321
|
+
self.collect_biovalidator_validation_results()
|
|
322
|
+
self._collect_semantic_metadata_results()
|
|
323
|
+
if self.metadata_xlsx:
|
|
324
|
+
self._convert_biovalidator_validation_to_spreadsheet()
|
|
325
|
+
self._write_spreadsheet_validation_results()
|
|
326
|
+
self._collect_file_info_to_metadata()
|
|
327
|
+
|
|
328
|
+
def _load_spreadsheet_conversion_errors(self):
|
|
329
|
+
errors_file = resolve_single_file_path(os.path.join(self.output_dir, 'other_validations',
|
|
330
|
+
'metadata_conversion_errors.yml'))
|
|
331
|
+
if not errors_file:
|
|
332
|
+
return
|
|
333
|
+
with open(errors_file) as open_yaml:
|
|
334
|
+
self.results['metadata_check']['spreadsheet_errors'] = yaml.safe_load(open_yaml)
|
|
335
|
+
|
|
336
|
+
def collect_biovalidator_validation_results(self):
|
|
337
|
+
"""
|
|
338
|
+
Read the biovalidator's report and extract the list of validation errors
|
|
339
|
+
"""
|
|
340
|
+
metadata_check_file = resolve_single_file_path(os.path.join(self.output_dir, 'other_validations',
|
|
341
|
+
'metadata_validation.txt'))
|
|
342
|
+
errors = parse_biovalidator_validation_results(metadata_check_file)
|
|
343
|
+
self.results['metadata_check'].update({
|
|
344
|
+
'json_report_path': metadata_check_file,
|
|
345
|
+
'json_errors': errors
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
def _collect_semantic_metadata_results(self):
|
|
349
|
+
errors_file = resolve_single_file_path(os.path.join(self.output_dir, 'other_validations',
|
|
350
|
+
'metadata_semantic_check.yml'))
|
|
351
|
+
if not errors_file:
|
|
352
|
+
return
|
|
353
|
+
with open(errors_file) as open_yaml:
|
|
354
|
+
# errors is a list of dicts matching format of biovalidator errors
|
|
355
|
+
errors = yaml.safe_load(open_yaml)
|
|
356
|
+
# biovalidator error parsing always places a list here, even if no errors
|
|
357
|
+
self.results['metadata_check']['json_errors'] += errors
|
|
358
|
+
|
|
359
|
+
def _convert_biovalidator_validation_to_spreadsheet(self):
|
|
360
|
+
config_file = os.path.join(ETC_DIR, "spreadsheet2json_conf.yaml")
|
|
361
|
+
with open(config_file) as open_file:
|
|
362
|
+
xls2json_conf = yaml.safe_load(open_file)
|
|
363
|
+
|
|
364
|
+
if 'spreadsheet_errors' not in self.results['metadata_check']:
|
|
365
|
+
self.results['metadata_check']['spreadsheet_errors'] = []
|
|
366
|
+
for error in self.results['metadata_check'].get('json_errors', {}):
|
|
367
|
+
sheet_json, row_json, attribute_json = parse_metadata_property(error['property'])
|
|
368
|
+
# There should only be one Project but adding the row back means it's easier for users to find
|
|
369
|
+
if sheet_json == 'project' and row_json is None:
|
|
370
|
+
row_json = 0
|
|
371
|
+
sheet = convert_metadata_sheet(sheet_json, xls2json_conf)
|
|
372
|
+
row = convert_metadata_row(sheet, row_json, xls2json_conf)
|
|
373
|
+
column = convert_metadata_attribute(sheet, attribute_json, xls2json_conf)
|
|
374
|
+
if row_json is None and attribute_json is None and sheet is not None:
|
|
375
|
+
new_description = f'Sheet "{sheet}" is missing'
|
|
376
|
+
elif row_json is None:
|
|
377
|
+
if 'have required' not in error['description']:
|
|
378
|
+
new_description = error['description']
|
|
379
|
+
else:
|
|
380
|
+
new_description = f'Column "{column}" is not populated'
|
|
381
|
+
elif attribute_json and column:
|
|
382
|
+
if 'have required' not in error['description']:
|
|
383
|
+
new_description = error['description']
|
|
384
|
+
else:
|
|
385
|
+
new_description = f'Column "{column}" is not populated'
|
|
386
|
+
else:
|
|
387
|
+
new_description = error["description"].replace(sheet_json, sheet)
|
|
388
|
+
if column is None:
|
|
389
|
+
# We do not know this attribute.
|
|
390
|
+
continue
|
|
391
|
+
if 'schema' in new_description:
|
|
392
|
+
# This is an error specific to json schema
|
|
393
|
+
continue
|
|
394
|
+
self.results['metadata_check']['spreadsheet_errors'].append({
|
|
395
|
+
'sheet': sheet, 'row': row, 'column': column,
|
|
396
|
+
'description': new_description
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
def _write_spreadsheet_validation_results(self):
|
|
400
|
+
if ('spreadsheet_errors' in self.results['metadata_check']
|
|
401
|
+
and 'json_report_path' in self.results['metadata_check']):
|
|
402
|
+
spreadsheet_report_file = os.path.join(os.path.dirname(self.results['metadata_check']['json_report_path']),
|
|
403
|
+
'metadata_spreadsheet_validation.txt')
|
|
404
|
+
with open(spreadsheet_report_file, 'w') as open_file:
|
|
405
|
+
for error_dict in self.results['metadata_check']['spreadsheet_errors']:
|
|
406
|
+
open_file.write(error_dict.get('description') + '\n')
|
|
407
|
+
self.results['metadata_check']['spreadsheet_report_path'] = spreadsheet_report_file
|
|
408
|
+
|
|
409
|
+
def _collect_file_info_to_metadata(self):
|
|
410
|
+
md5sum_file = resolve_single_file_path(os.path.join(self.output_dir, 'other_validations', 'file_info.txt'))
|
|
411
|
+
file_path_2_md5 = {}
|
|
412
|
+
file_name_2_md5 = {}
|
|
413
|
+
file_path_2_file_size = {}
|
|
414
|
+
file_name_2_file_size = {}
|
|
415
|
+
if md5sum_file:
|
|
416
|
+
with open(md5sum_file) as open_file:
|
|
417
|
+
for line in open_file:
|
|
418
|
+
sp_line = line.split(' ')
|
|
419
|
+
md5sum = sp_line[0]
|
|
420
|
+
file_size = int(sp_line[1])
|
|
421
|
+
vcf_file = sp_line[2].strip()
|
|
422
|
+
file_path_2_md5[vcf_file] = md5sum
|
|
423
|
+
file_name_2_md5[os.path.basename(vcf_file)] = md5sum
|
|
424
|
+
file_path_2_file_size[vcf_file] = file_size
|
|
425
|
+
file_name_2_file_size[os.path.basename(vcf_file)] = file_size
|
|
426
|
+
else:
|
|
427
|
+
self.error(
|
|
428
|
+
f"Cannot locate file_info.txt at {os.path.join(self.output_dir, 'other_validations', 'file_info.txt')}"
|
|
429
|
+
)
|
|
430
|
+
if self.metadata_json_post_validation:
|
|
431
|
+
with open(self.metadata_json_post_validation) as open_file:
|
|
432
|
+
try:
|
|
433
|
+
json_data = json.load(open_file)
|
|
434
|
+
file_rows = []
|
|
435
|
+
files_from_metadata = json_data.get('files', [])
|
|
436
|
+
if files_from_metadata:
|
|
437
|
+
for file_dict in json_data.get('files', []):
|
|
438
|
+
file_path = self._validation_file_path_for(file_dict.get('fileName'))
|
|
439
|
+
file_dict['md5'] = file_path_2_md5.get(file_path) or \
|
|
440
|
+
file_name_2_md5.get(file_dict.get('fileName')) or ''
|
|
441
|
+
file_dict['fileSize'] = file_path_2_file_size.get(file_path) or \
|
|
442
|
+
file_name_2_file_size.get(file_dict.get('fileName')) or ''
|
|
443
|
+
file_rows.append(file_dict)
|
|
444
|
+
else:
|
|
445
|
+
self.error('No file found in metadata and multiple analysis alias exist: '
|
|
446
|
+
'cannot infer the relationship between files and analysis alias')
|
|
447
|
+
json_data['files'] = file_rows
|
|
448
|
+
except Exception as e:
|
|
449
|
+
# Skip adding the md5
|
|
450
|
+
self.error('Error while loading or parsing metadata json: ' + str(e))
|
|
451
|
+
if json_data:
|
|
452
|
+
with open(self.metadata_json_post_validation, 'w') as open_file:
|
|
453
|
+
json.dump(json_data, open_file)
|
|
454
|
+
else:
|
|
455
|
+
self.error(f'Cannot locate the metadata in JSON format in {os.path.join(self.output_dir, "metadata.json")}')
|
|
456
|
+
|
|
457
|
+
def _collect_trim_down_metrics(self):
|
|
458
|
+
self.results['shallow_validation']['metrics'] = {}
|
|
459
|
+
shallow_validation_required = False
|
|
460
|
+
for vcf_file in self.vcf_files:
|
|
461
|
+
basename = os.path.basename(vcf_file)
|
|
462
|
+
vcf_name, _ = os.path.splitext(basename)
|
|
463
|
+
trimmed_down_metrics = resolve_single_file_path(os.path.join(self.output_dir, 'other_validations',
|
|
464
|
+
f'{vcf_name}_trim_down.yml'))
|
|
465
|
+
with open(trimmed_down_metrics) as open_file:
|
|
466
|
+
metrics = yaml.safe_load(open_file)
|
|
467
|
+
shallow_validation_required = shallow_validation_required or metrics['trim_down_required']
|
|
468
|
+
self.results['shallow_validation']['metrics'][vcf_file] = metrics
|
|
469
|
+
self.results['shallow_validation']['required'] = shallow_validation_required
|
|
470
|
+
|
|
471
|
+
def get_vcf_fasta_analysis_mapping(self):
|
|
472
|
+
vcf_fasta_analysis_mapping = []
|
|
473
|
+
with open(self.mapping_file) as open_file:
|
|
474
|
+
reader = csv.DictReader(open_file, delimiter=',')
|
|
475
|
+
for row in reader:
|
|
476
|
+
vcf_fasta_analysis_mapping.append({'vcf_file': row['vcf'], 'fasta_file': row['fasta']})
|
|
477
|
+
|
|
478
|
+
if self.metadata_json_post_validation:
|
|
479
|
+
with open(self.metadata_json_post_validation) as open_file:
|
|
480
|
+
try:
|
|
481
|
+
vcf_analysis_dict = {}
|
|
482
|
+
json_data = json.load(open_file)
|
|
483
|
+
if json_data.get('files', []):
|
|
484
|
+
for file in json_data.get('files', []):
|
|
485
|
+
if file.get('fileName', []) and file.get('analysisAlias', []):
|
|
486
|
+
vcf_analysis_dict[file.get('fileName')] = file.get('analysisAlias')
|
|
487
|
+
|
|
488
|
+
for vcf_fasta_mapping in vcf_fasta_analysis_mapping:
|
|
489
|
+
vcf_file = vcf_fasta_mapping.get('vcf_file')
|
|
490
|
+
if vcf_file in vcf_analysis_dict:
|
|
491
|
+
vcf_fasta_mapping.update({'analysis': vcf_analysis_dict.get(vcf_file)})
|
|
492
|
+
else:
|
|
493
|
+
vcf_fasta_mapping.update({'analysis': 'Could not be linked'})
|
|
494
|
+
|
|
495
|
+
return vcf_fasta_analysis_mapping
|
|
496
|
+
except Exception as e:
|
|
497
|
+
self.error('Error building Validation Report : Error getting info from metadata file' + str(e))
|
|
498
|
+
else:
|
|
499
|
+
self.error('Error building validation report : Metadata file not present')
|
|
500
|
+
|
|
501
|
+
def create_reports(self):
|
|
502
|
+
report_html = generate_html_report(self.results, self.validation_date, self.submission_dir,
|
|
503
|
+
self.get_vcf_fasta_analysis_mapping(),
|
|
504
|
+
self.project_title)
|
|
505
|
+
file_path = os.path.join(self.output_dir, 'report.html')
|
|
506
|
+
with open(file_path, "w") as f:
|
|
507
|
+
f.write(report_html)
|
|
508
|
+
self.info(f'Validation result: {"SUCCESS" if self.verify_ready_for_submission_to_eva() else "FAILURE"}')
|
|
509
|
+
self.info(f'View the full report in your browser: {file_path}')
|
|
510
|
+
return file_path
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: eva_sub_cli
|
|
3
|
+
Version: 0.4.4
|
|
4
|
+
Summary: EBI EVA - validation and submission command line tool
|
|
5
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
6
|
+
Classifier: Environment :: Console
|
|
7
|
+
Classifier: Intended Audience :: Science/Research
|
|
8
|
+
Classifier: Natural Language :: English
|
|
9
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
10
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.6
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
17
|
+
Classifier: Topic :: Communications :: File Sharing
|
|
18
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
19
|
+
Requires-Python: >=3.6
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: ebi_eva_common_pyutils==0.6.11
|
|
23
|
+
Requires-Dist: jinja2
|
|
24
|
+
Requires-Dist: jsonschema
|
|
25
|
+
Requires-Dist: openpyxl
|
|
26
|
+
Requires-Dist: pyyaml
|
|
27
|
+
Requires-Dist: requests
|
|
28
|
+
Requires-Dist: retry
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# eva-sub-cli
|
|
32
|
+
The eva-sub-cli tool is a command line interface tool for data validation and upload. The tool transforms the submission process at EVA by enabling users to take control of data validation process. Previously handled by our helpdesk team, validation can now be performed directly by users, streamlining and improving the overall submission workflow at the EVA.
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
There are currently three ways to install and run the tool :
|
|
38
|
+
- Using conda
|
|
39
|
+
- From source using Docker
|
|
40
|
+
- From source natively (i.e. installing dependencies yourself)
|
|
41
|
+
|
|
42
|
+
### 1. Conda
|
|
43
|
+
|
|
44
|
+
The most straightforward way to install eva-sub-cli and its dependencies is through conda.
|
|
45
|
+
For instance, the following commands install eva-sub-cli in a new environment called `eva`, activate the environment, and print
|
|
46
|
+
the help message:
|
|
47
|
+
```bash
|
|
48
|
+
conda create -n eva -c conda-forge -c bioconda eva-sub-cli
|
|
49
|
+
conda activate eva
|
|
50
|
+
eva-sub-cli.py --help
|
|
51
|
+
````
|
|
52
|
+
|
|
53
|
+
### 2. From source using Docker
|
|
54
|
+
|
|
55
|
+
Docker provides an easy way to run eva-sub-cli without installing dependencies separately.
|
|
56
|
+
This method requires just Python 3.8+ and [Docker](https://docs.docker.com/engine/install/) to be installed.
|
|
57
|
+
Then you can install the most recent version from [PyPI](https://pypi.org/project/eva-sub-cli/) in a virtual environment:
|
|
58
|
+
```bash
|
|
59
|
+
pip install eva-sub-cli
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
To verify that the cli tool is installed correctly, run the following command, and you should see the help message displayed :
|
|
63
|
+
```bash
|
|
64
|
+
eva-sub-cli.py -h
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 3. From source natively
|
|
68
|
+
|
|
69
|
+
This installation method requires the following :
|
|
70
|
+
* Python 3.8+
|
|
71
|
+
* [Nextflow](https://www.nextflow.io/docs/latest/getstarted.html) 21.10+
|
|
72
|
+
* [biovalidator](https://github.com/elixir-europe/biovalidator) 2.1.0+
|
|
73
|
+
* [vcf-validator](https://github.com/EBIvariation/vcf-validator) 0.9.7+
|
|
74
|
+
|
|
75
|
+
Install each of these and ensure they are included in your PATH. Then install the latest release as previously described.
|
|
76
|
+
|
|
77
|
+
## Getting started with the eva-sub-cli tool
|
|
78
|
+
|
|
79
|
+
The ["Getting Started" guide](Getting_Started_with_eva_sub_cli.md) serves as an introduction for users of the eva-sub-cli tool. It includes instructions on how to prepare your data and metadata, ensuring that users are equipped with the necessary information to successfully submit variant data. This guide is essential for new users, offering practical advice and tips for a smooth onboarding experience with the eva-sub-cli tool.
|
|
80
|
+
|
|
81
|
+
## Options and parameters guide
|
|
82
|
+
|
|
83
|
+
The eva-sub-cli tool provides several options and parameters that you can use to tailor its functionality to your needs.
|
|
84
|
+
You can view all the available parameters with the command `eva-sub-cli.py -h` and view detailed explanations for the
|
|
85
|
+
input file requirements in the ["Getting Started" guide](Getting_Started_with_eva_sub_cli.md).
|
|
86
|
+
Below is an overview of the key parameters.
|
|
87
|
+
|
|
88
|
+
### Submission directory
|
|
89
|
+
|
|
90
|
+
This is the directory where all processing will take place, and where configuration and reports will be saved.
|
|
91
|
+
Crucially, the eva-sub-cli tool requires that there be **only one submission per directory** and that the submission directory not be reused.
|
|
92
|
+
Running multiple submissions from a single directory can result in data loss during validation and submission.
|
|
93
|
+
|
|
94
|
+
### Metadata file
|
|
95
|
+
|
|
96
|
+
Metadata can be provided in one of two files.
|
|
97
|
+
|
|
98
|
+
#### The metadata spreadsheet
|
|
99
|
+
|
|
100
|
+
The metadata template can be found within the [etc folder](eva_sub_cli/etc/EVA_Submission_template.xlsx). It should be populated following the instructions provided within the template.
|
|
101
|
+
This is passed using the option `--metadata_xlsx`.
|
|
102
|
+
|
|
103
|
+
#### The metadata JSON
|
|
104
|
+
|
|
105
|
+
The metadata can also be provided via a JSON file, which should conform to the schema located [here](eva_sub_cli/etc/eva_schema.json).
|
|
106
|
+
This is passed using the option `--metadata_json`.
|
|
107
|
+
|
|
108
|
+
### VCF files and Reference FASTA
|
|
109
|
+
|
|
110
|
+
These can be provided either in the metadata file directly, or on the command line using the `--vcf_files` and `--reference_fata` options.
|
|
111
|
+
Note that if you are using more than one reference FASTA, you **cannot** use the command line options; you must specify which VCF files use which FASTA files in the metadata.
|
|
112
|
+
|
|
113
|
+
VCF files can be either uncompressed or compressed using bgzip.
|
|
114
|
+
Other types of compression are not allowed and will result in errors during validation.
|
|
115
|
+
FASTA files must be uncompressed.
|
|
116
|
+
|
|
117
|
+
## Execution
|
|
118
|
+
|
|
119
|
+
### Validate only
|
|
120
|
+
|
|
121
|
+
To validate and not submit, run the following command:
|
|
122
|
+
|
|
123
|
+
```shell
|
|
124
|
+
eva-sub-cli.py --metadata_xlsx metadata_spreadsheet.xlsx --submission_dir submission_dir --tasks VALIDATE
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Note for Docker users:**
|
|
128
|
+
|
|
129
|
+
Make sure that Docker is running in the background, e.g. by opening Docker Desktop.
|
|
130
|
+
For each of the below commands, add the command line option `--executor docker`, which will
|
|
131
|
+
fetch and manage the docker container for you.
|
|
132
|
+
|
|
133
|
+
```shell
|
|
134
|
+
eva-sub-cli.py --metadata_xlsx metadata_spreadsheet.xlsx --submission_dir submission_dir --tasks VALIDATE --executor docker
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Validate and submit your dataset
|
|
138
|
+
|
|
139
|
+
To validate and submit, run the following command:
|
|
140
|
+
|
|
141
|
+
```shell
|
|
142
|
+
eva-sub-cli.py --metadata_xlsx metadata_spreadsheet.xlsx \
|
|
143
|
+
--vcf_files vcf_file1.vcf vcf_file2.vcf --reference_fasta assembly.fa --submission_dir submission_dir
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
### Submit only
|
|
148
|
+
|
|
149
|
+
All submissions must have been validated. You cannot run the submission without validation. Once validated, execute the following command:
|
|
150
|
+
|
|
151
|
+
```shell
|
|
152
|
+
eva-sub-cli.py --metadata_xlsx metadata_spreadsheet.xlsx --submission_dir submission_dir
|
|
153
|
+
```
|
|
154
|
+
or
|
|
155
|
+
```shell
|
|
156
|
+
eva-sub-cli.py --metadata_xlsx metadata_spreadsheet.xlsx --submission_dir submission_dir --tasks SUBMIT
|
|
157
|
+
```
|
|
158
|
+
This will only submit the data and not validate.
|
|
159
|
+
|
|
160
|
+
### Shallow validation
|
|
161
|
+
|
|
162
|
+
If you are working with large VCF files and find that validation takes a very long time, you can add the
|
|
163
|
+
argument `--shallow` to the command, which will validate only the first 10,000 lines in each VCF. Note that running
|
|
164
|
+
shallow validation will **not** be sufficient for actual submission.
|