# Copyright (C) 2026 Danilo M. GPL-2.0-only from firefly_cli.errors import ResolutionError class Resolver: def __init__(self, client): self.client = client def _list(self, path): resp = self.client.request("GET", path, params={"limit": 1000}) out = [] for item in resp.get("data", []): attrs = item.get("attributes", {}) out.append({"id": item.get("id"), **attrs}) return out def _match(self, kind, items, name): hits = [i for i in items if str(i.get("name", "")).lower() == name.lower()] if len(hits) == 1: return hits[0] names = ", ".join(f'{i.get("name")}(id={i.get("id")})' for i in items) if not hits: raise ResolutionError( f'No {kind} named "{name}". Available: {names or "(none)"}') raise ResolutionError( f'Ambiguous {kind} "{name}" matches ids ' + ", ".join(i["id"] for i in hits)) def account(self, name): return self._match("account", self._list("/api/v1/accounts"), name) def account_by_id(self, acc_id): # Escape hatch for same-name accounts (ISSUES.md #2): GET the account # directly; a bad id errors and client.request surfaces a FireflyError. # ponytail: Firefly returns 401 (not 404) for an unknown account id, so # the message reads "Unauthenticated"; the write is still blocked, which # is what matters. Remap to "account not found" only if it confuses users. item = self.client.request("GET", f"/api/v1/accounts/{acc_id}")["data"] return {"id": item["id"], **item.get("attributes", {})} def tag(self, name): return self._match("tag", self._list("/api/v1/tags"), name) def category(self, name): return self._match("category", self._list("/api/v1/categories"), name) def budget(self, name_or_id): # Budgets must pre-exist (Firefly does not auto-create them for a # transaction), so we resolve to an id. A numeric-looking ref goes # straight to the show endpoint (lets same-name-safe callers pin an id); # otherwise match by name against the list. # ponytail: sniffing digits makes a budget literally named "7" # unreachable by name (same lossy tradeoff ISSUES.md #2 hit for # accounts). All-digit budget names are rare; add a --budget-id flag to # tx add if this ever bites, mirroring --from-id/--to-id. if str(name_or_id).isdigit(): return self.budget_by_id(name_or_id) return self._match("budget", self._list("/api/v1/budgets"), name_or_id) def budget_by_id(self, budget_id): item = self.client.request("GET", f"/api/v1/budgets/{budget_id}")["data"] return {"id": item["id"], **item.get("attributes", {})}