akt-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
akt/output.py ADDED
@@ -0,0 +1,84 @@
1
+ """Output helpers: JSON or aligned text tables."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sys
7
+ from typing import Any, Sequence
8
+
9
+
10
+ def print_json(obj: Any) -> None:
11
+ json.dump(obj, sys.stdout, indent=2, ensure_ascii=False, default=str)
12
+ sys.stdout.write("\n")
13
+
14
+
15
+ def _stringify(value: Any) -> str:
16
+ if value is None:
17
+ return ""
18
+ if isinstance(value, bool):
19
+ return "yes" if value else "no"
20
+ if isinstance(value, (dict, list)):
21
+ return json.dumps(value, ensure_ascii=False, default=str)
22
+ return str(value)
23
+
24
+
25
+ def _get(row: dict, path: str) -> Any:
26
+ """Fetch a possibly-nested value using dotted path (a.b.c)."""
27
+ cur: Any = row
28
+ for part in path.split("."):
29
+ if isinstance(cur, dict):
30
+ cur = cur.get(part)
31
+ else:
32
+ return None
33
+ return cur
34
+
35
+
36
+ def print_table(rows: Sequence[dict], columns: Sequence[str], *, headers: Sequence[str] | None = None) -> None:
37
+ """Render rows as an aligned table. ``columns`` are dotted field paths."""
38
+ if not rows:
39
+ print("(no records)")
40
+ return
41
+ head = list(headers) if headers else [c.split(".")[-1] for c in columns]
42
+ table = [head]
43
+ for row in rows:
44
+ table.append([_stringify(_get(row, c)) for c in columns])
45
+
46
+ widths = [0] * len(head)
47
+ for line in table:
48
+ for i, cell in enumerate(line):
49
+ widths[i] = max(widths[i], len(cell))
50
+
51
+ def fmt(line: list[str]) -> str:
52
+ return " ".join(cell.ljust(widths[i]) for i, cell in enumerate(line)).rstrip()
53
+
54
+ print(fmt(table[0]))
55
+ print(" ".join("-" * w for w in widths))
56
+ for line in table[1:]:
57
+ print(fmt(line))
58
+
59
+
60
+ def emit(data: Any, *, as_json: bool, columns: Sequence[str] | None = None,
61
+ headers: Sequence[str] | None = None) -> None:
62
+ """Top-level dispatch used by commands."""
63
+ if as_json or columns is None:
64
+ print_json(data)
65
+ return
66
+ if isinstance(data, list):
67
+ print_table(data, columns, headers=headers)
68
+ elif isinstance(data, dict):
69
+ # single record -> key/value listing
70
+ print_table([{"field": k, "value": v} for k, v in _flatten(data).items()],
71
+ ["field", "value"])
72
+ else:
73
+ print(_stringify(data))
74
+
75
+
76
+ def _flatten(d: dict, prefix: str = "") -> dict:
77
+ out: dict[str, Any] = {}
78
+ for k, v in d.items():
79
+ key = f"{prefix}{k}"
80
+ if isinstance(v, dict) and "data" in v and isinstance(v["data"], (list, dict)):
81
+ out[key] = v["data"]
82
+ else:
83
+ out[key] = v
84
+ return out
akt/registry.py ADDED
@@ -0,0 +1,263 @@
1
+ """The concrete set of resources akt exposes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .resources import (
6
+ Resource,
7
+ f,
8
+ build_document_create,
9
+ build_document_update,
10
+ build_payment_create,
11
+ build_transfer_create,
12
+ resolve_payment_delete,
13
+ )
14
+
15
+ # Common column sets
16
+ _CONTACT_COLS = [
17
+ ("ID", "id"), ("Name", "name"), ("Email", "email"),
18
+ ("Phone", "phone"), ("Currency", "currency_code"), ("Enabled", "enabled"),
19
+ ]
20
+ _DOC_COLS = [
21
+ ("ID", "id"), ("Number", "document_number"), ("Contact", "contact_name"),
22
+ ("Status", "status"), ("Total", "amount_formatted"),
23
+ ("Due", "amount_due_formatted"), ("Issued", "issued_at"), ("Due date", "due_at"),
24
+ ]
25
+ _TXN_COLS = [
26
+ ("ID", "id"), ("Number", "number"), ("Type", "type"), ("Contact", "contact.data.name"),
27
+ ("Amount", "amount_formatted"), ("Paid at", "paid_at"), ("Method", "payment_method"),
28
+ ("Doc", "document_id"),
29
+ ]
30
+
31
+
32
+ def _contact_fields() -> list:
33
+ return [
34
+ f("name", "Display name", required=True),
35
+ f("email", "Email address"),
36
+ f("phone", "Phone number"),
37
+ f("tax-number", "Tax / VAT number"),
38
+ f("website", "Website URL"),
39
+ f("currency-code", "Currency code (e.g. USD)", default="USD"),
40
+ f("reference", "Internal reference"),
41
+ f("address", "Street address"),
42
+ f("city", "City"),
43
+ f("zip-code", "Postal / ZIP code"),
44
+ f("state", "State / province"),
45
+ f("country", "Country code (e.g. US)"),
46
+ f("enabled", "Enable the record", is_flag=True, default=1),
47
+ ]
48
+
49
+
50
+ CUSTOMER = Resource(
51
+ noun="customer",
52
+ endpoint="contacts",
53
+ type_scope="customer",
54
+ body_type="customer",
55
+ fields=_contact_fields(),
56
+ columns=_CONTACT_COLS,
57
+ help="Customers (sales contacts)",
58
+ )
59
+
60
+ VENDOR = Resource(
61
+ noun="vendor",
62
+ endpoint="contacts",
63
+ type_scope="vendor",
64
+ body_type="vendor",
65
+ fields=_contact_fields(),
66
+ columns=_CONTACT_COLS,
67
+ help="Vendors / suppliers (purchase contacts)",
68
+ )
69
+
70
+ ITEM = Resource(
71
+ noun="item",
72
+ endpoint="items",
73
+ fields=[
74
+ f("name", "Item name", required=True),
75
+ f("description", "Description"),
76
+ f("type", "product or service", default="product", choices=["product", "service"]),
77
+ f("sale-price", "Sale price"),
78
+ f("purchase-price", "Purchase price"),
79
+ f("category-id", "Category id"),
80
+ f("enabled", "Enable the record", is_flag=True, default=1),
81
+ ],
82
+ columns=[
83
+ ("ID", "id"), ("Name", "name"), ("Type", "type"),
84
+ ("Sale", "sale_price_formatted"), ("Purchase", "purchase_price_formatted"),
85
+ ("Enabled", "enabled"),
86
+ ],
87
+ help="Products and services",
88
+ )
89
+
90
+ ACCOUNT = Resource(
91
+ noun="account",
92
+ endpoint="accounts",
93
+ fields=[
94
+ f("name", "Account name", required=True),
95
+ f("number", "Account number", required=True),
96
+ f("type", "Account type", default="bank"),
97
+ f("currency-code", "Currency code", default="USD"),
98
+ f("opening-balance", "Opening balance", default=0),
99
+ f("bank-name", "Bank name"),
100
+ f("bank-phone", "Bank phone"),
101
+ f("bank-address", "Bank address"),
102
+ f("enabled", "Enable the record", is_flag=True, default=1),
103
+ ],
104
+ columns=[
105
+ ("ID", "id"), ("Name", "name"), ("Number", "number"),
106
+ ("Currency", "currency_code"), ("Balance", "current_balance_formatted"),
107
+ ("Enabled", "enabled"),
108
+ ],
109
+ help="Bank / cash accounts",
110
+ )
111
+
112
+ CATEGORY = Resource(
113
+ noun="category",
114
+ endpoint="categories",
115
+ fields=[
116
+ f("name", "Category name", required=True),
117
+ f("type", "income | expense | item | other", required=True,
118
+ choices=["income", "expense", "item", "other"]),
119
+ f("color", "Hex color", default="#00bcd4"),
120
+ f("enabled", "Enable the record", is_flag=True, default=1),
121
+ ],
122
+ columns=[
123
+ ("ID", "id"), ("Name", "name"), ("Type", "type"),
124
+ ("Color", "color"), ("Enabled", "enabled"),
125
+ ],
126
+ help="Income / expense / item categories",
127
+ )
128
+
129
+ TAX = Resource(
130
+ noun="tax",
131
+ endpoint="taxes",
132
+ fields=[
133
+ f("name", "Tax name", required=True),
134
+ f("rate", "Tax rate (percent)", required=True),
135
+ f("type", "normal | inclusive | compound | withholding | fixed",
136
+ default="normal",
137
+ choices=["normal", "inclusive", "compound", "withholding", "fixed"]),
138
+ f("enabled", "Enable the record", is_flag=True, default=1),
139
+ ],
140
+ columns=[
141
+ ("ID", "id"), ("Name", "name"), ("Rate", "rate"),
142
+ ("Type", "type"), ("Enabled", "enabled"),
143
+ ],
144
+ help="Tax rates",
145
+ )
146
+
147
+ CURRENCY = Resource(
148
+ noun="currency",
149
+ endpoint="currencies",
150
+ fields=[
151
+ f("name", "Currency name", required=True),
152
+ f("code", "ISO code (e.g. EUR)", required=True),
153
+ f("rate", "Exchange rate vs default", required=True),
154
+ f("precision", "Decimal precision", default=2),
155
+ f("symbol", "Symbol"),
156
+ f("symbol-first", "Symbol before amount (1/0)", default=1),
157
+ f("decimal-mark", "Decimal mark", default="."),
158
+ f("thousands-separator", "Thousands separator", default=","),
159
+ f("enabled", "Enable the record", is_flag=True, default=1),
160
+ ],
161
+ columns=[
162
+ ("ID", "id"), ("Name", "name"), ("Code", "code"),
163
+ ("Rate", "rate"), ("Enabled", "enabled"),
164
+ ],
165
+ help="Currencies",
166
+ )
167
+
168
+ # Documents -----------------------------------------------------------------
169
+
170
+ _DOC_FIELDS = [
171
+ f("contact", "Contact id (customer for invoice / vendor for bill)"),
172
+ f("number", "Document number (auto-generated if omitted)"),
173
+ f("status", "draft | sent | received | paid | cancelled ..."),
174
+ f("issued-at", "Issue date (YYYY-MM-DD)"),
175
+ f("due-at", "Due date (YYYY-MM-DD)"),
176
+ f("currency-code", "Currency code"),
177
+ f("currency-rate", "Currency rate"),
178
+ f("category-id", "Category id"),
179
+ f("order-number", "Order number"),
180
+ f("notes", "Notes"),
181
+ ]
182
+
183
+ INVOICE = Resource(
184
+ noun="invoice",
185
+ endpoint="documents",
186
+ type_scope="invoice",
187
+ body_type="invoice",
188
+ fields=_DOC_FIELDS,
189
+ columns=_DOC_COLS,
190
+ build_create=build_document_create,
191
+ build_update=build_document_update,
192
+ help="Sales invoices",
193
+ )
194
+
195
+ BILL = Resource(
196
+ noun="bill",
197
+ endpoint="documents",
198
+ type_scope="bill",
199
+ body_type="bill",
200
+ fields=_DOC_FIELDS,
201
+ columns=_DOC_COLS,
202
+ build_create=build_document_create,
203
+ build_update=build_document_update,
204
+ help="Purchase bills",
205
+ )
206
+
207
+ # Payments (transactions) ---------------------------------------------------
208
+
209
+ PAYMENT = Resource(
210
+ noun="payment",
211
+ endpoint="transactions",
212
+ fields=[
213
+ f("type", "income | expense", choices=["income", "expense"]),
214
+ f("invoice", "Invoice id to apply an income payment to"),
215
+ f("bill", "Bill id to apply an expense payment to"),
216
+ f("document-id", "Linked document id (advanced)"),
217
+ f("contact-id", "Contact id"),
218
+ f("amount", "Payment amount"),
219
+ f("account-id", "Bank/cash account id"),
220
+ f("category-id", "Category id"),
221
+ f("paid-at", "Payment date/time (YYYY-MM-DD)"),
222
+ f("currency-code", "Currency code"),
223
+ f("currency-rate", "Currency rate"),
224
+ f("payment-method", "Payment method code", default="offline-payments.cash.1"),
225
+ f("number", "Transaction number (auto if omitted)"),
226
+ f("reference", "Reference"),
227
+ f("description", "Description"),
228
+ ],
229
+ columns=_TXN_COLS,
230
+ supports_toggle=False,
231
+ build_create=build_payment_create,
232
+ delete_resolver=resolve_payment_delete,
233
+ help="Payments / transactions (income & expense)",
234
+ )
235
+
236
+ TRANSFER = Resource(
237
+ noun="transfer",
238
+ endpoint="transfers",
239
+ fields=[
240
+ f("from-account-id", "Source account id", required=True),
241
+ f("to-account-id", "Destination account id", required=True),
242
+ f("amount", "Amount", required=True),
243
+ f("transferred-at", "Transfer date/time (YYYY-MM-DD)"),
244
+ f("payment-method", "Payment method code", default="offline-payments.cash.1"),
245
+ f("reference", "Reference"),
246
+ f("description", "Description"),
247
+ ],
248
+ columns=[
249
+ ("ID", "id"), ("From", "from_account.data.name"), ("To", "to_account.data.name"),
250
+ ("Amount", "amount"),
251
+ ],
252
+ supports_toggle=False,
253
+ build_create=build_transfer_create,
254
+ help="Transfers between accounts",
255
+ )
256
+
257
+
258
+ RESOURCES: list[Resource] = [
259
+ CUSTOMER, VENDOR, ITEM, ACCOUNT, CATEGORY, TAX, CURRENCY,
260
+ INVOICE, BILL, PAYMENT, TRANSFER,
261
+ ]
262
+
263
+ BY_NOUN = {r.noun: r for r in RESOURCES}