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,142 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
{% from 'project_details.html' import project_details_report %}
|
|
3
|
+
{% from 'file_validation.html' import file_validation_report %}
|
|
4
|
+
{% from 'sample_name_check.html' import sample_name_check_report %}
|
|
5
|
+
{% from 'fasta_check.html' import fasta_check_report %}
|
|
6
|
+
{% from 'metadata_validation.html' import metadata_validation_report %}
|
|
7
|
+
{% from 'shallow_validation.html' import optional_shallow_validation_report %}
|
|
8
|
+
|
|
9
|
+
<html lang="EN">
|
|
10
|
+
<head>
|
|
11
|
+
<meta charset="UTF-8">
|
|
12
|
+
<title>Validation Report</title>
|
|
13
|
+
<style>
|
|
14
|
+
body { font-family: Verdana, sans-serif; }
|
|
15
|
+
|
|
16
|
+
.logo { float: left; padding: 20px; }
|
|
17
|
+
.title { padding: 8px; }
|
|
18
|
+
header { overflow: auto; }
|
|
19
|
+
header::after { clear: both; }
|
|
20
|
+
|
|
21
|
+
section { padding: 20px 20px 10px 40px; }
|
|
22
|
+
.description { padding-bottom: 10px}
|
|
23
|
+
.error-description { padding: 10px 0px 10px}
|
|
24
|
+
|
|
25
|
+
.report-section { padding: 12px; border: 1px solid white; margin-bottom: 5px}
|
|
26
|
+
.active, .collapsible:hover.fail { background-color: #e6a3ae; }
|
|
27
|
+
.active, .collapsible:hover.info { background-color: #c0c0c0; }
|
|
28
|
+
|
|
29
|
+
table { border-collapse: collapse; padding: 10px; margin-bottom: 20px}
|
|
30
|
+
th, td { border: 1px solid black; padding: 8px; text-align: left; }
|
|
31
|
+
th { background-color: lightgrey; }
|
|
32
|
+
.fail { background-color: #FFB6C1; }
|
|
33
|
+
.pass { background-color: #90EE90; }
|
|
34
|
+
.info { background-color: #dadada; }
|
|
35
|
+
.error-list, .no-show { display: none; }
|
|
36
|
+
</style>
|
|
37
|
+
</head>
|
|
38
|
+
<body>
|
|
39
|
+
|
|
40
|
+
<header>
|
|
41
|
+
<div class="logo">
|
|
42
|
+
<img src="data:image/png;base64,{{ logo_data }}" width="100" height="100" alt="EVA logo">
|
|
43
|
+
</div>
|
|
44
|
+
<div class="title">
|
|
45
|
+
<h1>Validation Report</h1>
|
|
46
|
+
<h6>eva-sub-cli v{{cli_version}}</h6>
|
|
47
|
+
</div>
|
|
48
|
+
</header>
|
|
49
|
+
|
|
50
|
+
{{ optional_shallow_validation_report(validation_results) }}
|
|
51
|
+
|
|
52
|
+
<section>
|
|
53
|
+
<h2>Project Summary</h2>
|
|
54
|
+
<div class="description">
|
|
55
|
+
General details about the project
|
|
56
|
+
</div>
|
|
57
|
+
{{ project_details_report(project_title, validation_date, submission_dir, vcf_fasta_analysis_mapping) }}
|
|
58
|
+
</section>
|
|
59
|
+
|
|
60
|
+
<section>
|
|
61
|
+
<h2>Metadata validation results</h2>
|
|
62
|
+
<div class="description">
|
|
63
|
+
Ensures that required fields are present and values are formatted correctly.
|
|
64
|
+
For requirements, please refer to the <a href="https://www.ebi.ac.uk/eva/?Submit-Data" target=”_blank”>EVA website</a>.
|
|
65
|
+
</div>
|
|
66
|
+
{{ metadata_validation_report(validation_results) }}
|
|
67
|
+
</section>
|
|
68
|
+
|
|
69
|
+
<section>
|
|
70
|
+
<h2>VCF validation results</h2>
|
|
71
|
+
<div class="description">
|
|
72
|
+
Checks whether each file is compliant with the <a href="http://samtools.github.io/hts-specs/VCFv4.3.pdf" target=”_blank”>VCF specification</a>.
|
|
73
|
+
Also checks whether the variants' reference alleles match against the reference assembly.
|
|
74
|
+
</div>
|
|
75
|
+
{% for file_name in vcf_files %}
|
|
76
|
+
{% if file_name!= "pass"%}
|
|
77
|
+
<h3>{{ file_name }}</h3>
|
|
78
|
+
{{ file_validation_report(validation_results, file_name) }}
|
|
79
|
+
{% endif %}
|
|
80
|
+
{% endfor %}
|
|
81
|
+
</section>
|
|
82
|
+
|
|
83
|
+
<section>
|
|
84
|
+
<h2>Sample name concordance check</h2>
|
|
85
|
+
<div class="description">
|
|
86
|
+
Checks whether information in the metadata is concordant with that contained in the VCF files, in particular sample names.
|
|
87
|
+
</div>
|
|
88
|
+
{{ sample_name_check_report(validation_results)}}
|
|
89
|
+
</section>
|
|
90
|
+
|
|
91
|
+
<section>
|
|
92
|
+
<h2>Reference genome INSDC check</h2>
|
|
93
|
+
<div class="description">
|
|
94
|
+
Checks that the reference sequences in the FASTA file used to call the variants are accessioned in INSDC.
|
|
95
|
+
Also checks if the reference assembly accession in the metadata matches the one determined from the FASTA file.
|
|
96
|
+
</div>
|
|
97
|
+
{% for file_name in fasta_files %}
|
|
98
|
+
{% if file_name!= "pass"%}
|
|
99
|
+
<h3>{{ file_name }}</h3>
|
|
100
|
+
{{ fasta_check_report(validation_results, file_name) }}
|
|
101
|
+
{% endif %}
|
|
102
|
+
{% endfor %}
|
|
103
|
+
</section>
|
|
104
|
+
|
|
105
|
+
<script>
|
|
106
|
+
let collapsibles = document.querySelectorAll('.collapsible');
|
|
107
|
+
for (let collapsible of collapsibles) {
|
|
108
|
+
collapsible.addEventListener('click', function() {
|
|
109
|
+
this.classList.toggle('active');
|
|
110
|
+
|
|
111
|
+
let icon = this.querySelector('.expand_icon');
|
|
112
|
+
if (this.classList.contains('active')) {
|
|
113
|
+
icon.innerHTML = '▼';
|
|
114
|
+
} else {
|
|
115
|
+
icon.innerHTML = '▶';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let content = this.nextElementSibling;
|
|
119
|
+
if (content.style.display === 'block') {
|
|
120
|
+
content.style.display = 'none';
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
content.style.display = 'block';
|
|
124
|
+
content.scrollIntoView({block: 'nearest'});
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function toggleSampleAllErrorsList(id) {
|
|
130
|
+
var element = document.getElementById(id);
|
|
131
|
+
var link = document.querySelector(`a[onclick="toggleSampleAllErrorsList('${id}'); return false;"]`);
|
|
132
|
+
if (element.style.display === "none" || element.style.display === "") {
|
|
133
|
+
element.style.display = "block";
|
|
134
|
+
link.innerHTML = "Hide All Errors for Category";
|
|
135
|
+
} else {
|
|
136
|
+
element.style.display = "none";
|
|
137
|
+
link.innerHTML = "Show All Errors for Category";
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
</script>
|
|
141
|
+
</body>
|
|
142
|
+
</html>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
|
|
2
|
+
{% macro metadata_validation_report(validation_results) -%}
|
|
3
|
+
{% set results = validation_results.get('metadata_check', {}) %}
|
|
4
|
+
{% set spreadsheet_errors = results.get('spreadsheet_errors', []) %}
|
|
5
|
+
{% set json_errors = results.get('json_errors', []) %}
|
|
6
|
+
|
|
7
|
+
{% set has_errors = spreadsheet_errors or json_errors %}
|
|
8
|
+
{% if has_errors %}
|
|
9
|
+
{% set expand_icon = "▶" %}
|
|
10
|
+
{% set icon = "❌" %}
|
|
11
|
+
{% set row_class = "report-section fail collapsible" %}
|
|
12
|
+
{% else %}
|
|
13
|
+
{% set icon = "✔" %}
|
|
14
|
+
{% set row_class = "report-section pass" %}
|
|
15
|
+
{% endif %}
|
|
16
|
+
<div class='{{ row_class }}'><span class="expand_icon">{{ expand_icon }}</span> {{ icon }} Metadata validation check </div>
|
|
17
|
+
{% if spreadsheet_errors %}
|
|
18
|
+
<div class="error-list">
|
|
19
|
+
<div class="error-description"><strong>Full report:</strong> {{ results.get('spreadsheet_report_path', '') }}</div>
|
|
20
|
+
<table>
|
|
21
|
+
<tr>
|
|
22
|
+
<th>Sheet</th><th>Row</th><th>Column</th><th>Description</th>
|
|
23
|
+
</tr>
|
|
24
|
+
{% for error in spreadsheet_errors %}
|
|
25
|
+
<tr>
|
|
26
|
+
<td><strong>{{ error.get('sheet') }}</strong></td>
|
|
27
|
+
<td><strong>{{ error.get('row') }}</strong></td>
|
|
28
|
+
<td><strong>{{ error.get('column') }}</strong></td>
|
|
29
|
+
<td> {{ error.get('description') }}</td>
|
|
30
|
+
</tr>
|
|
31
|
+
{% endfor %}
|
|
32
|
+
</table>
|
|
33
|
+
</div>
|
|
34
|
+
{% endif %}
|
|
35
|
+
|
|
36
|
+
{% if json_errors %}
|
|
37
|
+
<div class="error-list">
|
|
38
|
+
<div class="error-description"><strong>Full report:</strong> {{ results.get('json_report_path', '') }}</div>
|
|
39
|
+
<table>
|
|
40
|
+
<tr>
|
|
41
|
+
<th>JSON Property</th><th>Error Description</th>
|
|
42
|
+
</tr>
|
|
43
|
+
{% for error in json_errors %}
|
|
44
|
+
<tr>
|
|
45
|
+
<td><strong>{{ error.get('property') }}</strong></td>
|
|
46
|
+
<td> {{ error.get('description') }}</td>
|
|
47
|
+
</tr>
|
|
48
|
+
{% endfor %}
|
|
49
|
+
</table>
|
|
50
|
+
</div>
|
|
51
|
+
{% endif %}
|
|
52
|
+
{%- endmacro %}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{% macro project_details_report(project_title, validation_date, submission_dir, vcf_fasta_analysis_mapping) -%}
|
|
2
|
+
<div class="report-section info">
|
|
3
|
+
{% if project_title %}
|
|
4
|
+
<p><b>Project Title:</b> {{ project_title }}</p>
|
|
5
|
+
{% else %}
|
|
6
|
+
<p><b>Project Title:</b> Not Found</p>
|
|
7
|
+
{% endif %}
|
|
8
|
+
<p><b>Validation Date:</b> {{ validation_date }}</p>
|
|
9
|
+
<p><b>Submission Directory:</b> {{ submission_dir }}</p>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
{% if vcf_fasta_analysis_mapping %}
|
|
13
|
+
{% set row_class = "report-section info collapsible" %}
|
|
14
|
+
{% set expand_icon = "▶" %}
|
|
15
|
+
<div class='{{ row_class }}'><span class="expand_icon">{{ expand_icon }}</span> Files mapping</div>
|
|
16
|
+
<div class="no-show">
|
|
17
|
+
<table>
|
|
18
|
+
<tr>
|
|
19
|
+
<th><strong>VCF File</strong></th>
|
|
20
|
+
<th><strong>Fasta File</strong></th>
|
|
21
|
+
<th><strong>Analysis</strong></th>
|
|
22
|
+
</tr>
|
|
23
|
+
{% for mapping in vcf_fasta_analysis_mapping %}
|
|
24
|
+
<tr>
|
|
25
|
+
<td>{{ mapping.vcf_file }}</td>
|
|
26
|
+
<td>{{ mapping.fasta_file }}</td>
|
|
27
|
+
<td>{{ mapping.analysis }}</td>
|
|
28
|
+
</tr>
|
|
29
|
+
{% endfor %}
|
|
30
|
+
</table>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
{% endif %}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
{%- endmacro %}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
|
|
2
|
+
{% macro sample_name_check_report(validation_results) -%}
|
|
3
|
+
{% set results = validation_results.get('sample_check', {}) %}
|
|
4
|
+
{% macro format_error(error) %}
|
|
5
|
+
{{ error | replace(' ', '•') }}
|
|
6
|
+
{% endmacro %}
|
|
7
|
+
|
|
8
|
+
{% for analysis, results_for_analysis in results.get('results_per_analysis', {}).items() %}
|
|
9
|
+
{% if results_for_analysis.get('difference') %}
|
|
10
|
+
{% set expand_icon = "▶" %}
|
|
11
|
+
{% set icon = "❌" %}
|
|
12
|
+
{% set row_class = "report-section fail collapsible" %}
|
|
13
|
+
{% set text = "Sample names in metadata do not match with those in VCF files" %}
|
|
14
|
+
{% else %}
|
|
15
|
+
{% set icon = "✔" %}
|
|
16
|
+
{% set row_class = "report-section pass" %}
|
|
17
|
+
{% set text = "Sample names in metadata match with those in VCF files" %}
|
|
18
|
+
{% endif %}
|
|
19
|
+
<div class='{{ row_class }}'><span class="expand_icon">{{ expand_icon }}</span> {{ icon }} <b>{{ analysis }}</b>: {{ text }} </div>
|
|
20
|
+
{% if results_for_analysis.get('difference') %}
|
|
21
|
+
<div class="error-list">
|
|
22
|
+
<table>
|
|
23
|
+
<tr>
|
|
24
|
+
<th>Category</th><th>First 5 Errors For Category</th><th>Link To View All Errors</th>
|
|
25
|
+
</tr>
|
|
26
|
+
<tr>
|
|
27
|
+
<td><strong>Samples described in the metadata but not in the VCF files</strong></td>
|
|
28
|
+
<td>{{ results_for_analysis.get('more_metadata_submitted_files')[:5]|join(", ") }}</td>
|
|
29
|
+
<td> <a href="#" onclick="toggleSampleAllErrorsList('metadata-{{ loop.index0 }}'); return false;">Show All Errors For Category</a></td>
|
|
30
|
+
</tr>
|
|
31
|
+
<tr>
|
|
32
|
+
<td><strong>Samples in the VCF files but not described in the metadata</strong></td>
|
|
33
|
+
<td>{{ results_for_analysis.get('more_submitted_files_metadata')[:5]|join(", ") }}</td>
|
|
34
|
+
<td> <a href="#" onclick="toggleSampleAllErrorsList('vcf-{{ loop.index0 }}'); return false;">Show All Errors For Category</a></td>
|
|
35
|
+
</tr>
|
|
36
|
+
</table>
|
|
37
|
+
|
|
38
|
+
<div id="metadata-{{ loop.index0 }}" class="description no-show">
|
|
39
|
+
<div class="error-description"><strong>All Errors For Category - Samples described in the metadata but not in the VCF files:</strong></div>
|
|
40
|
+
<ol>
|
|
41
|
+
{% for error in results_for_analysis.get('more_metadata_submitted_files') %}
|
|
42
|
+
<li>{{ format_error(error) }}</li>
|
|
43
|
+
{% endfor %}
|
|
44
|
+
</ol>
|
|
45
|
+
<a href="#" onclick="toggleSampleAllErrorsList('metadata-{{ loop.index0 }}'); return false;">Hide</a>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div id="vcf-{{ loop.index0 }}" class="description no-show">
|
|
49
|
+
<div class="error-description"><strong>All Errors For Category - Samples in the VCF files but not described in the metadata:</strong></div>
|
|
50
|
+
<ol>
|
|
51
|
+
{% for error in results_for_analysis.get('more_submitted_files_metadata') %}
|
|
52
|
+
<li>{{ format_error(error) }}</li>
|
|
53
|
+
{% endfor %}
|
|
54
|
+
</ol>
|
|
55
|
+
<a href="#" onclick="toggleSampleAllErrorsList('vcf-{{ loop.index0 }}'); return false;">Hide</a>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
{% endif %}
|
|
59
|
+
{% endfor %}
|
|
60
|
+
{%- endmacro %}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
|
|
2
|
+
{% macro optional_shallow_validation_report(validation_results) -%}
|
|
3
|
+
{% set results = validation_results.get('shallow_validation', {}) %}
|
|
4
|
+
|
|
5
|
+
{% if results.get('required') %}
|
|
6
|
+
<section>
|
|
7
|
+
<div class="report-section fail collapsible"> <span class="expand_icon">▶</span>
|
|
8
|
+
❌ <b>You requested to run the shallow validation, please run full validation before submitting the data</b>
|
|
9
|
+
</div>
|
|
10
|
+
<div class="no-show">
|
|
11
|
+
<table>
|
|
12
|
+
<tr>
|
|
13
|
+
<th><strong>VCF File</strong></th>
|
|
14
|
+
<th><strong>Variant lines validated in VCF</strong></th>
|
|
15
|
+
<th><strong>Entries used in Fasta</strong></th>
|
|
16
|
+
</tr>
|
|
17
|
+
{% for vcf_file in results.get('metrics') %}
|
|
18
|
+
<tr>
|
|
19
|
+
<td>{{ vcf_file }}</td>
|
|
20
|
+
<td>{{ results.get('metrics').get(vcf_file).get('trim_down_vcf_record') }}</td>
|
|
21
|
+
<td>{{ results.get('metrics').get(vcf_file).get('number_sequence_found') }}</td>
|
|
22
|
+
</tr>
|
|
23
|
+
{% endfor %}
|
|
24
|
+
</table>
|
|
25
|
+
</div>
|
|
26
|
+
</section>
|
|
27
|
+
{% endif %}
|
|
28
|
+
|
|
29
|
+
{%- endmacro %}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
|
|
4
|
+
from ebi_eva_common_pyutils.logger import logging_config
|
|
5
|
+
|
|
6
|
+
logger = logging_config.get_logger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_samples_per_analysis(metadata):
|
|
10
|
+
"""Returns mapping of analysis alias to sample names, based on metadata."""
|
|
11
|
+
samples_per_analysis = defaultdict(list)
|
|
12
|
+
for sample_info in metadata.get('sample', []):
|
|
13
|
+
for analysis_alias in sample_info.get('analysisAlias', []):
|
|
14
|
+
samples_per_analysis[analysis_alias].append(sample_info.get('sampleInVCF'))
|
|
15
|
+
return {
|
|
16
|
+
analysis_alias: set(samples)
|
|
17
|
+
for analysis_alias, samples in samples_per_analysis.items()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_files_per_analysis(metadata):
|
|
22
|
+
"""Returns mapping of analysis alias to filenames, based on metadata."""
|
|
23
|
+
files_per_analysis = defaultdict(list)
|
|
24
|
+
for file_info in metadata.get('files', []):
|
|
25
|
+
files_per_analysis[file_info.get('analysisAlias')].append(file_info.get('fileName'))
|
|
26
|
+
return {
|
|
27
|
+
analysis_alias: set(filepaths)
|
|
28
|
+
for analysis_alias, filepaths in files_per_analysis.items()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_reference_assembly_for_analysis(metadata, analysis_alias):
|
|
33
|
+
"""Returns the reference assembly for this analysis (does not validate format)."""
|
|
34
|
+
for analysis in metadata.get('analysis', []):
|
|
35
|
+
if analysis.get('analysisAlias') == analysis_alias:
|
|
36
|
+
return analysis.get('referenceGenome')
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_analysis_for_vcf_file(vcf_file, files_per_analysis):
|
|
41
|
+
"""Returns list of analysis aliases associated with the vcf file path."""
|
|
42
|
+
if not os.path.exists(vcf_file):
|
|
43
|
+
raise FileNotFoundError(f'{vcf_file} cannot be resolved')
|
|
44
|
+
analysis_aliases = [analysis_alias for analysis_alias in files_per_analysis
|
|
45
|
+
if os.path.basename(vcf_file) in files_per_analysis[analysis_alias]]
|
|
46
|
+
return analysis_aliases
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
#!/usr/bin/env nextflow
|
|
2
|
+
|
|
3
|
+
nextflow.enable.dsl=2
|
|
4
|
+
|
|
5
|
+
def helpMessage() {
|
|
6
|
+
log.info"""
|
|
7
|
+
Validate a set of VCF files and metadata to check if they are valid to be submitted to EVA.
|
|
8
|
+
|
|
9
|
+
Inputs:
|
|
10
|
+
--vcf_files_mapping csv file with the mappings for vcf files, fasta and assembly report
|
|
11
|
+
--output_dir output_directory where the reports will be output
|
|
12
|
+
--metadata_json Json file describing the project, analysis, samples and files
|
|
13
|
+
--metadata_xlsx Excel file describing the project, analysis, samples and files
|
|
14
|
+
"""
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
params.vcf_files_mapping = null
|
|
18
|
+
params.output_dir = null
|
|
19
|
+
params.metadata_json = null
|
|
20
|
+
params.metadata_xlsx = null
|
|
21
|
+
|
|
22
|
+
// executables - external tools
|
|
23
|
+
params.executable = [
|
|
24
|
+
"vcf_validator": "vcf_validator",
|
|
25
|
+
"vcf_assembly_checker": "vcf_assembly_checker",
|
|
26
|
+
"biovalidator": "biovalidator"
|
|
27
|
+
]
|
|
28
|
+
// python scripts - installed as part of eva-sub-cli
|
|
29
|
+
params.python_scripts = [
|
|
30
|
+
"samples_checker": "samples_checker.py",
|
|
31
|
+
"fasta_checker": "check_fasta_insdc.py",
|
|
32
|
+
"xlsx2json": "xlsx2json.py",
|
|
33
|
+
"semantic_checker": "check_metadata_semantics.py",
|
|
34
|
+
"trim_down": "trim_down.py"
|
|
35
|
+
]
|
|
36
|
+
// prefix to prepend to all provided path
|
|
37
|
+
params.base_dir = ""
|
|
38
|
+
// help
|
|
39
|
+
params.help = null
|
|
40
|
+
params.shallow_validation = false
|
|
41
|
+
|
|
42
|
+
// Show help message
|
|
43
|
+
if (params.help) exit 0, helpMessage()
|
|
44
|
+
|
|
45
|
+
// Test input files
|
|
46
|
+
if (!params.vcf_files_mapping || !params.output_dir || (!params.metadata_json && !params.metadata_xlsx)) {
|
|
47
|
+
if (!params.vcf_files_mapping) log.warn('Provide a csv file with the mappings (vcf, fasta, assembly report) --vcf_files_mapping')
|
|
48
|
+
if (!params.metadata_xlsx) log.warn('Provide a json file with the metadata description of the project and analysis --metadata_json')
|
|
49
|
+
if (!params.output_dir) log.warn('Provide an output directory where the reports will be copied using --output_dir')
|
|
50
|
+
if (!params.metadata_json && !params.metadata_xlsx) log.warn('Provide a json or Excel file with the metadata description of the project and analysis --metadata_json or --metadata_xlsx')
|
|
51
|
+
exit 1, helpMessage()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
schema_dir = file(projectDir).parent + '/etc'
|
|
55
|
+
conversion_configuration = schema_dir + '/spreadsheet2json_conf.yaml'
|
|
56
|
+
|
|
57
|
+
def joinBasePath(path) {
|
|
58
|
+
if (path){
|
|
59
|
+
return params.base_dir + '/' + path
|
|
60
|
+
}
|
|
61
|
+
return 'NO_FILE'
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
output_dir = joinBasePath(params.output_dir)
|
|
65
|
+
|
|
66
|
+
workflow {
|
|
67
|
+
// Prepare the file path
|
|
68
|
+
vcf_and_ref_ch = Channel.fromPath(joinBasePath(params.vcf_files_mapping))
|
|
69
|
+
.splitCsv(header:true)
|
|
70
|
+
.map{row -> tuple(
|
|
71
|
+
file(joinBasePath(row.vcf)),
|
|
72
|
+
file(joinBasePath(row.fasta)),
|
|
73
|
+
file(joinBasePath(row.report))
|
|
74
|
+
)}
|
|
75
|
+
if (params.shallow_validation){
|
|
76
|
+
// create a smaller vcf and fasta then replace the channel
|
|
77
|
+
trim_down_vcf(vcf_and_ref_ch)
|
|
78
|
+
vcf_and_ref_ch = trim_down_vcf.out.vcf_and_ref
|
|
79
|
+
}
|
|
80
|
+
vcf_files = vcf_and_ref_ch.map{row -> row[0]}
|
|
81
|
+
fasta_to_vcfs = vcf_and_ref_ch.map{row -> tuple(row[1], row[0])}.groupTuple(by:0)
|
|
82
|
+
// VCF checks
|
|
83
|
+
check_vcf_valid(vcf_and_ref_ch)
|
|
84
|
+
check_vcf_reference(vcf_and_ref_ch)
|
|
85
|
+
|
|
86
|
+
generate_file_size_and_md5_digests(vcf_files)
|
|
87
|
+
collect_file_size_and_md5(generate_file_size_and_md5_digests.out.file_size_and_digest_info.collect())
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
// Metadata conversion
|
|
91
|
+
if (params.metadata_xlsx && !params.metadata_json){
|
|
92
|
+
convert_xlsx_2_json(joinBasePath(params.metadata_xlsx))
|
|
93
|
+
metadata_json = convert_xlsx_2_json.out.metadata_json
|
|
94
|
+
} else {
|
|
95
|
+
metadata_json = joinBasePath(params.metadata_json)
|
|
96
|
+
}
|
|
97
|
+
if (metadata_json) {
|
|
98
|
+
// Metadata checks and concordance checks
|
|
99
|
+
metadata_json_validation(metadata_json)
|
|
100
|
+
metadata_semantic_check(metadata_json)
|
|
101
|
+
sample_name_concordance(metadata_json, vcf_files.collect())
|
|
102
|
+
insdc_checker(metadata_json, fasta_to_vcfs)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
process trim_down_vcf {
|
|
108
|
+
publishDir output_dir, overwrite: false, mode: "copy", pattern: "*.log"
|
|
109
|
+
publishDir output_dir, overwrite: false, mode: "copy", pattern: "*.yml"
|
|
110
|
+
|
|
111
|
+
input:
|
|
112
|
+
tuple path(vcf), path(fasta), path(report)
|
|
113
|
+
|
|
114
|
+
output:
|
|
115
|
+
tuple path("output/$vcf"), path("output/$fasta"), path(report), emit: vcf_and_ref
|
|
116
|
+
path "${vcf.getBaseName()}_trim_down.log", emit: trim_down_log
|
|
117
|
+
path "${vcf.getBaseName()}_trim_down.yml", emit: trim_down_metric
|
|
118
|
+
|
|
119
|
+
"""
|
|
120
|
+
mkdir output
|
|
121
|
+
$params.python_scripts.trim_down --vcf_file $vcf --output_vcf_file output/$vcf --fasta_file $fasta --output_fasta_file output/$fasta --output_yaml_file ${vcf.getBaseName()}_trim_down.yml > ${vcf.getBaseName()}_trim_down.log
|
|
122
|
+
# This is needed to ensure that a missing (NO_FILE) report can still be passed down to subsequent steps
|
|
123
|
+
touch $report
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/*
|
|
129
|
+
* Validate the VCF file format
|
|
130
|
+
*/
|
|
131
|
+
process check_vcf_valid {
|
|
132
|
+
publishDir output_dir,
|
|
133
|
+
overwrite: false,
|
|
134
|
+
mode: "copy"
|
|
135
|
+
|
|
136
|
+
input:
|
|
137
|
+
tuple path(vcf), path(fasta), path(report)
|
|
138
|
+
|
|
139
|
+
output:
|
|
140
|
+
path "vcf_format/*.errors.*.txt", emit: vcf_validation_txt
|
|
141
|
+
path "vcf_format/*.vcf_format.log", emit: vcf_validation_log
|
|
142
|
+
|
|
143
|
+
"""
|
|
144
|
+
trap 'if [[ \$? == 1 ]]; then exit 0; fi' EXIT
|
|
145
|
+
|
|
146
|
+
mkdir -p vcf_format
|
|
147
|
+
$params.executable.vcf_validator -i $vcf -r text -o vcf_format --require-evidence > vcf_format/${vcf}.vcf_format.log 2>&1
|
|
148
|
+
"""
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/*
|
|
152
|
+
* Validate the VCF reference allele
|
|
153
|
+
*/
|
|
154
|
+
process check_vcf_reference {
|
|
155
|
+
publishDir output_dir,
|
|
156
|
+
overwrite: true,
|
|
157
|
+
mode: "copy"
|
|
158
|
+
|
|
159
|
+
input:
|
|
160
|
+
tuple path(vcf), path(fasta), path(report)
|
|
161
|
+
|
|
162
|
+
output:
|
|
163
|
+
path "assembly_check/*text_assembly_report*", emit: assembly_check_report
|
|
164
|
+
path "assembly_check/*.assembly_check.log", emit: assembly_check_log
|
|
165
|
+
|
|
166
|
+
script:
|
|
167
|
+
def report_opt = report.name != 'NO_FILE' ? "-a $report" : ''
|
|
168
|
+
|
|
169
|
+
"""
|
|
170
|
+
trap 'if [[ \$? == 1 || \$? == 139 ]]; then exit 0; fi' EXIT
|
|
171
|
+
|
|
172
|
+
mkdir -p assembly_check
|
|
173
|
+
$params.executable.vcf_assembly_checker -i $vcf -f $fasta $report_opt -r summary,text -o assembly_check --require-genbank > assembly_check/${vcf}.assembly_check.log 2>&1
|
|
174
|
+
"""
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
process generate_file_size_and_md5_digests {
|
|
178
|
+
input:
|
|
179
|
+
path(vcf_file)
|
|
180
|
+
|
|
181
|
+
output:
|
|
182
|
+
path "${vcf_file}_file_size_and_digest_info.txt", emit: file_size_and_digest_info
|
|
183
|
+
|
|
184
|
+
script:
|
|
185
|
+
// Capture the realpath of the vcf to be able to resolve the file based on path instead of name
|
|
186
|
+
"""
|
|
187
|
+
echo "\$(md5sum $vcf_file | awk '{print \$1}') \$(stat -c%s \$(readlink -f $vcf_file)) \$(readlink -f $vcf_file)" > ${vcf_file}_file_size_and_digest_info.txt
|
|
188
|
+
"""
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
process collect_file_size_and_md5 {
|
|
192
|
+
publishDir output_dir,
|
|
193
|
+
overwrite: true,
|
|
194
|
+
mode: "copy"
|
|
195
|
+
|
|
196
|
+
input:
|
|
197
|
+
path(file_size_and_digests)
|
|
198
|
+
|
|
199
|
+
output:
|
|
200
|
+
path "file_info.txt", emit: file_info_log
|
|
201
|
+
|
|
202
|
+
script:
|
|
203
|
+
"""
|
|
204
|
+
cat $file_size_and_digests > file_info.txt
|
|
205
|
+
"""
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
process convert_xlsx_2_json {
|
|
210
|
+
publishDir output_dir,
|
|
211
|
+
overwrite: true,
|
|
212
|
+
mode: "copy"
|
|
213
|
+
|
|
214
|
+
input:
|
|
215
|
+
path(metadata_xlsx)
|
|
216
|
+
|
|
217
|
+
output:
|
|
218
|
+
path "metadata.json", emit: metadata_json, optional: true
|
|
219
|
+
path "metadata_conversion_errors.yml", emit: errors_yaml
|
|
220
|
+
path "xlsx2json.log", emit: xlsx2json_log
|
|
221
|
+
|
|
222
|
+
script:
|
|
223
|
+
metadata_json = metadata_xlsx.getBaseName() + '.json'
|
|
224
|
+
|
|
225
|
+
"""
|
|
226
|
+
$params.python_scripts.xlsx2json --metadata_xlsx $metadata_xlsx --metadata_json metadata.json --errors_yaml metadata_conversion_errors.yml --conversion_configuration $conversion_configuration > xlsx2json.log 2>&1
|
|
227
|
+
"""
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
process metadata_json_validation {
|
|
231
|
+
publishDir output_dir,
|
|
232
|
+
overwrite: true,
|
|
233
|
+
mode: "copy"
|
|
234
|
+
|
|
235
|
+
input:
|
|
236
|
+
path(metadata_json)
|
|
237
|
+
|
|
238
|
+
output:
|
|
239
|
+
path "metadata_validation.txt", emit: metadata_validation
|
|
240
|
+
|
|
241
|
+
script:
|
|
242
|
+
"""
|
|
243
|
+
$params.executable.biovalidator --schema $schema_dir/eva_schema.json --ref $schema_dir/eva-biosamples.json --data $metadata_json > metadata_validation.txt 2>&1
|
|
244
|
+
"""
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
process sample_name_concordance {
|
|
248
|
+
publishDir output_dir,
|
|
249
|
+
overwrite: true,
|
|
250
|
+
mode: "copy"
|
|
251
|
+
|
|
252
|
+
input:
|
|
253
|
+
path(metadata_json)
|
|
254
|
+
path(vcf_files)
|
|
255
|
+
|
|
256
|
+
output:
|
|
257
|
+
path "sample_checker.yml", emit: sample_checker_yml
|
|
258
|
+
path "sample_checker.log", emit: sample_checker_log
|
|
259
|
+
|
|
260
|
+
script:
|
|
261
|
+
"""
|
|
262
|
+
$params.python_scripts.samples_checker --metadata_json $metadata_json --vcf_files $vcf_files --output_yaml sample_checker.yml > sample_checker.log 2>&1
|
|
263
|
+
"""
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
process insdc_checker {
|
|
268
|
+
publishDir output_dir,
|
|
269
|
+
overwrite: true,
|
|
270
|
+
mode: "copy"
|
|
271
|
+
|
|
272
|
+
input:
|
|
273
|
+
path(metadata_json)
|
|
274
|
+
tuple(path(fasta_file), path(vcf_files))
|
|
275
|
+
|
|
276
|
+
output:
|
|
277
|
+
path "${fasta_file}_check.yml", emit: fasta_checker_yml
|
|
278
|
+
path "fasta_checker.log", emit: fasta_checker_log
|
|
279
|
+
|
|
280
|
+
script:
|
|
281
|
+
"""
|
|
282
|
+
$params.python_scripts.fasta_checker --metadata_json $metadata_json --vcf_files $vcf_files --input_fasta $fasta_file --output_yaml ${fasta_file}_check.yml > fasta_checker.log 2>&1
|
|
283
|
+
"""
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
process metadata_semantic_check {
|
|
288
|
+
publishDir output_dir,
|
|
289
|
+
overwrite: true,
|
|
290
|
+
mode: "copy"
|
|
291
|
+
|
|
292
|
+
input:
|
|
293
|
+
path(metadata_json)
|
|
294
|
+
|
|
295
|
+
output:
|
|
296
|
+
path "metadata_semantic_check.yml", emit: metadata_semantic_check_yml
|
|
297
|
+
path "semantic_checker.log", emit: semantic_checker_log
|
|
298
|
+
|
|
299
|
+
script:
|
|
300
|
+
"""
|
|
301
|
+
$params.python_scripts.semantic_checker --metadata_json $metadata_json --output_yaml metadata_semantic_check.yml > semantic_checker.log 2>&1
|
|
302
|
+
"""
|
|
303
|
+
}
|