diff options
| -rw-r--r-- | SKILL.md | 6 | ||||
| -rw-r--r-- | completions/firefly.bash | 2 | ||||
| -rw-r--r-- | firefly_cli/__init__.py | 2 | ||||
| -rw-r--r-- | firefly_cli/commands/transaction.py | 20 | ||||
| -rw-r--r-- | pyproject.toml | 2 | ||||
| -rw-r--r-- | tests/unit/test_commands_transaction.py | 48 |
6 files changed, 75 insertions, 5 deletions
@@ -61,7 +61,7 @@ firefly tx edit <id> [--amount N] [--date YYYY-MM-DD] [--desc TEXT] [--from <acct>] [--to <acct>] [--category NAME] [--tags a,b] [--type T] # only fields passed are changed firefly tx delete <id> --yes # --yes required, no prompt -firefly tx list [--since YYYY-MM-DD] [--until YYYY-MM-DD] [--account NAME] [--limit N] +firefly tx list [--since YYYY-MM-DD] [--until YYYY-MM-DD] [--account NAME] [--limit N] [--all] firefly tx get <id> firefly tx search <query> firefly category list @@ -132,6 +132,10 @@ firefly tx delete 76 --yes # remove a duplicate - `tx list` with no transactions in range returns `[]`. Empty is not an error; it means no matches, not a failure. +- `tx list` returns at most `--limit` rows (default 20). When more match, it + prints `showing N of M (use --all for all)` to **stderr** (stdout JSON stays + clean). Pass `--all` to auto-paginate every page. Critical for reconciliation: + a truncated list makes a correct ledger look wrong. - Amounts are strings in responses, often with trailing zeros (`"0.010000000000"`). Compare numerically, do not string-match. - Dates in `tx list` filter by transaction date; omit them to use Firefly's diff --git a/completions/firefly.bash b/completions/firefly.bash index b66b400..697cd1a 100644 --- a/completions/firefly.bash +++ b/completions/firefly.bash @@ -42,7 +42,7 @@ _firefly() { "tx add") leaf_opts="--category --date --desc --from --tags --to --type";; "tx delete") leaf_opts="--yes";; "tx edit") leaf_opts="--amount --category --date --desc --from --tags --to --type";; - "tx list") leaf_opts="--account --limit --since --until";; + "tx list") leaf_opts="--account --all --limit --since --until";; esac # Leaves per group. diff --git a/firefly_cli/__init__.py b/firefly_cli/__init__.py index f41267d..79ea6e3 100644 --- a/firefly_cli/__init__.py +++ b/firefly_cli/__init__.py @@ -2,4 +2,4 @@ # Copyright (C) 2026 Danilo M. <danix@danix.xyz> # Licensed under the GNU General Public License v2.0 only. -__version__ = "0.3.0" +__version__ = "0.3.1" diff --git a/firefly_cli/commands/transaction.py b/firefly_cli/commands/transaction.py index af8a4fb..1d6f219 100644 --- a/firefly_cli/commands/transaction.py +++ b/firefly_cli/commands/transaction.py @@ -110,6 +110,8 @@ def _list_args(p): p.add_argument("--until", default=None, help="end date YYYY-MM-DD") p.add_argument("--account", default=None, help="filter by account name") p.add_argument("--limit", type=int, default=20) + p.add_argument("--all", action="store_true", + help="fetch every page (ignores --limit truncation)") @registry.command("tx list", help="list recent transactions (newest first)", args=_list_args) def cmd_list(args, ctx): @@ -123,7 +125,25 @@ def cmd_list(args, ctx): params["start"] = args.since if args.until: params["end"] = args.until + + if args.all: + rows, page = [], 1 + while True: + resp = ctx.client.request("GET", path, params={**params, "page": page}) + rows.extend(output.unwrap(resp)) + pg = (resp.get("meta") or {}).get("pagination") or {} + if page >= (pg.get("total_pages") or 1): + break + page += 1 + output.emit(rows, human=ctx.human) + return 0 + resp = ctx.client.request("GET", path, params=params) + pg = (resp.get("meta") or {}).get("pagination") or {} + total, count = pg.get("total"), pg.get("count") + if total is not None and count is not None and count < total: + import sys + print(f"showing {count} of {total} (use --all for all)", file=sys.stderr) output.emit(output.unwrap(resp), human=ctx.human) return 0 diff --git a/pyproject.toml b/pyproject.toml index 4637c1e..cb0fdaa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "firefly-iii-agent" -version = "0.3.0" +version = "0.3.1" description = "CLI tool for agent interaction with Firefly III" readme = "README.md" requires-python = ">=3.11" diff --git a/tests/unit/test_commands_transaction.py b/tests/unit/test_commands_transaction.py index 325dcb7..d8b81da 100644 --- a/tests/unit/test_commands_transaction.py +++ b/tests/unit/test_commands_transaction.py @@ -139,9 +139,55 @@ class TestTxList(unittest.TestCase): ctx, client, _ = make_ctx() client.request.return_value = {"data": []} args = MagicMock(since="2026-06-01", until="2026-06-30", - account=None, limit=10) + account=None, limit=10, all=False) tx.cmd_list(args, ctx) params = client.request.call_args[1]["params"] self.assertEqual(params["start"], "2026-06-01") self.assertEqual(params["end"], "2026-06-30") self.assertEqual(params["limit"], 10) + + def test_list_warns_when_truncated(self): + import io + from contextlib import redirect_stderr + ctx, client, _ = make_ctx() + client.request.return_value = { + "data": [{"id": str(i)} for i in range(20)], + "meta": {"pagination": {"total": 90, "count": 20, + "current_page": 1, "total_pages": 5}}, + } + args = MagicMock(since=None, until=None, account=None, limit=20, all=False) + buf = io.StringIO() + with redirect_stderr(buf): + tx.cmd_list(args, ctx) + self.assertIn("showing 20 of 90", buf.getvalue()) + self.assertEqual(client.request.call_count, 1) + + def test_list_no_warn_when_complete(self): + import io + from contextlib import redirect_stderr + ctx, client, _ = make_ctx() + client.request.return_value = { + "data": [{"id": "1"}], + "meta": {"pagination": {"total": 1, "count": 1, + "current_page": 1, "total_pages": 1}}, + } + args = MagicMock(since=None, until=None, account=None, limit=20, all=False) + buf = io.StringIO() + with redirect_stderr(buf): + tx.cmd_list(args, ctx) + self.assertEqual(buf.getvalue(), "") + + def test_list_all_paginates(self): + ctx, client, _ = make_ctx() + def page(method, path, params=None, body=None): + p = params["page"] + return { + "data": [{"id": f"{p}-{i}"} for i in range(2)], + "meta": {"pagination": {"total": 4, "count": 2, + "current_page": p, "total_pages": 2}}, + } + client.request.side_effect = page + args = MagicMock(since=None, until=None, account=None, limit=2, all=True) + rc = tx.cmd_list(args, ctx) + self.assertEqual(rc, 0) + self.assertEqual(client.request.call_count, 2) |
