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,18 @@
1
+ import argparse
2
+ import json
3
+
4
+ from eva_sub_cli.semantic_metadata import SemanticMetadataChecker
5
+
6
+
7
+ def main():
8
+ arg_parser = argparse.ArgumentParser(description='Perform semantic checks on the metadata')
9
+ arg_parser.add_argument('--metadata_json', required=True, dest='metadata_json', help='EVA metadata json file')
10
+ arg_parser.add_argument('--output_yaml', required=True, dest='output_yaml',
11
+ help='Path to the location of the results')
12
+ args = arg_parser.parse_args()
13
+
14
+ with open(args.metadata_json) as open_json:
15
+ metadata = json.load(open_json)
16
+ checker = SemanticMetadataChecker(metadata)
17
+ checker.check_all()
18
+ checker.write_result_yaml(args.output_yaml)
@@ -0,0 +1,115 @@
1
+ import sys
2
+
3
+ import eva_sub_cli
4
+ from eva_sub_cli.exceptions.submission_not_found_exception import SubmissionNotFoundException
5
+ from eva_sub_cli.exceptions.submission_status_exception import SubmissionStatusException
6
+
7
+ if not sys.warnoptions:
8
+ import warnings
9
+ warnings.simplefilter("ignore")
10
+
11
+ import logging
12
+ import os
13
+ from argparse import ArgumentParser
14
+ from ebi_eva_common_pyutils.logger import logging_config
15
+
16
+ from eva_sub_cli import orchestrator
17
+ from eva_sub_cli.orchestrator import VALIDATE, SUBMIT, DOCKER, NATIVE
18
+ from eva_sub_cli.file_utils import is_submission_dir_writable, DirLockError, DirLock
19
+
20
+
21
+ def validate_command_line_arguments(args, argparser):
22
+ if (args.vcf_files and not args.reference_fasta) or (not args.vcf_files and args.reference_fasta):
23
+ print("When using --vcf_files and --reference_fasta, both need to be specified")
24
+ argparser.print_usage()
25
+ sys.exit(1)
26
+
27
+ if SUBMIT in args.tasks and (
28
+ not (args.username or os.environ.get('ENAWEBINACCOUNT')) or
29
+ not (args.password or os.environ.get('ENAWEBINPASSWORD'))):
30
+ print("To submit your data, you need to provide a Webin username and password")
31
+ argparser.print_usage()
32
+ sys.exit(1)
33
+
34
+ if not is_submission_dir_writable(args.submission_dir):
35
+ print(f"'{args.submission_dir}' does not have write permissions or is not a directory.")
36
+ sys.exit(1)
37
+
38
+
39
+ def parse_args(cmd_line_args):
40
+ argparser = ArgumentParser(prog='eva-sub-cli',
41
+ description='EVA Submission CLI - validate and submit data to EVA. '
42
+ 'For full details, please see https://github.com/EBIvariation/eva-sub-cli')
43
+ argparser.add_argument('--version', action='version', version=f'%(prog)s {eva_sub_cli.__version__}')
44
+ argparser.add_argument('--submission_dir', required=True, type=str,
45
+ help='Path to the directory where all processing is done and submission info is stored')
46
+ vcf_group = argparser.add_argument_group(
47
+ 'Input VCF and assembly',
48
+ "Specify the VCF files and associated assembly with the following options. If you used different assemblies "
49
+ "for different VCF files, then you must include these in the metadata file rather than specifying them here."
50
+ )
51
+ vcf_group.add_argument('--vcf_files', nargs='+', help="One or more VCF files to validate")
52
+ vcf_group.add_argument('--reference_fasta',
53
+ help="The FASTA file containing the reference genome from which the variants were derived")
54
+
55
+ metadata_group = argparser.add_argument_group('Metadata', 'Specify the metadata in a spreadsheet or in a JSON file')
56
+ metadata_group = metadata_group.add_mutually_exclusive_group(required=True)
57
+ metadata_group.add_argument("--metadata_json",
58
+ help="JSON file that describes the project, analysis, samples and files")
59
+ metadata_group.add_argument("--metadata_xlsx",
60
+ help="Excel spreadsheet that describes the project, analysis, samples and files")
61
+ argparser.add_argument('--tasks', nargs='+', choices=[VALIDATE, SUBMIT], default=[SUBMIT], type=str.lower,
62
+ help='Select a task to perform (default SUBMIT). VALIDATE will run the validation'
63
+ ' regardless of the outcome of previous runs. SUBMIT will run validate only if'
64
+ ' the validation was not performed successfully before and then run the submission.')
65
+ argparser.add_argument('--executor', choices=[DOCKER, NATIVE], default=NATIVE, type=str.lower,
66
+ help='Select the execution type for running validation (default native)')
67
+ credential_group = argparser.add_argument_group('Credentials', 'Specify the ENA Webin credentials you want to use '
68
+ 'to submit to the EVA')
69
+ credential_group.add_argument("--username", help="Username for your ENA Webin account")
70
+ credential_group.add_argument("--password", help="Password for your ENA Webin account")
71
+ argparser.add_argument('--shallow', action='store_true', default=False, dest='shallow_validation',
72
+ help='Set the validation to be performed on the first 10000 records of the VCF. '
73
+ 'Only applies if the number of records exceed 10000')
74
+ argparser.add_argument('--debug', action='store_true', default=False,
75
+ help='Set the script to output debug messages')
76
+ args = argparser.parse_args(cmd_line_args)
77
+ validate_command_line_arguments(args, argparser)
78
+ return args
79
+
80
+
81
+ def main():
82
+ exit_status = 0
83
+ args = parse_args(sys.argv[1:])
84
+
85
+ args.submission_dir = os.path.abspath(args.submission_dir)
86
+
87
+ if args.debug:
88
+ logging_config.add_stdout_handler(logging.DEBUG)
89
+ else:
90
+ logging_config.add_stdout_handler(logging.INFO)
91
+
92
+ try:
93
+ # lock the submission directory
94
+
95
+ with DirLock(os.path.join(args.submission_dir)) as lock:
96
+ # Create the log file
97
+ logging_config.add_file_handler(os.path.join(args.submission_dir, 'eva_submission.log'), logging.DEBUG)
98
+ # Pass on all the arguments to the orchestrator
99
+ orchestrator.orchestrate_process(**args.__dict__)
100
+ except DirLockError:
101
+ print(f'Could not acquire the lock file for {args.submission_dir} because another process is using this '
102
+ f'directory or a previous process did not terminate correctly. '
103
+ f'If the problem persists, remove the lock file manually.')
104
+ exit_status = 65
105
+ except FileNotFoundError as fne:
106
+ print(fne)
107
+ exit_status = 66
108
+ except SubmissionNotFoundException as snfe:
109
+ print(f'{snfe}. Please contact EVA Helpdesk')
110
+ exit_status = 67
111
+ except SubmissionStatusException as sse:
112
+ print(f'{sse}. Please try again later. If the problem persists, please contact EVA Helpdesk')
113
+ exit_status = 68
114
+ return exit_status
115
+
@@ -0,0 +1,144 @@
1
+ import argparse
2
+ import gzip
3
+ import json
4
+ import os
5
+
6
+ from ebi_eva_common_pyutils.logger import logging_config
7
+
8
+ import yaml
9
+
10
+ from eva_sub_cli.file_utils import open_gzip_if_required
11
+ from eva_sub_cli.metadata_utils import get_samples_per_analysis, get_files_per_analysis, get_analysis_for_vcf_file
12
+
13
+ logger = logging_config.get_logger(__name__)
14
+
15
+
16
+ def get_samples_from_vcf(vcf_file):
17
+ """
18
+ Get the list of samples present in a single VCF file
19
+ """
20
+ with open_gzip_if_required(vcf_file) as vcf_in:
21
+ for line in vcf_in:
22
+ if line.startswith('#CHROM'):
23
+ sp_line = line.strip().split('\t')
24
+ if len(sp_line) > 9:
25
+ return sp_line[9:]
26
+ else:
27
+ logger.warning(f"No Sample names found in file {vcf_file}")
28
+ return []
29
+
30
+
31
+ def compare_names_in_files_and_samples(sample_name_in_analysis, sample_name_per_file):
32
+ """
33
+ Compare the sample names provided in vcf files and the one provided in a set of sample rows.
34
+ This is meant to compare the samples and files provided for a single analysis.
35
+ """
36
+ has_difference = False
37
+ sample_names_in_vcf = set(
38
+ sample_name
39
+ for sample_name_list in sample_name_per_file.values()
40
+ for sample_name in sample_name_list
41
+ )
42
+ more_metadata_submitted_files = list(set(sample_name_in_analysis) - sample_names_in_vcf)
43
+ more_submitted_files_metadata = list(sample_names_in_vcf - set(sample_name_in_analysis))
44
+ more_per_submitted_files_metadata = {}
45
+ if more_submitted_files_metadata:
46
+ for file_name in sample_name_per_file:
47
+ more_per_submitted_files_metadata[os.path.basename(file_name)] = list(set(sample_name_per_file[file_name]) - set(sample_name_in_analysis))
48
+ has_difference = True
49
+
50
+ if more_metadata_submitted_files:
51
+ has_difference = True
52
+
53
+ return (has_difference, more_per_submitted_files_metadata, more_submitted_files_metadata,
54
+ more_metadata_submitted_files)
55
+
56
+
57
+ def compare_all_analysis(samples_per_analysis, files_per_analysis):
58
+ overall_differences = False
59
+ results_per_analysis_alias = {}
60
+ all_analysis_alias = set(samples_per_analysis) | set(files_per_analysis)
61
+ for analysis_alias in all_analysis_alias:
62
+ sample_name_in_analysis = samples_per_analysis.get(analysis_alias, [])
63
+ sample_name_per_file = {
64
+ file_path: get_samples_from_vcf(file_path)
65
+ for file_path in files_per_analysis.get(analysis_alias, [])
66
+ }
67
+ (
68
+ has_difference, more_per_submitted_files_metadata,
69
+ more_submitted_files_metadata, more_metadata_submitted_files
70
+ ) = compare_names_in_files_and_samples(sample_name_in_analysis, sample_name_per_file)
71
+ overall_differences = overall_differences or has_difference
72
+ results_per_analysis_alias[analysis_alias] = {
73
+ 'difference': has_difference,
74
+ 'more_per_submitted_files_metadata': more_per_submitted_files_metadata,
75
+ 'more_submitted_files_metadata': more_submitted_files_metadata,
76
+ 'more_metadata_submitted_files': more_metadata_submitted_files
77
+ }
78
+ return overall_differences, results_per_analysis_alias
79
+
80
+
81
+ def read_metadata_json(json_file):
82
+ with open(json_file) as open_json:
83
+ metadata = json.load(open_json)
84
+ return get_samples_per_analysis(metadata), get_files_per_analysis(metadata)
85
+
86
+
87
+ def associate_vcf_path_with_analysis(vcf_files, files_per_analysis):
88
+ """
89
+ Match the files names associated with analysis provided in the metadata with the file path given on the command
90
+ line.
91
+ :param vcf_files the list of full path to the vcf files
92
+ :param files_per_analysis: dictionary of the analysis and their associated VCF file names
93
+ :returns dictionary of analysis and their associated vcf file path
94
+ """
95
+ result_files_per_analysis = dict()
96
+ for analysis in files_per_analysis:
97
+ result_files_per_analysis[analysis] = []
98
+ for vcf_file in vcf_files:
99
+ analysis_aliases = get_analysis_for_vcf_file(vcf_file, files_per_analysis)
100
+ if len(analysis_aliases) == 1:
101
+ result_files_per_analysis[analysis_aliases[0]].append(vcf_file)
102
+ elif len(analysis_aliases) == 0:
103
+ logger.error(f'No analysis found for vcf {vcf_file}')
104
+ if 'No analysis' not in result_files_per_analysis:
105
+ result_files_per_analysis['No analysis'] = []
106
+ result_files_per_analysis['No analysis'].append(vcf_file)
107
+ else:
108
+ logger.error(f'More than one analysis were match to vcf {vcf_file}')
109
+
110
+ return result_files_per_analysis
111
+
112
+
113
+ def write_result_yaml(output_yaml, overall_differences, results_per_analysis_alias):
114
+ with open(output_yaml, 'w') as open_yaml:
115
+ yaml.safe_dump(data={
116
+ 'overall_differences': overall_differences,
117
+ 'results_per_analysis': results_per_analysis_alias
118
+ }, stream=open_yaml)
119
+
120
+
121
+ def check_sample_name_concordance(metadata_json, vcf_files, output_yaml):
122
+ """
123
+ Take the metadata following EVA standard and formatted in JSON then compare the sample names in it to the ones
124
+ found in the VCF files
125
+ """
126
+ samples_per_analysis, files_per_analysis = read_metadata_json(metadata_json)
127
+ file_path_per_analysis = associate_vcf_path_with_analysis(vcf_files, files_per_analysis)
128
+ overall_differences, results_per_analysis_alias = compare_all_analysis(samples_per_analysis, file_path_per_analysis)
129
+ write_result_yaml(output_yaml, overall_differences, results_per_analysis_alias)
130
+
131
+
132
+ def main():
133
+ arg_parser = argparse.ArgumentParser(
134
+ description='Compare the sample name in the VCF file and the one specified in the metadata.')
135
+ arg_parser.add_argument('--metadata_json', required=True, dest='metadata_json',
136
+ help='EVA metadata json file')
137
+ arg_parser.add_argument('--vcf_files', dest='vcf_files', nargs='+',
138
+ help='Path to the vcf files to compare to the metadata')
139
+ arg_parser.add_argument('--output_yaml', required=True, dest='output_yaml',
140
+ help='Path to the location of the results')
141
+
142
+ args = arg_parser.parse_args()
143
+ logging_config.add_stdout_handler()
144
+ check_sample_name_concordance(args.metadata_json, args.vcf_files, args.output_yaml)
@@ -0,0 +1,78 @@
1
+ import argparse
2
+ import os
3
+
4
+ import yaml
5
+ from ebi_eva_common_pyutils.logger import logging_config
6
+ from eva_sub_cli.file_utils import open_gzip_if_required, fasta_iter
7
+
8
+ logger = logging_config.get_logger(__name__)
9
+
10
+
11
+ max_nb_lines = 10000
12
+
13
+
14
+ def trim_down_vcf(vcf_file, output_vcf):
15
+ """
16
+ Produce a smaller vcf files containing a maximum of 10000 records
17
+ """
18
+ with open_gzip_if_required(vcf_file) as vcf_in, open(output_vcf, 'w') as vcf_out:
19
+ line_count = 0
20
+ ref_seq_names = set()
21
+ for line in vcf_in:
22
+ if line.startswith('#') or line_count < max_nb_lines:
23
+ vcf_out.write(line)
24
+ if not line.startswith('#'):
25
+ line_count += 1
26
+ ref_seq_names.add(line.split('\t')[0])
27
+ else:
28
+ break
29
+ if line_count != max_nb_lines:
30
+ logger.warning(f'Only {line_count} found in the source VCF {vcf_file} ')
31
+ return line_count, ref_seq_names
32
+
33
+
34
+ def trim_down_fasta(fasta_file, output_fasta, ref_seq_names):
35
+ """
36
+ Produce a smaller fasta files containing only the reference sequences found in the VCF file
37
+ """
38
+ found_sequences = set()
39
+ with open(output_fasta, 'w') as fasta_out:
40
+ for header, sequence in fasta_iter(fasta_file):
41
+ name = header.split()[0]
42
+ if name in ref_seq_names:
43
+ found_sequences.add(name)
44
+ print(f'>{header}', file=fasta_out)
45
+ for i in range(0, len(sequence), 80):
46
+ print(sequence[i:i+80], file=fasta_out)
47
+ return found_sequences
48
+
49
+
50
+ def main():
51
+ arg_parser = argparse.ArgumentParser(
52
+ description=f'Take a VCF file and only keep {max_nb_lines} lines and remove unused fasta sequence from the '
53
+ f'associated reference genome')
54
+ arg_parser.add_argument('--vcf_file', dest='vcf_file', required=True,
55
+ help='Path to the vcf file to be trimmed down')
56
+ arg_parser.add_argument('--output_vcf_file', dest='output_vcf_file', required=True,
57
+ help='Path to the output vcf file')
58
+ arg_parser.add_argument('--fasta_file', dest='fasta_file', required=True,
59
+ help='Path to the fasta file to be trimmed down')
60
+ arg_parser.add_argument('--output_fasta_file', dest='output_fasta_file', required=True,
61
+ help='Path to the output fasta file')
62
+ arg_parser.add_argument('--output_yaml_file', dest='output_yaml_file', required=True,
63
+ help='Path to the yaml file containing the trim down metrics')
64
+
65
+ args = arg_parser.parse_args()
66
+ logging_config.add_stdout_handler()
67
+
68
+ line_count, ref_sequence = trim_down_vcf(args.vcf_file, args.output_vcf_file)
69
+ sequence_found = trim_down_fasta(args.fasta_file, args.output_fasta_file, ref_sequence)
70
+ trim_down_metrics = {'trim_down_vcf_record': line_count, 'number_sequence_found': sequence_found,
71
+ 'trim_down_required': line_count == max_nb_lines}
72
+ if len(sequence_found) != len(ref_sequence):
73
+ logger.warning(f'Not all sequences were found in the fasta file. Cancelling trimming down of fasta file')
74
+ os.link(args.fasta_file, args.output_fasta_file)
75
+ trim_down_metrics.pop('number_sequence_found')
76
+ with open(args.output_yaml_file) as open_file:
77
+ yaml.safe_dump(trim_down_metrics, open_file)
78
+