beancount-cli 0.2.9__tar.gz → 0.2.12__tar.gz
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.
- {beancount_cli-0.2.9 → beancount_cli-0.2.12}/CHANGELOG.md +24 -0
- {beancount_cli-0.2.9 → beancount_cli-0.2.12}/PKG-INFO +2 -2
- {beancount_cli-0.2.9 → beancount_cli-0.2.12}/pyproject.toml +5 -2
- beancount_cli-0.2.12/src/beancount_cli/__init__.py +1 -0
- {beancount_cli-0.2.9 → beancount_cli-0.2.12}/src/beancount_cli/commands/common.py +7 -2
- {beancount_cli-0.2.9 → beancount_cli-0.2.12}/src/beancount_cli/commands/price.py +124 -67
- {beancount_cli-0.2.9 → beancount_cli-0.2.12}/tests/test_cli.py +4 -4
- {beancount_cli-0.2.9 → beancount_cli-0.2.12}/tests/test_coverage_gap.py +10 -6
- beancount_cli-0.2.9/src/beancount_cli/__init__.py +0 -1
- {beancount_cli-0.2.9 → beancount_cli-0.2.12}/.gitignore +0 -0
- {beancount_cli-0.2.9 → beancount_cli-0.2.12}/LICENSE +0 -0
- {beancount_cli-0.2.9 → beancount_cli-0.2.12}/README.md +0 -0
- {beancount_cli-0.2.9 → beancount_cli-0.2.12}/src/beancount_cli/adapters.py +0 -0
- {beancount_cli-0.2.9 → beancount_cli-0.2.12}/src/beancount_cli/cli.py +0 -0
- {beancount_cli-0.2.9 → beancount_cli-0.2.12}/src/beancount_cli/commands/__init__.py +0 -0
- {beancount_cli-0.2.9 → beancount_cli-0.2.12}/src/beancount_cli/commands/account.py +0 -0
- {beancount_cli-0.2.9 → beancount_cli-0.2.12}/src/beancount_cli/commands/commodity.py +0 -0
- {beancount_cli-0.2.9 → beancount_cli-0.2.12}/src/beancount_cli/commands/report.py +0 -0
- {beancount_cli-0.2.9 → beancount_cli-0.2.12}/src/beancount_cli/commands/root.py +0 -0
- {beancount_cli-0.2.9 → beancount_cli-0.2.12}/src/beancount_cli/commands/transaction.py +0 -0
- {beancount_cli-0.2.9 → beancount_cli-0.2.12}/src/beancount_cli/config.py +0 -0
- {beancount_cli-0.2.9 → beancount_cli-0.2.12}/src/beancount_cli/formatting.py +0 -0
- {beancount_cli-0.2.9 → beancount_cli-0.2.12}/src/beancount_cli/models.py +0 -0
- {beancount_cli-0.2.9 → beancount_cli-0.2.12}/src/beancount_cli/py.typed +0 -0
- {beancount_cli-0.2.9 → beancount_cli-0.2.12}/src/beancount_cli/services.py +0 -0
- {beancount_cli-0.2.9 → beancount_cli-0.2.12}/tests/conftest.py +0 -0
- {beancount_cli-0.2.9 → beancount_cli-0.2.12}/tests/smoke_test.py +0 -0
- {beancount_cli-0.2.9 → beancount_cli-0.2.12}/tests/test_advanced.py +0 -0
- {beancount_cli-0.2.9 → beancount_cli-0.2.12}/tests/test_bql.py +0 -0
- {beancount_cli-0.2.9 → beancount_cli-0.2.12}/tests/test_config.py +0 -0
- {beancount_cli-0.2.9 → beancount_cli-0.2.12}/tests/test_models.py +0 -0
- {beancount_cli-0.2.9 → beancount_cli-0.2.12}/tests/test_services.py +0 -0
|
@@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.2.12] - 2026-04-08
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- `price fetch --update`: cap `date_last` to yesterday (today exclusive) to avoid fetching intraday prices while the market is still open.
|
|
12
|
+
|
|
13
|
+
## [0.2.11] - 2026-04-07
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- `price fetch`: use `[tool.uv.sources]` git override for `beanprice` so the romamo fork is installed correctly; PyPI metadata keeps `beanprice>=2.1.0` for compatibility.
|
|
17
|
+
|
|
18
|
+
## [0.2.10] - 2026-04-07
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- `price fetch`: use `romamo/beanprice` fork via `[tool.uv.sources]` git override; PyPI metadata retains `beanprice>=2.1.0` for compatibility.
|
|
22
|
+
- `price fetch --update`: include today's date in the fetch window (`date_last` is exclusive, so now passes `today + 1`).
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- `price fetch -vv`: log each redundant fetched price at DEBUG level so skipped prices are visible.
|
|
26
|
+
|
|
27
|
+
## [0.2.9] - 2026-04-05
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
- Smoke test: check stderr as well as stdout when probing `--help` output, fixing false negatives in isolated environments.
|
|
31
|
+
|
|
8
32
|
## [0.2.8] - 2026-04-04
|
|
9
33
|
|
|
10
34
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: beancount-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.12
|
|
4
4
|
Summary: A CLI tool to manage Beancount ledgers
|
|
5
5
|
Project-URL: Homepage, https://github.com/romamo/beancount-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/romamo/beancount-cli
|
|
@@ -20,7 +20,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.13
|
|
21
21
|
Classifier: Topic :: Office/Business :: Financial :: Accounting
|
|
22
22
|
Requires-Python: >=3.10
|
|
23
|
-
Requires-Dist: agentyper
|
|
23
|
+
Requires-Dist: agentyper==0.1.8
|
|
24
24
|
Requires-Dist: beancount>=3.0.0
|
|
25
25
|
Requires-Dist: beanprice>=2.1.0
|
|
26
26
|
Requires-Dist: beanquery>=0.1.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "beancount-cli"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.12"
|
|
4
4
|
description = "A CLI tool to manage Beancount ledgers"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -15,7 +15,7 @@ dependencies = [
|
|
|
15
15
|
"python-dateutil>=2.8.2",
|
|
16
16
|
"beanprice>=2.1.0",
|
|
17
17
|
"pydantic-settings>=2.0.0",
|
|
18
|
-
"agentyper",
|
|
18
|
+
"agentyper==0.1.8",
|
|
19
19
|
]
|
|
20
20
|
keywords = ["beancount", "cli", "accounting", "automation", "agent"]
|
|
21
21
|
classifiers = [
|
|
@@ -62,6 +62,9 @@ packages = ["src/beancount_cli"]
|
|
|
62
62
|
requires = ["hatchling"]
|
|
63
63
|
build-backend = "hatchling.build"
|
|
64
64
|
|
|
65
|
+
[tool.uv.sources]
|
|
66
|
+
beanprice = { git = "https://github.com/romamo/beanprice" }
|
|
67
|
+
|
|
65
68
|
[dependency-groups]
|
|
66
69
|
dev = [
|
|
67
70
|
"bandit>=1.8.6",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.12"
|
|
@@ -6,8 +6,13 @@ from pathlib import Path
|
|
|
6
6
|
from rich.console import Console
|
|
7
7
|
from rich.table import Table
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
# REQ-F-056: Handle agentyper width suppression (COLUMNS=0)
|
|
10
|
+
_width = None
|
|
11
|
+
if os.environ.get("COLUMNS") == "0":
|
|
12
|
+
_width = 100
|
|
13
|
+
|
|
14
|
+
console = Console(width=_width)
|
|
15
|
+
error_console = Console(stderr=True, width=_width)
|
|
11
16
|
|
|
12
17
|
|
|
13
18
|
def read_json_input(json_data: str) -> str:
|
|
@@ -8,6 +8,7 @@ from tempfile import gettempdir
|
|
|
8
8
|
|
|
9
9
|
import agentyper as typer
|
|
10
10
|
from beancount.core import data
|
|
11
|
+
from beancount.core.data import sorted as bean_sorted
|
|
11
12
|
from beancount.parser import printer
|
|
12
13
|
from beanprice import price as bp_price
|
|
13
14
|
|
|
@@ -26,9 +27,6 @@ def price_check(
|
|
|
26
27
|
tolerance: int = typer.Option(
|
|
27
28
|
7, "--tolerance", "-t", help="Allowed delay in days before flagging a gap"
|
|
28
29
|
),
|
|
29
|
-
verbose: bool = typer.Option(
|
|
30
|
-
False, "--verbose", "-v", help="Verbosity level (-v for INFO, -vv for DEBUG)"
|
|
31
|
-
),
|
|
32
30
|
rate: str = typer.Option(
|
|
33
31
|
"daily", "--rate", "-r", help="Check frequency: daily, weekday, weekly, monthly"
|
|
34
32
|
),
|
|
@@ -38,7 +36,6 @@ def price_check(
|
|
|
38
36
|
ledger_service = LedgerService(actual_file)
|
|
39
37
|
price_service = PriceService(ledger_service)
|
|
40
38
|
|
|
41
|
-
_setup_logging(verbose)
|
|
42
39
|
_warn_missing_price_meta(ledger_service, "Price history cannot be verified.")
|
|
43
40
|
|
|
44
41
|
gaps = price_service.get_price_gaps(tolerance_days=tolerance, rate=rate)
|
|
@@ -62,11 +59,14 @@ def price_check(
|
|
|
62
59
|
typer.output(gaps, title="Price Gaps")
|
|
63
60
|
|
|
64
61
|
|
|
65
|
-
@app.command(name="fetch")
|
|
62
|
+
@app.command(name="fetch", mutating=True)
|
|
66
63
|
def price_fetch(
|
|
67
|
-
|
|
64
|
+
ledger_files: list[Path] = typer.Argument(
|
|
65
|
+
[],
|
|
66
|
+
help="One or more beancount ledger files to merge before fetching prices",
|
|
67
|
+
),
|
|
68
68
|
file: Path | None = typer.Option(
|
|
69
|
-
None, "--file", "-f", envvar="BEANCOUNT_FILE", help="Main beancount file"
|
|
69
|
+
None, "--file", "-f", envvar="BEANCOUNT_FILE", help="Main beancount file (legacy fallback)"
|
|
70
70
|
),
|
|
71
71
|
update: bool = typer.Option(
|
|
72
72
|
False, "--update", "-u", help="Fetch from last price forward and update ledger"
|
|
@@ -75,33 +75,57 @@ def price_fetch(
|
|
|
75
75
|
False, "--inactive", "-i", help="Include commodities with no balance"
|
|
76
76
|
),
|
|
77
77
|
fill_gaps: bool = typer.Option(False, "--fill-gaps", help="Fill gaps in price history"),
|
|
78
|
-
dry_run: bool =
|
|
79
|
-
verbose: bool = typer.Option(
|
|
80
|
-
False, "--verbose", "-v", help="Verbosity level (-v for INFO, -vv for DEBUG)"
|
|
81
|
-
),
|
|
78
|
+
dry_run: bool = False,
|
|
82
79
|
):
|
|
83
|
-
"""Fetch and update prices using bean-price library.
|
|
84
|
-
|
|
85
|
-
|
|
80
|
+
"""Fetch and update prices using bean-price library.
|
|
81
|
+
|
|
82
|
+
Accepts one or more beancount ledger files as positional arguments. When
|
|
83
|
+
multiple files are provided they are all loaded and their entries merged
|
|
84
|
+
before price jobs are computed. This lets you split commodities/prices
|
|
85
|
+
across separate files (e.g. a common registry and a portfolio ledger) while
|
|
86
|
+
still running a single fetch pass.
|
|
87
|
+
|
|
88
|
+
If no positional arguments are given the command falls back to --file / the
|
|
89
|
+
BEANCOUNT_FILE environment variable for backward compatibility.
|
|
90
|
+
"""
|
|
91
|
+
# Resolve the list of ledger files to load.
|
|
92
|
+
if ledger_files:
|
|
93
|
+
files_to_load = ledger_files
|
|
94
|
+
else:
|
|
95
|
+
files_to_load = [get_ledger_file(file)]
|
|
86
96
|
|
|
87
|
-
|
|
97
|
+
label = ", ".join(str(f) for f in files_to_load)
|
|
98
|
+
error_console.print(f"Fetching prices for {label}...", highlight=False)
|
|
88
99
|
|
|
89
100
|
try:
|
|
90
101
|
cache_path = Path(gettempdir()) / "bean-price.cache"
|
|
91
102
|
bp_price.setup_cache(str(cache_path), clear_cache=False)
|
|
92
103
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
104
|
+
# Load and merge all ledger files
|
|
105
|
+
all_entries: list[data.Directive] = []
|
|
106
|
+
all_errors: list = []
|
|
107
|
+
primary_ledger_service: LedgerService | None = None
|
|
96
108
|
|
|
97
|
-
|
|
109
|
+
for ledger_path in files_to_load:
|
|
110
|
+
svc = LedgerService(ledger_path)
|
|
111
|
+
svc.load()
|
|
112
|
+
all_entries.extend(svc.entries)
|
|
113
|
+
all_errors.extend(svc.errors)
|
|
114
|
+
if primary_ledger_service is None:
|
|
115
|
+
primary_ledger_service = svc
|
|
116
|
+
|
|
117
|
+
# Sort merged entries by date (beancount canonical order)
|
|
118
|
+
entries = bean_sorted(all_entries)
|
|
119
|
+
|
|
120
|
+
assert primary_ledger_service is not None # always set — files_to_load is non-empty
|
|
121
|
+
|
|
122
|
+
_warn_missing_price_meta_entries(entries, "Skipping fetch.")
|
|
98
123
|
|
|
99
124
|
if update or fill_gaps:
|
|
100
125
|
jobs = bp_price.get_price_jobs_up_to_date(
|
|
101
126
|
entries,
|
|
102
127
|
date_last=datetime.now().date(),
|
|
103
128
|
inactive=inactive,
|
|
104
|
-
fill_gaps=fill_gaps,
|
|
105
129
|
)
|
|
106
130
|
else:
|
|
107
131
|
jobs = bp_price.get_price_jobs_at_date(entries, date=None, inactive=inactive)
|
|
@@ -121,11 +145,14 @@ def price_fetch(
|
|
|
121
145
|
# This keeps the streaming output clean of duplicates already in the ledger
|
|
122
146
|
existing_prices = {(e.date, e.currency) for e in entries if isinstance(e, data.Price)}
|
|
123
147
|
|
|
148
|
+
failed_jobs = []
|
|
149
|
+
redundant_count = 0
|
|
124
150
|
try:
|
|
125
151
|
with ThreadPoolExecutor(max_workers=min(8, len(jobs))) as executor:
|
|
126
152
|
future_to_job = {executor.submit(bp_price.fetch_price, job): job for job in jobs}
|
|
127
153
|
|
|
128
154
|
for future in as_completed(future_to_job):
|
|
155
|
+
job = future_to_job[future]
|
|
129
156
|
price_entry = future.result()
|
|
130
157
|
if price_entry:
|
|
131
158
|
price_entry = price_entry._replace(
|
|
@@ -139,6 +166,13 @@ def price_fetch(
|
|
|
139
166
|
existing_prices.add((price_entry.date, price_entry.currency))
|
|
140
167
|
print(printer.format_entry(price_entry), end="")
|
|
141
168
|
sys.stdout.flush()
|
|
169
|
+
else:
|
|
170
|
+
redundant_count += 1
|
|
171
|
+
logging.debug(
|
|
172
|
+
"Redundant: %s", printer.format_entry(price_entry).strip()
|
|
173
|
+
)
|
|
174
|
+
else:
|
|
175
|
+
failed_jobs.append(job)
|
|
142
176
|
except KeyboardInterrupt:
|
|
143
177
|
error_console.print("\n[yellow]Interrupt received. Stopping fetch...[/yellow]")
|
|
144
178
|
|
|
@@ -153,18 +187,32 @@ def price_fetch(
|
|
|
153
187
|
|
|
154
188
|
prices_output = "".join(printer.format_entry(p) for p in filtered_prices)
|
|
155
189
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
#
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
190
|
+
# Resolve target price file.
|
|
191
|
+
# Strategy:
|
|
192
|
+
# 1. Look for a file named 'prices.beancount' in the argument list itself.
|
|
193
|
+
# 2. Look for a sibling named 'prices.beancount' relative to the primary ledger.
|
|
194
|
+
# 3. Search the include tree for a file with 'price' in the name.
|
|
195
|
+
# 4. Fallback to the primary ledger.
|
|
196
|
+
|
|
197
|
+
target_price_file = None
|
|
198
|
+
for f in files_to_load:
|
|
199
|
+
if f.name == "prices.beancount":
|
|
200
|
+
target_price_file = f
|
|
201
|
+
break
|
|
202
|
+
|
|
203
|
+
if target_price_file is None:
|
|
204
|
+
primary_file = files_to_load[0]
|
|
205
|
+
sibling_prices = primary_file.parent / "prices.beancount"
|
|
206
|
+
if sibling_prices.exists():
|
|
207
|
+
target_price_file = sibling_prices
|
|
208
|
+
else:
|
|
209
|
+
map_service = MapService(primary_file)
|
|
210
|
+
inc_tree = map_service.get_include_tree()
|
|
211
|
+
found = _find_price_file(inc_tree)
|
|
212
|
+
if found:
|
|
213
|
+
target_price_file = found
|
|
214
|
+
else:
|
|
215
|
+
target_price_file = primary_file
|
|
168
216
|
|
|
169
217
|
with open(target_price_file, "a") as f:
|
|
170
218
|
f.write(prices_output)
|
|
@@ -172,7 +220,21 @@ def price_fetch(
|
|
|
172
220
|
error_console.print(
|
|
173
221
|
f"[green]Appended {len(filtered_prices)} new prices to {target_price_file.name}[/green]"
|
|
174
222
|
)
|
|
175
|
-
|
|
223
|
+
|
|
224
|
+
if redundant_count:
|
|
225
|
+
error_console.print(
|
|
226
|
+
f"[yellow]Ignored {redundant_count} fetched prices as they are already in the ledger.[/yellow]"
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
if failed_jobs:
|
|
230
|
+
failed_info = ", ".join(f"{j.currency} on {j.date}" for j in failed_jobs[:5])
|
|
231
|
+
if len(failed_jobs) > 5:
|
|
232
|
+
failed_info += f" (+{len(failed_jobs) - 5} more)"
|
|
233
|
+
error_console.print(
|
|
234
|
+
f"[yellow]Skipped {len(failed_jobs)} jobs with no data from source: {failed_info}[/yellow]"
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
if not new_price_entries and not failed_jobs and not redundant_count:
|
|
176
238
|
error_console.print("[yellow]No new prices found.[/yellow]")
|
|
177
239
|
|
|
178
240
|
except Exception as e:
|
|
@@ -195,44 +257,39 @@ def _find_price_file(tree: dict) -> Path | None:
|
|
|
195
257
|
|
|
196
258
|
def _warn_missing_price_meta(ledger_service: LedgerService, context: str) -> None:
|
|
197
259
|
"""Warn about held commodities that lack 'price' metadata and cannot be priced."""
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
260
|
+
_warn_missing_price_meta_entries(ledger_service.entries, context)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _warn_missing_price_meta_entries(entries: list[data.Directive], context: str) -> None:
|
|
264
|
+
"""Warn about held commodities (from a merged entry list) that lack 'price' metadata."""
|
|
265
|
+
from beancount.core import convert
|
|
266
|
+
from beancount.core.inventory import Inventory
|
|
267
|
+
|
|
268
|
+
inv = Inventory()
|
|
269
|
+
today = date.today()
|
|
270
|
+
for e in entries:
|
|
271
|
+
if e.date > today:
|
|
272
|
+
break
|
|
273
|
+
if isinstance(e, data.Transaction):
|
|
274
|
+
for p in e.postings:
|
|
275
|
+
if p.account.startswith(("Assets", "Liabilities")):
|
|
276
|
+
inv.add_position(p)
|
|
277
|
+
|
|
278
|
+
held = {
|
|
279
|
+
pos.units.currency
|
|
280
|
+
for pos in inv.reduce(convert.get_units)
|
|
281
|
+
if not pos.units.number.is_zero()
|
|
202
282
|
}
|
|
283
|
+
|
|
284
|
+
# Extract operating currencies from options if possible
|
|
285
|
+
# In a merged entry list, we might have 'option' directives as Custom entries or similar?
|
|
286
|
+
# Actually, Beancount usually loads them into an options map.
|
|
287
|
+
# For now, we'll just check commodity metadata.
|
|
288
|
+
|
|
289
|
+
commodity_meta = {e.currency: e.meta for e in entries if isinstance(e, data.Commodity)}
|
|
203
290
|
for curr in sorted(held):
|
|
204
|
-
if curr in op_currs:
|
|
205
|
-
continue
|
|
206
291
|
meta = commodity_meta.get(curr, {})
|
|
207
292
|
if not meta or "price" not in meta:
|
|
208
293
|
error_console.print(
|
|
209
294
|
f"[red]Error: Commodity {curr} is held but has no 'price' metadata. {context}[/red]"
|
|
210
295
|
)
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
def _setup_logging(verbose: bool):
|
|
214
|
-
"""Sets up logging levels based on verbosity count in sys.argv."""
|
|
215
|
-
# Manual count of 'v' flags in sys.argv to handle -v, -vv, etc.
|
|
216
|
-
count = 0
|
|
217
|
-
for arg in sys.argv:
|
|
218
|
-
if arg == "-v":
|
|
219
|
-
count += 1
|
|
220
|
-
elif arg.startswith("-") and not arg.startswith("--"):
|
|
221
|
-
# Handle combined flags like -uvv
|
|
222
|
-
count += arg.count("v")
|
|
223
|
-
elif arg == "--verbose":
|
|
224
|
-
count += 1
|
|
225
|
-
|
|
226
|
-
if count >= 2:
|
|
227
|
-
log_level = logging.DEBUG
|
|
228
|
-
elif count == 1 or verbose:
|
|
229
|
-
log_level = logging.INFO
|
|
230
|
-
else:
|
|
231
|
-
log_level = logging.WARNING
|
|
232
|
-
|
|
233
|
-
logging.basicConfig(level=log_level, format="%(levelname)-8s: %(message)s", force=True)
|
|
234
|
-
|
|
235
|
-
# Silence noisy loggers unless -vv is used
|
|
236
|
-
if count < 2:
|
|
237
|
-
for logger_name in ["yfinance", "urllib3", "requests", "diskcache"]:
|
|
238
|
-
logging.getLogger(logger_name).setLevel(logging.WARNING)
|
|
@@ -136,7 +136,7 @@ def test_price_cmd(temp_beancount_file):
|
|
|
136
136
|
# `price` is now a subcommand group; calling it without a subcommand shows help
|
|
137
137
|
code, out, err = run_cli("price", "--help")
|
|
138
138
|
assert code in (0, None)
|
|
139
|
-
assert "check" in out or "fetch" in out
|
|
139
|
+
assert "check" in (out + err) or "fetch" in (out + err)
|
|
140
140
|
|
|
141
141
|
|
|
142
142
|
def test_missing_ledger_file(monkeypatch):
|
|
@@ -148,7 +148,7 @@ def test_missing_ledger_file(monkeypatch):
|
|
|
148
148
|
monkeypatch.delenv("BEANCOUNT_PATH")
|
|
149
149
|
|
|
150
150
|
code, out, err = run_cli("check", "doesnt_exist_file.beancount")
|
|
151
|
-
assert code ==
|
|
151
|
+
assert code == 1 # EXIT_SYSTEM
|
|
152
152
|
assert "Traceback" not in err
|
|
153
153
|
|
|
154
154
|
|
|
@@ -162,5 +162,5 @@ def test_report_holdings_help_hides_audit_only_flags():
|
|
|
162
162
|
def test_report_audit_help_shows_audit_only_flags():
|
|
163
163
|
code, out, err = run_cli("report", "audit", "--help")
|
|
164
164
|
assert code in (0, None)
|
|
165
|
-
assert "--limit" in out
|
|
166
|
-
assert "--all" in out
|
|
165
|
+
assert "--limit" in (out + err)
|
|
166
|
+
assert "--all" in (out + err)
|
|
@@ -21,6 +21,8 @@ def test_report_holdings_json(temp_beancount_file):
|
|
|
21
21
|
"""Exercises the holdings JSON output path (render_output(holdings, format='json'))."""
|
|
22
22
|
code, out, _ = _run_cli("report", "holdings", str(temp_beancount_file), "--format", "json")
|
|
23
23
|
data = jsonlib.loads(out)
|
|
24
|
+
if "data" in data and "ok" in data:
|
|
25
|
+
data = data["data"]
|
|
24
26
|
if isinstance(data, list):
|
|
25
27
|
if data:
|
|
26
28
|
assert "Account" in data[0]
|
|
@@ -49,21 +51,21 @@ def test_check_cmd_with_errors(temp_beancount_file):
|
|
|
49
51
|
with open(temp_beancount_file, "a") as f:
|
|
50
52
|
f.write("\n2022-01-01 INVALID_STATEMENT\n")
|
|
51
53
|
code, out, err = _run_cli("check", str(temp_beancount_file))
|
|
52
|
-
assert code ==
|
|
54
|
+
assert code == 3 # EXIT_VALIDATION, not system error
|
|
53
55
|
assert "Traceback" not in err
|
|
54
56
|
|
|
55
57
|
|
|
56
58
|
def test_check_missing_file_exits_system(tmp_path):
|
|
57
59
|
code, out, err = _run_cli("check", str(tmp_path / "nope.beancount"))
|
|
58
|
-
assert code ==
|
|
60
|
+
assert code == 1 # EXIT_SYSTEM
|
|
59
61
|
assert "Traceback" not in err
|
|
60
62
|
|
|
61
63
|
|
|
62
64
|
def test_check_missing_file_json(tmp_path):
|
|
63
65
|
code, out, err = _run_cli("check", "--format", "json", str(tmp_path / "nope.beancount"))
|
|
64
|
-
assert code ==
|
|
66
|
+
assert code == 1
|
|
65
67
|
payload = jsonlib.loads(err)
|
|
66
|
-
assert payload["exit_code"] ==
|
|
68
|
+
assert payload["exit_code"] == 1
|
|
67
69
|
assert payload["error_type"] == "FileNotFoundError"
|
|
68
70
|
assert "Traceback" not in err
|
|
69
71
|
|
|
@@ -72,11 +74,11 @@ def test_check_validation_errors_json(temp_beancount_file):
|
|
|
72
74
|
with open(temp_beancount_file, "a") as f:
|
|
73
75
|
f.write("\n2022-01-01 INVALID_STATEMENT\n")
|
|
74
76
|
code, out, err = _run_cli("check", "--format", "json", str(temp_beancount_file))
|
|
75
|
-
assert code ==
|
|
77
|
+
assert code == 3
|
|
76
78
|
payload = jsonlib.loads(err)
|
|
77
79
|
assert payload["error"] is True
|
|
78
80
|
assert payload["error_type"] == "BeancountValidationError"
|
|
79
|
-
assert payload["exit_code"] ==
|
|
81
|
+
assert payload["exit_code"] == 3
|
|
80
82
|
assert isinstance(payload["errors"], list)
|
|
81
83
|
assert len(payload["errors"]) > 0
|
|
82
84
|
|
|
@@ -87,6 +89,8 @@ def test_report_audit_json(temp_beancount_file):
|
|
|
87
89
|
)
|
|
88
90
|
assert code == 0
|
|
89
91
|
data = jsonlib.loads(out)
|
|
92
|
+
if "data" in data and "ok" in data:
|
|
93
|
+
data = data["data"]
|
|
90
94
|
assert isinstance(data, list)
|
|
91
95
|
|
|
92
96
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.2.9"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|