Skip to content

Commit e022697

Browse files
committed
UPD: don't list files first, just greedily iterat over them
1 parent dc35d9d commit e022697

1 file changed

Lines changed: 75 additions & 63 deletions

File tree

scripts/filter_gbk_by_product.py

Lines changed: 75 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -8,59 +8,74 @@
88
import re
99
import shutil
1010
from pathlib import Path
11-
from typing import Iterable, Sequence
11+
from typing import Sequence
1212

1313
from tqdm import tqdm
1414

15-
16-
# Regex to capture all product qualifiers
15+
# Regex to capture all product qualifiers on a line
1716
PRODUCT_RE = re.compile(r'/product="([^"]*)"', re.IGNORECASE)
1817

1918

20-
def _iter_gbk_files(root: Path, exts: Sequence[str]) -> Iterable[Path]:
21-
"""Yield all files with specified extensions."""
22-
exts = {e.lower() for e in exts}
23-
for p in root.rglob("*"):
24-
if p.is_file() and p.suffix.lower() in exts:
25-
yield p
19+
def _iter_gbk_files(root: Path, exts: Sequence[str]):
20+
"""Greedily walk the tree and yield matching files as we see them."""
21+
exts = tuple(ext.lower() for ext in exts)
22+
root = str(root)
23+
for dirpath, _dirnames, filenames in os.walk(root):
24+
for name in filenames:
25+
# simple suffix match, case-insensitive
26+
lower_name = name.lower()
27+
if lower_name.endswith(exts):
28+
yield Path(dirpath) / name
2629

2730

28-
def _file_matches(
29-
gbk_path: str,
30-
products: Sequence[str],
31-
) -> dict | None:
31+
def _file_matches(gbk_path: Path, products: Sequence[str]) -> dict | None:
3232
"""
33-
Return match info if the GBK contains cand_cluster/protocluster and has a product EXACTLY equal
34-
to one of the given search terms (case-insensitive).
33+
Return match info if the GBK contains cand_cluster/protocluster and has a product
34+
EXACTLY equal to one of the given search terms (case-insensitive).
35+
Stream + early exit.
3536
"""
36-
path = Path(gbk_path)
37+
has_cluster = False
38+
matched: set[str] = set()
3739

3840
try:
39-
text = path.read_text(errors="ignore")
40-
except Exception as exc:
41-
return {"error": f"read failed: {exc}", "file": gbk_path}
42-
43-
lower = text.lower()
44-
45-
# quick skip
46-
if "cand_cluster" not in lower and "protocluster" not in lower:
47-
return None
48-
49-
# extract all /product="..."`
50-
found_products = PRODUCT_RE.findall(text)
51-
found_products_norm = {fp.strip().lower() for fp in found_products}
52-
53-
# find exact matches
54-
matched = sorted(fp for fp in found_products_norm if fp in products)
41+
with gbk_path.open("r", errors="ignore") as fh:
42+
for line in fh:
43+
lower = line.lower()
44+
45+
# detect cluster keywords
46+
if not has_cluster and ("cand_cluster" in lower or "protocluster" in lower):
47+
has_cluster = True
48+
if matched:
49+
return {
50+
"file": gbk_path.name,
51+
"path": str(gbk_path),
52+
"matched_products": sorted(matched),
53+
}
54+
55+
# detect product qualifiers
56+
if '/product="' in line:
57+
for m in PRODUCT_RE.finditer(line):
58+
prod_val = m.group(1).strip().lower()
59+
if prod_val in products:
60+
matched.add(prod_val)
61+
if has_cluster:
62+
return {
63+
"file": gbk_path.name,
64+
"path": str(gbk_path),
65+
"matched_products": sorted(matched),
66+
}
67+
68+
if has_cluster and matched:
69+
return {
70+
"file": gbk_path.name,
71+
"path": str(gbk_path),
72+
"matched_products": sorted(matched),
73+
}
5574

56-
if not matched:
5775
return None
5876

59-
return {
60-
"file": path.name,
61-
"path": str(path),
62-
"matched_products": matched,
63-
}
77+
except Exception as exc:
78+
return {"error": f"read failed: {exc}", "file": str(gbk_path), "detail": str(exc)}
6479

6580

6681
def cli() -> argparse.Namespace:
@@ -72,7 +87,7 @@ def cli() -> argparse.Namespace:
7287
"--products",
7388
nargs="+",
7489
required=True,
75-
help='Exact product terms to match (case-insensitive)',
90+
help="Exact product terms to match (case-insensitive)",
7691
)
7792
parser.add_argument(
7893
"--exts",
@@ -88,43 +103,40 @@ def main() -> None:
88103
input_dir: Path = args.input_dir
89104
output_dir: Path = args.output_dir
90105

91-
# normalize to lowercase for exact matching
92106
products = [p.lower() for p in args.products]
93107

94108
if not input_dir.exists():
95109
raise SystemExit(f"Input directory does not exist: {input_dir}")
96110

97-
files = list(_iter_gbk_files(input_dir, args.exts))
98-
if not files:
99-
raise SystemExit("No GenBank files found.")
100-
101111
output_dir.mkdir(parents=True, exist_ok=True)
102112

103113
copied = 0
104114
errors = 0
115+
scanned = 0
116+
117+
# No total: just chew through files as we discover them
118+
with tqdm(desc="Scanning GBKs", unit="file") as pbar:
119+
for path in _iter_gbk_files(input_dir, args.exts):
120+
scanned += 1
121+
result = _file_matches(path, products)
122+
123+
if result:
124+
if "error" in result:
125+
errors += 1
126+
else:
127+
try:
128+
shutil.copy2(result["path"], output_dir / result["file"])
129+
copied += 1
130+
except Exception:
131+
errors += 1
105132

106-
with tqdm(total=len(files), desc="Scanning GBKs", unit="file") as pbar:
107-
for path in files:
108-
result = _file_matches(str(path), products)
109133
pbar.update(1)
110-
111-
if not result:
112-
continue
113-
114-
if "error" in result:
115-
errors += 1
116-
pbar.set_postfix(copied=copied, errors=errors)
117-
continue
118-
119-
try:
120-
shutil.copy2(result["path"], output_dir / result["file"])
121-
copied += 1
122-
except Exception as exc:
123-
errors += 1
124-
125134
pbar.set_postfix(copied=copied, errors=errors)
126135

127-
print(f"\nDone. Copied: {copied}, errors: {errors}, total scanned: {len(files)}")
136+
if scanned == 0:
137+
print("No GenBank files found with the given extensions.")
138+
else:
139+
print(f"\nDone. Copied: {copied}, errors: {errors}, total scanned: {scanned}")
128140

129141

130142
if __name__ == "__main__":

0 commit comments

Comments
 (0)