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/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