ledgerkit 1.0.0.dev1__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.
- ledgerkit/__init__.py +51 -0
- ledgerkit/__main__.py +12 -0
- ledgerkit/_pandas_compat.py +21 -0
- ledgerkit/checks.py +617 -0
- ledgerkit/cli.py +448 -0
- ledgerkit/commodity_style.py +274 -0
- ledgerkit/editor_model.py +193 -0
- ledgerkit/loader.py +311 -0
- ledgerkit/models.py +459 -0
- ledgerkit/parser.py +1547 -0
- ledgerkit/reports.py +573 -0
- ledgerkit/writer.py +97 -0
- ledgerkit-1.0.0.dev1.dist-info/METADATA +203 -0
- ledgerkit-1.0.0.dev1.dist-info/RECORD +18 -0
- ledgerkit-1.0.0.dev1.dist-info/WHEEL +5 -0
- ledgerkit-1.0.0.dev1.dist-info/entry_points.txt +2 -0
- ledgerkit-1.0.0.dev1.dist-info/licenses/LICENSE +21 -0
- ledgerkit-1.0.0.dev1.dist-info/top_level.txt +1 -0
ledgerkit/cli.py
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
"""Command-line interface for ledgerkit.
|
|
2
|
+
|
|
3
|
+
Parses arguments, loads the journal, calls reports, and formats output.
|
|
4
|
+
Entry point: main() — wired to `ledgerkit` via pyproject.toml.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import contextlib
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
import time
|
|
14
|
+
from decimal import Decimal
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
# Captured as early as possible so elapsed time includes import overhead.
|
|
18
|
+
_PROGRAM_START = time.perf_counter()
|
|
19
|
+
|
|
20
|
+
from ledgerkit import __version__
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
COMMANDS = ("balance", "register", "accounts", "print", "stats", "check")
|
|
24
|
+
|
|
25
|
+
_ANSI_RED = "\033[31m"
|
|
26
|
+
_ANSI_RESET = "\033[0m"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Formats a quantity+commodity pair. When a CommodityStyle is provided it
|
|
30
|
+
# takes priority; otherwise falls back to hledger-style prefix+comma format.
|
|
31
|
+
def _fmt_amount(quantity: Decimal, commodity: str, style: object = None) -> str:
|
|
32
|
+
if style is not None:
|
|
33
|
+
return style.format(quantity) # type: ignore[attr-defined]
|
|
34
|
+
if quantity < 0:
|
|
35
|
+
return f"{commodity}-{abs(quantity):,.2f}"
|
|
36
|
+
return f"{commodity}{quantity:,.2f}"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Formats the running balance; shows bare "0" (no commodity) when zero,
|
|
40
|
+
# matching hledger's convention for balanced-transaction nets.
|
|
41
|
+
def _fmt_balance(balance: Decimal, commodity: str, style: object = None) -> str:
|
|
42
|
+
if balance == 0:
|
|
43
|
+
return "0"
|
|
44
|
+
return _fmt_amount(balance, commodity, style)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# Abbreviates a colon-separated account name to fit within max_width by
|
|
48
|
+
# progressively shortening leading components (never the last component).
|
|
49
|
+
# First pass: shorten each oversized leading component to 2 chars.
|
|
50
|
+
# Second pass: shorten to 1 char if still too long.
|
|
51
|
+
# Matches hledger's visual shortening — e.g. "expenses:food:rent" → "ex:food:rent".
|
|
52
|
+
def _abbreviate_account(account: str, max_width: int = 20) -> str:
|
|
53
|
+
if len(account) <= max_width:
|
|
54
|
+
return account
|
|
55
|
+
parts = account.split(":")
|
|
56
|
+
for min_len in (2, 1):
|
|
57
|
+
for i in range(len(parts) - 1):
|
|
58
|
+
if len(parts[i]) > min_len:
|
|
59
|
+
parts[i] = parts[i][:min_len]
|
|
60
|
+
if len(":".join(parts)) <= max_width:
|
|
61
|
+
return ":".join(parts)
|
|
62
|
+
return ":".join(parts)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
67
|
+
p = argparse.ArgumentParser(
|
|
68
|
+
prog="ledgerkit",
|
|
69
|
+
description="Plain-text accounting (hledger-compatible)",
|
|
70
|
+
)
|
|
71
|
+
p.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
72
|
+
p.add_argument(
|
|
73
|
+
"-v", "--verbose",
|
|
74
|
+
action="store_true",
|
|
75
|
+
help="Show more detailed output (stats: commodity names)",
|
|
76
|
+
)
|
|
77
|
+
p.add_argument(
|
|
78
|
+
"-1",
|
|
79
|
+
dest="one_line",
|
|
80
|
+
action="store_true",
|
|
81
|
+
help="Show a single tab-separated line of output (stats only)",
|
|
82
|
+
)
|
|
83
|
+
p.add_argument(
|
|
84
|
+
"-o", "--output-file",
|
|
85
|
+
metavar="FILE",
|
|
86
|
+
help="Write output to FILE instead of stdout",
|
|
87
|
+
)
|
|
88
|
+
p.add_argument(
|
|
89
|
+
"-f", "--file",
|
|
90
|
+
metavar="FILE",
|
|
91
|
+
dest="files",
|
|
92
|
+
action="append",
|
|
93
|
+
help="Read data from FILE, or stdin if -. May be specified more than once.",
|
|
94
|
+
)
|
|
95
|
+
p.add_argument(
|
|
96
|
+
"-s", "--strict",
|
|
97
|
+
action="store_true",
|
|
98
|
+
help="Enable strict mode: also check that accounts and commodities are declared",
|
|
99
|
+
)
|
|
100
|
+
p.add_argument(
|
|
101
|
+
"-I", "--ignore-assertions",
|
|
102
|
+
dest="ignore_assertions",
|
|
103
|
+
action="store_true",
|
|
104
|
+
help="Disable balance assertion checking",
|
|
105
|
+
)
|
|
106
|
+
p.add_argument(
|
|
107
|
+
"-c", "--commodity-style",
|
|
108
|
+
dest="commodity_styles_override",
|
|
109
|
+
action="append",
|
|
110
|
+
metavar="STYLE",
|
|
111
|
+
help=(
|
|
112
|
+
"Override display style for a commodity, e.g. '$1,000.00' or "
|
|
113
|
+
"'1.000,00 EUR'. May be specified more than once."
|
|
114
|
+
),
|
|
115
|
+
)
|
|
116
|
+
p.add_argument(
|
|
117
|
+
"command",
|
|
118
|
+
choices=COMMANDS,
|
|
119
|
+
help=f"Report to run: {', '.join(COMMANDS)}",
|
|
120
|
+
)
|
|
121
|
+
p.add_argument(
|
|
122
|
+
"journal",
|
|
123
|
+
nargs="*",
|
|
124
|
+
help=(
|
|
125
|
+
"Journal file (shorthand for -f; all commands except check), "
|
|
126
|
+
"or check names (check command only)"
|
|
127
|
+
),
|
|
128
|
+
)
|
|
129
|
+
return p
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _resolve_files(args: argparse.Namespace) -> list[str]:
|
|
133
|
+
"""Return the ordered list of files to load from parsed CLI arguments.
|
|
134
|
+
|
|
135
|
+
Resolution order:
|
|
136
|
+
1. ``-f``/``--file`` flags (all of them, in order given)
|
|
137
|
+
2. Positional arguments when command is not ``check`` (backward-compatible
|
|
138
|
+
shorthand for a single journal file)
|
|
139
|
+
3. ``$LEDGER_FILE`` environment variable
|
|
140
|
+
4. ``~/.hledger.journal`` if it exists
|
|
141
|
+
|
|
142
|
+
For the ``check`` command all positional arguments are treated as check names,
|
|
143
|
+
not journal files; the journal file must come from ``-f`` or the environment.
|
|
144
|
+
|
|
145
|
+
Raises:
|
|
146
|
+
SystemExit(1): if both ``-f`` and a positional file are given, or if no
|
|
147
|
+
file can be determined.
|
|
148
|
+
"""
|
|
149
|
+
is_check = getattr(args, "command", None) == "check"
|
|
150
|
+
# args.journal is a list (nargs="*"); non-empty only when positionals given
|
|
151
|
+
positional_file = args.journal[0] if (args.journal and not is_check) else None
|
|
152
|
+
|
|
153
|
+
if args.files and positional_file:
|
|
154
|
+
print(
|
|
155
|
+
"ledgerkit: cannot combine -f/--file flags with a positional "
|
|
156
|
+
"journal argument",
|
|
157
|
+
file=sys.stderr,
|
|
158
|
+
)
|
|
159
|
+
raise SystemExit(1)
|
|
160
|
+
if args.files:
|
|
161
|
+
return args.files
|
|
162
|
+
if positional_file:
|
|
163
|
+
return [positional_file]
|
|
164
|
+
env = os.environ.get("LEDGER_FILE")
|
|
165
|
+
if env:
|
|
166
|
+
return [env]
|
|
167
|
+
default = Path.home() / ".hledger.journal"
|
|
168
|
+
if default.exists():
|
|
169
|
+
return [str(default)]
|
|
170
|
+
print(
|
|
171
|
+
"ledgerkit: no journal file specified; use -f FILE or set $LEDGER_FILE",
|
|
172
|
+
file=sys.stderr,
|
|
173
|
+
)
|
|
174
|
+
raise SystemExit(1)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def main(argv: list[str] | None = None) -> int:
|
|
178
|
+
"""Entry point for the ledgerkit CLI.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
argv: Argument list (defaults to sys.argv[1:] when None).
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
Exit code (0 = success, 1 = error).
|
|
185
|
+
"""
|
|
186
|
+
parser = _build_parser()
|
|
187
|
+
args = parser.parse_args(argv)
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
file_list = _resolve_files(args)
|
|
191
|
+
except SystemExit as exc:
|
|
192
|
+
return int(exc.code) if exc.code is not None else 1
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
from ledgerkit.loader import load_journal, load_journal_stdin, merge_journals
|
|
196
|
+
from ledgerkit.models import Journal
|
|
197
|
+
|
|
198
|
+
loaded: list[Journal] = []
|
|
199
|
+
for f in file_list:
|
|
200
|
+
if f == "-":
|
|
201
|
+
loaded.append(load_journal_stdin())
|
|
202
|
+
else:
|
|
203
|
+
loaded.append(load_journal(f))
|
|
204
|
+
journal = merge_journals(loaded)
|
|
205
|
+
except FileNotFoundError as exc:
|
|
206
|
+
print(f"ledgerkit: file not found: {exc}", file=sys.stderr)
|
|
207
|
+
return 1
|
|
208
|
+
except Exception as exc:
|
|
209
|
+
print(f"ledgerkit: {exc}", file=sys.stderr)
|
|
210
|
+
return 1
|
|
211
|
+
|
|
212
|
+
# --- Build commodity styles map (inferred + -c overrides) ---
|
|
213
|
+
from ledgerkit.commodity_style import CommodityStyle as _CommodityStyle
|
|
214
|
+
commodity_styles: dict = journal.commodity_styles
|
|
215
|
+
for override_str in (getattr(args, "commodity_styles_override", None) or []):
|
|
216
|
+
try:
|
|
217
|
+
override = _CommodityStyle.parse_style_override(override_str)
|
|
218
|
+
commodity_styles[override.commodity] = override
|
|
219
|
+
except ValueError as exc:
|
|
220
|
+
print(f"ledgerkit: invalid -c value {override_str!r}: {exc}", file=sys.stderr)
|
|
221
|
+
return 1
|
|
222
|
+
|
|
223
|
+
# --- Default basic-check gate (runs before every command) ---
|
|
224
|
+
from ledgerkit import checks as _checks
|
|
225
|
+
|
|
226
|
+
_skip: frozenset[str] = (
|
|
227
|
+
frozenset({"assertions"}) if getattr(args, "ignore_assertions", False) else frozenset()
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
basic_errors = _checks.run_basic_checks(journal, skip=_skip)
|
|
231
|
+
if basic_errors:
|
|
232
|
+
for e in basic_errors:
|
|
233
|
+
print(f"ledgerkit: {e.message}", file=sys.stderr)
|
|
234
|
+
return 1
|
|
235
|
+
|
|
236
|
+
# --- Strict-mode checks (-s/--strict) ---
|
|
237
|
+
if args.strict:
|
|
238
|
+
strict_only = [
|
|
239
|
+
e for e in _checks.run_strict_checks(journal, skip=_skip)
|
|
240
|
+
if e.check_name not in _checks.BASIC_CHECK_NAMES
|
|
241
|
+
]
|
|
242
|
+
if strict_only:
|
|
243
|
+
for e in strict_only:
|
|
244
|
+
print(f"ledgerkit: {e.message}", file=sys.stderr)
|
|
245
|
+
return 1
|
|
246
|
+
|
|
247
|
+
# --- check command (no output on success; errors → stderr + exit 1) ---
|
|
248
|
+
if args.command == "check":
|
|
249
|
+
check_names = args.journal # positional args are check names for this command
|
|
250
|
+
try:
|
|
251
|
+
errors = _checks.run_checks(
|
|
252
|
+
journal, names=list(check_names), strict=args.strict, skip=_skip
|
|
253
|
+
)
|
|
254
|
+
except ValueError as exc:
|
|
255
|
+
print(f"ledgerkit: {exc}", file=sys.stderr)
|
|
256
|
+
return 1
|
|
257
|
+
if errors:
|
|
258
|
+
for e in errors:
|
|
259
|
+
print(f"ledgerkit: {e.message}", file=sys.stderr)
|
|
260
|
+
return 1
|
|
261
|
+
return 0
|
|
262
|
+
|
|
263
|
+
outfile = None
|
|
264
|
+
if args.output_file:
|
|
265
|
+
try:
|
|
266
|
+
outfile = open(args.output_file, "w", encoding="utf-8")
|
|
267
|
+
except OSError as exc:
|
|
268
|
+
print(f"ledgerkit: cannot open output file: {exc}", file=sys.stderr)
|
|
269
|
+
return 1
|
|
270
|
+
|
|
271
|
+
try:
|
|
272
|
+
import ledgerkit.reports as reports
|
|
273
|
+
|
|
274
|
+
with contextlib.redirect_stdout(outfile) if outfile else contextlib.nullcontext():
|
|
275
|
+
if args.command == "balance":
|
|
276
|
+
result = reports.balance(journal)
|
|
277
|
+
# result: dict[str, dict[str, Decimal]] — account → commodity → net
|
|
278
|
+
|
|
279
|
+
# Flatten to (account, commodity, qty) rows; sorted alphabetically.
|
|
280
|
+
# Zero-balance commodity lines are omitted (matching hledger behaviour).
|
|
281
|
+
lines: list[tuple[str, str, Decimal]] = [
|
|
282
|
+
(acct, comm, qty)
|
|
283
|
+
for acct in sorted(result)
|
|
284
|
+
for comm, qty in sorted(result[acct].items())
|
|
285
|
+
if qty != 0
|
|
286
|
+
]
|
|
287
|
+
|
|
288
|
+
if not lines:
|
|
289
|
+
pass
|
|
290
|
+
else:
|
|
291
|
+
# Per-commodity grand totals (from filtered lines only).
|
|
292
|
+
commodity_totals: dict[str, Decimal] = {}
|
|
293
|
+
for _, comm, qty in lines:
|
|
294
|
+
commodity_totals[comm] = commodity_totals.get(comm, Decimal(0)) + qty
|
|
295
|
+
|
|
296
|
+
formatted_amts = [
|
|
297
|
+
_fmt_amount(qty, comm, commodity_styles.get(comm))
|
|
298
|
+
for _, comm, qty in lines
|
|
299
|
+
]
|
|
300
|
+
# Only show non-zero commodity totals; a single bare "0" when all net zero.
|
|
301
|
+
nonzero_totals = [
|
|
302
|
+
(comm, qty)
|
|
303
|
+
for comm, qty in sorted(commodity_totals.items())
|
|
304
|
+
if qty != 0
|
|
305
|
+
]
|
|
306
|
+
total_strs = [
|
|
307
|
+
_fmt_amount(qty, comm, commodity_styles.get(comm))
|
|
308
|
+
for comm, qty in nonzero_totals
|
|
309
|
+
]
|
|
310
|
+
col_w = max(
|
|
311
|
+
20,
|
|
312
|
+
*(len(s) for s in formatted_amts),
|
|
313
|
+
*(len(s) for s in total_strs),
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
# Determine the last (account, commodity) row index per account
|
|
317
|
+
# so the account name is printed only on that row.
|
|
318
|
+
acct_last_idx: dict[str, int] = {
|
|
319
|
+
acct: i for i, (acct, _, _) in enumerate(lines)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
for i, ((acct, comm, qty), amt_str) in enumerate(
|
|
323
|
+
zip(lines, formatted_amts)
|
|
324
|
+
):
|
|
325
|
+
padded = f"{amt_str:>{col_w}}"
|
|
326
|
+
if qty < 0:
|
|
327
|
+
padded = f"{_ANSI_RED}{padded}{_ANSI_RESET}"
|
|
328
|
+
if i == acct_last_idx[acct]:
|
|
329
|
+
print(f"{padded} {acct}")
|
|
330
|
+
else:
|
|
331
|
+
print(padded)
|
|
332
|
+
|
|
333
|
+
print("-" * col_w)
|
|
334
|
+
if nonzero_totals:
|
|
335
|
+
for (comm, total), tot_str in zip(nonzero_totals, total_strs):
|
|
336
|
+
tot = f"{tot_str:>{col_w}}"
|
|
337
|
+
if total < 0:
|
|
338
|
+
tot = f"{_ANSI_RED}{tot}{_ANSI_RESET}"
|
|
339
|
+
print(tot)
|
|
340
|
+
else:
|
|
341
|
+
print(f"{'0':>{col_w}}")
|
|
342
|
+
|
|
343
|
+
elif args.command == "register":
|
|
344
|
+
rows = reports.register(journal)
|
|
345
|
+
prev_key: tuple | None = None
|
|
346
|
+
for row in rows:
|
|
347
|
+
cur_key = (row.date, row.description)
|
|
348
|
+
is_first = cur_key != prev_key
|
|
349
|
+
date_str = str(row.date) if is_first else ""
|
|
350
|
+
desc_str = row.description if is_first else ""
|
|
351
|
+
acct_str = _abbreviate_account(row.account)
|
|
352
|
+
commodity = row.amount.commodity
|
|
353
|
+
style = commodity_styles.get(commodity)
|
|
354
|
+
|
|
355
|
+
# Right-align before colorising — ANSI escape codes have nonzero
|
|
356
|
+
# string length but zero display width, so padding must come first.
|
|
357
|
+
amt_str = f"{_fmt_amount(row.amount.quantity, commodity, style):>12}"
|
|
358
|
+
if row.amount.quantity < 0:
|
|
359
|
+
amt_str = f"{_ANSI_RED}{amt_str}{_ANSI_RESET}"
|
|
360
|
+
|
|
361
|
+
bal_str = f"{_fmt_balance(row.running_balance, commodity, style):>13}"
|
|
362
|
+
if row.running_balance < 0:
|
|
363
|
+
bal_str = f"{_ANSI_RED}{bal_str}{_ANSI_RESET}"
|
|
364
|
+
|
|
365
|
+
print(
|
|
366
|
+
f"{date_str:10} {desc_str:21} {acct_str:<22} {amt_str} {bal_str}"
|
|
367
|
+
)
|
|
368
|
+
prev_key = cur_key
|
|
369
|
+
|
|
370
|
+
elif args.command == "accounts":
|
|
371
|
+
for name in reports.accounts(journal):
|
|
372
|
+
print(name)
|
|
373
|
+
|
|
374
|
+
elif args.command == "print":
|
|
375
|
+
for txn in sorted(journal.transactions, key=lambda t: t.date):
|
|
376
|
+
flag = " * " if txn.cleared else " ! " if txn.pending else " "
|
|
377
|
+
print(f"{txn.date}{flag}{txn.description}")
|
|
378
|
+
for posting in txn.postings:
|
|
379
|
+
if posting.amount:
|
|
380
|
+
amt_style = commodity_styles.get(posting.amount.commodity)
|
|
381
|
+
amt_txt = _fmt_amount(
|
|
382
|
+
posting.amount.quantity,
|
|
383
|
+
posting.amount.commodity,
|
|
384
|
+
amt_style,
|
|
385
|
+
)
|
|
386
|
+
print(f" {posting.account:<40} {amt_txt}")
|
|
387
|
+
else:
|
|
388
|
+
print(f" {posting.account}")
|
|
389
|
+
print()
|
|
390
|
+
|
|
391
|
+
elif args.command == "stats":
|
|
392
|
+
s = reports.stats(journal)
|
|
393
|
+
elapsed = time.perf_counter() - _PROGRAM_START
|
|
394
|
+
txns_per_s = s.transaction_count / elapsed if elapsed > 0 else 0.0
|
|
395
|
+
|
|
396
|
+
span_str = (
|
|
397
|
+
f"{s.date_range[0]} to {s.date_range[1]} ({s.txns_span_days} days)"
|
|
398
|
+
if s.date_range else "(none)"
|
|
399
|
+
)
|
|
400
|
+
last_str = (
|
|
401
|
+
f"{s.last_txn_date} ({s.last_txn_days_ago} days ago)"
|
|
402
|
+
if s.last_txn_date else "(none)"
|
|
403
|
+
)
|
|
404
|
+
commodity_str = (
|
|
405
|
+
f"{s.commodity_count} ({', '.join(s.commodities)})"
|
|
406
|
+
if args.verbose and s.commodities else str(s.commodity_count)
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
elapsed_str = f"{elapsed:.2f}" if elapsed >= 0.01 else f"{elapsed:.3f}"
|
|
410
|
+
if args.one_line:
|
|
411
|
+
print("\t".join([
|
|
412
|
+
__version__,
|
|
413
|
+
s.source_file or "(none)",
|
|
414
|
+
f"{elapsed_str} s elapsed",
|
|
415
|
+
f"{txns_per_s:.0f} txns/s",
|
|
416
|
+
]))
|
|
417
|
+
else:
|
|
418
|
+
print(f"Main file : {s.source_file or '(none)'}")
|
|
419
|
+
print(f"Included files : {s.included_files}")
|
|
420
|
+
print(f"Txns span : {span_str}")
|
|
421
|
+
print(f"Last txn : {last_str}")
|
|
422
|
+
print(f"Txns : {s.transaction_count} ({s.txns_per_day:.1f} per day)")
|
|
423
|
+
print(f"Txns last 30 days : {s.txns_last_30_days} ({s.txns_per_day_last_30:.1f} per day)")
|
|
424
|
+
print(f"Txns last 7 days : {s.txns_last_7_days} ({s.txns_per_day_last_7:.1f} per day)")
|
|
425
|
+
print(f"Payees/descriptions : {s.payee_count}")
|
|
426
|
+
print(f"Accounts : {s.account_count} (depth {s.account_depth})")
|
|
427
|
+
print(f"Commodities : {commodity_str}")
|
|
428
|
+
print(f"Market prices : {s.price_count}")
|
|
429
|
+
print(f"Runtime stats : {elapsed_str} s elapsed, {txns_per_s:.0f} txns/s")
|
|
430
|
+
|
|
431
|
+
except NotImplementedError:
|
|
432
|
+
print(
|
|
433
|
+
f"ledgerkit: '{args.command}' is not yet implemented",
|
|
434
|
+
file=sys.stderr,
|
|
435
|
+
)
|
|
436
|
+
return 1
|
|
437
|
+
except Exception as exc:
|
|
438
|
+
print(f"ledgerkit: {exc}", file=sys.stderr)
|
|
439
|
+
return 1
|
|
440
|
+
finally:
|
|
441
|
+
if outfile:
|
|
442
|
+
outfile.close()
|
|
443
|
+
|
|
444
|
+
return 0
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
if __name__ == "__main__":
|
|
448
|
+
sys.exit(main())
|