aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-06-30 11:08:45 +0200
committerDanilo M. <danix@danix.xyz>2026-06-30 11:08:45 +0200
commit864429f9a63c2f67df1e30809724a05cd8b2a865 (patch)
tree28919d43d7ae273e7c9f6690e3afcc79245853da
parenta9b7872fd85cbde483bc65fc1540a9d7f0c5d193 (diff)
downloadfirefly-cli-864429f9a63c2f67df1e30809724a05cd8b2a865.tar.gz
firefly-cli-864429f9a63c2f67df1e30809724a05cd8b2a865.zip
feat: CLI wiring from command registry
-rw-r--r--firefly_cli/cli.py56
-rw-r--r--tests/unit/test_cli.py26
2 files changed, 82 insertions, 0 deletions
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. <danix@danix.xyz> 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)