hogprof 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hogprof/__init__.py +5 -0
- hogprof/cli.py +484 -0
- hogprof/extract_levels_hits.py +518 -0
- hogprof/generate_jaccard_distr.py +516 -0
- hogprof/lshbuilder.py +710 -0
- hogprof/metadata.py +126 -0
- hogprof/presets.py +21 -0
- hogprof/profiler.py +333 -0
- hogprof/splitter.py +53 -0
- hogprof/utils/__init__.py +1 -0
- hogprof/utils/goautils.py +229 -0
- hogprof/utils/hashutils.py +190 -0
- hogprof/utils/orthoxml.py +83 -0
- hogprof/utils/phylo.py +305 -0
- hogprof/utils/pyhamutils.py +256 -0
- hogprof-0.3.0.dist-info/METADATA +231 -0
- hogprof-0.3.0.dist-info/RECORD +21 -0
- hogprof-0.3.0.dist-info/WHEEL +5 -0
- hogprof-0.3.0.dist-info/entry_points.txt +3 -0
- hogprof-0.3.0.dist-info/licenses/LICENSE +21 -0
- hogprof-0.3.0.dist-info/top_level.txt +1 -0
hogprof/__init__.py
ADDED
hogprof/cli.py
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
"""User-facing console I/O for command-line tools"""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import glob
|
|
5
|
+
import logging
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
from collections.abc import Iterable
|
|
9
|
+
from datetime import timedelta
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import TypeVar
|
|
12
|
+
|
|
13
|
+
import polars as pl
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
from rich.logging import RichHandler
|
|
16
|
+
from rich.panel import Panel
|
|
17
|
+
from rich.progress import track
|
|
18
|
+
from rich.table import Table
|
|
19
|
+
from rich.text import Text
|
|
20
|
+
from rich.traceback import install as install_rich_traceback
|
|
21
|
+
|
|
22
|
+
from hogprof import __version__
|
|
23
|
+
from hogprof.presets import DB_PRESETS
|
|
24
|
+
|
|
25
|
+
_Item = TypeVar("_Item")
|
|
26
|
+
|
|
27
|
+
# Rich Console shared by both logging and progress
|
|
28
|
+
console = Console(stderr=True)
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def setup_cli(verbose: bool = False,
|
|
33
|
+
*,
|
|
34
|
+
output_console: Console = console) -> None:
|
|
35
|
+
"""Configure application logging and terminal tracebacks for a CLI run."""
|
|
36
|
+
level = logging.DEBUG if verbose else logging.INFO
|
|
37
|
+
|
|
38
|
+
handler = RichHandler(
|
|
39
|
+
console=output_console,
|
|
40
|
+
markup=False,
|
|
41
|
+
rich_tracebacks=True,
|
|
42
|
+
tracebacks_show_locals=verbose,
|
|
43
|
+
show_path=verbose,
|
|
44
|
+
)
|
|
45
|
+
logging.basicConfig(
|
|
46
|
+
level=level,
|
|
47
|
+
format="%(message)s",
|
|
48
|
+
datefmt="[%X]",
|
|
49
|
+
handlers=[handler],
|
|
50
|
+
force=True,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# if in console (not redirected to file),
|
|
54
|
+
# format error traceback nicely with Rich
|
|
55
|
+
if output_console.is_terminal:
|
|
56
|
+
install_rich_traceback(console=output_console,
|
|
57
|
+
show_locals=verbose,
|
|
58
|
+
word_wrap=True)
|
|
59
|
+
|
|
60
|
+
def print_startup(args=None, *, output_console: Console = console) -> None:
|
|
61
|
+
"""Print the banner and, when supplied, the initial CLI configuration."""
|
|
62
|
+
|
|
63
|
+
table = Table.grid(padding=(0, 3))
|
|
64
|
+
table.add_column(style="dim")
|
|
65
|
+
table.add_column()
|
|
66
|
+
|
|
67
|
+
header = Table.grid(padding=(0, 1))
|
|
68
|
+
header.add_column(justify="center")
|
|
69
|
+
header.add_column()
|
|
70
|
+
|
|
71
|
+
logo = Text("┌─┼─┐\n■ □ ■", style="bold cyan")
|
|
72
|
+
name = Text.assemble(
|
|
73
|
+
("HogProf", "bold cyan"),
|
|
74
|
+
(f" v{__version__}", "dim"),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
header.add_row(logo, name)
|
|
78
|
+
|
|
79
|
+
table.add_row(header)
|
|
80
|
+
table.add_row()
|
|
81
|
+
table.add_row(
|
|
82
|
+
"Cite",
|
|
83
|
+
"[link=https://doi.org/10.1371/journal.pcbi.1007553]"
|
|
84
|
+
"Moi et al. (2020), PLOS Comp Biol[/link]",
|
|
85
|
+
)
|
|
86
|
+
if args is not None:
|
|
87
|
+
source = (
|
|
88
|
+
f"OMA: {args['oma']}" if args["oma"]
|
|
89
|
+
else f"OrthoXML: {args['orthoxml_glob']}"
|
|
90
|
+
if args["orthoxml_glob"]
|
|
91
|
+
else f"OrthoXML tar: {args['orthoxml_tar']}"
|
|
92
|
+
)
|
|
93
|
+
table.add_section()
|
|
94
|
+
table.add_row()
|
|
95
|
+
table.add_row("Input", source)
|
|
96
|
+
table.add_row("Output", str(args["output_dir"]))
|
|
97
|
+
table.add_row("Workers", str(args["njobs"]))
|
|
98
|
+
table.add_row("Seed", str(args["seed"]))
|
|
99
|
+
|
|
100
|
+
output_console.print(
|
|
101
|
+
Panel(table,
|
|
102
|
+
title_align="left",
|
|
103
|
+
subtitle_align="right",
|
|
104
|
+
border_style="cyan",
|
|
105
|
+
padding=(1, 2),
|
|
106
|
+
expand=False,
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
output_console.print("\n")
|
|
110
|
+
if args is not None:
|
|
111
|
+
output_console.print("[dim]Initializing...[/]")
|
|
112
|
+
output_console.print("\n")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def track_progress(
|
|
116
|
+
sequence: Iterable[_Item],
|
|
117
|
+
*,
|
|
118
|
+
description: str,
|
|
119
|
+
total: int | None = None,
|
|
120
|
+
output_console: Console = console,
|
|
121
|
+
) -> Iterable[_Item]:
|
|
122
|
+
"""Iterate with a Rich progress bar when output is an interactive terminal."""
|
|
123
|
+
|
|
124
|
+
if output_console.is_terminal:
|
|
125
|
+
# If output is CLI, it's all simple -- use Rich
|
|
126
|
+
yield from track(
|
|
127
|
+
sequence,
|
|
128
|
+
description=description,
|
|
129
|
+
total=total,
|
|
130
|
+
console=output_console,
|
|
131
|
+
disable=not output_console.is_terminal,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
else:
|
|
135
|
+
# Otherwise if output is redirected to file (e.g. SLURM)
|
|
136
|
+
# -- report progress differently
|
|
137
|
+
if total is None:
|
|
138
|
+
try:
|
|
139
|
+
total = len(sequence)
|
|
140
|
+
except TypeError:
|
|
141
|
+
pass
|
|
142
|
+
|
|
143
|
+
start = last_log = time.monotonic()
|
|
144
|
+
last_percent = 0
|
|
145
|
+
count = 0
|
|
146
|
+
|
|
147
|
+
# how often in % to report
|
|
148
|
+
log_step = 10
|
|
149
|
+
# if log_step takes too long, how often in seconds to report anyway
|
|
150
|
+
# -- every 1 min
|
|
151
|
+
log_interval = 1 * 60
|
|
152
|
+
|
|
153
|
+
first_message = f"{description}: 0% " + f"(0/{total})" if total else ""
|
|
154
|
+
logger.info(first_message)
|
|
155
|
+
for count, item in enumerate(sequence, 1):
|
|
156
|
+
|
|
157
|
+
# First, yield item -- to feed the generator
|
|
158
|
+
yield item
|
|
159
|
+
|
|
160
|
+
now = time.monotonic()
|
|
161
|
+
elapsed = now - start
|
|
162
|
+
|
|
163
|
+
if total:
|
|
164
|
+
percent = min(100, int(100 * count / total))
|
|
165
|
+
due = percent >= last_percent + log_step
|
|
166
|
+
else:
|
|
167
|
+
percent = None
|
|
168
|
+
due = True
|
|
169
|
+
|
|
170
|
+
if due or now - last_log >= log_interval:
|
|
171
|
+
if total:
|
|
172
|
+
eta = elapsed / count * (total - count)
|
|
173
|
+
eta_str = str(timedelta(seconds=round(eta)))
|
|
174
|
+
logger.info(
|
|
175
|
+
"%s: %d%% (%d/%d) | ETA %s",
|
|
176
|
+
description, percent, count, total, eta_str,
|
|
177
|
+
)
|
|
178
|
+
last_percent = percent
|
|
179
|
+
else:
|
|
180
|
+
logger.info(
|
|
181
|
+
"%s: %d items | %.1f items/s",
|
|
182
|
+
description, count, count / elapsed,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
last_log = now
|
|
186
|
+
|
|
187
|
+
logger.info(
|
|
188
|
+
"%s: done (%d items in %.1fs)",
|
|
189
|
+
description, count, time.monotonic() - start,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def print_search_result(df: pl.DataFrame) -> None:
|
|
194
|
+
"""Pretty print for `hogprof search` results"""
|
|
195
|
+
stdout = Console(file=sys.stdout)
|
|
196
|
+
|
|
197
|
+
if not stdout.is_terminal:
|
|
198
|
+
df.write_csv(sys.stdout, separator="\t")
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
table = Table(show_header=True, header_style="bold cyan", show_lines=False)
|
|
202
|
+
|
|
203
|
+
for column in df.columns:
|
|
204
|
+
table.add_column(
|
|
205
|
+
column,
|
|
206
|
+
justify="right" if df.schema[column].is_numeric() else "left",
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
for row in df.iter_rows():
|
|
210
|
+
table.add_row(*(
|
|
211
|
+
f"{value:.4f}" if isinstance(value, float)
|
|
212
|
+
else "—" if value is None
|
|
213
|
+
else str(value)
|
|
214
|
+
for value in row
|
|
215
|
+
))
|
|
216
|
+
|
|
217
|
+
stdout.print(table)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class RichArgumentParser(argparse.ArgumentParser):
|
|
221
|
+
"""Render argparse output with Rich while preserving streams and exit codes."""
|
|
222
|
+
|
|
223
|
+
def print_help(self, file=None):
|
|
224
|
+
file = sys.stdout if file is None else file
|
|
225
|
+
print_startup(output_console=Console(file=file))
|
|
226
|
+
super().print_help(file)
|
|
227
|
+
|
|
228
|
+
def _print_message(self, message, file=None):
|
|
229
|
+
if message:
|
|
230
|
+
text = Text(message)
|
|
231
|
+
text.highlight_regex(r"(?<!\w)--?[\w-]+", style="cyan")
|
|
232
|
+
text.highlight_regex(
|
|
233
|
+
r"(?m)^(?:usage|options|positional arguments):", style="bold"
|
|
234
|
+
)
|
|
235
|
+
if message.startswith(f"{self.prog}: error:"):
|
|
236
|
+
text.stylize("bold red")
|
|
237
|
+
Console(file=sys.stderr if file is None else file).print(
|
|
238
|
+
text, end="", soft_wrap=True
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
def _validate_args(parser, parsed_args):
|
|
242
|
+
if parsed_args.reformat_names:
|
|
243
|
+
parser.error('--reformat-names is not supported in this version')
|
|
244
|
+
|
|
245
|
+
if parsed_args.orthoxml_tar is not None:
|
|
246
|
+
parser.error('--orthoxml-tar is not supported in this version; '
|
|
247
|
+
'extract the archive and use --orthoxml-glob')
|
|
248
|
+
|
|
249
|
+
# check inputs
|
|
250
|
+
for option, path in (('--oma', parsed_args.oma), ('--species-tree', parsed_args.tree)):
|
|
251
|
+
if path is not None and not path.is_file():
|
|
252
|
+
parser.error(f'{option}: input file does not exist or is not a file: {path}')
|
|
253
|
+
|
|
254
|
+
# check Glob if provided
|
|
255
|
+
if parsed_args.orthoxml_glob is not None:
|
|
256
|
+
matches = glob.glob(parsed_args.orthoxml_glob)
|
|
257
|
+
if not matches:
|
|
258
|
+
parser.error('--orthoxml-glob: pattern does not match any input files')
|
|
259
|
+
|
|
260
|
+
if any(not Path(path).is_file() for path in matches):
|
|
261
|
+
parser.error('--orthoxml-glob: pattern must match only input files')
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
if parsed_args.tax_weights is not None:
|
|
265
|
+
for suffix in ('.json', '.h5'):
|
|
266
|
+
path = Path(parsed_args.tax_weights + suffix)
|
|
267
|
+
if not path.is_file():
|
|
268
|
+
parser.error(f'--tax-weights: model file does not exist or is not a file: {path}')
|
|
269
|
+
|
|
270
|
+
if parsed_args.output_dir.exists() and not parsed_args.output_dir.is_dir():
|
|
271
|
+
parser.error(f'--output-dir: path is not a directory: {parsed_args.output_dir}')
|
|
272
|
+
|
|
273
|
+
for option, value in (('--jobs', parsed_args.njobs),
|
|
274
|
+
('--num-permutations', parsed_args.num_permutations)):
|
|
275
|
+
if value < 1:
|
|
276
|
+
parser.error(f'{option} must be greater than zero')
|
|
277
|
+
|
|
278
|
+
if parsed_args.min_species < 0:
|
|
279
|
+
parser.error('--min-species must be nonnegative')
|
|
280
|
+
|
|
281
|
+
#if parsed_args.min_events < 0:
|
|
282
|
+
# parser.error('--min-events must be nonnegative')
|
|
283
|
+
|
|
284
|
+
def add_build_arguments(parser):
|
|
285
|
+
# Input files
|
|
286
|
+
# - 'source' is one of three options: OMA DB, tarfile with .orthoxml, or glob
|
|
287
|
+
source = parser.add_mutually_exclusive_group(required=True)
|
|
288
|
+
source.add_argument('--oma',
|
|
289
|
+
'--OMA', # compatibility option name; to remove
|
|
290
|
+
dest='oma',
|
|
291
|
+
help='Path to the OMA database file', type=Path)
|
|
292
|
+
source.add_argument('--orthoxml-glob',
|
|
293
|
+
'--OrthoGlob', # compatibility option name; to remove
|
|
294
|
+
dest='orthoxml_glob',
|
|
295
|
+
type=str,
|
|
296
|
+
help='A glob expression for orthoxml files')
|
|
297
|
+
source.add_argument('--orthoxml-tar',
|
|
298
|
+
'--tarfile', # compatibility option name; to remove
|
|
299
|
+
dest='orthoxml_tar',
|
|
300
|
+
type=Path,
|
|
301
|
+
help=argparse.SUPPRESS)
|
|
302
|
+
|
|
303
|
+
# - species tree
|
|
304
|
+
parser.add_argument('--species-tree', '--tree',
|
|
305
|
+
'--mastertree', # compatibility option name; to remove
|
|
306
|
+
dest='tree',
|
|
307
|
+
help='Newick species tree with node names matching the OrthoXML',
|
|
308
|
+
type=Path, required=True)
|
|
309
|
+
|
|
310
|
+
# Output
|
|
311
|
+
parser.add_argument('--output-dir', '--output',
|
|
312
|
+
'--outpath', # compatibility option name; to remove
|
|
313
|
+
'-o',
|
|
314
|
+
dest='output_dir',
|
|
315
|
+
help='Output directory path', type=Path, required=True)
|
|
316
|
+
|
|
317
|
+
# Configuration options
|
|
318
|
+
parser.add_argument('--tax-weights',
|
|
319
|
+
'--taxweights', # compatibility option name; to remove
|
|
320
|
+
dest='tax_weights',
|
|
321
|
+
help=argparse.SUPPRESS,
|
|
322
|
+
type=str
|
|
323
|
+
)
|
|
324
|
+
parser.add_argument('--taxon-mask',
|
|
325
|
+
'--taxmask', # compatibility option name; to remove
|
|
326
|
+
dest='taxon_mask',
|
|
327
|
+
help='Keep only this clade, identified by its tree node name (e.g. Sauria)',
|
|
328
|
+
type=str)
|
|
329
|
+
parser.add_argument('--exclude-taxa',
|
|
330
|
+
'--taxfilter', # compatibility option name; to remove
|
|
331
|
+
dest='exclude_taxa',
|
|
332
|
+
help='Tree node names of clades to exclude',
|
|
333
|
+
type=str, nargs='+')
|
|
334
|
+
parser.add_argument('--min-species',
|
|
335
|
+
'--specieslim', # compatibility option name; to remove
|
|
336
|
+
dest='min_species',
|
|
337
|
+
help='Species threshold: root HOGs must exceed it; subHOGs must meet it',
|
|
338
|
+
type=int, default=10)
|
|
339
|
+
parser.add_argument('--min-events',
|
|
340
|
+
'--eventslim', # compatibility option name; to remove
|
|
341
|
+
dest='min_events',
|
|
342
|
+
help='Keep subHOGs exceeding this loss or duplication count (-1 keeps all)',
|
|
343
|
+
type=int, default=0)
|
|
344
|
+
parser.add_argument('--db-type',
|
|
345
|
+
'--dbtype', # compatibility option name; to remove
|
|
346
|
+
dest='db_type',
|
|
347
|
+
help='Taxonomic range preset; --taxon-mask and --exclude-taxa override it',
|
|
348
|
+
choices=DB_PRESETS)
|
|
349
|
+
parser.add_argument('--num-permutations',
|
|
350
|
+
'--nperm', # compatibility option name; to remove
|
|
351
|
+
dest='num_permutations',
|
|
352
|
+
help='Number of hash functions used to construct each profile',
|
|
353
|
+
type=int, default=256)
|
|
354
|
+
parser.add_argument('--seed',
|
|
355
|
+
type=int, default=None,
|
|
356
|
+
help="Random seed for reproducibility (default: random)")
|
|
357
|
+
|
|
358
|
+
# Flags
|
|
359
|
+
events = parser.add_mutually_exclusive_group()
|
|
360
|
+
events.add_argument('--loss-only',
|
|
361
|
+
'--lossonly', # compatibility option name; to remove
|
|
362
|
+
dest='loss_only',
|
|
363
|
+
help='Only compile loss events', action='store_true')
|
|
364
|
+
events.add_argument('--duplication-only',
|
|
365
|
+
'--duplonly', # compatibility option name; to remove
|
|
366
|
+
dest='duplication_only',
|
|
367
|
+
help='Only compile duplication events', action='store_true')
|
|
368
|
+
parser.add_argument('--use-tax-ids',
|
|
369
|
+
'--taxcodes', # compatibility option name; to remove
|
|
370
|
+
dest='use_tax_ids',
|
|
371
|
+
help='Match species to tree nodes by NCBI taxon ID instead of species name',
|
|
372
|
+
action='store_true')
|
|
373
|
+
parser.add_argument('--slice-subhogs',
|
|
374
|
+
'--slicesubhogs', # compatibility option name; to remove
|
|
375
|
+
dest='slice_subhogs',
|
|
376
|
+
help='Make profiles for subHOGs', action='store_true')
|
|
377
|
+
|
|
378
|
+
# Retain unsupported legacy options only to give an actionable CLI error.
|
|
379
|
+
parser.add_argument('--reformat-names',
|
|
380
|
+
'--reformat_names',
|
|
381
|
+
dest='reformat_names',
|
|
382
|
+
help=argparse.SUPPRESS,
|
|
383
|
+
action='store_true')
|
|
384
|
+
|
|
385
|
+
# Other options
|
|
386
|
+
parser.add_argument('--jobs', '--njobs', '-j',
|
|
387
|
+
'--nthreads', # compatibility option name; to remove
|
|
388
|
+
dest="njobs",
|
|
389
|
+
help="Number of worker processes", type=int, default=1)
|
|
390
|
+
|
|
391
|
+
parser.add_argument('--verbose', '-v', help='Print verbose output', action='store_true')
|
|
392
|
+
return parser
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def add_search_arguments(parser):
|
|
396
|
+
parser.add_argument("hog_id",
|
|
397
|
+
help="HOG ID for similarity search",
|
|
398
|
+
type=str),
|
|
399
|
+
parser.add_argument("--k", "-k",
|
|
400
|
+
help="Number of nearest neighbors to consider",
|
|
401
|
+
type=int, default=10)
|
|
402
|
+
parser.add_argument("--level",
|
|
403
|
+
help="Taxonomic level at which the query is defined."
|
|
404
|
+
"Used for ambiguous HOG names defined at"
|
|
405
|
+
"multiple levels.",
|
|
406
|
+
type=str)
|
|
407
|
+
parser.add_argument("--index-dir",
|
|
408
|
+
help="Directory containing HogProf index files. "
|
|
409
|
+
"Produced by `build`", type=str, required=True)
|
|
410
|
+
parser.add_argument("--oma", help="Path to OmaServer.h5 file", type=str)
|
|
411
|
+
parser.add_argument("--nsamples", help="no of samples", type=int, default=256)
|
|
412
|
+
parser.add_argument("--slice-subhogs",
|
|
413
|
+
help="Use slice_subhogs mode",
|
|
414
|
+
action="store_true")
|
|
415
|
+
return parser
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def run_build(parsed_args):
|
|
419
|
+
if parsed_args.verbose:
|
|
420
|
+
setup_cli(verbose=True)
|
|
421
|
+
|
|
422
|
+
# Suppress pyham's noisy INFO messages, including during initialization.
|
|
423
|
+
logging.getLogger('pyham').setLevel(logging.WARNING)
|
|
424
|
+
|
|
425
|
+
print_startup(vars(parsed_args))
|
|
426
|
+
|
|
427
|
+
build_args = vars(parsed_args).copy()
|
|
428
|
+
del build_args['reformat_names']
|
|
429
|
+
del build_args['orthoxml_tar']
|
|
430
|
+
del build_args['command']
|
|
431
|
+
del build_args['func']
|
|
432
|
+
|
|
433
|
+
# import and run as late as possible to not stagger CLI
|
|
434
|
+
from hogprof.lshbuilder import run
|
|
435
|
+
run(**build_args)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def run_search(parsed_args):
|
|
439
|
+
setup_cli(verbose=False)
|
|
440
|
+
|
|
441
|
+
search_args = vars(parsed_args).copy()
|
|
442
|
+
del search_args["command"]
|
|
443
|
+
del search_args["func"]
|
|
444
|
+
|
|
445
|
+
from hogprof.profiler import run_search
|
|
446
|
+
results = run_search(**search_args)
|
|
447
|
+
print_search_result(results)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def main(argv=None):
|
|
451
|
+
# setup Rich CLI before the parser. This is to provide
|
|
452
|
+
# rich-formatted output for argparse argument validation, --help, etc.
|
|
453
|
+
setup_cli()
|
|
454
|
+
parser = RichArgumentParser(prog="hogprof")
|
|
455
|
+
|
|
456
|
+
parser.add_argument(
|
|
457
|
+
"--version",
|
|
458
|
+
action="version",
|
|
459
|
+
version=f"%(prog)s {__version__}",
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
# set up the 'build' command
|
|
463
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
464
|
+
builder_parser = subparsers.add_parser("build", help="Build a searchable LSH index")
|
|
465
|
+
add_build_arguments(builder_parser)
|
|
466
|
+
builder_parser.set_defaults(func=run_build)
|
|
467
|
+
|
|
468
|
+
# set up the `search` command
|
|
469
|
+
search_parser = subparsers.add_parser("search", help="Query an LSH index")
|
|
470
|
+
add_search_arguments(search_parser)
|
|
471
|
+
search_parser.set_defaults(func=run_search)
|
|
472
|
+
|
|
473
|
+
# parse and go
|
|
474
|
+
argv = sys.argv[1:] if argv is None else argv
|
|
475
|
+
args = parser.parse_args(argv if argv else ["-h"])
|
|
476
|
+
if args.command == "build":
|
|
477
|
+
_validate_args(builder_parser, args)
|
|
478
|
+
args.func(args)
|
|
479
|
+
|
|
480
|
+
return 0
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
if __name__ == '__main__':
|
|
484
|
+
main()
|