Skip to content

Commit 99d99f8

Browse files
tanghaibaoclaude
andcommitted
Remove deprecated modules: apps.blastplus, apps.bowtie, apps.lastz,
utils.ez_setup Relocate reusable code to consuming modules: - Bowtie2 wrappers (BowtieLogFile, check_index, align) -> apps.align - Download utilities (ALL_DOWNLOADERS, get_best_downloader) -> apps.base - LASTZ score conversion functions -> formats.maf Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8cf0daf commit 99d99f8

10 files changed

Lines changed: 282 additions & 787 deletions

File tree

src/jcvi/annotation/automaton.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ def tophat(args):
218218
219219
Run tophat on a folder of reads.
220220
"""
221-
from jcvi.apps.bowtie import check_index
221+
from jcvi.apps.align import check_index
222222
from jcvi.formats.fastq import guessoffset
223223

224224
p = OptionParser(tophat.__doc__)

src/jcvi/apps/align.py

Lines changed: 167 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,185 @@
1111
from subprocess import STDOUT, CalledProcessError
1212
import sys
1313

14-
from ..utils.cbook import depends
14+
from ..formats.base import BaseFile
15+
from ..formats.sam import get_samfile, output_bam
16+
from ..utils.cbook import depends, percentage
1517
from .base import (
1618
ActionDispatcher,
1719
OptionParser,
1820
cleanup,
1921
get_abs_path,
2022
logger,
2123
mkdir,
24+
need_update,
2225
sh,
2326
which,
2427
)
2528
from .grid import MakeManager
2629

30+
first_tag = lambda fp: next(fp).split()[0]
31+
32+
33+
class BowtieLogFile(BaseFile):
34+
"""
35+
Simple file that contains mapping rate:
36+
37+
100000 reads; of these:
38+
100000 (100.00%) were unpaired; of these:
39+
88453 (88.45%) aligned 0 times
40+
9772 (9.77%) aligned exactly 1 time
41+
1775 (1.77%) aligned >1 times
42+
11.55% overall alignment rate
43+
"""
44+
45+
def __init__(self, filename):
46+
super().__init__(filename)
47+
fp = open(filename)
48+
self.total = int(first_tag(fp))
49+
self.unpaired = int(first_tag(fp))
50+
self.unmapped = int(first_tag(fp))
51+
self.unique = int(first_tag(fp))
52+
self.multiple = int(first_tag(fp))
53+
self.mapped = self.unique + self.multiple
54+
self.rate = float(first_tag(fp).rstrip("%"))
55+
fp.close()
56+
57+
def __str__(self):
58+
return "Total mapped: {0}".format(percentage(self.mapped, self.total))
59+
60+
__repr__ = __str__
61+
62+
63+
def check_index(dbfile):
64+
dbfile = get_abs_path(dbfile)
65+
safile = dbfile + ".1.bt2"
66+
if need_update(dbfile, safile):
67+
cmd = "bowtie2-build {0} {0}".format(dbfile)
68+
sh(cmd)
69+
else:
70+
logger.error("`{0}` exists. `bowtie2-build` already run.".format(safile))
71+
72+
return dbfile
73+
74+
75+
def bowtie_align(args):
76+
"""
77+
%prog bowtie_align database.fasta read1.fq [read2.fq]
78+
79+
Wrapper for `bowtie2` single-end or paired-end, depending on the number of args.
80+
"""
81+
from jcvi.formats.fastq import guessoffset
82+
from jcvi.formats.sam import get_prefix
83+
84+
p = OptionParser(bowtie_align.__doc__)
85+
p.set_firstN(firstN=0)
86+
p.add_argument(
87+
"--full",
88+
default=False,
89+
action="store_true",
90+
help="Enforce end-to-end alignment [default: local]",
91+
)
92+
p.add_argument(
93+
"--reorder",
94+
default=False,
95+
action="store_true",
96+
help="Keep the input read order",
97+
)
98+
p.add_argument(
99+
"--null",
100+
default=False,
101+
action="store_true",
102+
help="Do not write to SAM/BAM output",
103+
)
104+
p.add_argument(
105+
"--fasta", default=False, action="store_true", help="Query reads are FASTA"
106+
)
107+
p.set_cutoff(cutoff=800)
108+
p.set_mateorientation(mateorientation="+-")
109+
p.set_sam_options(bowtie=True)
110+
111+
opts, args = p.parse_args(args)
112+
extra = opts.extra
113+
mo = opts.mateorientation
114+
if mo == "+-":
115+
extra += ""
116+
elif mo == "-+":
117+
extra += "--rf"
118+
else:
119+
extra += "--ff"
120+
121+
PE = True
122+
if len(args) == 2:
123+
logger.debug("Single-end alignment")
124+
PE = False
125+
elif len(args) == 3:
126+
logger.debug("Paired-end alignment")
127+
else:
128+
sys.exit(not p.print_help())
129+
130+
firstN = opts.firstN
131+
mapped = opts.mapped
132+
unmapped = opts.unmapped
133+
fasta = opts.fasta
134+
gl = "--end-to-end" if opts.full else "--local"
135+
136+
dbfile, readfile = args[0:2]
137+
dbfile = check_index(dbfile)
138+
prefix = get_prefix(readfile, dbfile)
139+
samfile, mapped, unmapped = get_samfile(
140+
readfile, dbfile, bowtie=True, mapped=mapped, unmapped=unmapped, bam=opts.bam
141+
)
142+
logfile = prefix + ".log"
143+
if not fasta:
144+
offset = guessoffset([readfile])
145+
146+
if not need_update(dbfile, samfile):
147+
logger.error("`{0}` exists. `bowtie2` already run.".format(samfile))
148+
return samfile, logfile
149+
150+
cmd = "bowtie2 -x {0}".format(dbfile)
151+
if PE:
152+
r1, r2 = args[1:3]
153+
cmd += " -1 {0} -2 {1}".format(r1, r2)
154+
cmd += " --maxins {0}".format(opts.cutoff)
155+
mtag, utag = "--al-conc", "--un-conc"
156+
else:
157+
cmd += " -U {0}".format(readfile)
158+
mtag, utag = "--al", "--un"
159+
160+
if mapped:
161+
cmd += " {0} {1}".format(mtag, mapped)
162+
if unmapped:
163+
cmd += " {0} {1}".format(utag, unmapped)
164+
165+
if firstN:
166+
cmd += " --upto {0}".format(firstN)
167+
cmd += " -p {0}".format(opts.cpus)
168+
if fasta:
169+
cmd += " -f"
170+
else:
171+
cmd += " --phred{0}".format(offset)
172+
cmd += " {0}".format(gl)
173+
if opts.reorder:
174+
cmd += " --reorder"
175+
176+
cmd += " {0}".format(extra)
177+
# Finally the log
178+
cmd += " 2> {0}".format(logfile)
179+
180+
if opts.null:
181+
samfile = "/dev/null"
182+
183+
cmd = output_bam(cmd, samfile)
184+
sh(cmd)
185+
print(open(logfile).read(), file=sys.stderr)
186+
187+
return samfile, logfile
188+
189+
190+
# Backward compatible alias
191+
align = bowtie_align
192+
27193

28194
@depends
29195
def run_formatdb(infile=None, outfile=None, dbtype="nucl"):

src/jcvi/apps/base.py

Lines changed: 101 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -248,8 +248,6 @@ def set_downloader(self, downloader=None):
248248
"""
249249
Add --downloader options for given command line program.
250250
"""
251-
from jcvi.utils.ez_setup import ALL_DOWNLOADERS
252-
253251
downloader_choices = [x[0] for x in ALL_DOWNLOADERS]
254252
self.add_argument(
255253
"--downloader",
@@ -1407,6 +1405,107 @@ def ls_ftp(dir):
14071405
return [op.basename(x) for x in ftp.list(o.path)]
14081406

14091407

1408+
def _download_file_powershell(url, target, cookies=None):
1409+
if cookies:
1410+
raise NotImplementedError
1411+
target = os.path.abspath(target)
1412+
cmd = [
1413+
"powershell",
1414+
"-Command",
1415+
f"(new-object System.Net.WebClient).DownloadFile({url}, {target})",
1416+
]
1417+
check_output(cmd)
1418+
1419+
1420+
def _has_powershell():
1421+
if platform.system() != "Windows":
1422+
return False
1423+
try:
1424+
check_output(["powershell", "-Command", "echo test"])
1425+
except (FileNotFoundError, CalledProcessError):
1426+
return False
1427+
return True
1428+
1429+
1430+
_download_file_powershell.viable = _has_powershell
1431+
1432+
1433+
def _download_file_curl(url, target, cookies=None):
1434+
cmd = ["curl", url, "--output", target, "-L"]
1435+
if url.startswith("ftp:"):
1436+
cmd += ["-P", "-"]
1437+
if cookies:
1438+
cmd += ["-b", cookies]
1439+
check_output(cmd)
1440+
1441+
1442+
def _has_curl():
1443+
try:
1444+
check_output(["curl", "--version"])
1445+
except (FileNotFoundError, CalledProcessError):
1446+
return False
1447+
return True
1448+
1449+
1450+
_download_file_curl.viable = _has_curl
1451+
1452+
1453+
def _download_file_wget(url, target, cookies=None):
1454+
cmd = ["wget", url, "--output-document", target, "--no-check-certificate"]
1455+
if url.startswith("ftp:"):
1456+
cmd += ["--passive-ftp"]
1457+
if cookies:
1458+
cmd += ["--load-cookies", cookies]
1459+
check_output(cmd)
1460+
1461+
1462+
def _has_wget():
1463+
try:
1464+
check_output(["wget", "--version"])
1465+
except (FileNotFoundError, NotADirectoryError, CalledProcessError):
1466+
return False
1467+
return True
1468+
1469+
1470+
_download_file_wget.viable = _has_wget
1471+
1472+
1473+
def _download_file_insecure(url, target, cookies=None):
1474+
if cookies:
1475+
raise NotImplementedError
1476+
from urllib.request import urlopen
1477+
1478+
src = dst = None
1479+
try:
1480+
src = urlopen(url)
1481+
data = src.read()
1482+
dst = open(target, "wb")
1483+
dst.write(data)
1484+
finally:
1485+
if src:
1486+
src.close()
1487+
if dst:
1488+
dst.close()
1489+
1490+
1491+
_download_file_insecure.viable = lambda: True
1492+
1493+
ALL_DOWNLOADERS = [
1494+
("wget", _download_file_wget),
1495+
("curl", _download_file_curl),
1496+
("powershell", _download_file_powershell),
1497+
("insecure", _download_file_insecure),
1498+
]
1499+
1500+
1501+
def get_best_downloader(downloader=None):
1502+
for dl_name, dl in ALL_DOWNLOADERS:
1503+
if downloader and dl_name != downloader:
1504+
continue
1505+
if dl.viable():
1506+
return dl
1507+
1508+
14101509
def download(
14111510
url, filename=None, debug=True, cookies=None, handle_gzip=False, downloader=None
14121511
):
@@ -1451,8 +1550,6 @@ def download(
14511550
logger.info("File `%s` exists. Download skipped.", final_filename)
14521551
success = True
14531552
else:
1454-
from jcvi.utils.ez_setup import get_best_downloader
1455-
14561553
downloader = get_best_downloader(downloader=downloader)
14571554
if downloader:
14581555
try:

0 commit comments

Comments
 (0)