From 864429f9a63c2f67df1e30809724a05cd8b2a865 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Tue, 30 Jun 2026 11:08:45 +0200 Subject: feat: CLI wiring from command registry --- firefly_cli/cli.py | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++ tests/unit/test_cli.py | 26 +++++++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 firefly_cli/cli.py create mode 100644 tests/unit/test_cli.py diff --git a/firefly_cli/cli.py b/firefly_cli/cli.py new file mode 100644 index 0000000..d547455 --- /dev/null +++ b/firefly_cli/cli.py @@ -0,0 +1,56 @@ +# Copyright (C) 2026 Danilo M. GPL-2.0-only +import argparse +import sys +from firefly_cli import config, registry, output +from firefly_cli.client import Client +from firefly_cli.resolver import Resolver +from firefly_cli.context import Context +from firefly_cli.errors import FireflyError +import firefly_cli.commands # noqa: F401 triggers registration + +# Commands that must work without a configured client. +_NO_CLIENT = {"auth set"} + +def _build_parser(): + parser = argparse.ArgumentParser(prog="firefly", + description="CLI for Firefly III") + parser.add_argument("--human", action="store_true", + help="human-readable tables instead of JSON") + parser.add_argument("--url", help="override base URL for this call") + parser.add_argument("--token", help="override token for this call") + sub = parser.add_subparsers(dest="_group", required=True) + + # Group "tx add" / "account list" into nested subparsers. + groups = {} + for cmd in registry.all_commands(): + group, _, leaf = cmd.name.partition(" ") + if group not in groups: + gp = sub.add_parser(group) + groups[group] = gp.add_subparsers(dest="_leaf", required=True) + lp = groups[group].add_parser(leaf, help=cmd.help) + if cmd.args: + cmd.args(lp) + lp.set_defaults(_handler=cmd.handler, _cmdname=cmd.name) + return parser + +def main(argv=None): + argv = sys.argv[1:] if argv is None else argv + parser = _build_parser() + args = parser.parse_args(argv) + try: + if args._cmdname in _NO_CLIENT: + ctx = Context(client=None, resolver=None, human=args.human) + else: + cfg = config.load() + url = args.url or cfg["url"] + token = args.token or cfg["token"] + client = Client(url, token) + ctx = Context(client=client, resolver=Resolver(client), + human=args.human) + return args._handler(args, ctx) + except FireflyError as e: + output.emit_error(str(e)) + return 1 + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py new file mode 100644 index 0000000..883f200 --- /dev/null +++ b/tests/unit/test_cli.py @@ -0,0 +1,26 @@ +import unittest +from unittest.mock import patch, MagicMock +from firefly_cli import cli + +class TestCli(unittest.TestCase): + @patch("firefly_cli.cli.config.load") + @patch("firefly_cli.cli.Client") + def test_dispatches_account_list(self, Client, load): + load.return_value = {"url": "https://f", "token": "t"} + Client.return_value.request.return_value = {"data": []} + rc = cli.main(["account", "list"]) + self.assertEqual(rc, 0) + + @patch("firefly_cli.cli.config.load") + def test_config_error_returns_nonzero(self, load): + from firefly_cli.errors import ConfigError + load.side_effect = ConfigError("no config") + rc = cli.main(["account", "list"]) + self.assertEqual(rc, 1) + + def test_auth_set_does_not_require_config(self): + # auth set must run even with no config/client + with patch("firefly_cli.cli.config.write") as w: + w.return_value = "/tmp/x" + rc = cli.main(["auth", "set", "--url", "https://f", "--token", "t"]) + self.assertEqual(rc, 0) -- cgit v1.2.3