1+ #!/usr/bin/env python3
12"""Parse a directory of GenBank files with BioCracker and dump readouts to JSONL."""
23
34from __future__ import annotations
78import os
89import sys
910from concurrent .futures import ProcessPoolExecutor
11+ from itertools import islice
1012from pathlib import Path
1113from typing import Iterable , Sequence
1214
1820
1921
2022def _iter_gbk_files (root : Path , exts : Sequence [str ]) -> Iterable [Path ]:
21- """Yield GenBank files under root matching extensions."""
22- for ext in exts :
23- yield from root .rglob (f"*{ ext } " )
23+ """Yield GenBank files under root matching extensions (single tree walk)."""
24+ exts = {e .lower () for e in exts }
25+ for p in root .rglob ("*" ):
26+ if p .is_file () and p .suffix .lower () in exts :
27+ yield p
2428
2529
2630def _process_file (
@@ -70,7 +74,6 @@ def _process_file(
7074 if acc :
7175 accessions .add (acc )
7276
73- # Keep the full record alongside the readout (matches repo helpers)
7477 readouts_for_target .append ({
7578 "rec" : rec ,
7679 "readout" : readout_dict .get ("readout" , []),
@@ -131,6 +134,12 @@ def cli() -> argparse.Namespace:
131134 default = 8 ,
132135 help = "Chunksize for process pool mapping" ,
133136 )
137+ parser .add_argument (
138+ "--discover-batch" ,
139+ type = int ,
140+ default = 1000 ,
141+ help = "Number of files to queue at a time before dispatching to workers" ,
142+ )
134143 return parser .parse_args ()
135144
136145
@@ -140,40 +149,54 @@ def main() -> None:
140149 if not input_dir .exists ():
141150 raise SystemExit (f"Input directory does not exist: { input_dir } " )
142151
143- files = list (_iter_gbk_files (input_dir , args .exts ))
144- if not files :
145- raise SystemExit ("No GenBank files found." )
146-
147152 args .output .parent .mkdir (parents = True , exist_ok = True )
148153
149- total = len (files )
150154 processed = 0
151155 successes = 0
152156 errors = 0
153157
158+ cache_arg = str (args .cache_dir ) if args .cache_dir else None
159+ file_iter = _iter_gbk_files (input_dir , args .exts )
160+
154161 with args .output .open ("w" , encoding = "utf-8" ) as outf :
155162 with ProcessPoolExecutor (max_workers = args .workers ) as executor :
156- with tqdm (total = total , desc = "Parsing GBKs" , unit = "file" ) as pbar :
157- for result in executor .map (
158- _process_file ,
159- (str (p ) for p in files ),
160- [str (args .cache_dir ) if args .cache_dir else None ] * len (files ),
161- [args .pred_threshold ] * len (files ),
162- chunksize = args .chunksize ,
163- ):
164- processed += 1
165- pbar .update (1 )
166-
167- if not result :
168- continue
169-
170- if isinstance (result , dict ) and "error" in result :
171- errors += 1
172- else :
173- successes += 1
174-
175- outf .write (json .dumps (result , ensure_ascii = False ) + "\n " )
176- pbar .set_postfix (ok = successes , errors = errors )
163+ # Start with unknown total, grow as we discover files
164+ with tqdm (total = 0 , desc = "Parsing GBKs" , unit = "file" ) as pbar :
165+ while True :
166+ batch = list (islice (file_iter , args .discover_batch ))
167+ if not batch :
168+ break
169+
170+ # Increase total as new files are discovered
171+ pbar .total += len (batch )
172+ pbar .refresh ()
173+
174+ for result in executor .map (
175+ _process_file ,
176+ (str (p ) for p in batch ),
177+ [cache_arg ] * len (batch ),
178+ [args .pred_threshold ] * len (batch ),
179+ chunksize = args .chunksize ,
180+ ):
181+ processed += 1
182+ pbar .update (1 )
183+
184+ if not result :
185+ continue
186+
187+ if isinstance (result , dict ) and "error" in result :
188+ errors += 1
189+ else :
190+ successes += 1
191+
192+ outf .write (json .dumps (result , ensure_ascii = False ) + "\n " )
193+ pbar .set_postfix (ok = successes , errors = errors )
194+
195+ # Optional: final summary to stderr
196+ print (
197+ f"Done. processed={ processed } , ok={ successes } , errors={ errors } " ,
198+ file = sys .stderr ,
199+ )
177200
178201
179202if __name__ == "__main__" :
0 commit comments