From 8306fb4c3e0298d644a2828326c24614f4a0d1c3 Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 27 Feb 2025 16:45:57 +0000 Subject: [PATCH] Add cmd line app --- docs/cmd.md | 4 +- pyproject.toml | 3 + src/python/qubed/__main__.py | 158 +++++++++++++++++++++-------------- 3 files changed, 101 insertions(+), 64 deletions(-) diff --git a/docs/cmd.md b/docs/cmd.md index fcbd44b..c446dd2 100644 --- a/docs/cmd.md +++ b/docs/cmd.md @@ -23,5 +23,7 @@ use `--input` and `--output` to specify input and output files respectively. There's some handy test data in the `tests/data` directory. For example: ```bash -gzip -dc tests/data/fdb_list_compact.gz| qubed --from=fdblist +gzip -dc tests/data/fdb_list_compact.gz| qubed convert --from=fdb --to=text --output=qube.txt +gzip -dc tests/data/fdb_list_porcelain.gz| qubed convert --from=fdb --to=json --output=qube.json +gzip -dc tests/data/fdb_list_compact.gz | qubed convert --from=fdb --to=html --output=qube.html ``` diff --git a/pyproject.toml b/pyproject.toml index a53609f..ee1585d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,9 @@ requires-python = ">= 3.11" dynamic = ["version"] dependencies = [ "frozendict", + "rich", + "numpy", + "click", ] # Because this is a mixed rust/python project the structure is src/python/qubed rather than the more typical src/qubed diff --git a/src/python/qubed/__main__.py b/src/python/qubed/__main__.py index 5bed010..9cceb53 100644 --- a/src/python/qubed/__main__.py +++ b/src/python/qubed/__main__.py @@ -1,82 +1,114 @@ -import argparse -import sys +import time +import click +import psutil from rich.console import Console +from rich.layout import Layout +from rich.live import Live +from rich.panel import Panel +from rich.spinner import Spinner +from rich.text import Text from qubed import Qube from qubed.convert import parse_fdb_list console = Console(stderr=True) +process = psutil.Process() + +PRINT_INTERVAL = 0.25 +@click.group() def main(): - parser = argparse.ArgumentParser( - description="Generate a compressed tree from various inputs." - ) - - subparsers = parser.add_subparsers(title="subcommands", required=True) - parser_convert = subparsers.add_parser( - "convert", help="Convert trees from one format to another." - ) - # parser_another = subparsers.add_parser( - # "another_subcommand", help="Does something else" - # ) - - parser_convert.add_argument( - "--input", - type=argparse.FileType("r"), - default=sys.stdin, - help="Specify the input file (default: standard input).", - ) - parser_convert.add_argument( - "--output", - type=argparse.FileType("w"), - default=sys.stdout, - help="Specify the output file (default: standard output).", - ) - - parser_convert.add_argument( - "--input_format", - choices=["fdb", "mars"], - default="fdb", - help="""Specify the input format: - fdb: the output of fdb list --porcelain - mars: the output of mars list - """, - ) - - parser_convert.add_argument( - "--output_format", - choices=["text", "html"], - default="text", - help="Specify the output format (text or html).", - ) - parser_convert.set_defaults(func=convert) - - args = parser.parse_args() - args.func(args) + """Command-line tool for working with trees.""" + pass -def convert(args): +@main.command() +@click.option( + "--input", + type=click.File("r"), + default="-", + help="Specify the input file (default: standard input).", +) +@click.option( + "--output", + type=click.File("w"), + default="-", + help="Specify the output file (default: standard output).", +) +@click.option( + "--from", + "from_format", + type=click.Choice(["fdb", "mars"]), + default="fdb", + help="Specify the input format: fdb (fdb list --porcelain) or mars (mars list).", +) +@click.option( + "--to", + "to_format", + type=click.Choice(["text", "html", "json"]), + default="text", + help="Specify the output format: text, html, json.", +) +def convert(input, output, from_format, to_format): + """Convert trees from one format to another.""" q = Qube.empty() - for datacube in parse_fdb_list(args.input): - new_branch = Qube.from_datacube(datacube) - q = q | Qube.from_datacube(datacube) + t = time.time() + i0 = 0 + n0 = 0 + depth = 5 + log = Text() + summary = Layout() + summary.split_column( + Layout(name="upper"), + Layout(name="qube"), + ) + summary["upper"].split_row( + Layout(name="performance"), + Layout(log, name="log"), + ) + spinner = Spinner("aesthetic", text="Performance", speed=0.3) - # output = match args.output_format: - # case "text": - # str(q) - # case "html": - # q.html() - output = "fw" + with Live(summary, auto_refresh=False, transient=True, console=console) as live: + for i, datacube in enumerate(parse_fdb_list(input)): + new_branch = Qube.from_datacube(datacube) + q = q | new_branch - with open(args.output, "w") as f: - f.write(output) + if time.time() - t > PRINT_INTERVAL: + tree = q.__str__(depth=depth) + if tree.count("\n") > 20: + depth -= 1 + if tree.count("\n") < 5: + depth += 1 - console.print([1, 2, 3]) - console.print("[blue underline]Looks like a link") - console.print(locals()) - console.print("FOO", style="white on blue") + summary["performance"].update( + Panel( + Text.assemble( + f"The Qube has {q.n_leaves} leaves and {q.n_nodes} internal nodes so far.\n", + f"{(i - i0) / (time.time() - t) / PRINT_INTERVAL:.0f} lines per second. ", + f"{(q.n_leaves - n0) / (time.time() - t):.0f} leaves per second.\n", + f"Memory usage: {process.memory_info().rss / 1024 / 1024:.0f} MB\n", + ), + title=spinner.render(time.time()), + border_style="blue", + ) + ) + summary["qube"].update( + Panel(tree, title=f"Qube (depth {depth})", border_style="blue") + ) + summary["log"].update( + Panel( + f"{datacube}", border_style="blue", title="Last Datacube Added" + ) + ) + live.refresh() + i0 = i + n0 = q.n_leaves + t = time.time() + + output_content = str(q) if to_format == "text" else q.html().html + output.write(output_content) if __name__ == "__main__":