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.
Files changed (46) hide show
  1. eva_sub_cli/__init__.py +27 -0
  2. eva_sub_cli/_version.py +21 -0
  3. eva_sub_cli/auth.py +106 -0
  4. eva_sub_cli/date_utils.py +32 -0
  5. eva_sub_cli/etc/EVA_Submission_template.xlsx +0 -0
  6. eva_sub_cli/etc/eva-biosamples.json +174 -0
  7. eva_sub_cli/etc/eva_logo.png +0 -0
  8. eva_sub_cli/etc/eva_schema.json +369 -0
  9. eva_sub_cli/etc/spreadsheet2json_conf.yaml +135 -0
  10. eva_sub_cli/exceptions/__init__.py +0 -0
  11. eva_sub_cli/exceptions/invalid_file_type_exception.py +4 -0
  12. eva_sub_cli/exceptions/submission_not_found_exception.py +4 -0
  13. eva_sub_cli/exceptions/submission_status_exception.py +5 -0
  14. eva_sub_cli/executables/__init__.py +0 -0
  15. eva_sub_cli/executables/check_fasta_insdc.py +159 -0
  16. eva_sub_cli/executables/check_metadata_semantics.py +18 -0
  17. eva_sub_cli/executables/cli.py +115 -0
  18. eva_sub_cli/executables/samples_checker.py +144 -0
  19. eva_sub_cli/executables/trim_down.py +78 -0
  20. eva_sub_cli/executables/xlsx2json.py +335 -0
  21. eva_sub_cli/file_utils.py +117 -0
  22. eva_sub_cli/jinja_templates/fasta_check.html +86 -0
  23. eva_sub_cli/jinja_templates/file_validation.html +93 -0
  24. eva_sub_cli/jinja_templates/html_report.html +142 -0
  25. eva_sub_cli/jinja_templates/metadata_validation.html +52 -0
  26. eva_sub_cli/jinja_templates/project_details.html +37 -0
  27. eva_sub_cli/jinja_templates/sample_name_check.html +60 -0
  28. eva_sub_cli/jinja_templates/shallow_validation.html +29 -0
  29. eva_sub_cli/metadata_utils.py +46 -0
  30. eva_sub_cli/nextflow/validation.nf +303 -0
  31. eva_sub_cli/orchestrator.py +233 -0
  32. eva_sub_cli/report.py +40 -0
  33. eva_sub_cli/semantic_metadata.py +243 -0
  34. eva_sub_cli/submission_ws.py +61 -0
  35. eva_sub_cli/submit.py +111 -0
  36. eva_sub_cli/validators/__init__.py +0 -0
  37. eva_sub_cli/validators/docker_validator.py +205 -0
  38. eva_sub_cli/validators/native_validator.py +68 -0
  39. eva_sub_cli/validators/validation_results_parsers.py +210 -0
  40. eva_sub_cli/validators/validator.py +510 -0
  41. eva_sub_cli-0.4.4.dist-info/METADATA +164 -0
  42. eva_sub_cli-0.4.4.dist-info/RECORD +46 -0
  43. eva_sub_cli-0.4.4.dist-info/WHEEL +5 -0
  44. eva_sub_cli-0.4.4.dist-info/entry_points.txt +7 -0
  45. eva_sub_cli-0.4.4.dist-info/licenses/LICENSE +201 -0
  46. 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.