From ef562778eecde85b45c9122eef0f75f2e1bbf152 Mon Sep 17 00:00:00 2001 From: Warkanlock Date: Tue, 1 Sep 2026 15:42:58 -0400 Subject: [PATCH 1/2] fix(cli): drop nine commands with no v2 route Factory registered create/update/get on read-only resources. Those verbs only ever 404 or 405. Inbox get is removed, not repointed: the two real inbox detail routes need a type discriminator the generic get cannot send. --- src/dualentry_cli/commands/__init__.py | 6 ++-- src/dualentry_cli/main.py | 10 +++--- tests/test_stale_commands.py | 44 ++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 7 deletions(-) create mode 100644 tests/test_stale_commands.py diff --git a/src/dualentry_cli/commands/__init__.py b/src/dualentry_cli/commands/__init__.py index 41f6db1..f429e99 100644 --- a/src/dualentry_cli/commands/__init__.py +++ b/src/dualentry_cli/commands/__init__.py @@ -121,6 +121,7 @@ def make_resource_app( resource: str, path: str, *, + has_get: bool = True, has_create: bool = True, has_update: bool = True, has_delete: bool = False, @@ -180,7 +181,7 @@ def list_cmd( sig = inspect.signature(list_cmd) list_cmd.__signature__ = sig.replace(parameters=[p for p in sig.parameters.values() if p.name not in remove]) - if has_number: + if has_get and has_number: @app.command("get") def get_cmd_auto( @@ -234,7 +235,8 @@ def get_cmd_by_id( format_output(data, resource=resource, fmt=output) get_cmd_by_id.__doc__ = f"Get a {resource} by ID." - else: + + elif has_get: @app.command("get") def get_cmd( diff --git a/src/dualentry_cli/main.py b/src/dualentry_cli/main.py index f50302f..30d6dae 100644 --- a/src/dualentry_cli/main.py +++ b/src/dualentry_cli/main.py @@ -53,13 +53,13 @@ app.add_typer(make_resource_app("journal entries", "journal-entry", "journal-entries", has_number=True), name="journal-entries") app.add_typer(make_resource_app("bank transfers", "bank-transfer", "bank-transfers", has_number=True), name="bank-transfers") app.add_typer(make_resource_app("fixed assets", "fixed-asset", "fixed-assets", has_number=True), name="fixed-assets") -app.add_typer(make_resource_app("depreciation books", "depreciation-book", "depreciation-books"), name="depreciation-books") +app.add_typer(make_resource_app("depreciation books", "depreciation-book", "depreciation-books", has_create=False, has_update=False), name="depreciation-books") # Entities app.add_typer(make_resource_app("customers", "customer", "customers"), name="customers") app.add_typer(make_resource_app("vendors", "vendor", "vendors"), name="vendors") app.add_typer(make_resource_app("items", "item", "items"), name="items") -app.add_typer(make_resource_app("companies", "company", "companies"), name="companies") +app.add_typer(make_resource_app("companies", "company", "companies", has_create=False, has_update=False), name="companies") app.add_typer(make_resource_app("classifications", "classification", "classifications"), name="classifications") # Recurring @@ -71,7 +71,7 @@ # Other app.add_typer(make_resource_app("contracts", "contract", "contracts"), name="contracts") -app.add_typer(make_resource_app("budgets", "budget", "budgets"), name="budgets") +app.add_typer(make_resource_app("budgets", "budget", "budgets", has_create=False, has_update=False), name="budgets") app.add_typer(make_resource_app("workflows", "workflow", "workflows", has_create=False, has_update=False), name="workflows") app.add_typer( make_resource_app( @@ -87,8 +87,8 @@ ), name="intercompany-journal-entries", ) -app.add_typer(make_resource_app("paper checks", "paper-check", "paper-checks", has_number=True), name="paper-checks") -app.add_typer(make_resource_app("inbox items", "inbox-item", "inbox", has_create=False, has_update=False), name="inbox") +app.add_typer(make_resource_app("paper checks", "paper-check", "paper-checks", has_create=False, has_update=False), name="paper-checks") +app.add_typer(make_resource_app("inbox items", "inbox-item", "inbox", has_get=False, has_create=False, has_update=False), name="inbox") def version_callback(value: bool): diff --git a/tests/test_stale_commands.py b/tests/test_stale_commands.py new file mode 100644 index 0000000..f3fbf39 --- /dev/null +++ b/tests/test_stale_commands.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +import pytest + +from dualentry_cli.main import app + +STALE_COMMANDS = [ + ("companies", "create"), + ("companies", "update"), + ("budgets", "create"), + ("budgets", "update"), + ("depreciation-books", "create"), + ("depreciation-books", "update"), + ("paper-checks", "create"), + ("paper-checks", "update"), + ("inbox", "get"), +] + + +def _commands(resource: str) -> set[str]: + group = next(g for g in app.registered_groups if g.name == resource) + return {c.name for c in group.typer_instance.registered_commands} + + +@pytest.mark.parametrize(("resource", "command"), STALE_COMMANDS) +def test_stale_command_is_not_registered(resource: str, command: str): + assert command not in _commands(resource), f"'dualentry {resource} {command}' has no v2 route and must not be registered" + + +@pytest.mark.parametrize("resource", ["companies", "budgets", "depreciation-books", "paper-checks"]) +def test_read_only_resource_keeps_its_read_commands(resource: str): + assert _commands(resource) == {"list", "get"} + + +def test_inbox_keeps_only_list(): + assert _commands("inbox") == {"list"} + + +def test_paper_checks_has_no_number_lookups(): + assert not _commands("paper-checks") & {"get-number", "get-id"} + + +def test_writable_resource_is_untouched(): + assert {"create", "update"} <= _commands("invoices") From cc205e82253d9758373c041c8f9a1595f7e95228 Mon Sep 17 00:00:00 2001 From: Warkanlock Date: Tue, 1 Sep 2026 15:59:18 -0400 Subject: [PATCH 2/2] fix(cli): make get-vs-number factory branch explicit elif has_get already excluded has_number via the if above. Spell not has_number so the three states stay obvious. --- src/dualentry_cli/commands/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dualentry_cli/commands/__init__.py b/src/dualentry_cli/commands/__init__.py index f429e99..d801a94 100644 --- a/src/dualentry_cli/commands/__init__.py +++ b/src/dualentry_cli/commands/__init__.py @@ -236,7 +236,7 @@ def get_cmd_by_id( get_cmd_by_id.__doc__ = f"Get a {resource} by ID." - elif has_get: + elif has_get and not has_number: @app.command("get") def get_cmd(