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,335 @@
1
+ import argparse
2
+ import datetime
3
+ import json
4
+ from json import JSONEncoder
5
+
6
+ import openpyxl
7
+ import yaml
8
+ from ebi_eva_common_pyutils.logger import logging_config
9
+ from openpyxl.reader.excel import load_workbook
10
+ from openpyxl.workbook import Workbook
11
+
12
+ WORKSHEETS_KEY_NAME = 'worksheets'
13
+ REQUIRED_HEADERS_KEY_NAME = 'required'
14
+ OPTIONAL_HEADERS_KEY_NAME = 'optional'
15
+ HEADERS_KEY_ROW = 'header_row'
16
+ CAST_KEY_NAME = 'cast'
17
+ PROJECT = 'Project'
18
+ SAMPLE = 'Sample'
19
+ ANALYSIS_ALIAS_KEY = 'Analysis Alias'
20
+ SAMPLE_NAME_IN_VCF_KEY = 'Sample Name in VCF'
21
+ SAMPLE_ACCESSION_KEY = 'Sample Accession'
22
+ SAMPLE_NAME_KEY = 'BioSample Name'
23
+ SCIENTIFIC_NAME_KEY = 'Scientific Name'
24
+ SPECIES = 'species'
25
+
26
+ logger = logging_config.get_logger(__name__)
27
+
28
+
29
+ class DateTimeEncoder(JSONEncoder):
30
+ # Override the default method
31
+ def default(self, obj):
32
+ if isinstance(obj, (datetime.date, datetime.datetime)):
33
+ return obj.isoformat()
34
+
35
+
36
+ class XlsxParser:
37
+ """
38
+ Base parser for Excel file for the fields from worksheets defined in a configuration file.
39
+ It implements the base functionality for opening and validating the spreadsheet.
40
+ """
41
+
42
+ def __init__(self, xlsx_filename, conf_filename):
43
+ """
44
+ Constructor
45
+ :param xlsx_filename: Excel file path
46
+ :type xlsx_filename: basestring
47
+ :param conf_filename: configuration file path
48
+ :type conf_filename: basestring
49
+ """
50
+ self.errors = []
51
+ with open(conf_filename, 'r') as conf_file:
52
+ self.xlsx_conf = yaml.safe_load(conf_file)
53
+ try:
54
+ self.workbook = load_workbook(xlsx_filename, read_only=True)
55
+ except Exception as e:
56
+ self.add_error(f'Error loading {xlsx_filename}: {repr(e)}')
57
+ self.file_loaded = False
58
+ return
59
+ self.worksheets = []
60
+ self._active_worksheet = None
61
+ self.row_offset = {}
62
+ self.headers = {}
63
+ self.file_loaded = True
64
+ self.valid_worksheets()
65
+
66
+ @property
67
+ def active_worksheet(self):
68
+ return self._active_worksheet
69
+
70
+ @active_worksheet.setter
71
+ def active_worksheet(self, worksheet):
72
+ if self.worksheets is None:
73
+ self.valid_worksheets()
74
+ if worksheet not in self.worksheets:
75
+ self.add_error(f'Tried to access an invalid worksheet {worksheet}', sheet=worksheet)
76
+ return
77
+
78
+ self._active_worksheet = worksheet
79
+
80
+ def valid_worksheets(self):
81
+ """
82
+ Get the list of the names of worksheets which have the expected title and header row.
83
+ """
84
+ sheet_titles = self.workbook.sheetnames
85
+
86
+ for title in self.xlsx_conf[WORKSHEETS_KEY_NAME]:
87
+ # Check worksheet exists
88
+ if title not in sheet_titles:
89
+ continue
90
+
91
+ # Check number of rows
92
+ worksheet = self.workbook[title]
93
+ header_row = self.xlsx_conf[title].get(HEADERS_KEY_ROW, 1)
94
+ if worksheet.max_row < header_row + 1:
95
+ continue
96
+ # Store headers and worksheet title
97
+ self.headers[title] = [cell.value if cell.value is None else cell.value.strip()
98
+ for cell in worksheet[header_row]]
99
+ self.worksheets.append(title)
100
+
101
+ @staticmethod
102
+ def cast_value(value, type_name):
103
+ # Do not cast None values
104
+ if type_name and value is not None:
105
+ if type_name == 'string':
106
+ return str(value)
107
+ if type_name == 'boolean':
108
+ return str(value).lower() in ['true', '1', 't', 'y', 'yes']
109
+ if type_name == 'list':
110
+ # split and remove empty values
111
+ return [XlsxParser.trim_value(v)
112
+ for v in value.split(',') if v]
113
+ if type_name == 'date':
114
+ return XlsxParser.serialize(value)
115
+ return value
116
+
117
+ @staticmethod
118
+ def trim_value(value):
119
+ """Remove whitespace from the start and end of cells"""
120
+ if isinstance(value, str):
121
+ return value.strip()
122
+ return value
123
+
124
+ def base_row_offset(self, worksheet):
125
+ return self.xlsx_conf[worksheet].get(HEADERS_KEY_ROW, 1)
126
+
127
+ def get_rows(self):
128
+ """
129
+ Retrieve all the data rows.
130
+ :return: list of hash containing all the REQUIRED and OPTIONAL fields as keys
131
+ and the corresponding data as values
132
+ :rtype: list
133
+ """
134
+ worksheet = self.active_worksheet
135
+ if worksheet is None:
136
+ logger.warning('No worksheet is specified!')
137
+ return None
138
+
139
+ if worksheet not in self.row_offset:
140
+ self.row_offset[worksheet] = self.base_row_offset(worksheet)
141
+ self.row_offset[worksheet] += 1
142
+
143
+ required_headers = self.xlsx_conf[worksheet].get(REQUIRED_HEADERS_KEY_NAME, {})
144
+ optional_headers = self.xlsx_conf[worksheet].get(OPTIONAL_HEADERS_KEY_NAME, {})
145
+
146
+ rows = []
147
+
148
+ for row in self.workbook[worksheet].iter_rows(min_row=self.row_offset[worksheet]):
149
+ num_cells = 0
150
+ for cell in row:
151
+ num_cells += 1
152
+
153
+ data = {}
154
+ has_notnull = False
155
+ for header in list(required_headers.keys()) + list(optional_headers.keys()):
156
+ header_index = num_cells
157
+ if header in self.headers[worksheet]:
158
+ header_index = self.headers[worksheet].index(header)
159
+ if header_index >= num_cells:
160
+ data[header] = None
161
+ continue
162
+
163
+ cell = row[header_index]
164
+ if cell.value is not None:
165
+ has_notnull = True
166
+ data[header] = self.trim_value(self.cast_value(
167
+ cell.value, self.xlsx_conf[worksheet].get(CAST_KEY_NAME, {}).get(header)
168
+ ))
169
+
170
+ if has_notnull:
171
+ data['row_num'] = self.row_offset[worksheet]
172
+ rows.append(data)
173
+
174
+ # no data on this row, continue to next
175
+ self.row_offset[worksheet] += 1
176
+
177
+ return rows
178
+
179
+ def get_project_json_data(self):
180
+ project_data = self.get_rows()
181
+ if len(project_data) > 1:
182
+ logger.warning(f"{PROJECT} worksheet expects a single row of info but more than one found in the file. "
183
+ f"Only the first row's data will be taken into consideration")
184
+
185
+ first_row = project_data[0]
186
+ first_row.pop('row_num')
187
+
188
+ json_key = self.xlsx_conf[WORKSHEETS_KEY_NAME][PROJECT]
189
+ json_value = {self.translate_header(PROJECT, k): v for k, v in first_row.items() if v is not None}
190
+ return {json_key: json_value}
191
+
192
+ def get_biosample_object(self, data):
193
+ sample_name = self.xlsx_conf[SAMPLE][OPTIONAL_HEADERS_KEY_NAME][SAMPLE_NAME_KEY]
194
+ scientific_name = self.xlsx_conf[SAMPLE][OPTIONAL_HEADERS_KEY_NAME][SCIENTIFIC_NAME_KEY]
195
+
196
+ # BioSample expects any of organism or species field
197
+ if scientific_name in data:
198
+ data[SPECIES] = data[scientific_name]
199
+ # BioSample name goes in its own attribute, not part of characteristics
200
+ biosample_name = data.pop(sample_name, None)
201
+
202
+ # For all characteristics, BioSample expects value in arrays of objects
203
+ biosample_object = {
204
+ 'characteristics': {k: [{'text': self.serialize(v)}] for k, v in data.items()}
205
+ }
206
+ if biosample_name is not None:
207
+ biosample_object['name'] = biosample_name
208
+
209
+ return biosample_object
210
+
211
+ @staticmethod
212
+ def serialize(value):
213
+ """Create a text representation of the value provided"""
214
+ if isinstance(value, datetime.date):
215
+ return value.strftime('%Y-%m-%d')
216
+ return str(value)
217
+
218
+ def get_sample_data_with_split_analysis_alias(self, data, analysis_alias, sample_name_in_vcf):
219
+ analysis_alias_list = data[analysis_alias].split(',')
220
+ sample_in_vcf_val = data[sample_name_in_vcf]
221
+ return {analysis_alias: analysis_alias_list, sample_name_in_vcf: sample_in_vcf_val}
222
+
223
+ def get_sample_json_data(self):
224
+ json_key = self.xlsx_conf[WORKSHEETS_KEY_NAME][SAMPLE]
225
+ sample_json = {json_key: []}
226
+ for row in self.get_rows():
227
+ row_num = row.pop('row_num')
228
+ json_value = {self.translate_header(SAMPLE, k): v for k, v in row.items() if v is not None}
229
+ bio_sample_acc = self.xlsx_conf[SAMPLE][OPTIONAL_HEADERS_KEY_NAME][SAMPLE_ACCESSION_KEY]
230
+
231
+ analysis_alias = self.xlsx_conf[SAMPLE][REQUIRED_HEADERS_KEY_NAME][ANALYSIS_ALIAS_KEY]
232
+ sample_name_in_vcf = self.xlsx_conf[SAMPLE][REQUIRED_HEADERS_KEY_NAME][SAMPLE_NAME_IN_VCF_KEY]
233
+ sample_data = self.get_sample_data_with_split_analysis_alias(json_value, analysis_alias, sample_name_in_vcf)
234
+
235
+ if bio_sample_acc in json_value and json_value[bio_sample_acc]:
236
+ sample_data.update(bioSampleAccession=json_value[bio_sample_acc])
237
+ sample_json[json_key].append(sample_data)
238
+ else:
239
+ json_value.pop(analysis_alias)
240
+ json_value.pop(sample_name_in_vcf)
241
+
242
+ biosample_obj = self.get_biosample_object(json_value)
243
+ sample_data.update(bioSampleObject=biosample_obj)
244
+ sample_json[json_key].append(sample_data)
245
+
246
+ return sample_json
247
+
248
+ def json(self, output_json_file):
249
+ # If the file could not be loaded at all, return without generating JSON.
250
+ if not self.file_loaded:
251
+ return
252
+ json_data = {}
253
+ for title in self.xlsx_conf[WORKSHEETS_KEY_NAME]:
254
+ self.active_worksheet = title
255
+ if title == PROJECT:
256
+ json_data.update(self.get_project_json_data())
257
+ elif title == SAMPLE:
258
+ sample_data = self.get_sample_json_data()
259
+ json_data.update(sample_data)
260
+ else:
261
+ json_data[self.xlsx_conf[WORKSHEETS_KEY_NAME][title]] = []
262
+ for row in self.get_rows():
263
+ # Remove the row number
264
+ row.pop('row_num')
265
+ # Remove any None and translate header name
266
+ json_data[self.xlsx_conf[WORKSHEETS_KEY_NAME][title]].append(
267
+ {self.translate_header(title, k): v for k, v in row.items() if v is not None}
268
+ )
269
+
270
+ with open(output_json_file, 'w') as open_file:
271
+ json.dump(json_data, open_file, cls=DateTimeEncoder)
272
+
273
+ def translate_header(self, title, header):
274
+ if header in self.xlsx_conf[title].get(REQUIRED_HEADERS_KEY_NAME, {}):
275
+ return self.xlsx_conf[title][REQUIRED_HEADERS_KEY_NAME][header]
276
+ if header in self.xlsx_conf[title].get(OPTIONAL_HEADERS_KEY_NAME, {}):
277
+ return self.xlsx_conf[title][OPTIONAL_HEADERS_KEY_NAME][header]
278
+ logger.warning(
279
+ f'Header {header} in {title} sheet does not have translation in the config file. Leave it as is')
280
+ return header
281
+
282
+ def add_error(self, message, sheet='', row='', column=''):
283
+ """Adds a conversion error using the same structure as other validation errors,
284
+ and marks the spreadsheet as invalid."""
285
+ self.errors.append({'sheet': sheet, 'row': row, 'column': column, 'description': message})
286
+
287
+ def save_errors(self, errors_yaml_file):
288
+ with open(errors_yaml_file, 'w') as open_file:
289
+ yaml.safe_dump(self.errors, stream=open_file)
290
+
291
+
292
+ def create_xls_template_from_yaml(xlsx_filename, conf_filename):
293
+ """
294
+ Create a XLS empty template with the expected worksheet and header based on the configuration provided
295
+ """
296
+ with open(conf_filename, 'r') as conf_file:
297
+ xlsx_conf = yaml.safe_load(conf_file)
298
+ workbook = Workbook()
299
+ workbook.remove(workbook.active)
300
+
301
+ for title in xlsx_conf[WORKSHEETS_KEY_NAME]:
302
+ # Create worksheet
303
+ workbook.create_sheet(title)
304
+ # For each Worksheet create the header row
305
+ header_row = xlsx_conf[title].get(HEADERS_KEY_ROW, 1)
306
+ header_col_index = 0
307
+ for header_name in xlsx_conf[title].get(REQUIRED_HEADERS_KEY_NAME, []):
308
+ header_col_index += 1
309
+ cell = workbook[title].cell(column=header_col_index, row=header_row, value=header_name)
310
+ cell.font = openpyxl.styles.Font(bold=True)
311
+ for header_name in xlsx_conf[title].get(OPTIONAL_HEADERS_KEY_NAME, []):
312
+ header_col_index += 1
313
+ workbook[title].cell(column=header_col_index, row=header_row, value=header_name)
314
+ workbook.save(xlsx_filename)
315
+
316
+
317
+ def main():
318
+ arg_parser = argparse.ArgumentParser(
319
+ description='Convert an xlsx spreadsheet containing expected sheets to json prior to validation')
320
+ arg_parser.add_argument('--metadata_json', required=True, dest='metadata_json',
321
+ help='Path to output EVA metadata json file')
322
+ arg_parser.add_argument('--metadata_xlsx', required=True, dest='metadata_xlsx', help='EVA metadata Excel file')
323
+ arg_parser.add_argument('--conversion_configuration', dest='conversion_configuration',
324
+ help='Configuration file describing the expected content of the Excel spreadsheet')
325
+ arg_parser.add_argument('--errors_yaml', required=True, dest='errors_yaml', help='Path to output errors as YAML')
326
+
327
+ args = arg_parser.parse_args()
328
+ logging_config.add_stdout_handler()
329
+ parser = XlsxParser(args.metadata_xlsx, args.conversion_configuration)
330
+ try:
331
+ parser.json(args.metadata_json)
332
+ except Exception as e:
333
+ parser.add_error(f'An Error was raised while converting the spreadsheet to JSON: {repr(e)}')
334
+ finally:
335
+ parser.save_errors(args.errors_yaml)
@@ -0,0 +1,117 @@
1
+ import glob
2
+ import gzip
3
+ import os
4
+ import shutil
5
+ import time
6
+ from itertools import groupby
7
+
8
+
9
+ def resolve_single_file_path(file_path):
10
+ files = glob.glob(file_path)
11
+ if len(files) == 0:
12
+ return None
13
+ elif len(files) > 0:
14
+ return files[0]
15
+
16
+
17
+ def is_submission_dir_writable(submission_dir):
18
+ if not os.path.exists(submission_dir):
19
+ os.makedirs(submission_dir)
20
+ if not os.path.isdir(submission_dir):
21
+ return False
22
+ if not os.access(submission_dir, os.W_OK):
23
+ return False
24
+ return True
25
+
26
+
27
+ def backup_file_or_directory(file_name, max_backups=None):
28
+ """
29
+ Rename a file or directory by adding a '.1' at the end. If the '.1' file exists it move it to a '.2' and so on.
30
+ Keep at most the specified number of backups, if None will keep all.
31
+ """
32
+ suffix = 1
33
+ backup_name = f'{file_name}.{suffix}'
34
+ while os.path.exists(backup_name):
35
+ suffix += 1
36
+ backup_name = f'{file_name}.{suffix}'
37
+
38
+ for i in range(suffix, 1, -1):
39
+ if max_backups and i > max_backups:
40
+ if os.path.isfile(file_name):
41
+ os.remove(f'{file_name}.{i - 1}')
42
+ else:
43
+ shutil.rmtree(f'{file_name}.{i - 1}')
44
+ else:
45
+ os.rename(f'{file_name}.{i - 1}', f'{file_name}.{i}')
46
+ os.rename(file_name, file_name + '.1')
47
+
48
+
49
+ def open_gzip_if_required(input_file):
50
+ """Open a file in read mode using gzip if the file extension says .gz"""
51
+ if input_file.endswith('.gz'):
52
+ return gzip.open(input_file, 'rt')
53
+ else:
54
+ return open(input_file, 'r')
55
+
56
+
57
+ def fasta_iter(input_fasta):
58
+ """
59
+ Given a fasta file. yield tuples of header, sequence
60
+ """
61
+ # first open the file outside
62
+ with open_gzip_if_required(input_fasta) as open_file:
63
+ # ditch the boolean (x[0]) and just keep the header or sequence since
64
+ # we know they alternate.
65
+ faiter = (x[1] for x in groupby(open_file, lambda line: line[0] == ">"))
66
+
67
+ for header in faiter:
68
+ # drop the ">"
69
+ headerStr = header.__next__()[1:].strip()
70
+
71
+ # join all sequence lines to one.
72
+ seq = "".join(s.strip() for s in faiter.__next__())
73
+ yield (headerStr, seq)
74
+
75
+
76
+ class DirLockError(Exception):
77
+ pass
78
+
79
+
80
+ class DirLock(object):
81
+
82
+ _SPIN_PERIOD_SECONDS = 0.05
83
+
84
+ def __init__(self, dirname, timeout=3):
85
+ """Prepare a file lock to protect access to dirname. timeout is the
86
+ period (in seconds) after an acquisition attempt is aborted.
87
+ The directory must exist, otherwise aquire() will timeout.
88
+ """
89
+ self._lockfilename = os.path.join(dirname, ".lock")
90
+ self._timeout = timeout
91
+
92
+ def acquire(self):
93
+ start_time = time.time()
94
+ while True:
95
+ try:
96
+ # O_EXCL: fail if file exists or create it (atomically)
97
+ os.close(os.open(self._lockfilename,
98
+ os.O_CREAT | os.O_EXCL | os.O_RDWR))
99
+ break
100
+ except OSError:
101
+ if (time.time() - start_time) > self._timeout:
102
+ raise DirLockError(f"could not create {self._lockfilename} after {self._timeout} seconds")
103
+ else:
104
+ time.sleep(self._SPIN_PERIOD_SECONDS)
105
+
106
+ def release(self):
107
+ try:
108
+ os.remove(self._lockfilename)
109
+ except OSError:
110
+ pass
111
+
112
+ def __enter__(self):
113
+ self.acquire()
114
+ return self
115
+
116
+ def __exit__(self, type_, value, traceback):
117
+ self.release()
@@ -0,0 +1,86 @@
1
+
2
+ {% macro fasta_check_report(validation_results, file_name) -%}
3
+ {% set results_for_fasta = validation_results['fasta_check'][file_name] %}
4
+
5
+ {% if results_for_fasta.get('connection_error') %}
6
+ <div class="report-section fail">
7
+ <strong>Warning:</strong> The following results may be incomplete due to problems with external services. Please try again later for
8
+ complete results.
9
+ <br><strong>Error message:</strong> {{ results_for_fasta.get('connection_error') }}
10
+ </div>
11
+ {% endif %}
12
+
13
+ <!-- All INSDC check results -->
14
+ {% set expand_icon = "" %}
15
+ {% if results_for_fasta.get('all_insdc') %}
16
+ {% set icon = "&#10004;" %}
17
+ {% set row_class = "report-section pass" %}
18
+ {% set text = "All sequences are INSDC accessioned" %}
19
+ {% else %}
20
+ {% set expand_icon = "&#9654;" %}
21
+ {% set icon = "&#10060;" %}
22
+ {% set row_class = "report-section fail collapsible" %}
23
+ {% set text = "Some sequences are not INSDC accessioned" %}
24
+ {% endif %}
25
+ <div class='{{ row_class }}'><span class="expand_icon">{{ expand_icon }}</span> {{ icon }} {{ text }} </div>
26
+ {% if not results_for_fasta.get('all_insdc') %}
27
+ <div class="error-list">
28
+ <div class="error-description">First 10 sequences not in INSDC. <strong>Full report:</strong> {{ results_for_fasta.get('report_path', '') }}</div>
29
+ <table>
30
+ <tr>
31
+ <th>Sequence name</th><th>Refget md5</th>
32
+ </tr>
33
+ {% set sequence_info_list = results_for_fasta.get('sequences', [])|rejectattr("insdc")|list %}
34
+ {% for sequence_info in sequence_info_list[:10] %}
35
+ <tr>
36
+ <td><strong>{{sequence_info.get('sequence_name') }}</strong></td><td> {{ sequence_info.get('sequence_md5') }}</td>
37
+ </tr>
38
+ {% endfor %}
39
+ </table>
40
+ </div>
41
+ {% endif %}
42
+
43
+ <!-- INSDC concordance check results (optional) -->
44
+ {% if 'possible_assemblies' in results_for_fasta %}
45
+ {% if 'metadata_assembly_compatible' in results_for_fasta %}
46
+ <!-- found assembly in metadata, so definitely know the analysis -->
47
+ {% set analysis_text = results_for_fasta.get('associated_analyses')|join(", ") %}
48
+ {% set expand_icon = "" %}
49
+ {% if results_for_fasta.get('metadata_assembly_compatible') %}
50
+ {% set icon = "&#10004;" %}
51
+ {% set row_class = "report-section pass" %}
52
+ {% set text = analysis_text + ": Assembly accession in metadata is compatible" %}
53
+ {% else %}
54
+ {% set expand_icon = "&#9654;" %}
55
+ {% set icon = "&#10060;" %}
56
+ {% set row_class = "report-section fail collapsible" %}
57
+ {% set text = analysis_text + ": Assembly accession in metadata is not compatible" %}
58
+ {% endif %}
59
+ {% else %}
60
+ <!-- has possible assemblies but no metadata assembly -->
61
+ {% set expand_icon = "&#9654;" %}
62
+ {% set icon = "&#10060;" %}
63
+ {% set row_class = "report-section fail collapsible" %}
64
+ {% set text = "No assembly accession found in metadata" %}
65
+ {% endif %}
66
+ <div class='{{ row_class }}'><span class="expand_icon">{{ expand_icon }}</span> {{ icon }} {{ text }} </div>
67
+ {% if 'metadata_assembly_compatible' not in results_for_fasta or not results_for_fasta['metadata_assembly_compatible'] %}
68
+ <div class="error-list">
69
+ <div class="error-description"><strong>Full report:</strong> {{ results_for_fasta.get('report_path', '') }}</div>
70
+ <table>
71
+ <tr>
72
+ <th>Category</th><th>Accessions</th>
73
+ </tr>
74
+ <tr>
75
+ <td><strong>Assembly accession found in metadata</strong></td>
76
+ <td>{{ results_for_fasta.get('assembly_in_metadata', 'Not found') }}</td>
77
+ </tr>
78
+ <tr>
79
+ <td><strong>Assembly accession(s) compatible with FASTA</strong></td>
80
+ <td>{{ results_for_fasta.get('possible_assemblies')|join(", ") }}</td>
81
+ </tr>
82
+ </table>
83
+ </div>
84
+ {% endif %}
85
+ {% endif %}
86
+ {%- endmacro %}
@@ -0,0 +1,93 @@
1
+
2
+ {% macro file_validation_report(validation_results, file_name) -%}
3
+ {% for check_type, check_per_file in validation_results.items() %}
4
+ {% set result = check_per_file.get(file_name, {}) %}
5
+ {% if check_type == "assembly_check" %}
6
+ {{ assembly_check(result) }}
7
+ {% elif check_type == "vcf_check" %}
8
+ {{ vcf_check(result) }}
9
+ {% endif %}
10
+ {% endfor %}
11
+ {%- endmacro %}
12
+
13
+ {% macro vcf_check(vcf_check_result) %}
14
+ {% set critical_count = vcf_check_result.get("critical_count", 0) %}
15
+ {% set error_count = vcf_check_result.get("error_count", 0) %}
16
+ {% set expand_icon = "" %}
17
+ {% if critical_count > 0 %}
18
+ {% set expand_icon = "&#9654;" %}
19
+ {% set icon = "&#10060;" %}
20
+ {% set row_class = "report-section fail collapsible" %}
21
+ {% elif error_count > 0 %}
22
+ {% set expand_icon = "&#9654;" %}
23
+ {% set icon = "&#10060;" %}
24
+ {% set row_class = "report-section warn collapsible" %}
25
+ {% else %}
26
+ {% set icon = "&#10004;" %}
27
+ {% set row_class = "report-section pass" %}
28
+ {% endif %}
29
+ <div class='{{ row_class }}'><span class="expand_icon">{{ expand_icon }}</span> {{ icon }} VCF check: {{ critical_count }} critical errors, {{ error_count }} non-critical errors </div>
30
+ {% set critical_list = vcf_check_result.get("critical_list") %}
31
+ {% set error_list = vcf_check_result.get("error_list") %}
32
+
33
+ {% if critical_list or error_list%}
34
+ <div class="error-list">
35
+ <div class="error-description">First 10 errors per category are below. <strong>Full report:</strong> {{ vcf_check_result.get('report_path', '') }}</div>
36
+ <table>
37
+ <tr>
38
+ <th>Category</th><th>Error</th>
39
+ </tr>
40
+ {% for error in critical_list[:10] %}
41
+ <tr>
42
+ <td><strong>critical error</strong></td><td> {{ error }}</td>
43
+ </tr>
44
+ {% endfor %}
45
+ {% for error in error_list[:10] %}
46
+ <tr>
47
+ <td><strong>non-critical error</strong></td><td> {{ error }}</td>
48
+ </tr>
49
+ {% endfor %}
50
+ </table>
51
+ </div>
52
+ {% endif %}
53
+ {%- endmacro %}
54
+
55
+ {% macro assembly_check(assembly_check_result) %}
56
+ {% set nb_match = assembly_check_result.get("match", 0) %}
57
+ {% set nb_total = assembly_check_result.get("total", 0) %}
58
+ {% set match_percentage = nb_match / nb_total * 100 if nb_total else 0 %}
59
+ {% set expand_icon = "" %}
60
+ {% if assembly_check_result.get("nb_mismatch", 0) > 0 or nb_total == 0 %}
61
+ {% set expand_icon = "&#9654;" %}
62
+ {% set icon = "&#10060;" %}
63
+ {% set row_class = "report-section fail collapsible" %}
64
+ {% else %}
65
+ {% set icon = "&#10004;" %}
66
+ {% set row_class = "report-section pass" %}
67
+ {% endif %}
68
+ <div class='{{ row_class }}'><span class="expand_icon">{{ expand_icon }}</span> {{ icon }} Assembly check: {{ nb_match }}/{{ nb_total }} ({{ match_percentage|round(2) }}%)</div>
69
+ {% set mismatch_list = assembly_check_result.get("mismatch_list") %}
70
+ {% set error_list = assembly_check_result.get("error_list") %}
71
+ {% if mismatch_list or error_list %}
72
+ <div class="error-list">
73
+ <div class="error-description">First 10 errors per category are below. <strong>Full report:</strong> {{ assembly_check_result.get('report_path', '') }}</div>
74
+ <table>
75
+ <tr>
76
+ <th>Category</th><th>Error</th>
77
+ </tr>
78
+ {% for error in error_list[:10] %}
79
+ <tr>
80
+ <td><strong>Parsing Error</strong></td><td> {{ error }}</td>
81
+ </tr>
82
+ {% endfor %}
83
+ {% for error in mismatch_list[:10] %}
84
+ <tr>
85
+ <td><strong>mismatch error</strong></td><td> {{ error }}</td>
86
+ </tr>
87
+ {% endfor %}
88
+ </table>
89
+ </div>
90
+ {% endif %}
91
+ {%- endmacro %}
92
+
93
+