FunVIP 0.3.20__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.
src/validate_input.py ADDED
@@ -0,0 +1,846 @@
1
+ import os
2
+ import sys
3
+ import shutil
4
+ import re
5
+ import subprocess
6
+ import json
7
+ import pandas as pd
8
+ import numpy as np
9
+ import logging
10
+
11
+ from copy import deepcopy
12
+
13
+ from unidecode import unidecode
14
+ from Bio import SeqIO
15
+ from funvip.src.tool import (
16
+ initialize_path,
17
+ get_genus_species,
18
+ get_id,
19
+ manage_unicode,
20
+ mkdir,
21
+ )
22
+ from pathlib import Path
23
+
24
+ from funvip.src import save
25
+ from funvip.src.logics import isnewicklegal, isuniquecolumn, isvalidcolor
26
+ from funvip.src.hasher import decode, newick_legal, hash_funinfo_list
27
+
28
+
29
+ ## newick illegal characters
30
+ # fmt: off
31
+ NEWICK_ILLEGAL = ("(",'"',"[",":",";","/","[","]","{","}","(",")",",","]","+",'"',")"," ",)
32
+ # fmt: on
33
+
34
+
35
+ # Default funinfo class
36
+ # Abbreviated as FI in most of the codes
37
+ class Funinfo:
38
+ def __init__(self):
39
+ self.original_id = "" # original id, can be newick illegal
40
+ self.id = "" # newick illegal characters removed
41
+ self.hash = "" # hash : HSXXHE
42
+ self.description = "" # full description from fasta
43
+ self.ori_genus = "" # original genus
44
+ self.genus = "" # final genus
45
+ self.ori_species = "" # original species
46
+ self.bygene_species = {"concatenated": ""} # species name designated by gene
47
+ self.final_species = "" # final species designated by concatenated analysis
48
+ # species identifier if multiple branches with same species exists - ambiguous in result
49
+ self.species_identifier = 0
50
+ self.source = ""
51
+ self.datatype = "" # DB or Query
52
+ self.group = "" # original taxonomic group
53
+ self.adjusted_group = "" # adjusted taxonomic group by group clustering
54
+ self.seq = {}
55
+ self.unclassified_seq = []
56
+ self.color = None # color for highlighting in phylogenetic tree
57
+ self.flat = [] # list of flat species in concatenated tree
58
+ self.issues = set() # list of issues to this FI
59
+
60
+ def update_seqrecord(self, seq, gene=None):
61
+ flag = 0
62
+ self.description = seq.description
63
+ self.genus, self.ori_species = get_genus_species(seq.description)
64
+
65
+ if gene in self.seq:
66
+ logging.error(
67
+ f"More than 1 sequence for {gene} found for {self.id} during update_seqrecord"
68
+ )
69
+ flag = -1
70
+ elif gene is None:
71
+ self.unclassified_seq.append(str(seq.seq.ungap("-")))
72
+ else:
73
+ self.seq[gene] = str(seq.seq.ungap("-"))
74
+
75
+ self.bygene_species[gene] = self.ori_species
76
+
77
+ return flag
78
+
79
+ def update_seq(self, gene, seq): # get input as Entrez seqrecord! Important!
80
+ flag = 0
81
+ if gene in self.seq:
82
+ if (
83
+ self.seq[gene] != seq
84
+ ): # if more than 1 sequence per gene gets in, and if they are different
85
+ logging.error(
86
+ f"More than 1 sequence for {gene} found for {self.id} during update_seq"
87
+ )
88
+ flag = -1
89
+ else:
90
+ pass
91
+ else:
92
+ self.seq[gene] = seq
93
+
94
+ self.bygene_species[gene] = self.ori_species
95
+
96
+ # Update concatenated
97
+ self.bygene_species["concatenated"] = self.ori_species
98
+
99
+ return flag
100
+
101
+ def update_description(self, description):
102
+ self.description = description
103
+
104
+ def update_genus(self, genus):
105
+ flag = 0
106
+ # Try to solve illegal unicode characters
107
+ if pd.isnull(genus):
108
+ genus = ""
109
+
110
+ # Genus with space causes error while mafft
111
+ # Genus with slash causes error while tree construction
112
+ genus = genus.strip().replace(" ", "_").replace("/", "_")
113
+ genus = manage_unicode(genus, column="Genus")
114
+
115
+ # Check ambiguity
116
+ if self.genus != "" and self.genus != genus:
117
+ flag = -1
118
+ logging.error(
119
+ f"Colliding genus info found for {self.original_id}, {self.genus} and {genus}"
120
+ )
121
+
122
+ # Update original if should
123
+ if self.ori_genus == "":
124
+ self.ori_genus = genus
125
+
126
+ # Update genus
127
+ self.genus = genus
128
+
129
+ return flag
130
+
131
+ def update_ori_species(self, species):
132
+ flag = 0
133
+ # Try to solve illegal unicode characters
134
+ if pd.isnull(species):
135
+ species = ""
136
+
137
+ # Genus with space causes error while mafft
138
+ # Genus with slash causes error while tree construction
139
+ species = species.strip().replace(" ", "_").replace("/", "_")
140
+ species = manage_unicode(species, column="Species")
141
+
142
+ # Check ambiguity
143
+ if self.ori_species != "" and self.ori_species != species:
144
+ flag = -1
145
+ logging.error(
146
+ f"Colliding species info found for {self.original_id}, {self.ori_species} and {species}"
147
+ )
148
+
149
+ # Update original if should
150
+ if self.ori_species == "":
151
+ self.ori_species = species
152
+
153
+ return flag
154
+
155
+ def update_species(self, gene, species):
156
+ self.bygene_species[gene] = species
157
+
158
+ def update_group(self, group):
159
+ flag = 0
160
+ # Try to solve illegal unicode characters
161
+ if pd.isnull(group):
162
+ group = ""
163
+
164
+ # Group with space causes error while mafft
165
+ group = group.strip().replace(" ", "_")
166
+ group = manage_unicode(group, column="Group")
167
+
168
+ # Check ambiguity
169
+ if self.group != "" and self.group != group:
170
+ logging.error(
171
+ f"Colliding group info found for {self.original_id}, {self.group} and {group}"
172
+ )
173
+ flag = -1
174
+
175
+ # Update group
176
+ self.group = group
177
+
178
+ return flag
179
+
180
+ def update_color(self, color):
181
+ if pd.isnull(color):
182
+ color = None
183
+ self.color = color
184
+ else:
185
+ color = manage_unicode(str(color).strip(), column="Color")
186
+ if isvalidcolor(color) is True:
187
+ self.color = color
188
+ else:
189
+ logging.error(
190
+ f"Color {color} does not seems to be valid svg color nor hex code"
191
+ )
192
+ raise Exception
193
+
194
+ def update_datatype(self, datatype):
195
+ flag = 0
196
+ # Available datatypes : db, query
197
+ if not (datatype in ("db", "query", "outgroup")):
198
+ logging.error(f"DEVELOPMENTAL ERROR: {datatype} is not available datatype")
199
+ raise Exception
200
+
201
+ # Check ambiguity
202
+ if self.datatype != "" and self.datatype != datatype:
203
+ flag = -1
204
+ logging.error(
205
+ f"Colliding datatype found for {self.original_id}, {self.datatype} and {datatype}"
206
+ )
207
+
208
+ self.datatype = datatype
209
+
210
+ return flag
211
+
212
+ def update_id(self, id_, regexs=None):
213
+ if not regexs == None:
214
+ id_ = get_id(id_, tuple(regexs))
215
+
216
+ # if cannot find id by regex
217
+ # Even for original, new line character makes significant errors while working fasta, so remove it
218
+ id_ = str(id_).replace("\n", " ")
219
+ self.original_id = id_
220
+ id_ = newick_legal(id_)
221
+ self.id = id_
222
+
223
+ def update_hash(self, n):
224
+ self.hash = f"HS{n}HE"
225
+
226
+ def get_issue_str(self):
227
+ flat_issues = []
228
+ alignfail_issues = []
229
+ polyphyly_issues = []
230
+ other_issues = []
231
+ for issue in self.issues:
232
+ if issue.startswith("flat"):
233
+ flat_issues.append(issue)
234
+ elif issue.startswith("alignfail"):
235
+ alignfail_issues.append(issue)
236
+ elif issue.startswith("polyphyly"):
237
+ polyphyly_issues.append(issue)
238
+ else:
239
+ other_issues.append(issue)
240
+
241
+ issues_string = ", ".join(other_issues)
242
+ if len(flat_issues) > 0:
243
+ flat_issue_str = "flat:" + "/".join(
244
+ sorted([issue.split(":")[1] for issue in flat_issues])
245
+ )
246
+ if len(issues_string) == 0:
247
+ issues_string = flat_issue_str
248
+ else:
249
+ issues_string += f", {flat_issue_str}"
250
+
251
+ if len(alignfail_issues) > 0:
252
+ alignfail_issue_str = "alignfail:" + "/".join(
253
+ sorted([issue.split(":")[1] for issue in alignfail_issues])
254
+ )
255
+ if len(issues_string) == 0:
256
+ issues_string = alignfail_issue_str
257
+ else:
258
+ issues_string += f", {alignfail_issue_str}"
259
+
260
+ if len(polyphyly_issues) > 0:
261
+ polyphyly_issue_str = "polyphyly:" + "/".join(
262
+ sorted([issue.split(":")[1] for issue in polyphyly_issues])
263
+ )
264
+ if len(issues_string) == 0:
265
+ issues_string = polyphyly_issue_str
266
+ else:
267
+ issues_string += f", {polyphyly_issue_str}"
268
+
269
+ return issues_string
270
+
271
+ def __repr__(self):
272
+ return f"FI: {self.id}"
273
+
274
+ def __hash__(self):
275
+ return hash((self.original_id, self.hash, self.description))
276
+
277
+ def __eq__(self, other):
278
+ if not isinstance(other, type(self)):
279
+ return NotImplemented
280
+ return (
281
+ self.original_id == other.original_id
282
+ and self.hash == other.hash
283
+ and self.description == other.description
284
+ )
285
+
286
+
287
+ # getting data input from fasta file
288
+ def input_fasta(path, opt, fasta_list, funinfo_dict, datatype):
289
+ # initialize path to use function "get_genus_species"
290
+ initialize_path(path)
291
+
292
+ # Fasta files only
293
+ for file in fasta_list:
294
+ # Copy input files to designation
295
+ if datatype == "query":
296
+ shutil.copy(file, path.out_query)
297
+ elif datatype == "db":
298
+ shutil.copy(file, path.out_db)
299
+
300
+ tmp_list = []
301
+
302
+ full = ".".join(file.split(".")[:-1]) # full file path
303
+ name = ".".join(file.split("/")[-1].split(".")[:-1]) # name
304
+
305
+ logging.info(f"{file}: Fasta file")
306
+
307
+ error_flag = 0
308
+
309
+ try:
310
+ seq_list = list(SeqIO.parse(file, "fasta"))
311
+ for seq in seq_list:
312
+ if not opt.regex == None:
313
+ id_ = get_id(seq.description, tuple(opt.regex))
314
+ else:
315
+ id_ = seq.description
316
+
317
+ id_ = newick_legal(id_)
318
+
319
+ if id_ in funinfo_dict:
320
+ error_flag += funinfo_dict[id_].update_seqrecord(seq)
321
+ error_flag += funinfo_dict[id_].update_datatype(datatype)
322
+ error_flag += funinfo_dict[id_].update_group("")
323
+ if get_genus_species(seq.description)[0] != "":
324
+ error_flag += funinfo_dict[id_].update_genus(
325
+ get_genus_species(seq.description)[0]
326
+ )
327
+
328
+ if get_genus_species(seq.description)[1] != "":
329
+ error_flag += funinfo_dict[id_].update_ori_species(
330
+ get_genus_species(seq.description)[1]
331
+ )
332
+
333
+ # For new Funinfo
334
+ else:
335
+ newinfo = Funinfo()
336
+ error_flag += newinfo.update_seqrecord(seq)
337
+ error_flag += newinfo.update_datatype(datatype)
338
+ error_flag += newinfo.update_group(
339
+ ""
340
+ ) # because group not designated yet
341
+ # id by regex match
342
+ newinfo.update_id(seq.description, regexs=opt.regex)
343
+ if get_genus_species(seq.description)[0] != "":
344
+ error_flag += newinfo.update_genus(
345
+ get_genus_species(seq.description)[0]
346
+ )
347
+
348
+ if get_genus_species(seq.description)[1] != "":
349
+ error_flag += newinfo.update_ori_species(
350
+ get_genus_species(seq.description)[1]
351
+ )
352
+
353
+ funinfo_dict[id_] = deepcopy(newinfo)
354
+
355
+ except:
356
+ logging.warning(f"{file} does not seems to be valid fasta file skipping")
357
+
358
+ if len(seq_list) == 0:
359
+ logging.error(f"Fasta file {file} seems to be empty please check")
360
+ raise Exception
361
+
362
+ if error_flag < 0:
363
+ raise Exception
364
+
365
+ return funinfo_dict
366
+
367
+
368
+ # getting datafile from excel or tabular file
369
+ def input_table(funinfo_dict, path, opt, table_list, datatype):
370
+ # Whether to check if GenMine has run
371
+ GenMine_flag = 0
372
+ string_error = 0
373
+
374
+ initialize_path(path) # this one is ugly
375
+ df_list = []
376
+
377
+ # extensionto filetype translation
378
+ dict_extension = {
379
+ ".csv": "csv",
380
+ ".tsv": "csv",
381
+ ".xlsx": "excel",
382
+ ".xls": "excel",
383
+ ".parquet": "parquet",
384
+ ".ftr": "feather",
385
+ ".feather": "feather",
386
+ }
387
+
388
+ # Running table by table operations
389
+ for table in table_list:
390
+ # Read each of the table by each of the extensions
391
+ flag_read_table = 0
392
+ for extension in dict_extension:
393
+ if table.endswith(extension):
394
+ try:
395
+ if dict_extension[extension] == "csv":
396
+ df = pd.read_csv(table)
397
+ flag_read_table = 1
398
+ elif dict_extension[extension] == "excel":
399
+ df = pd.read_excel(table)
400
+ flag_read_table = 1
401
+ elif dict_extension[extension] == "parquet":
402
+ df = pd.read_parquet(table, engine="pyarrow")
403
+ flag_read_table = 1
404
+ elif dict_extension[extension] == "feather":
405
+ df = pd.read_feather(table, use_threads=True)
406
+ flag_read_table = 1
407
+
408
+ # To prevent nan error
409
+ df = df.fillna("")
410
+ except:
411
+ logging.error(
412
+ f"Table {table} cannot be read as {dict_extension[extension]} file. Please check files, extensions and seperators"
413
+ )
414
+ raise Exception
415
+
416
+ if flag_read_table == 0:
417
+ logging.error(
418
+ f"Table {table} cannot recognized as either csv, xlsx, feather or parquet. Please check if extensions endswith .csv, .tsv, .xlsx, .parquet, .ftr or .feather"
419
+ )
420
+ raise Exception
421
+
422
+ # Lower case column names
423
+ df.columns = df.columns.str.lower()
424
+ df.columns = df.columns.str.strip()
425
+ df_list.append(df)
426
+
427
+ # Clean up columns
428
+ # Check if "id" column exists and unique
429
+ flag_id = isuniquecolumn(
430
+ list_column=list(df.columns), column=("accession", "id"), table_name=table
431
+ )
432
+
433
+ # If old accession column, change to id
434
+ if flag_id == "accession":
435
+ df.rename(columns={"accession": "id"}, inplace=True)
436
+
437
+ # Check if "genus" column exists and unique
438
+ # Column "genus" is mandatory in db, and optional in query
439
+ check_none = True if datatype == "db" else False
440
+ flag_genus = isuniquecolumn(
441
+ list_column=list(df.columns),
442
+ column=tuple(("genus",)),
443
+ table_name=table,
444
+ check_none=check_none,
445
+ )
446
+
447
+ # Check if "species" column exists and unique
448
+ # Column "species" is mandatory in db, and optional in query
449
+ check_none = True if datatype == "db" else False
450
+ flag_species = isuniquecolumn(
451
+ list_column=list(df.columns),
452
+ column=tuple(("species",)),
453
+ table_name=table,
454
+ check_none=check_none,
455
+ )
456
+
457
+ # Check "opt.level" column exists and unique
458
+ # Column "opt.level" is mandatory in db, and optional inquery
459
+ check_none = True if datatype == "db" else False
460
+ flag_level = isuniquecolumn(
461
+ list_column=list(df.columns),
462
+ column=tuple((opt.level,)),
463
+ table_name=table,
464
+ check_none=check_none,
465
+ )
466
+
467
+ # Check column color
468
+ # color column is optional
469
+ flag_color = isuniquecolumn(
470
+ list_column=list(df.columns),
471
+ column=tuple(("color",)),
472
+ table_name=table,
473
+ check_none=False,
474
+ )
475
+
476
+ # Sequence column operations, download sequences with GenMine
477
+ if 1: # If download on/off option added, change this part
478
+ download_dict = {} # for downloaded sequences
479
+ download_set = set()
480
+ # 1 letter + 5 digit regex should be last, because they overlap with 2 letter + 6 digit ids / shotgun
481
+ regex_genbank = r"(([A-Z]{1}[0-9]{5})(\.[0-9]{1}){0,1})|(([A-Z]{2}[\_]{0,1}[0-9]{6}){1}([\.][0-9]){0,1})|(([A-Z]{4}[0-9]{8})(\.[0-9]{1}){0,1})|(([A-Z]{6}[0-9]{9,})(\.[0-9]{1}){0,1})"
482
+
483
+ # if gene name were not designated by user, use seq
484
+ opt.gene = list(set([gene.lower().strip() for gene in opt.gene]))
485
+
486
+ # find all NCBI accessions in seq
487
+ for gene in opt.gene:
488
+ if isuniquecolumn(
489
+ list_column=df.columns,
490
+ column=tuple((gene,)),
491
+ table_name=table,
492
+ check_none=False,
493
+ ):
494
+ for n, _ in enumerate(df[gene]):
495
+ if not (pd.isna(df[gene][n])):
496
+ if re.search(regex_genbank, df[gene][n]):
497
+ # remove unexpected indents with strip
498
+ download_set.add(df[gene][n].strip())
499
+
500
+ # if NCBI accessions detected in sequence part, download it
501
+ if len(download_set) > 0:
502
+ GenMine_flag = 1
503
+ if opt.email == "":
504
+ logging.error(
505
+ "Your database includes GenBank accession but email not provided. --email is required to connect to GenBank"
506
+ )
507
+ raise Exception
508
+
509
+ logging.info(
510
+ f"Running GenMine to download {len(download_set)} sequences from GenBank"
511
+ )
512
+ # logging.info(download_set)
513
+
514
+ # Write GenMine input file
515
+ with open(f"{path.GenMine}/Accessions.txt", "w") as fg:
516
+ for acc in download_set:
517
+ fg.write(f"{acc.strip()}\n")
518
+
519
+ # Run GenMine
520
+ accession_path = f"{path.GenMine}/Accessions.txt"
521
+ # To prevent space errors in windows
522
+ if " " in path.GenMine:
523
+ accession_path = f'"{accession_path}"'
524
+ GenMine_path = f'"{path.GenMine}"'
525
+ else:
526
+ GenMine_path = path.GenMine
527
+
528
+ cmd = f"GenMine -c {accession_path} -o {GenMine_path} -e {opt.email}"
529
+ logging.info(cmd)
530
+
531
+ return_code = subprocess.call(cmd, shell=True)
532
+
533
+ if return_code != 0:
534
+ logging.error(f"GenMine failed with return_code: {return_code}")
535
+ logging.error(f"This is usually NCBI server connection error")
536
+ logging.error(
537
+ f"Check your network problems, or replace all accession numbers in your db file with actual sequences to run locally"
538
+ )
539
+ raise Exception
540
+
541
+ GenMine_df_list = [
542
+ file
543
+ for file in os.listdir(path.GenMine)
544
+ if file.endswith("_result.xlsx")
545
+ ]
546
+
547
+ if len(GenMine_df_list) == 1:
548
+ download_df = pd.read_excel(f"{path.GenMine}/{GenMine_df_list[0]}")
549
+
550
+ # Generate download_dict (I think this can be done with pandas operation, but a bit tricky. Will be done later)
551
+ for n, acc in enumerate(download_df["acc"]):
552
+ download_dict[acc.strip()] = download_df["seq"][n]
553
+
554
+ # replace accession to sequence downloaded
555
+ def update_from_GenMine(string):
556
+ string = str(string)
557
+ accession_wo_version = string.strip().split(".")[0]
558
+ accession_w_version = string.strip()
559
+
560
+ # Do not consider sequence version
561
+ if accession_wo_version in download_dict:
562
+ return download_dict[accession_wo_version]
563
+
564
+ # Remove accessions which failed download to prevent conflict with sequence validation
565
+ elif not (accession_wo_version in download_dict) and (
566
+ accession_w_version in download_set
567
+ ):
568
+ logging.warning(f"Failed updating {string}")
569
+ return ""
570
+
571
+ # For sequence input
572
+ else:
573
+ return string
574
+
575
+ for gene in opt.gene:
576
+ if gene in df.columns:
577
+ df[gene] = df[gene].apply(update_from_GenMine)
578
+
579
+ # Remove GenMine results to prevent collision with next set
580
+ for file in os.listdir(path.GenMine):
581
+ if "_result.xlsx" in file:
582
+ os.remove(f"{path.GenMine}/{file}")
583
+
584
+ elif len(GenMine_df_list) == 0:
585
+ logging.warning(
586
+ f"None of the GenMine results were succesfully parsed"
587
+ )
588
+ else:
589
+ logging.error(
590
+ f"DEVELOPMENTAL ERROR: Multiple GenMine result colliding!"
591
+ )
592
+ raise Exception
593
+
594
+ error_flag = 0
595
+
596
+ # Manage unicode for ID
597
+ df["id"] = df["id"].apply(
598
+ lambda x: manage_unicode(str(x), column="ID/Accession")
599
+ )
600
+
601
+ # To prevent errors on genus / spcies column
602
+ # if this function operates with query mode, there might be no genus or species column
603
+ if "genus" in df.columns:
604
+ df["genus"] = df["genus"].apply(lambda x: x.replace(" ", "_"))
605
+ df["genus"] = df["genus"].apply(lambda x: x.replace("/", "_"))
606
+ if "species" in df.columns:
607
+ df["species"] = df["species"].apply(lambda x: x.replace(" ", "_"))
608
+ df["species"] = df["species"].apply(lambda x: x.replace("/", "_"))
609
+
610
+ # Empty id check
611
+ empty_error = []
612
+ for n, acc in enumerate(df["id"]):
613
+ if df["id"][n].strip() == "" or df["id"][n].strip() == "-":
614
+ empty_error.append(n)
615
+
616
+ if len(empty_error) > 0:
617
+ logging.error(f"Empty id found in {table}, line {empty_error}!")
618
+ raise Exception
619
+
620
+ # Generate funinfo by each row
621
+ for n, acc in enumerate(df["id"]):
622
+ # Check if each of the ids are unique
623
+ # Remove non-unicode first
624
+ new_acc = True
625
+
626
+ # Generate funinfo for each id
627
+ # Duplicate id check
628
+ if df["id"][n] in funinfo_dict:
629
+ newinfo = funinfo_dict[df["id"][n]]
630
+ new_acc = False
631
+ logging.warning(f"Duplicate id {df['id'][n]} found!")
632
+ else:
633
+ funinfo_dict[df["id"][n]] = Funinfo()
634
+ newinfo = funinfo_dict[df["id"][n]]
635
+ newinfo.update_id(df["id"][n])
636
+
637
+ # if flag_genus is true, try to parse genus
638
+ if not (flag_genus is None or flag_genus is False):
639
+ error_flag += newinfo.update_genus(df["genus"][n])
640
+
641
+ # if flag_species is true, try to parse species
642
+ if not (flag_species is None or flag_species is False):
643
+ error_flag += newinfo.update_ori_species(df["species"][n])
644
+
645
+ # if flag_level is true, try to parse the optimal taxonomic group
646
+ if not (flag_level is None or flag_level is False):
647
+ error_flag += newinfo.update_group(df[flag_level][n])
648
+
649
+ # if flag_color is true, try to parse color for taxon
650
+ if not (flag_color is None or flag_color is False):
651
+ newinfo.update_color(df[flag_color][n])
652
+
653
+ # update datatype
654
+ error_flag += newinfo.update_datatype(datatype)
655
+
656
+ # parse each of the genes
657
+ for gene in opt.gene:
658
+ seq_error = 0
659
+ if gene in df.columns:
660
+ if not (pd.isna(df[gene][n])) or str(df[gene][n]).strip() == "":
661
+ # skip blank sequences
662
+ if df[gene][n].startswith(">"):
663
+ # remove fasta header
664
+ seq_string = "".join(df[gene][n].split("\n")[1:])
665
+ else:
666
+ seq_string = df[gene][n]
667
+
668
+ # adjust seq_string
669
+ seq_string = seq_string.replace("\n", "").replace(" ", "")
670
+
671
+ # Finding if DNA sequence contains error
672
+ seq_error_cnt = 0
673
+ seq_error_list = []
674
+
675
+ seq_string = manage_unicode(seq_string)
676
+ for x in seq_string: # x is every character of sequence
677
+ if not x.lower() in "acgtryswkmbdhvn-.":
678
+ seq_error_cnt += 1
679
+ seq_error_list.append(x)
680
+
681
+ if seq_error_cnt > 0:
682
+ logging.warning(
683
+ f"Illegal DNA character {seq_error_list} found in {gene} of {datatype} {df['id'][n]}"
684
+ )
685
+ elif seq_string.lower().strip() in ("nan", "na"):
686
+ logging.warning(
687
+ f"Sequence {df['id'][n]} {seq_string} detected as nan, removing it"
688
+ )
689
+ elif seq_error_cnt == 0:
690
+ # remove gaps for preventing BLAST error
691
+ error_flag += newinfo.update_seq(
692
+ gene, seq_string.replace("-", "").replace(".", "")
693
+ )
694
+
695
+ if error_flag < 0:
696
+ raise Exception
697
+
698
+ # After successfully parsed this table, save it
699
+ save.save_df(
700
+ df,
701
+ f"{path.out_db}/Saved_{'.'.join(table.split('/')[-1].split('.')[:-1])}.{opt.tableformat}",
702
+ fmt=opt.tableformat,
703
+ )
704
+ return funinfo_dict, GenMine_flag
705
+
706
+
707
+ def db_input(funinfo_dict, opt, path) -> list:
708
+ # Get DB input
709
+ logging.info(f"Input DB list: {opt.db}")
710
+
711
+ funinfo_dict, GenMine_flag = input_table(
712
+ funinfo_dict=funinfo_dict, path=path, opt=opt, table_list=opt.db, datatype="db"
713
+ )
714
+
715
+ # validate dataset
716
+ # if only one group exists, outgroup cannot work
717
+ group_set = set(funinfo_dict[key].group for key in funinfo_dict)
718
+ group_set.discard("")
719
+ if len(group_set) <= 1:
720
+ logging.error(
721
+ f"Only 1 group soley detected : {group_set}. Please add outgroup sequences"
722
+ )
723
+ raise Exception
724
+
725
+ # check if minimum outgroup number count exceeds minimum group
726
+ group_cnt_dict = {x: 0 for x in group_set}
727
+ for key in funinfo_dict:
728
+ if type(funinfo_dict[key].group) is str:
729
+ if funinfo_dict[key].group in group_set:
730
+ group_cnt_dict[funinfo_dict[key].group] += 1
731
+
732
+ if any(group_cnt_dict[x] < opt.maxoutgroup for x in group_cnt_dict):
733
+ logging.warning(
734
+ f"Sequences in database of some group has lower number than MINIMUM_OUTGROUP_COUNT. It may cause error when outgroup selection, or may select not most appropriate outgroup to group. Please lower number of MINIMUM_OUTGROUP_COUNT in option or add more sequences to these groups"
735
+ )
736
+
737
+ return funinfo_dict, GenMine_flag
738
+
739
+
740
+ def query_input(funinfo_dict, opt, path):
741
+ query_fasta = [
742
+ file
743
+ for file in opt.query
744
+ if any(file.endswith(x) for x in (".fa", ".fna", ".fas", ".fasta", ".txt"))
745
+ ]
746
+ query_table = [
747
+ file
748
+ for file in opt.query
749
+ if any(
750
+ file.endswith(x)
751
+ for x in (".csv", ".tsv", ".xlsx", ".ftr", ".feather", ".parquet")
752
+ )
753
+ ]
754
+
755
+ funinfo_dict, GenMine_flag = input_table(
756
+ funinfo_dict=funinfo_dict,
757
+ path=path,
758
+ opt=opt,
759
+ table_list=query_table,
760
+ datatype="query",
761
+ )
762
+ funinfo_dict = input_fasta(
763
+ path=path,
764
+ opt=opt,
765
+ fasta_list=query_fasta,
766
+ funinfo_dict=funinfo_dict,
767
+ datatype="query",
768
+ )
769
+
770
+ # Save query initially in raw format, because gene has not been assigned yet
771
+ for file in query_table:
772
+ shutil.copy(f"{file}", f"{path.out_query}")
773
+
774
+ for file in query_fasta:
775
+ shutil.copy(f"{file}", f"{path.out_query}")
776
+
777
+ logging.info(
778
+ f"Total {len([funinfo_dict[key].datatype =='query' for key in funinfo_dict.keys()])} sequences parsed from query"
779
+ )
780
+
781
+ return funinfo_dict, GenMine_flag
782
+
783
+
784
+ # combined db and query input
785
+ def data_input(V, R, opt, path):
786
+ funinfo_dict = {}
787
+
788
+ # get database input
789
+ funinfo_dict, GenMine_flag_db = db_input(
790
+ funinfo_dict=funinfo_dict, opt=opt, path=path
791
+ )
792
+
793
+ # get query input
794
+ funinfo_dict, GenMine_flag_query = query_input(
795
+ funinfo_dict=funinfo_dict, opt=opt, path=path
796
+ )
797
+
798
+ if GenMine_flag_db != 0 or GenMine_flag_query != 0:
799
+ GenMine_flag = 1
800
+ else:
801
+ GenMine_flag = 0
802
+
803
+ # combine all data
804
+ V.list_FI = [funinfo_dict[key] for key in funinfo_dict]
805
+
806
+ # hashing data for safety in tree analysis
807
+ V.list_FI = hash_funinfo_list(V.list_FI)
808
+
809
+ # make hash dict
810
+ for FI in V.list_FI:
811
+ V.dict_hash_FI[FI.hash] = FI
812
+
813
+ # If all genes are empty, indicate them
814
+ for FI in V.list_FI:
815
+ flag_available_gene = 0
816
+ for gene in FI.seq:
817
+ if FI.seq[gene] != "":
818
+ flag_available_gene = 1
819
+
820
+ if flag_available_gene == 0:
821
+ FI.issues.add("noseq")
822
+
823
+ return V, R, opt, GenMine_flag
824
+
825
+
826
+ # This function updates initial input query files to saved files with matrices and downloaded sequences
827
+ def update_queryfile(V, path, opt):
828
+ # Dictionary to generate dataframe
829
+ out_dict = {"ID": []}
830
+
831
+ for gene in opt.gene:
832
+ out_dict[gene] = []
833
+
834
+ for FI in V.list_FI:
835
+ if FI.datatype == "query":
836
+ out_dict["ID"].append(FI.original_id)
837
+ for gene in opt.gene:
838
+ if gene in FI.seq:
839
+ out_dict[gene].append(FI.seq[gene])
840
+ else:
841
+ out_dict[gene].append("")
842
+
843
+ df_out = pd.DataFrame(out_dict)
844
+ save.save_df(
845
+ df_out, f"{path.out_query}/Saved_query.{opt.tableformat}", fmt=opt.tableformat
846
+ )