Add new Excel report BEA25P077_RP.xlsx with multiple worksheets and styles
This commit is contained in:
@@ -0,0 +1,589 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import csv
|
||||
import os
|
||||
import re
|
||||
import ssl
|
||||
import statistics
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
import zipfile
|
||||
import xml.etree.ElementTree as ET
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
def col_to_index(col):
|
||||
idx = 0
|
||||
for c in col:
|
||||
idx = idx * 26 + (ord(c.upper()) - ord("A") + 1)
|
||||
return idx
|
||||
|
||||
|
||||
def parse_shared_strings(zf):
|
||||
try:
|
||||
xml = zf.read("xl/sharedStrings.xml")
|
||||
except KeyError:
|
||||
return []
|
||||
root = ET.fromstring(xml)
|
||||
ns = {"a": root.tag.split("}")[0].strip("{")}
|
||||
shared = []
|
||||
for si in root.findall(".//a:si", ns):
|
||||
text_parts = []
|
||||
for t in si.findall(".//a:t", ns):
|
||||
text_parts.append(t.text or "")
|
||||
shared.append("".join(text_parts))
|
||||
return shared
|
||||
|
||||
|
||||
def list_sheets(zf):
|
||||
wb = ET.fromstring(zf.read("xl/workbook.xml"))
|
||||
ns = {"a": wb.tag.split("}")[0].strip("{")}
|
||||
sheets = []
|
||||
for sh in wb.findall(".//a:sheets/a:sheet", ns):
|
||||
sheets.append(
|
||||
(sh.attrib["name"], sh.attrib["{http://schemas.openxmlformats.org/officeDocument/2006/relationships}id"])
|
||||
)
|
||||
|
||||
rels = ET.fromstring(zf.read("xl/_rels/workbook.xml.rels"))
|
||||
relmap = {}
|
||||
for rel in rels.findall(".//{http://schemas.openxmlformats.org/package/2006/relationships}Relationship"):
|
||||
relmap[rel.attrib["Id"]] = rel.attrib["Target"]
|
||||
|
||||
return [(name, "xl/" + relmap[rid]) for name, rid in sheets]
|
||||
|
||||
|
||||
def read_xlsx_sheet(path, sheet_name):
|
||||
with zipfile.ZipFile(path) as zf:
|
||||
shared = parse_shared_strings(zf)
|
||||
sheets = dict(list_sheets(zf))
|
||||
if sheet_name not in sheets:
|
||||
raise ValueError(f"Sheet '{sheet_name}' not found. Available: {', '.join(sheets.keys())}")
|
||||
|
||||
root = ET.fromstring(zf.read(sheets[sheet_name]))
|
||||
ns = {"a": root.tag.split("}")[0].strip("{")}
|
||||
row_dicts = []
|
||||
max_col_idx = 0
|
||||
|
||||
for row in root.findall(".//a:sheetData/a:row", ns):
|
||||
row_cells = {}
|
||||
for c in row.findall("a:c", ns):
|
||||
ref = c.attrib.get("r", "")
|
||||
col = "".join(ch for ch in ref if ch.isalpha())
|
||||
if not col:
|
||||
continue
|
||||
v = c.find("a:v", ns)
|
||||
value = v.text if v is not None else ""
|
||||
if c.attrib.get("t") == "s":
|
||||
try:
|
||||
value = shared[int(value)]
|
||||
except Exception:
|
||||
pass
|
||||
row_cells[col] = value
|
||||
max_col_idx = max(max_col_idx, col_to_index(col))
|
||||
if row_cells:
|
||||
row_dicts.append(row_cells)
|
||||
|
||||
if not row_dicts:
|
||||
return []
|
||||
|
||||
rows = []
|
||||
for row_cells in row_dicts:
|
||||
row_list = [""] * max_col_idx
|
||||
for col, val in row_cells.items():
|
||||
row_list[col_to_index(col) - 1] = val
|
||||
rows.append(row_list)
|
||||
|
||||
header = rows[0]
|
||||
ncols = len(header)
|
||||
while ncols > 0 and header[ncols - 1] == "":
|
||||
ncols -= 1
|
||||
|
||||
normalized = []
|
||||
for row in rows:
|
||||
if len(row) < ncols:
|
||||
row = row + [""] * (ncols - len(row))
|
||||
elif len(row) > ncols:
|
||||
row = row[:ncols]
|
||||
normalized.append(row)
|
||||
return normalized
|
||||
|
||||
|
||||
def score_counts(values, metric):
|
||||
if not values:
|
||||
return 0.0
|
||||
if metric == "mean":
|
||||
return sum(values) / len(values)
|
||||
if metric == "median":
|
||||
return statistics.median(values)
|
||||
if metric == "sum":
|
||||
return sum(values)
|
||||
if metric == "max":
|
||||
return max(values)
|
||||
raise ValueError(f"Unknown metric: {metric}")
|
||||
|
||||
|
||||
def canonical_mirna(name):
|
||||
s = name.strip().lower()
|
||||
s = re.sub(r"^[a-z]{3}-", "", s)
|
||||
return s
|
||||
|
||||
|
||||
def _detect_delimiter(sample):
|
||||
if "\t" in sample:
|
||||
return "\t"
|
||||
if "," in sample:
|
||||
return ","
|
||||
return "\t"
|
||||
|
||||
|
||||
def read_delimited_rows(path):
|
||||
with open(path, "r", newline="") as f:
|
||||
sample = f.readline()
|
||||
delim = _detect_delimiter(sample)
|
||||
f.seek(0)
|
||||
try:
|
||||
reader = csv.reader(f, delimiter=delim)
|
||||
rows = list(reader)
|
||||
except csv.Error:
|
||||
f.seek(0)
|
||||
reader = csv.reader(
|
||||
f,
|
||||
delimiter=delim,
|
||||
quoting=csv.QUOTE_NONE,
|
||||
escapechar="\\",
|
||||
)
|
||||
rows = list(reader)
|
||||
return rows
|
||||
|
||||
|
||||
def iter_delimited_rows(path):
|
||||
with open(path, "r", newline="") as f:
|
||||
sample = f.readline()
|
||||
delim = _detect_delimiter(sample)
|
||||
f.seek(0)
|
||||
try:
|
||||
reader = csv.reader(f, delimiter=delim)
|
||||
for row in reader:
|
||||
yield row
|
||||
except csv.Error:
|
||||
f.seek(0)
|
||||
reader = csv.reader(
|
||||
f,
|
||||
delimiter=delim,
|
||||
quoting=csv.QUOTE_NONE,
|
||||
escapechar="\\",
|
||||
)
|
||||
for row in reader:
|
||||
yield row
|
||||
|
||||
|
||||
def read_targets(path):
|
||||
rows = read_delimited_rows(path)
|
||||
return parse_targets_rows(rows)
|
||||
|
||||
|
||||
def normalize_header(col):
|
||||
return re.sub(r"[^a-z0-9]+", "", col.strip().lower())
|
||||
|
||||
|
||||
def normalize_species(value):
|
||||
v = re.sub(r"[^a-z]+", " ", value.strip().lower())
|
||||
v = re.sub(r"\s+", " ", v).strip()
|
||||
if v in {"human", "homo sapiens", "homo sapienst"}:
|
||||
return "homo sapiens"
|
||||
return v
|
||||
|
||||
|
||||
def download_url_to_file(url, dest_path, allow_insecure=False):
|
||||
os.makedirs(os.path.dirname(dest_path) or ".", exist_ok=True)
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
|
||||
context = ssl._create_unverified_context() if allow_insecure else None
|
||||
if context:
|
||||
response = urllib.request.urlopen(req, context=context)
|
||||
else:
|
||||
response = urllib.request.urlopen(req)
|
||||
with response as resp, open(dest_path, "wb") as f:
|
||||
while True:
|
||||
chunk = resp.read(1024 * 1024)
|
||||
if not chunk:
|
||||
break
|
||||
f.write(chunk)
|
||||
|
||||
|
||||
def find_mirtarbase_mti_url(download_page_url):
|
||||
req = urllib.request.Request(download_page_url, headers={"User-Agent": "Mozilla/5.0"})
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
html = resp.read().decode("utf-8", errors="ignore")
|
||||
links = re.findall(r'href=["\\\']([^"\\\']+)["\\\']', html, flags=re.IGNORECASE)
|
||||
scored = []
|
||||
for link in links:
|
||||
if "mirtarbase" not in link.lower():
|
||||
continue
|
||||
if "mti" not in link.lower():
|
||||
continue
|
||||
if not re.search(r"\\.(txt|tsv|csv|xls|xlsx|zip)$", link, flags=re.IGNORECASE):
|
||||
continue
|
||||
scored.append(link)
|
||||
if not scored:
|
||||
return None
|
||||
scored.sort(key=lambda s: (0 if s.lower().endswith(".txt") else 1, len(s)))
|
||||
return urllib.parse.urljoin(download_page_url, scored[0])
|
||||
|
||||
|
||||
def read_mirtarbase(
|
||||
path,
|
||||
species="Homo sapiens",
|
||||
support=None,
|
||||
sheet=None,
|
||||
include_mirnas=None,
|
||||
):
|
||||
species_norm = normalize_species(species) if species else None
|
||||
ext = os.path.splitext(path)[1].lower()
|
||||
if ext == ".xlsx":
|
||||
if sheet is None:
|
||||
with zipfile.ZipFile(path) as zf:
|
||||
sheets = list_sheets(zf)
|
||||
if not sheets:
|
||||
raise ValueError("miRTarBase .xlsx has no sheets.")
|
||||
sheet = sheets[0][0]
|
||||
rows = read_xlsx_sheet(path, sheet)
|
||||
if not rows:
|
||||
return {"raw": defaultdict(set), "canonical": defaultdict(set)}
|
||||
header = rows[0]
|
||||
row_iter = iter(rows[1:])
|
||||
else:
|
||||
row_iter = iter_delimited_rows(path)
|
||||
try:
|
||||
header = next(row_iter)
|
||||
except StopIteration:
|
||||
return {"raw": defaultdict(set), "canonical": defaultdict(set)}
|
||||
|
||||
idx_mirna = None
|
||||
idx_target = None
|
||||
idx_species_target = None
|
||||
idx_species_mirna = None
|
||||
idx_support = None
|
||||
|
||||
for i, col in enumerate(header):
|
||||
key = normalize_header(col)
|
||||
if key in {"mirna", "mirnaname", "mirnaid", "mirname"} and idx_mirna is None:
|
||||
idx_mirna = i
|
||||
if key in {"targetgene", "target", "gene", "genesymbol"} and idx_target is None:
|
||||
idx_target = i
|
||||
if key in {"speciestargetgene", "speciestarget", "targetspecies"} and idx_species_target is None:
|
||||
idx_species_target = i
|
||||
if key in {"speciesmirna", "speciessourcemiRNA", "speciesmir"} and idx_species_mirna is None:
|
||||
idx_species_mirna = i
|
||||
if key in {"supporttype", "support", "evidence"} and idx_support is None:
|
||||
idx_support = i
|
||||
|
||||
if idx_mirna is None or idx_target is None:
|
||||
raise ValueError(
|
||||
"miRTarBase format not recognized. Expected columns like 'miRNA' and 'Target Gene'."
|
||||
)
|
||||
|
||||
mapping = defaultdict(set)
|
||||
support_norm = support.strip().lower() if support else None
|
||||
|
||||
include_canonical = None
|
||||
if include_mirnas:
|
||||
include_canonical = {canonical_mirna(m) for m in include_mirnas}
|
||||
|
||||
for row in row_iter:
|
||||
if len(row) <= max(idx_mirna, idx_target):
|
||||
continue
|
||||
mirna = row[idx_mirna].strip()
|
||||
target = row[idx_target].strip()
|
||||
if not mirna or not target:
|
||||
continue
|
||||
|
||||
if include_canonical is not None and canonical_mirna(mirna) not in include_canonical:
|
||||
continue
|
||||
|
||||
if species_norm:
|
||||
species_value = ""
|
||||
if idx_species_target is not None and idx_species_target < len(row):
|
||||
species_value = row[idx_species_target]
|
||||
elif idx_species_mirna is not None and idx_species_mirna < len(row):
|
||||
species_value = row[idx_species_mirna]
|
||||
if species_value:
|
||||
if normalize_species(species_value) != species_norm:
|
||||
continue
|
||||
|
||||
if support_norm and idx_support is not None and idx_support < len(row):
|
||||
if support_norm not in row[idx_support].lower():
|
||||
continue
|
||||
|
||||
mapping[mirna].add(target)
|
||||
|
||||
canonical = defaultdict(set)
|
||||
for mirna, targets in mapping.items():
|
||||
canonical[canonical_mirna(mirna)].update(targets)
|
||||
|
||||
return {"raw": mapping, "canonical": canonical}
|
||||
def parse_targets_rows(rows):
|
||||
if not rows:
|
||||
return {}
|
||||
|
||||
header = rows[0]
|
||||
idx_mirna = None
|
||||
idx_target = None
|
||||
for i, col in enumerate(header):
|
||||
key = normalize_header(col)
|
||||
if key in {"mirna", "mir", "mirname", "mirid"} and idx_mirna is None:
|
||||
idx_mirna = i
|
||||
if key in {"target", "gene", "protein", "symbol", "targetgene", "targets"} and idx_target is None:
|
||||
idx_target = i
|
||||
|
||||
if idx_mirna is None or idx_target is None:
|
||||
idx_mirna, idx_target = 0, 1
|
||||
|
||||
mapping = defaultdict(set)
|
||||
for row in rows[1:]:
|
||||
if len(row) <= max(idx_mirna, idx_target):
|
||||
continue
|
||||
mirna = row[idx_mirna].strip()
|
||||
target_cell = row[idx_target].strip()
|
||||
if not mirna or not target_cell:
|
||||
continue
|
||||
targets = [target_cell]
|
||||
if ";" in target_cell:
|
||||
targets = [t.strip() for t in target_cell.split(";") if t.strip()]
|
||||
elif "," in target_cell:
|
||||
targets = [t.strip() for t in target_cell.split(",") if t.strip()]
|
||||
|
||||
for target in targets:
|
||||
mapping[mirna].add(target)
|
||||
|
||||
canonical = defaultdict(set)
|
||||
for mirna, targets in mapping.items():
|
||||
canonical[canonical_mirna(mirna)].update(targets)
|
||||
|
||||
return {"raw": mapping, "canonical": canonical}
|
||||
|
||||
|
||||
def select_high_expression(rows, metric, top_n, min_score, quantile):
|
||||
header = rows[0]
|
||||
sample_cols = header[1:]
|
||||
entries = []
|
||||
|
||||
for row in rows[1:]:
|
||||
if not row or not row[0]:
|
||||
continue
|
||||
counts = []
|
||||
for val in row[1:]:
|
||||
try:
|
||||
counts.append(float(val))
|
||||
except Exception:
|
||||
counts.append(0.0)
|
||||
score = score_counts(counts, metric)
|
||||
entries.append((row[0], counts, score))
|
||||
|
||||
entries.sort(key=lambda x: x[2], reverse=True)
|
||||
scores = [e[2] for e in entries]
|
||||
selected = entries
|
||||
|
||||
if top_n is not None:
|
||||
selected = entries[: max(0, top_n)]
|
||||
elif quantile is not None:
|
||||
if not scores:
|
||||
selected = []
|
||||
else:
|
||||
idx = int(round((len(scores) - 1) * quantile))
|
||||
thresh = sorted(scores)[idx]
|
||||
selected = [e for e in entries if e[2] >= thresh]
|
||||
elif min_score is not None:
|
||||
selected = [e for e in entries if e[2] >= min_score]
|
||||
else:
|
||||
selected = entries[:50]
|
||||
|
||||
return sample_cols, selected
|
||||
|
||||
|
||||
def write_high_expression(path, sample_cols, selected, metric):
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
with open(path, "w", newline="") as f:
|
||||
writer = csv.writer(f, delimiter="\t")
|
||||
writer.writerow(["miRNA", f"{metric}_score", *sample_cols])
|
||||
for mirna, counts, score in selected:
|
||||
writer.writerow([mirna, f"{score:.6g}", *counts])
|
||||
|
||||
|
||||
def write_targets(outdir, selected, target_map, metric):
|
||||
edges_path = os.path.join(outdir, "miRNA_protein_edges.tsv")
|
||||
summary_path = os.path.join(outdir, "protein_targets_summary.tsv")
|
||||
os.makedirs(outdir, exist_ok=True)
|
||||
|
||||
target_stats = defaultdict(lambda: {"miRNAs": set(), "score_sum": 0.0})
|
||||
with open(edges_path, "w", newline="") as f:
|
||||
writer = csv.writer(f, delimiter="\t")
|
||||
writer.writerow(["miRNA", "target", f"{metric}_score", "edge_weight"])
|
||||
for mirna, _counts, score in selected:
|
||||
targets = set()
|
||||
targets.update(target_map["raw"].get(mirna, set()))
|
||||
targets.update(target_map["canonical"].get(canonical_mirna(mirna), set()))
|
||||
for target in sorted(targets):
|
||||
writer.writerow([mirna, target, f"{score:.6g}", f"{score:.6g}"])
|
||||
target_stats[target]["miRNAs"].add(mirna)
|
||||
target_stats[target]["score_sum"] += score
|
||||
|
||||
with open(summary_path, "w", newline="") as f:
|
||||
writer = csv.writer(f, delimiter="\t")
|
||||
writer.writerow(["target", "miRNA_count", f"{metric}_score_sum"])
|
||||
for target, stats in sorted(
|
||||
target_stats.items(), key=lambda item: (-len(item[1]["miRNAs"]), item[0])
|
||||
):
|
||||
writer.writerow([target, len(stats["miRNAs"]), f"{stats['score_sum']:.6g}"])
|
||||
|
||||
return edges_path, summary_path
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Select high-expression miRNAs and build a target map.")
|
||||
parser.add_argument("--counts", default="miRNA_counts.xlsx", help="Path to miRNA counts .xlsx file.")
|
||||
parser.add_argument("--sheet", default="Mature", help="Sheet name to use (e.g., Mature or Hairpin).")
|
||||
parser.add_argument("--metric", choices=["mean", "median", "sum", "max"], default="mean")
|
||||
parser.add_argument("--top", type=int, default=None, help="Select top N miRNAs by metric.")
|
||||
parser.add_argument(
|
||||
"--min-score",
|
||||
type=float,
|
||||
default=5000.0,
|
||||
help="Select miRNAs with metric >= value. Default: 5000.",
|
||||
)
|
||||
parser.add_argument("--quantile", type=float, default=None, help="Select miRNAs with metric >= quantile (0-1).")
|
||||
parser.add_argument("--targets", default=None, help="TSV/CSV file with miRNA-to-target mappings.")
|
||||
parser.add_argument("--mirtarbase", default=None, help="miRTarBase TSV/CSV/XLSX file to build targets.")
|
||||
parser.add_argument(
|
||||
"--fetch-mirtarbase",
|
||||
action="store_true",
|
||||
help="Auto-download miRTarBase MTI file if missing (best-effort).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--mirtarbase-url",
|
||||
default=None,
|
||||
help="Direct URL to miRTarBase MTI file (overrides auto-detection).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--mirtarbase-download-page",
|
||||
default="http://mirtarbase.cuhk.edu.cn/php/download.php",
|
||||
help="miRTarBase download page URL to discover MTI file.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--mirtarbase-insecure",
|
||||
action="store_true",
|
||||
help="Allow insecure SSL (disable certificate verification) for downloads.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--mirtarbase-species",
|
||||
default="Homo sapiens",
|
||||
help="Species filter for miRTarBase (default: Homo sapiens).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--mirtarbase-support",
|
||||
default=None,
|
||||
help="Optional miRTarBase support filter (e.g., Strong).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--mirtarbase-sheet",
|
||||
default=None,
|
||||
help="Sheet name for miRTarBase .xlsx (defaults to first sheet).",
|
||||
)
|
||||
parser.add_argument("--outdir", default="out", help="Output directory.")
|
||||
args = parser.parse_args()
|
||||
|
||||
if not os.path.exists(args.counts):
|
||||
print(f"Counts file not found: {args.counts}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
try:
|
||||
rows = read_xlsx_sheet(args.counts, args.sheet)
|
||||
except Exception as exc:
|
||||
print(f"Failed to read sheet: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
if not rows:
|
||||
print("No data found in sheet.", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
sample_cols, selected = select_high_expression(
|
||||
rows, args.metric, args.top, args.min_score, args.quantile
|
||||
)
|
||||
if not selected:
|
||||
print("No miRNAs selected with the current criteria.", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
outdir = args.outdir
|
||||
high_path = os.path.join(outdir, "high_expression_miRNAs.tsv")
|
||||
write_high_expression(high_path, sample_cols, selected, args.metric)
|
||||
|
||||
edges_path = summary_path = None
|
||||
target_map = None
|
||||
if args.mirtarbase or args.fetch_mirtarbase:
|
||||
if args.mirtarbase is None:
|
||||
args.mirtarbase = os.path.join(args.outdir, "miRTarBase_MTI.txt")
|
||||
|
||||
if not os.path.exists(args.mirtarbase) and args.fetch_mirtarbase:
|
||||
try:
|
||||
url = args.mirtarbase_url
|
||||
if url is None:
|
||||
url = find_mirtarbase_mti_url(args.mirtarbase_download_page)
|
||||
if url is None:
|
||||
print(
|
||||
"Could not auto-detect miRTarBase MTI file URL from the download page.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 2
|
||||
print(f"Downloading miRTarBase MTI file from: {url}")
|
||||
try:
|
||||
download_url_to_file(url, args.mirtarbase, allow_insecure=args.mirtarbase_insecure)
|
||||
except urllib.error.URLError as exc:
|
||||
reason = getattr(exc, "reason", None)
|
||||
if isinstance(reason, ssl.SSLError) and url.startswith("https://"):
|
||||
http_url = "http://" + url[len("https://") :]
|
||||
print(f"SSL failed, retrying over HTTP: {http_url}")
|
||||
download_url_to_file(
|
||||
http_url, args.mirtarbase, allow_insecure=args.mirtarbase_insecure
|
||||
)
|
||||
else:
|
||||
raise
|
||||
except (urllib.error.URLError, ValueError, OSError) as exc:
|
||||
print(f"Failed to download miRTarBase file: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
if not os.path.exists(args.mirtarbase):
|
||||
print(f"miRTarBase file not found: {args.mirtarbase}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
try:
|
||||
target_map = read_mirtarbase(
|
||||
args.mirtarbase,
|
||||
species=args.mirtarbase_species,
|
||||
support=args.mirtarbase_support,
|
||||
sheet=args.mirtarbase_sheet,
|
||||
include_mirnas=[m for m, _c, _s in selected],
|
||||
)
|
||||
except Exception as exc:
|
||||
print(f"Failed to read miRTarBase file: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
elif args.targets:
|
||||
if not os.path.exists(args.targets):
|
||||
print(f"Targets file not found: {args.targets}", file=sys.stderr)
|
||||
return 2
|
||||
target_map = read_targets(args.targets)
|
||||
|
||||
if target_map:
|
||||
edges_path, summary_path = write_targets(outdir, selected, target_map, args.metric)
|
||||
|
||||
print(f"Wrote high-expression list: {high_path}")
|
||||
if edges_path:
|
||||
print(f"Wrote target edges: {edges_path}")
|
||||
print(f"Wrote target summary: {summary_path}")
|
||||
else:
|
||||
print("No target map generated (provide --mirtarbase or --targets).")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user