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/__init__.py +6 -0
- akt/cli.py +211 -0
- akt/client.py +240 -0
- akt/commands.py +88 -0
- akt/config.py +121 -0
- akt/output.py +84 -0
- akt/registry.py +263 -0
- akt/resources.py +467 -0
- akt_cli-0.1.0.dist-info/METADATA +274 -0
- akt_cli-0.1.0.dist-info/RECORD +13 -0
- akt_cli-0.1.0.dist-info/WHEEL +4 -0
- akt_cli-0.1.0.dist-info/entry_points.txt +3 -0
- akt_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
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}
|