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.
- FunVIP-0.3.20.dist-info/LICENSE +674 -0
- FunVIP-0.3.20.dist-info/METADATA +32 -0
- FunVIP-0.3.20.dist-info/RECORD +36 -0
- FunVIP-0.3.20.dist-info/WHEEL +5 -0
- FunVIP-0.3.20.dist-info/entry_points.txt +3 -0
- FunVIP-0.3.20.dist-info/top_level.txt +3 -0
- data/__init__.py +0 -0
- external/BLAST_Windows/bin/cleanup-blastdb-volumes.py +162 -0
- external/__init__.py +0 -0
- src/__init__.py +0 -0
- src/align.py +124 -0
- src/cluster.py +510 -0
- src/command.py +360 -0
- src/concatenate.py +356 -0
- src/dataset.py +716 -0
- src/ext.py +448 -0
- src/hasher.py +98 -0
- src/initialize.py +335 -0
- src/logger.py +69 -0
- src/logics.py +104 -0
- src/modeltest.py +443 -0
- src/ncbi.py +160 -0
- src/opt_generator.py +72 -0
- src/patch.py +261 -0
- src/reporter.py +875 -0
- src/save.py +181 -0
- src/search.py +440 -0
- src/tool.py +309 -0
- src/tree.py +222 -0
- src/tree_interpretation.py +1379 -0
- src/tree_interpretation_pipe.py +679 -0
- src/trim.py +118 -0
- src/validate_input.py +846 -0
- src/validate_option.py +1609 -0
- src/validation.py +38 -0
- src/version.py +337 -0
src/initialize.py
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
import json
|
|
6
|
+
import pandas as pd
|
|
7
|
+
import zipfile
|
|
8
|
+
import numpy as np
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
from Bio import SeqIO
|
|
12
|
+
from funvip.src.logics import isnan
|
|
13
|
+
from funvip.src.tool import (
|
|
14
|
+
initialize_path,
|
|
15
|
+
get_genus_species,
|
|
16
|
+
mkdir,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from funvip.src.validate_option import initialize_option
|
|
20
|
+
|
|
21
|
+
INF = 99999999
|
|
22
|
+
|
|
23
|
+
# Setup
|
|
24
|
+
# Make logics.py and move these unnecessary things to logics.py
|
|
25
|
+
# make directory if directory does not exists
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# checking input option
|
|
29
|
+
# value should be list of small letter cases (for developmentors)
|
|
30
|
+
def check(
|
|
31
|
+
obj,
|
|
32
|
+
type_,
|
|
33
|
+
criterion,
|
|
34
|
+
value=np.NaN,
|
|
35
|
+
min_=np.NaN,
|
|
36
|
+
max_=np.NaN,
|
|
37
|
+
default=np.NaN,
|
|
38
|
+
solve=False,
|
|
39
|
+
):
|
|
40
|
+
# obj : option things that should be checked
|
|
41
|
+
# type_ : type of the object that should be
|
|
42
|
+
# criterion : name of the option to be written on message
|
|
43
|
+
# min_ : minimum value accepted (int, float)
|
|
44
|
+
# max_ : maximum value accepted (int, float)
|
|
45
|
+
# default : default value if the option is not available
|
|
46
|
+
# solve : if True, if input is string, change to list form (list)
|
|
47
|
+
|
|
48
|
+
# Error messages for each of the type
|
|
49
|
+
# Change these things to logger if available
|
|
50
|
+
def err_msg(type__):
|
|
51
|
+
if type__ is bool:
|
|
52
|
+
return "true or false, in small letters and without quotations"
|
|
53
|
+
elif type__ is str:
|
|
54
|
+
return "string with quotations"
|
|
55
|
+
elif type__ is list:
|
|
56
|
+
return "list with square brackets, and items with quotations"
|
|
57
|
+
elif type__ is int:
|
|
58
|
+
return "integer number"
|
|
59
|
+
elif type__ is float:
|
|
60
|
+
return "floating point number"
|
|
61
|
+
else:
|
|
62
|
+
print(
|
|
63
|
+
"[ERROR] DEVELOPMENTAL ERROR. Please issue to wan101010@snu.ac.kr with logs and datasets"
|
|
64
|
+
)
|
|
65
|
+
raise ValueError
|
|
66
|
+
|
|
67
|
+
# Flexible solve to int and float
|
|
68
|
+
if (type(obj)) is int and type_ is float:
|
|
69
|
+
obj = float(obj)
|
|
70
|
+
elif (type(obj)) is float and type_ is int:
|
|
71
|
+
obj = int(obj)
|
|
72
|
+
|
|
73
|
+
# if type is right
|
|
74
|
+
if (type(obj)) is type_:
|
|
75
|
+
# if value is not in right range (for int and float)
|
|
76
|
+
if not (isnan(min_)) and not (isnan(max_)):
|
|
77
|
+
if type_ is int or type_ is float:
|
|
78
|
+
if obj < min_ or obj > max_:
|
|
79
|
+
print(
|
|
80
|
+
f"[ERROR] {criterion} should be between {min_} and {max_}. Your input is {obj}"
|
|
81
|
+
)
|
|
82
|
+
raise ValueError
|
|
83
|
+
|
|
84
|
+
else:
|
|
85
|
+
print(type_)
|
|
86
|
+
print(obj)
|
|
87
|
+
print(
|
|
88
|
+
f"[ERROR] DEVELOPMENTAL ERROR on {criterion}. Please issue to wan101010@snu.ac.kr with logs and datasets"
|
|
89
|
+
)
|
|
90
|
+
raise Exception
|
|
91
|
+
|
|
92
|
+
# if value is not in possible value (for str)
|
|
93
|
+
if type(value) is list:
|
|
94
|
+
if type_ is str:
|
|
95
|
+
if not (obj.lower() in value):
|
|
96
|
+
print(
|
|
97
|
+
f"[ERROR] {criterion} should be one of {value}. Your input is {obj}"
|
|
98
|
+
)
|
|
99
|
+
raise ValueError
|
|
100
|
+
else:
|
|
101
|
+
print(
|
|
102
|
+
"[ERROR] DEVELOPMENTAL ERROR. Please issue to wan101010@snu.ac.kr with logs and datasets"
|
|
103
|
+
)
|
|
104
|
+
raise Exception
|
|
105
|
+
|
|
106
|
+
return obj
|
|
107
|
+
|
|
108
|
+
# if solving single string to list is available
|
|
109
|
+
if solve is True:
|
|
110
|
+
if type_ is list and type(obj) is str:
|
|
111
|
+
obj = [obj]
|
|
112
|
+
elif type(obj) is not (list):
|
|
113
|
+
print(
|
|
114
|
+
f"[ERROR] FAILED to solve {criterion} to list. It should be {err_msg(str)} or {err_msg(list)}. Your input is {obj}"
|
|
115
|
+
)
|
|
116
|
+
raise TypeError
|
|
117
|
+
|
|
118
|
+
# if type is wrong
|
|
119
|
+
else:
|
|
120
|
+
# if default value exists
|
|
121
|
+
if not (isnan(default)):
|
|
122
|
+
print(
|
|
123
|
+
f"[WARNING] {criterion} should be {err_msg(type_)}. Your input is {obj}. Setting to default value {default}"
|
|
124
|
+
)
|
|
125
|
+
return default
|
|
126
|
+
|
|
127
|
+
# if default value does not exists and option.config should be corrected by user
|
|
128
|
+
else:
|
|
129
|
+
print(
|
|
130
|
+
f"[ERROR] {criterion} is mandatory and should be {err_msg(type_)}. Your input is {obj}"
|
|
131
|
+
)
|
|
132
|
+
raise TypeError
|
|
133
|
+
|
|
134
|
+
return obj
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# Change it to FunVIPPath
|
|
138
|
+
# Class with all path
|
|
139
|
+
# Use print instead of logging here, because logging has not been defined here
|
|
140
|
+
class Path:
|
|
141
|
+
def __init__(self, root):
|
|
142
|
+
# For universal data used in every run
|
|
143
|
+
self.sys_path = os.path.abspath(f"{os.path.dirname(__file__)}/../")
|
|
144
|
+
|
|
145
|
+
# Option manager file location
|
|
146
|
+
self.option_attributes = f"{self.sys_path}/data/Option_manager.xlsx"
|
|
147
|
+
|
|
148
|
+
# initializing MAFFT for windows
|
|
149
|
+
# MAFFT is occuring error when distributed in already used form
|
|
150
|
+
# Therefore, distribute in zipped format
|
|
151
|
+
if sys.platform == "win32":
|
|
152
|
+
if not os.path.exists(f"{self.sys_path}/external/MAFFT_Windows"):
|
|
153
|
+
with zipfile.ZipFile(
|
|
154
|
+
f"{self.sys_path}/external/MAFFT_Windows.zip",
|
|
155
|
+
"r",
|
|
156
|
+
) as zip_ref:
|
|
157
|
+
zip_ref.extractall(f"{self.sys_path}/external/MAFFT_Windows")
|
|
158
|
+
# For non-windows platform, check all programs installed properly
|
|
159
|
+
else:
|
|
160
|
+
install_flag = 0
|
|
161
|
+
# Not checking gblocks currently, because there are only interactive options without input file
|
|
162
|
+
# Mafft help does not returns 0
|
|
163
|
+
# "mafft": "mafft --help",
|
|
164
|
+
# FastTree check fails when run with nohup
|
|
165
|
+
# "Fasttree": "FastTree",
|
|
166
|
+
|
|
167
|
+
if sys.platform == "darwin":
|
|
168
|
+
check_commands = {
|
|
169
|
+
"RAxML": "raxmlHPC-PTHREADS -h",
|
|
170
|
+
"IQTREE": "iqtree -h",
|
|
171
|
+
"MMseqs2": "mmseqs -h",
|
|
172
|
+
"BLASTn": "blastn -help",
|
|
173
|
+
"Trimal": "trimal -h",
|
|
174
|
+
}
|
|
175
|
+
else:
|
|
176
|
+
check_commands = {
|
|
177
|
+
"RAxML": "raxmlHPC-PTHREADS-AVX -h",
|
|
178
|
+
"Modeltest-NG": "modeltest-ng --help",
|
|
179
|
+
"IQTREE": "iqtree -h",
|
|
180
|
+
"MMseqs2": "mmseqs -h",
|
|
181
|
+
"BLASTn": "blastn -help",
|
|
182
|
+
"Trimal": "trimal -h",
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
for program in check_commands:
|
|
186
|
+
cmd = check_commands[program]
|
|
187
|
+
# Quietly call each programs
|
|
188
|
+
return_code = subprocess.call(
|
|
189
|
+
cmd, shell=True, stdout=open(os.devnull, "wb")
|
|
190
|
+
)
|
|
191
|
+
if return_code != 0:
|
|
192
|
+
print(f"[ERROR] {program} not installed!")
|
|
193
|
+
install_flag = 1
|
|
194
|
+
|
|
195
|
+
if install_flag == 1:
|
|
196
|
+
if sys.platform == "darwin":
|
|
197
|
+
print(
|
|
198
|
+
f"[ERROR] Some of the dependencies not installed. Use \n conda install -c bioconda raxml iqtree mmseqs2 blast=2.12 mafft trimal gblocks fasttree \nto install dependencies"
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
else:
|
|
202
|
+
print(
|
|
203
|
+
f"[ERROR] Some of the dependencies not installed. Use \n conda install -c bioconda raxml iqtree modeltest-ng mmseqs2 blast=2.12 mafft trimal gblocks fasttree \nto install dependencies"
|
|
204
|
+
)
|
|
205
|
+
raise Exception
|
|
206
|
+
|
|
207
|
+
# Location for list of genus file
|
|
208
|
+
self.genusdb = f"{self.sys_path}/data/genus_line.txt"
|
|
209
|
+
|
|
210
|
+
# Make blast / mmseqs db caching directory
|
|
211
|
+
self.in_db = f"{self.sys_path}/db"
|
|
212
|
+
mkdir(self.in_db)
|
|
213
|
+
mkdir(f"{self.in_db}/mmseqs")
|
|
214
|
+
mkdir(f"{self.in_db}/blast")
|
|
215
|
+
|
|
216
|
+
def init_workspace(self, root, opt):
|
|
217
|
+
# Workspace directory
|
|
218
|
+
# root will be current run folder in "Result"
|
|
219
|
+
if opt.outdir is None:
|
|
220
|
+
self.root = root
|
|
221
|
+
# if output directory designated, use it
|
|
222
|
+
else:
|
|
223
|
+
self.root = f"{opt.outdir}/{opt.runname}"
|
|
224
|
+
mkdir(self.root)
|
|
225
|
+
|
|
226
|
+
# Change path to absolute to prevent RAxML error
|
|
227
|
+
self.root = os.path.abspath(self.root)
|
|
228
|
+
|
|
229
|
+
# Logging directory
|
|
230
|
+
self.log = f"{self.root}/log.txt"
|
|
231
|
+
self.extlog = f"{self.root}/log" # for saving external program logs
|
|
232
|
+
mkdir(self.extlog)
|
|
233
|
+
|
|
234
|
+
# main workspace
|
|
235
|
+
self.data = f"{self.root}"
|
|
236
|
+
|
|
237
|
+
# saving directory
|
|
238
|
+
self.save = f"{self.root}/save.shelve"
|
|
239
|
+
|
|
240
|
+
# GenMine downloader points
|
|
241
|
+
self.GenMine = f"{self.root}/00_GenMine"
|
|
242
|
+
mkdir(self.GenMine)
|
|
243
|
+
self.GenMine_tmp = f"{self.root}/GenMine/tmp"
|
|
244
|
+
mkdir(self.GenMine_tmp)
|
|
245
|
+
|
|
246
|
+
# DB input save point. Edited from io function
|
|
247
|
+
self.out_db = f"{self.root}/01_DB"
|
|
248
|
+
mkdir(self.out_db)
|
|
249
|
+
|
|
250
|
+
# Query sequence save point
|
|
251
|
+
self.out_query = f"{self.root}/02_Query"
|
|
252
|
+
mkdir(self.out_query)
|
|
253
|
+
|
|
254
|
+
# BLAST or mmseqss result saving point
|
|
255
|
+
self.out_matrix = f"{self.root}/03_Search"
|
|
256
|
+
mkdir(self.out_matrix)
|
|
257
|
+
|
|
258
|
+
# Outgroup adjusted before aligned sequence file
|
|
259
|
+
self.out_adjusted = f"{self.root}/04_Dataset"
|
|
260
|
+
mkdir(self.out_adjusted)
|
|
261
|
+
|
|
262
|
+
# Alignment file directory (non-trimmed, trimmed and concatenated)
|
|
263
|
+
self.out_alignment = f"{self.root}/05_Alignment"
|
|
264
|
+
mkdir(self.out_alignment)
|
|
265
|
+
# Also make alignment hash directory
|
|
266
|
+
mkdir(f"{self.out_alignment}/hash")
|
|
267
|
+
# For failed alignments
|
|
268
|
+
mkdir(f"{self.out_alignment}/failed")
|
|
269
|
+
|
|
270
|
+
# modeltest result directory
|
|
271
|
+
self.out_modeltest = f"{self.root}/06_Modeltest"
|
|
272
|
+
mkdir(self.out_modeltest)
|
|
273
|
+
|
|
274
|
+
# Tree directory
|
|
275
|
+
self.out_tree = f"{self.root}/07_Tree"
|
|
276
|
+
mkdir(self.out_tree)
|
|
277
|
+
|
|
278
|
+
# tmpfile directory
|
|
279
|
+
self.tmp = f"{self.root}/99_tmp"
|
|
280
|
+
mkdir(self.tmp)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
## Main run in initialize.py ##
|
|
284
|
+
def initialize(path_run, parser):
|
|
285
|
+
# Path expression for Linux system
|
|
286
|
+
# Use pathlib for better expression
|
|
287
|
+
if sys.platform != "win32":
|
|
288
|
+
path_run = path_run.replace("\\", "/")
|
|
289
|
+
|
|
290
|
+
# Move option loading as independent class or function
|
|
291
|
+
# Generate path class
|
|
292
|
+
print(f"Output location: {path_run}")
|
|
293
|
+
path = Path(path_run)
|
|
294
|
+
|
|
295
|
+
# Parsing options
|
|
296
|
+
opt, list_info, list_warning, list_error = initialize_option(parser, path_run)
|
|
297
|
+
|
|
298
|
+
# Make Run path
|
|
299
|
+
path_root = f"{path_run}/{opt.runname}"
|
|
300
|
+
|
|
301
|
+
# Setup path
|
|
302
|
+
path.init_workspace(path_root, opt)
|
|
303
|
+
|
|
304
|
+
# Clean up temporary files before start
|
|
305
|
+
for file in os.listdir(path.root):
|
|
306
|
+
if any(
|
|
307
|
+
file.endswith(x) for x in [".fasta", ".png", ".nwk", "nhr", "nin", "nsq"]
|
|
308
|
+
):
|
|
309
|
+
os.remove(f"{path.root}/{file}")
|
|
310
|
+
|
|
311
|
+
# make log file
|
|
312
|
+
log = open(path.log, "a")
|
|
313
|
+
|
|
314
|
+
return opt, path, list_info, list_warning, list_error
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
# Initialize available genus list
|
|
318
|
+
def get_genus_list(V, opt, path):
|
|
319
|
+
# put this to option in further development
|
|
320
|
+
# if opt.use_default_genus_list is True:
|
|
321
|
+
if 1:
|
|
322
|
+
with open(path.genusdb, "r") as f:
|
|
323
|
+
genus_set = set(f.read().splitlines())
|
|
324
|
+
# else:
|
|
325
|
+
# genus_set = set()
|
|
326
|
+
|
|
327
|
+
for FI in V.list_FI:
|
|
328
|
+
if not (FI.genus == ""):
|
|
329
|
+
genus_set.add(FI.genus)
|
|
330
|
+
|
|
331
|
+
if len(genus_set) == 0:
|
|
332
|
+
print("[Warning] No genus list found!")
|
|
333
|
+
|
|
334
|
+
V.tup_genus = tuple(genus_set)
|
|
335
|
+
return V
|
src/logger.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
import os
|
|
3
|
+
import shelve
|
|
4
|
+
import subprocess
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CustomFormatter(logging.Formatter):
|
|
9
|
+
# Initialize ansicode on windows
|
|
10
|
+
if os.name == "nt": # Only if we are running on Windows
|
|
11
|
+
from ctypes import windll
|
|
12
|
+
|
|
13
|
+
k = windll.kernel32
|
|
14
|
+
k.SetConsoleMode(k.GetStdHandle(-11), 7)
|
|
15
|
+
|
|
16
|
+
# ANSI \x1b
|
|
17
|
+
# UNICODE \u001b
|
|
18
|
+
|
|
19
|
+
grey = "\x1b[38;20m"
|
|
20
|
+
yellow = "\x1b[33;20m"
|
|
21
|
+
red = "\x1b[31;20m"
|
|
22
|
+
bold_red = "\x1b[31;1m"
|
|
23
|
+
white = "\x1b[38;97m"
|
|
24
|
+
reset = "\x1b[0m"
|
|
25
|
+
fmt = "%(asctime)s [%(levelname)s] %(message)s"
|
|
26
|
+
fmt_err = "%(asctime)s [%(levelname)s] %(message)s (%(filename)s:%(lineno)d)"
|
|
27
|
+
|
|
28
|
+
FORMATS = {
|
|
29
|
+
logging.DEBUG: grey + fmt + reset,
|
|
30
|
+
logging.INFO: white + fmt + reset,
|
|
31
|
+
logging.WARNING: yellow + fmt_err + reset,
|
|
32
|
+
logging.ERROR: bold_red + fmt_err + reset,
|
|
33
|
+
logging.CRITICAL: bold_red + fmt_err + reset,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
def format(self, record):
|
|
37
|
+
log_fmt = self.FORMATS.get(record.levelno)
|
|
38
|
+
formatter = logging.Formatter(log_fmt, datefmt="%m/%d/%Y %I:%M:%S %p")
|
|
39
|
+
return formatter.format(record)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def setup_logging(list_info, list_warning, list_error, path, opt, tool):
|
|
43
|
+
# setup logging
|
|
44
|
+
logging.basicConfig(
|
|
45
|
+
format="%(asctime)s [%(levelname)s] %(message)s",
|
|
46
|
+
datefmt="%m/%d/%Y %I:%M:%S %p",
|
|
47
|
+
encoding="utf-8",
|
|
48
|
+
level=tool.get_level(opt.verbose),
|
|
49
|
+
handlers=[logging.FileHandler(path.log), logging.StreamHandler()],
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Set the custom formatter
|
|
53
|
+
formatter = CustomFormatter()
|
|
54
|
+
for handler in logging.root.handlers:
|
|
55
|
+
handler.setFormatter(formatter)
|
|
56
|
+
|
|
57
|
+
# Delayed logging for option parsing
|
|
58
|
+
|
|
59
|
+
# I don't know why, but in some environment, ANSI color works only after first subprocess.call was done
|
|
60
|
+
_ = subprocess.call("", shell=True)
|
|
61
|
+
|
|
62
|
+
for info in list_info:
|
|
63
|
+
logging.info(info)
|
|
64
|
+
|
|
65
|
+
for warning in list_warning:
|
|
66
|
+
logging.warning(warning)
|
|
67
|
+
|
|
68
|
+
for error in list_error:
|
|
69
|
+
logging.error(error)
|
src/logics.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import re
|
|
3
|
+
from matplotlib import colors as mcolors # for color debugging
|
|
4
|
+
|
|
5
|
+
# Move all simple logics function deciding True all False here
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def isnan(value) -> bool:
|
|
9
|
+
if type(value) is float or type(value) is np.float64:
|
|
10
|
+
if np.isnan(value):
|
|
11
|
+
return True
|
|
12
|
+
return False
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def isvalidcolor(color: str) -> bool:
|
|
16
|
+
|
|
17
|
+
colors = dict(mcolors.BASE_COLORS, **mcolors.CSS4_COLORS)
|
|
18
|
+
# Check if color is named colors
|
|
19
|
+
if color in colors.keys():
|
|
20
|
+
return True
|
|
21
|
+
# Check if color is available hex color
|
|
22
|
+
elif re.fullmatch(r"^#(?:[0-9a-fA-F]{3}){1,2}$", color):
|
|
23
|
+
return True
|
|
24
|
+
else:
|
|
25
|
+
return False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def isnewicklegal(string: str) -> bool:
|
|
29
|
+
"""
|
|
30
|
+
if string is newick legal -> return True
|
|
31
|
+
if string is newick illegal -> return False
|
|
32
|
+
"""
|
|
33
|
+
NEWICK_ILLEGAL = (
|
|
34
|
+
"(",
|
|
35
|
+
'"',
|
|
36
|
+
"[",
|
|
37
|
+
":",
|
|
38
|
+
";",
|
|
39
|
+
"/",
|
|
40
|
+
"[",
|
|
41
|
+
"]",
|
|
42
|
+
"{",
|
|
43
|
+
"}",
|
|
44
|
+
"(",
|
|
45
|
+
")",
|
|
46
|
+
",",
|
|
47
|
+
"]",
|
|
48
|
+
"+",
|
|
49
|
+
'"',
|
|
50
|
+
")",
|
|
51
|
+
" ",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if any(x in string for x in NEWICK_ILLEGAL):
|
|
55
|
+
return True
|
|
56
|
+
else:
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def isuniquecolumn(
|
|
61
|
+
list_column: list, column: tuple, table_name: str, check_none: bool = True
|
|
62
|
+
) -> bool:
|
|
63
|
+
|
|
64
|
+
"""
|
|
65
|
+
if given column is unique in list_column -> return True
|
|
66
|
+
elif give column is not in list_column -> return False (Error when check_none is True)
|
|
67
|
+
if more than one column found -> raise Error
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
# Lower and make safe column names
|
|
71
|
+
columns = [x.lower().strip() for x in list_column]
|
|
72
|
+
|
|
73
|
+
# Count given columns
|
|
74
|
+
# check ambiguities
|
|
75
|
+
cnt_column = 0
|
|
76
|
+
selected_column = None
|
|
77
|
+
|
|
78
|
+
for col in column:
|
|
79
|
+
cnt_column += columns.count(col)
|
|
80
|
+
if columns.count(col) != 0:
|
|
81
|
+
selected_column = col
|
|
82
|
+
|
|
83
|
+
# If only one of the given column exists
|
|
84
|
+
if cnt_column == 1:
|
|
85
|
+
return selected_column
|
|
86
|
+
# If column does not exists and column is not mandatory
|
|
87
|
+
elif check_none is False and cnt_column == 0:
|
|
88
|
+
return False
|
|
89
|
+
# If more than one column exists
|
|
90
|
+
elif cnt_column > 1:
|
|
91
|
+
logging.error(
|
|
92
|
+
f'More than 1 column of "{" or ".join(column)}" found in {table_name}'
|
|
93
|
+
)
|
|
94
|
+
raise Exception
|
|
95
|
+
# If column is mandatory and does not exists
|
|
96
|
+
elif check_none is True and cnt_column == 0:
|
|
97
|
+
logging.error(f'Column "{column}" is mandatory, but not found in {table_name}')
|
|
98
|
+
raise Exception
|
|
99
|
+
else:
|
|
100
|
+
logging.error(f"Unknown error occured while checking table column names")
|
|
101
|
+
logging.debug(columns)
|
|
102
|
+
logging.debug(column)
|
|
103
|
+
logging.debug(cnt_column)
|
|
104
|
+
raise Exception
|