PyamilySeq 0.4.0__py3-none-any.whl → 0.5.0__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.
PyamilySeq/Constants.py CHANGED
@@ -1,15 +1,2 @@
1
- import subprocess
1
+ PyamilySeq_Version = 'v0.5.0'
2
2
 
3
- PyamilySeq_Version = 'v0.4.0'
4
-
5
-
6
-
7
- def is_tool_installed(tool_name):
8
- """Check if a tool is installed and available in PATH."""
9
- try:
10
- subprocess.run([tool_name, '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
11
- return True
12
- except subprocess.CalledProcessError:
13
- return False
14
- except FileNotFoundError:
15
- return False
PyamilySeq/PyamilySeq.py CHANGED
@@ -9,64 +9,13 @@ from PyamilySeq_Species import *
9
9
  try:
10
10
  from .PyamilySeq_Species import cluster
11
11
  from .Constants import *
12
+ from .utils import *
12
13
  except (ModuleNotFoundError, ImportError, NameError, TypeError) as error:
13
14
  from PyamilySeq_Species import cluster
14
15
  from Constants import *
16
+ from utils import *
17
+
15
18
 
16
- def reverse_complement(seq):
17
- complement = {'A': 'T', 'T': 'A', 'G': 'C', 'C': 'G', 'N': 'N'}
18
- return ''.join(complement[base] for base in reversed(seq))
19
-
20
-
21
- def read_separate_files(input_dir, name_split, combined_out):
22
- with open(combined_out, 'w') as combined_out_file:
23
- for fasta_file in glob.glob(os.path.join(input_dir, '*' + name_split)):
24
- genome_name = os.path.basename(fasta_file).split(name_split)[0]
25
- corresponding_gff_file = fasta_file.replace('.fasta', '.gff')
26
- if not os.path.exists(corresponding_gff_file):
27
- continue
28
- cds_sequences = extract_cds_from_gff(fasta_file, corresponding_gff_file)
29
- for gene_name, seq in cds_sequences:
30
- header = f">{genome_name}_{gene_name}\n"
31
- combined_out_file.write(header)
32
- combined_out_file.write(seq + '\n')
33
-
34
- def read_combined_files(input_dir, name_split, combined_out):
35
- with open(combined_out, 'w') as combined_out_file:
36
- for gff_file in glob.glob(os.path.join(input_dir, '*' + name_split)):
37
- genome_name = os.path.basename(gff_file).split(name_split)[0]
38
- fasta_dict = collections.defaultdict(str)
39
- gff_features = []
40
- with open(gff_file, 'r') as file:
41
- lines = file.readlines()
42
- fasta_section = False
43
- for line in lines:
44
- if line.startswith('##FASTA'):
45
- fasta_section = True
46
- continue
47
- if fasta_section:
48
- if line.startswith('>'):
49
- current_contig = line[1:].split()[0]
50
- fasta_dict[current_contig] = []
51
- else:
52
- fasta_dict[current_contig].append(line.strip())
53
- else:
54
- line_data = line.split('\t')
55
- if len(line_data) == 9:
56
- if line_data[2] == 'CDS':
57
- contig = line_data[0]
58
- feature = line_data[2]
59
- start, end = int(line_data[3]), int(line_data[4])
60
- seq_id = line_data[8].split('ID=')[1].split(';')[0]
61
- gff_features.append((contig, start, end, seq_id))
62
-
63
- if fasta_dict and gff_features:
64
- for contig, start, end, seq_id in gff_features:
65
- if contig in fasta_dict:
66
- full_sequence = ''.join(fasta_dict[contig])
67
- cds_sequence = full_sequence[start - 1:end]
68
- wrapped_sequence = '\n'.join([cds_sequence[i:i + 60] for i in range(0, len(cds_sequence), 60)])
69
- combined_out_file.write(f">{genome_name}|{seq_id}\n{wrapped_sequence}\n")
70
19
 
71
20
 
72
21
  def run_cd_hit(input_file, clustering_output, options):
@@ -81,45 +30,66 @@ def run_cd_hit(input_file, clustering_output, options):
81
30
  '-sc', "1",
82
31
  '-sf', "1"
83
32
  ]
84
- subprocess.run(cdhit_command)
85
-
86
-
87
-
88
-
89
-
90
-
33
+ if options.verbose == True:
34
+ subprocess.run(cdhit_command)
35
+ else:
36
+ subprocess.run(cdhit_command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
91
37
 
92
38
 
93
39
  def main():
94
- parser = argparse.ArgumentParser(
95
- description='PyamilySeq ' + PyamilySeq_Version + ': PyamilySeq Run Parameters.')
40
+ parser = argparse.ArgumentParser(description='PyamilySeq ' + PyamilySeq_Version + ': PyamilySeq Run Parameters.')
41
+ ### Required Arguments
96
42
  required = parser.add_argument_group('Required Arguments')
97
- required.add_argument("-id", action="store", dest="input_dir",
98
- help="Directory containing GFF/FASTA files.",
99
- required=True)
100
- required.add_argument("-od", action="store", dest="output_dir",
101
- help="Directory for all output files.",
102
- required=True)
103
- required.add_argument("-it", action="store", dest="input_type", choices=['separate', 'combined'],
104
- help="Type of input files: 'separate' for separate FASTA and GFF files,"
105
- " 'combined' for GFF files with embedded FASTA sequences.",
106
- required=True)
107
- required.add_argument("-ns", action="store", dest="name_split",
108
- help="Character used to split the filename and extract the genome name.",
109
- required=True)
110
- required.add_argument("-pid", action="store", dest="pident", type=float,
111
- help="Pident threshold for CD-HIT clustering.",
43
+ required.add_argument('-run_mode', action='store', dest='run_mode', choices=['Full','Partial'],
44
+ help='Run Mode: Should PyamilySeq be run in "Full" or "Partial" mode?',
112
45
  required=True)
113
- required.add_argument("-ld", action="store", dest="len_diff", type=float,
114
- help="Length difference (-s) threshold for CD-HIT clustering.",
46
+ required.add_argument('-group_mode', action='store', dest='group_type', choices=['Species','Genus'],
47
+ help='Group Mode: Should PyamilySeq be run in "Species" or "Genus" mode?',
115
48
  required=True)
116
- required.add_argument("-co", action="store", dest="clustering_out",
117
- help="Output file for initial clustering.",
49
+ required.add_argument("-clust_tool", action="store", dest="clust_tool", choices=['CD-HIT'],
50
+ help="Clustering tool to use: CD-HIT, DIAMOND, BLAST or MMseqs2.",
118
51
  required=True)
119
- required.add_argument("-ct", action="store", dest="clustering_type", choices=['CD-HIT', 'BLAST', 'DIAMOND', "MMseqs2"],
120
- help="Clustering format for PyamilySeq.",
52
+ required.add_argument("-output_dir", action="store", dest="output_dir",
53
+ help="Directory for all output files.",
121
54
  required=True)
55
+ ### Full-Mode Arguments
56
+ full_mode_args = parser.add_argument_group('Full-Mode Arguments - Required when "-run_mode Full" is used')
57
+ full_mode_args.add_argument("-input_type", action="store", dest="input_type", choices=['separate', 'combined'],
58
+ help="Type of input files: 'separate' for separate FASTA and GFF files,"
59
+ " 'combined' for GFF files with embedded FASTA sequences.",
60
+ required=False)
61
+ full_mode_args.add_argument("-input_dir", action="store", dest="input_dir",
62
+ help="Directory containing GFF/FASTA files.",
63
+ required=False)
64
+ full_mode_args.add_argument("-name_split", action="store", dest="name_split",
65
+ help="substring used to split the filename and extract the genome name ('_combined.gff3' or '.gff').",
66
+ required=False)
67
+ full_mode_args.add_argument("-pid", action="store", dest="pident", type=float, default=0.95,
68
+ help="Default 0.95: Pident threshold for clustering.",
69
+ required=False)
70
+ full_mode_args.add_argument("-len_diff", action="store", dest="len_diff", type=float, default=0.80,
71
+ help="Default 0.80: Minimum length difference between clustered sequences - (-s) threshold for CD-HIT clustering.",
72
+ required=False)
73
+
74
+
75
+ ###Partial-Mode Arguments
76
+ partial_mode_args = parser.add_argument_group('Partial-Mode Arguments - Required when "-run_mode Partial" is used')
77
+ partial_mode_args.add_argument('-cluster_file', action='store', dest='cluster_file',
78
+ help='Clustering output file containing CD-HIT, TSV or CSV Edge List',
79
+ required=False)
80
+
81
+ ###Grouping Arguments
82
+ grouping_args = parser.add_argument_group('Grouping Arguments - Use to fine-tune grouping of genes after clustering')
83
+ grouping_args.add_argument('-reclustered', action='store', dest='reclustered', help='Clustering output file from secondary round of clustering',
84
+ required=False)
85
+ grouping_args.add_argument('-seq_tag', action='store', dest='sequence_tag', default='StORF',
86
+ help='Default - "StORF": Unique identifier to be used to distinguish the second of two rounds of clustered sequences',
87
+ required=False)
88
+ grouping_args.add_argument('-groups', action="store", dest='core_groups', default="99,95,15",
89
+ help='Default - (\'99,95,15\'): Gene family groups to use',
90
+ required=False)
122
91
 
92
+ ###Output Arguments
123
93
  output_args = parser.add_argument_group('Output Parameters')
124
94
  output_args.add_argument('-w', action="store", dest='write_families', default=None,
125
95
  help='Default - No output: Output sequences of identified families (provide levels at which to output "-w 99,95"'
@@ -129,58 +99,139 @@ def main():
129
99
  help='Default - No output: Output aligned and concatinated sequences of identified families - used for MSA (provide levels at which to output "-w 99,95"'
130
100
  ' - Must provide FASTA file with -fasta',
131
101
  required=False)
132
- output_args.add_argument('-fasta', action='store', dest='fasta',
133
- help='FASTA file to use in conjunction with "-w" or "-con"',
102
+ output_args.add_argument('-original_fasta', action='store', dest='original_fasta',
103
+ help='FASTA file to use in conjunction with "-w" or "-con" when running in Partial Mode.',
134
104
  required=False)
135
-
136
- optional = parser.add_argument_group('Optional Arguments')
137
- optional.add_argument('-rc', action='store', dest='reclustered', help='Clustering output file from secondary round of clustering',
138
- required=False)
139
- optional.add_argument('-st', action='store', dest='sequence_tag', help='Default - "StORF": Unique identifier to be used to distinguish the second of two rounds of clustered sequences',
105
+ output_args.add_argument('-gpa', action='store', dest='gene_presence_absence_out', help='Default - False: If selected, a Roary formatted gene_presence_absence.csv will be created - Required for Coinfinder and other downstream tools',
140
106
  required=False)
141
- optional.add_argument('-groups', action="store", dest='core_groups', default="99,95,15",
142
- help='Default - (\'99,95,15\'): Gene family groups to use')
143
- optional.add_argument('-gpa', action='store', dest='gene_presence_absence_out', help='Default - False: If selected, a Roary formatted gene_presence_absence.csv will be created - Required for Coinfinder and other downstream tools',
107
+
108
+ ### Misc Arguments
109
+ misc = parser.add_argument_group('Misc')
110
+ misc.add_argument('-verbose', action='store', dest='verbose', default=False, type=eval, choices=[True, False],
111
+ help='Default - False: Print out runtime messages',
112
+ required = False)
113
+ misc.add_argument('-v', action='store_true', dest='version',
114
+ help='Default - False: Print out version number and exit',
144
115
  required=False)
145
116
 
146
- parser.add_argument("pyamilyseq_args", nargs=argparse.REMAINDER, help="Additional arguments for PyamilySeq.")
147
117
  options = parser.parse_args()
148
118
 
149
-
119
+ ### Checking all required parameters are provided by user
120
+ if options.run_mode == 'Full':
121
+ required_full_mode = [options.input_type, options.input_dir, options.name_split, options.clust_tool,
122
+ options.pident, options.len_diff]
123
+ if all(required_full_mode):
124
+ # Proceed with the Full mode
125
+ pass
126
+ else:
127
+ missing_options = [opt for opt in
128
+ ['input_type', 'input_dir', 'name_split', 'clust_tool', 'pident', 'len_diff'] if
129
+ not options.__dict__[opt]]
130
+ print(f"Missing required options for Full mode: {', '.join(missing_options)}")
131
+ elif options.run_mode == 'Partial':
132
+ required_partial_mode = [options.cluster_file, ]
133
+ if all(required_partial_mode):
134
+ # Proceed with the Partial mode
135
+ pass
136
+ else:
137
+ missing_options = [opt for opt in
138
+ ['cluster_file',] if
139
+ not options.__dict__[opt]]
140
+ print(f"Missing required options for Partial mode: {', '.join(missing_options)}")
141
+
142
+ if options.clust_tool == 'CD-HIT':
143
+ clust_affix = '.clstr'
144
+ elif options.clust_tool == 'TSV':
145
+ clust_affix = '.tsv'
146
+ elif options.clust_tool == 'CSV':
147
+ clust_affix = '.csv'
148
+
149
+
150
+
151
+ ###External tool checks:
152
+ ##MAFFT
153
+ if options.con_core == True:
154
+ if is_tool_installed('mafft'):
155
+ if options.verbose == True:
156
+ print("mafft is installed. Proceeding with alignment.")
157
+ else:
158
+ exit("mafft is not installed. Please install mafft to proceed.")
159
+ ##CD-HIT
160
+ if options.clust_tool == 'CD-HIT':
161
+ if is_tool_installed('cd-hit'):
162
+ if options.verbose == True:
163
+ print("cd-hit is installed. Proceeding with clustering.")
164
+ else:
165
+ exit("cd-hit is not installed. Please install cd-hit to proceed.")
166
+
167
+ if options.write_families != None and options.original_fasta == False:
168
+ exit("-fasta must br provided if -w is used")
169
+
170
+ options.core_groups = options.core_groups + ',0'
171
+
172
+
173
+ if options.cluster_file:
174
+ options.cluster_file = fix_path(options.cluster_file)
175
+ if options.reclustered:
176
+ options.reclustered = fix_path(options.reclustered)
177
+ if options.input_dir:
178
+ options.input_dir = fix_path(options.input_dir)
179
+ if options.output_dir:
180
+ options.output_dir = fix_path(options.output_dir)
150
181
 
151
182
  output_path = os.path.abspath(options.output_dir)
152
- combined_out_file = os.path.join(output_path,"end_to_end_combined_sequences.fasta")
153
- clustering_output = os.path.join(output_path,'clustering_'+options.clustering_type)
183
+ combined_out_file = os.path.join(output_path, "combined_sequences.fasta")
184
+ clustering_output = os.path.join(output_path, 'clustering_' + options.clust_tool)
154
185
 
155
186
 
187
+ if options.run_mode == 'Full':
156
188
 
157
- # Step 1: Read and rename sequences from files based on input type
158
- if options.input_type == 'separate':
159
- read_separate_files(options.input_dir, options.name_split, combined_out_file)
160
- else:
161
- read_combined_files(options.input_dir, options.name_split, combined_out_file)
162
189
 
163
- # Step 2: Run CD-HIT on the renamed sequences
164
- run_cd_hit(combined_out_file, clustering_output, options)
190
+
191
+ if options.input_type == 'separate':
192
+ read_separate_files(options.input_dir, options.name_split, combined_out_file)
193
+ else:
194
+ read_combined_files(options.input_dir, options.name_split, combined_out_file)
195
+
196
+ run_cd_hit(combined_out_file, clustering_output, options)
197
+ class clustering_options:
198
+ def __init__(self):
199
+ self.cluster_format = options.clust_tool
200
+ self.reclustered = options.reclustered
201
+ self.sequence_tag = options.sequence_tag
202
+ self.core_groups = '99,95,15,0'
203
+ self.clusters = clustering_output + clust_affix
204
+ self.gene_presence_absence_out = options.gene_presence_absence_out
205
+ self.write_families = options.write_families
206
+ self.con_core = options.con_core
207
+ self.fasta = combined_out_file
208
+ self.verbose = options.verbose
209
+
210
+ clustering_options = clustering_options()
211
+
212
+ elif options.run_mode == 'Partial':
213
+ class clustering_options:
214
+ def __init__(self):
215
+ self.cluster_format = options.clust_tool
216
+ self.reclustered = options.reclustered
217
+ self.sequence_tag = options.sequence_tag
218
+ self.core_groups = '99,95,15,0'
219
+ self.clusters = options.cluster_file
220
+ self.gene_presence_absence_out = options.gene_presence_absence_out
221
+ self.write_families = options.write_families
222
+ self.con_core = options.con_core
223
+ self.fasta = options.original_fasta
224
+ self.verbose = options.verbose
225
+
226
+ clustering_options = clustering_options()
165
227
 
166
228
 
167
- class clustering_options:
168
- def __init__(self):
169
- self.format = 'CD-HIT'
170
- self.reclustered = options.reclustered
171
- self.sequence_tag = 'StORF'
172
- self.core_groups = '99,95,15,0'
173
- self.clusters = clustering_output+'.clstr'
174
- self.gene_presence_absence_out = options.gene_presence_absence_out
175
- self.write_families = options.write_families
176
- self.con_core = options.con_core
177
229
 
178
- clustering_options = clustering_options()
179
230
 
180
- # Step 3: Run PyamilySeq with the CD-HIT output
181
231
  cluster(clustering_options)
182
- #run_pyamilyseq(options.clustering_out, options.clustering_type, combined_out_file, options.pyamilyseq_args)
183
232
 
233
+ print("Thank you for using PyamilySeq -- A detailed user manual can be found at https://github.com/NickJD/PyamilySeq\n"
234
+ "Please report any issues to: https://github.com/NickJD/PyamilySeq/issues\n#####")
184
235
 
185
236
  if __name__ == "__main__":
186
237
  main()