aboutsummaryrefslogtreecommitdiffstats
path: root/firefly_cli/resolver.py
blob: cb7e9a89095b87cb9b31b169ceba065f4d0cc835 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# Copyright (C) 2026 Danilo M. <danix@danix.xyz>  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", {})}