1717#
1818"""
1919Selectively extract reads following Centrifuge/Kraken output.
20+ Support for both FASTQ and FASTA sequence files.
2021"""
2122# pylint: disable=no-name-in-module, not-an-iterable
2223import argparse
@@ -69,7 +70,7 @@ def main():
6970 metavar = 'NUMBER' ,
7071 type = int ,
7172 default = None ,
72- help = ('limit of FASTQ reads to extract; '
73+ help = ('limit of sequence reads to extract; '
7374 'default: no limit' )
7475 )
7576 parser .add_argument (
@@ -78,7 +79,7 @@ def main():
7879 metavar = 'NUMBER' ,
7980 type = int ,
8081 default = None ,
81- help = ('maximum number of FASTQ reads to search for the taxa; '
82+ help = ('maximum number of sequence reads to search for the taxa; '
8283 'default: no maximum' )
8384 )
8485 parser .add_argument (
@@ -127,6 +128,15 @@ def main():
127128 help = ('minimum score/confidence of the classification of a read '
128129 'to pass the quality filter; all pass by default' )
129130 )
131+ parser .add_argument (
132+ '-z' , '--maxscore' ,
133+ action = 'store' ,
134+ metavar = 'NUMBER' ,
135+ type = lambda txt : Score (float (txt )),
136+ default = None ,
137+ help = ('maximum score/confidence of the classification of a read '
138+ 'to pass the quality filter; all pass by default' )
139+ )
130140 parser_in = parser .add_argument_group (
131141 'input' , 'Define Rextract input files' )
132142 filein = parser_in .add_mutually_exclusive_group (required = True )
@@ -135,28 +145,34 @@ def main():
135145 action = 'store' ,
136146 metavar = 'FILE' ,
137147 default = None ,
138- help = 'single FASTQ file (no paired-ends), which may be gzipped'
148+ help = 'single sequence file (no paired-ends), which may be gzipped'
139149 )
140150 filein .add_argument (
141151 '-1' , '--mate1' ,
142152 action = 'store' ,
143153 metavar = 'FILE' ,
144154 default = None ,
145- help = 'paired-ends FASTQ file (gzipped or not) for mate 1s '
155+ help = 'paired-ends sequence file (gzipped or not) for mate 1s '
146156 '(filename usually includes _1)'
147157 )
148158 parser_in .add_argument (
149159 '-2' , '--mate2' ,
150160 action = 'store' ,
151161 metavar = 'FILE' ,
152162 default = None ,
153- help = 'paired-ends FASTQ file (gzipped or not) for mate 2s '
163+ help = 'paired-ends sequence file (gzipped or not) for mate 2s '
154164 '(filename usually includes _2)'
155165 )
156166 parser .add_argument (
157167 '-c' , '--compress' ,
158168 action = 'store_true' ,
159- help = 'any generated FASTQ file will be gzipped'
169+ help = 'any generated sequence file will be gzipped'
170+ )
171+ parser .add_argument (
172+ '-a' , '--fasta' ,
173+ action = 'store_true' ,
174+ help = 'treat all the input and output sequence files as FASTA '
175+ 'instead of the default FASTQ'
160176 )
161177 parser .add_argument (
162178 '-V' , '--version' ,
@@ -190,12 +206,12 @@ def main():
190206 unclass : bool = args .unclassified
191207 excluding : Set [Id ] = set (args .exclude )
192208 including : Set [Id ] = set (args .include )
193- fastq_1 : Filename
194- fastq_2 : Filename = args .mate2
195- if not fastq_2 :
196- fastq_1 = args .fastq
209+ fast_1 : Filename
210+ fast_2 : Filename = args .mate2
211+ if not fast_2 :
212+ fast_1 = args .fastq
197213 else :
198- fastq_1 = args .mate1
214+ fast_1 = args .mate1
199215
200216 check_debug ()
201217
@@ -259,9 +275,10 @@ def main():
259275 continue
260276 score : Score = Score (record .annotations ['score' ])
261277 if args .minscore is not None and score < args .minscore :
262- continue # Ignore read if low confidence
263- else :
264- records .append (record )
278+ continue # Ignore read if confidence below threshold
279+ if args .maxscore is not None and score > args .maxscore :
280+ continue # Ignore read if confidence above threshold
281+ records .append (record )
265282 except FileNotFoundError :
266283 raise Exception (red ('ERROR!' ) + 'Cannot read "' +
267284 output_file + '"' )
@@ -287,34 +304,36 @@ def main():
287304 except FileNotFoundError :
288305 return False
289306
290- # FASTQ sequence dealing
307+ # FASTQ/FASTA sequence dealing
291308 records_ids : Set [str ] = {record .id for record in records
292309 if record .id is not None }
293310 seqs1 : List [SeqRecord .SeqRecord ] = []
294311 seqs2 : List [SeqRecord .SeqRecord ] = []
295312 extracted : int = 0
296313 i : int = 0
297- if fastq_2 :
298- print (gray ('Loading FASTQ files' ), fastq_1 , gray ('and' ), fastq_2 ,
314+ seq_format = 'quickfasta' if args .fasta else 'quickfastq'
315+ seq_label = 'FASTA' if args .fasta else 'FASTQ'
316+ if fast_2 :
317+ print (gray (f'Loading { seq_label } files' ), fast_1 , gray ('and' ), fast_2 ,
299318 gray ('...\n Mseqs: ' ), end = '' )
300319 sys .stdout .flush ()
301320 mate1handler : Callable [..., TextIO ] = open
302321 mate2handler : Callable [..., TextIO ] = open
303- if is_gzipped (fastq_1 ):
322+ if is_gzipped (fast_1 ):
304323 mate1handler = gzip .open
305324 else :
306325 mate1handler = open
307- if is_gzipped (fastq_2 ):
326+ if is_gzipped (fast_2 ):
308327 mate2handler = gzip .open
309328 else :
310329 mate2handler = open
311330 try :
312- with mate1handler (fastq_1 , 'rt' ) as file1 , \
313- mate2handler (fastq_2 , 'rt' ) as file2 :
331+ with mate1handler (fast_1 , 'rt' ) as file1 , \
332+ mate2handler (fast_2 , 'rt' ) as file2 :
314333 for i , (rec1 , rec2 ) in enumerate (zip (SeqIO .parse (file1 ,
315- 'quickfastq' ),
316- SeqIO .parse (file2 ,
317- 'quickfastq' ))
334+ seq_format ),
335+ SeqIO .parse (file2 ,
336+ seq_format ))
318337 ):
319338 if not records_ids :
320339 print (green (' [all records found]' ), end = '' )
@@ -344,17 +363,17 @@ def main():
344363 extracted += 1
345364
346365 except FileNotFoundError :
347- raise Exception ('\n \033 [91mERROR!\033 [0m Cannot read FASTQ files' )
366+ raise Exception (f '\n \033 [91mERROR!\033 [0m Cannot read { seq_label } files' )
348367 else :
349- print (gray ('Loading FASTQ file' ), f'{ fastq_1 } ' , gray ('...\n Mseqs: ' ),
368+ print (gray (f 'Loading { seq_label } file' ), f'{ fast_1 } ' , gray ('...\n Mseqs: ' ),
350369 end = '' )
351370 sys .stdout .flush ()
352371 fq1handler : Callable [..., TextIO ] = open
353- if is_gzipped (fastq_1 ):
372+ if is_gzipped (fast_1 ):
354373 fq1handler = gzip .open
355374 try :
356- with fq1handler (fastq_1 , 'rt' ) as file1 :
357- for i , rec1 in enumerate (SeqIO .parse (file1 , 'quickfastq' )):
375+ with fq1handler (fast_1 , 'rt' ) as file1 :
376+ for i , rec1 in enumerate (SeqIO .parse (file1 , seq_format )):
358377 if not records_ids :
359378 print (green (' [all records found]' ), end = '' )
360379 break
@@ -376,7 +395,7 @@ def main():
376395 seqs1 .append (rec1 )
377396 extracted += 1
378397 except FileNotFoundError :
379- raise Exception ('\n \033 [91mERROR!\033 [0m Cannot read FASTQ file' )
398+ raise Exception (f '\n \033 [91mERROR!\033 [0m Cannot read { seq_label } file' )
380399 print (cyan (f' { i / 1e+6 :.3g} Mseqs' ), green ('OK! ' ))
381400 missing : int = len (records_ids )
382401 if i > 0 :
@@ -385,30 +404,30 @@ def main():
385404 else :
386405 print (extracted , gray ('successfully extracted reads' ))
387406 if not extracted :
388- print (red ('ERROR!' ), 'No matching read(s) in the FASTQ file(s)!' )
407+ print (red ('ERROR!' ), f 'No matching read(s) in the { seq_label } file(s)!' )
389408 sys .exit (2 )
390409 if missing :
391410 print (yellow ('WARNING!' ), f'{ missing } reads from Centrifuge output' ,
392- 'not found in the FASTQ file(s)!' )
411+ f 'not found in the { seq_label } file(s)!' )
393412 sys .stdout .flush ()
394413
395414
396- def format_filename (fastq : Filename ) -> Filename :
415+ def format_filename (fast : Filename ) -> Filename :
397416 """Auxiliary function to properly format the output filenames.
398417
399418 Args:
400- fastq : Complete filename of the fastq input file (gzipped or not)
419+ fast : Complete filename of the sequence input file (gzipped or not)
401420
402- Returns: Filename of the rextracted fastq output file
421+ Returns: Filename of the rextracted sequence output file
403422 """
404423 # Get filename and extension, accounting for 2 extensions,
405- # typically found in filename.fastq.gz
406- fastq_filename , fastq_ext = os .path .splitext (fastq )
407- if fastq_ext .casefold () == GZEXT :
408- fastq_filename , fastq_ext = os .path .splitext (fastq_filename )
409- if args .compress and not fastq_ext .casefold ().endswith (GZEXT ):
410- fastq_ext += GZEXT
411- output_list : List [str ] = [fastq_filename , '_rxtr' ]
424+ # typically found in filename.fasta.gz or . fastq.gz
425+ fast_filename , fast_ext = os .path .splitext (fast )
426+ if fast_ext .casefold () == GZEXT :
427+ fast_filename , fast_ext = os .path .splitext (fast_filename )
428+ if args .compress and not fast_ext .casefold ().endswith (GZEXT ):
429+ fast_ext += GZEXT
430+ output_list : List [str ] = [fast_filename , '_rxtr' ]
412431 if unclass :
413432 output_list .append ('_unclass' )
414433 else :
@@ -418,7 +437,7 @@ def main():
418437 if excluding :
419438 output_list .append ('_excl' )
420439 output_list .extend ('_' .join (excluding ))
421- output_list .append (fastq_ext )
440+ output_list .append (fast_ext )
422441 return Filename ('' .join (output_list ))
423442
424443 def write_seqs (seqs : List [SeqRecord .SeqRecord ], fname_in : Filename ,
@@ -427,17 +446,17 @@ def main():
427446 fname_out : Filename = format_filename (fname_in )
428447 if gzipped :
429448 with gzip .open (fname_out , 'wt' ) as fgz :
430- SeqIO .write (seqs , fgz , 'quickfastq' )
449+ SeqIO .write (seqs , fgz , seq_format )
431450 print (gray ('Compressed' ), magenta (f'{ len (seqs )} ' ),
432451 gray ('reads in' ), fname_out )
433452 else :
434- SeqIO .write (seqs , fname_out , 'quickfastq' )
453+ SeqIO .write (seqs , fname_out , seq_format )
435454 print (gray ('Wrote' ), magenta (f'{ len (seqs )} ' ),
436455 gray ('reads in' ), fname_out )
437456
438- write_seqs (seqs1 , fastq_1 , gzipped = args .compress )
439- if fastq_2 :
440- write_seqs (seqs2 , fastq_2 , gzipped = args .compress )
457+ write_seqs (seqs1 , fast_1 , gzipped = args .compress )
458+ if fast_2 :
459+ write_seqs (seqs2 , fast_2 , gzipped = args .compress )
441460
442461 # Timing results
443462 print (gray ('Total elapsed time:' ), time .strftime (
0 commit comments