beancount-cli 0.2.7__tar.gz → 0.2.9__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.
Files changed (33) hide show
  1. {beancount_cli-0.2.7 → beancount_cli-0.2.9}/CHANGELOG.md +15 -0
  2. {beancount_cli-0.2.7 → beancount_cli-0.2.9}/PKG-INFO +39 -12
  3. {beancount_cli-0.2.7 → beancount_cli-0.2.9}/README.md +38 -11
  4. {beancount_cli-0.2.7 → beancount_cli-0.2.9}/pyproject.toml +1 -1
  5. beancount_cli-0.2.9/src/beancount_cli/__init__.py +1 -0
  6. {beancount_cli-0.2.7 → beancount_cli-0.2.9}/src/beancount_cli/adapters.py +12 -0
  7. {beancount_cli-0.2.7 → beancount_cli-0.2.9}/src/beancount_cli/cli.py +4 -3
  8. {beancount_cli-0.2.7 → beancount_cli-0.2.9}/src/beancount_cli/commands/account.py +33 -11
  9. beancount_cli-0.2.9/src/beancount_cli/commands/commodity.py +100 -0
  10. {beancount_cli-0.2.7 → beancount_cli-0.2.9}/src/beancount_cli/commands/common.py +8 -0
  11. beancount_cli-0.2.9/src/beancount_cli/commands/price.py +238 -0
  12. {beancount_cli-0.2.7 → beancount_cli-0.2.9}/src/beancount_cli/commands/root.py +31 -62
  13. {beancount_cli-0.2.7 → beancount_cli-0.2.9}/src/beancount_cli/commands/transaction.py +5 -10
  14. {beancount_cli-0.2.7 → beancount_cli-0.2.9}/src/beancount_cli/models.py +26 -0
  15. {beancount_cli-0.2.7 → beancount_cli-0.2.9}/src/beancount_cli/services.py +191 -37
  16. {beancount_cli-0.2.7 → beancount_cli-0.2.9}/tests/smoke_test.py +1 -1
  17. {beancount_cli-0.2.7 → beancount_cli-0.2.9}/tests/test_cli.py +7 -22
  18. {beancount_cli-0.2.7 → beancount_cli-0.2.9}/tests/test_coverage_gap.py +30 -2
  19. beancount_cli-0.2.7/src/beancount_cli/__init__.py +0 -1
  20. beancount_cli-0.2.7/src/beancount_cli/commands/commodity.py +0 -50
  21. {beancount_cli-0.2.7 → beancount_cli-0.2.9}/.gitignore +0 -0
  22. {beancount_cli-0.2.7 → beancount_cli-0.2.9}/LICENSE +0 -0
  23. {beancount_cli-0.2.7 → beancount_cli-0.2.9}/src/beancount_cli/commands/__init__.py +0 -0
  24. {beancount_cli-0.2.7 → beancount_cli-0.2.9}/src/beancount_cli/commands/report.py +0 -0
  25. {beancount_cli-0.2.7 → beancount_cli-0.2.9}/src/beancount_cli/config.py +0 -0
  26. {beancount_cli-0.2.7 → beancount_cli-0.2.9}/src/beancount_cli/formatting.py +0 -0
  27. {beancount_cli-0.2.7 → beancount_cli-0.2.9}/src/beancount_cli/py.typed +0 -0
  28. {beancount_cli-0.2.7 → beancount_cli-0.2.9}/tests/conftest.py +0 -0
  29. {beancount_cli-0.2.7 → beancount_cli-0.2.9}/tests/test_advanced.py +0 -0
  30. {beancount_cli-0.2.7 → beancount_cli-0.2.9}/tests/test_bql.py +0 -0
  31. {beancount_cli-0.2.7 → beancount_cli-0.2.9}/tests/test_config.py +0 -0
  32. {beancount_cli-0.2.7 → beancount_cli-0.2.9}/tests/test_models.py +0 -0
  33. {beancount_cli-0.2.7 → beancount_cli-0.2.9}/tests/test_services.py +0 -0
@@ -5,6 +5,21 @@ 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.8] - 2026-04-04
9
+
10
+ ### Added
11
+ - `commodity list`: List all declared commodities, with optional `--asset-class` filter.
12
+ - `commodity check`: Identify currencies used in transactions that are missing a `commodity` directive.
13
+ - `price check`: Identify periods of missing price data for held assets. Supports `--rate` (daily, weekday, weekly, monthly) and `--tolerance` (days before flagging a gap).
14
+ - `price fetch`: Fetch latest quotes via the `bean-price` library with `--update`, `--fill-gaps`, `--dry-run`, `--inactive`, and `--verbose` options.
15
+ - `account balance`: Add a `balance` assertion directive to the ledger via `--json`.
16
+ - `--target` flag on `transaction add`, `account create`, and `commodity create` to override the destination file, bypassing config-driven routing.
17
+ - Structured JSON error output for `check --format json` with typed `error_type` and `exit_code` fields.
18
+
19
+ ### Changed
20
+ - `price` is now a subcommand group (`price check`, `price fetch`), replacing the former single `price` command.
21
+ - `check` now exits with code `2` on missing/unreadable files (system error) and code `1` on validation errors, instead of raising an uncaught exception.
22
+
8
23
  ## [0.2.6] - 2026-03-03
9
24
 
10
25
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: beancount-cli
3
- Version: 0.2.7
3
+ Version: 0.2.9
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
@@ -42,9 +42,9 @@ A robust command-line interface and Python library for programmatically managing
42
42
  - Add transactions via CLI arguments or JSON (stdin supported).
43
43
  - Draft mode support (flag `!`).
44
44
  - **Entities**:
45
- - Manage Accounts (list, create).
46
- - Manage Commodities (create).
47
- - Manage Prices (fetch, update).
45
+ - Manage Accounts (list, create, balance assertion).
46
+ - Manage Commodities (list, create, check undeclared).
47
+ - Manage Prices (check gaps, fetch/update via bean-price).
48
48
  - **Formatting**:
49
49
  - Auto-format ledgers (`bean-format` wrapper).
50
50
  - Global output formatting: `--format` support (`table`, `json`, `csv`) for all data commands.
@@ -125,7 +125,7 @@ bean report trial-balance main.beancount
125
125
  bean report holdings main.beancount
126
126
 
127
127
  # Audit a specific currency (Source of Exposure)
128
- bean report audit USD main.beancount
128
+ bean report audit --currency USD main.beancount
129
129
  ```
130
130
 
131
131
  > [!TIP]
@@ -167,7 +167,7 @@ bean transaction add main.beancount --json ... --draft
167
167
 
168
168
  ### Manage Accounts & Commodities
169
169
 
170
- All creation commands (`transaction add`, `account create`, `commodity create`) support batch processing via JSON arrays on STDIN.
170
+ All creation commands (`transaction add`, `account create`, `commodity create`) support batch processing via JSON arrays on STDIN. Use `--target` to override the destination file.
171
171
 
172
172
  ```bash
173
173
  # Batch add transactions from a file
@@ -177,19 +177,46 @@ cat txs.json | bean transaction add --json -
177
177
  bean --format json account list --file old.beancount | bean account create --json -
178
178
  ```
179
179
 
180
- **Standard CLI usage:**
180
+ **Accounts:**
181
181
  ```bash
182
- # List Accounts
182
+ # List accounts
183
183
  bean account list
184
184
 
185
- # Create Account
185
+ # Create account
186
186
  bean account create --name "Assets:NewBank" --currency "USD"
187
187
 
188
- # Create Commodity
188
+ # Add a balance assertion
189
+ bean account balance --json '{"date": "2024-01-01", "account": "Assets:Bank", "amount": {"number": 1000, "currency": "USD"}}'
190
+ ```
191
+
192
+ **Commodities:**
193
+ ```bash
194
+ # List all declared commodities
195
+ bean commodity list
196
+
197
+ # List by asset class
198
+ bean commodity list --asset-class stock
199
+
200
+ # Find currencies used in transactions but missing a commodity directive
201
+ bean commodity check
202
+
203
+ # Create a commodity
189
204
  bean commodity create "BTC" --name "Bitcoin"
205
+ ```
206
+
207
+ **Prices:**
208
+ ```bash
209
+ # Check for periods of missing price data
210
+ bean price check
211
+
212
+ # Check with weekly rate and 14-day tolerance
213
+ bean price check --rate weekly --tolerance 14
214
+
215
+ # Fetch latest quotes (dry run)
216
+ bean price fetch --dry-run
190
217
 
191
- # Fetch and Update Prices
192
- bean price --update
218
+ # Fetch and write new prices to the ledger
219
+ bean price fetch --update
193
220
  ```
194
221
 
195
222
  `beancount-cli` is specifically optimized for AI agents, providing both operational guidance and machine-readable interfaces.
@@ -11,9 +11,9 @@ A robust command-line interface and Python library for programmatically managing
11
11
  - Add transactions via CLI arguments or JSON (stdin supported).
12
12
  - Draft mode support (flag `!`).
13
13
  - **Entities**:
14
- - Manage Accounts (list, create).
15
- - Manage Commodities (create).
16
- - Manage Prices (fetch, update).
14
+ - Manage Accounts (list, create, balance assertion).
15
+ - Manage Commodities (list, create, check undeclared).
16
+ - Manage Prices (check gaps, fetch/update via bean-price).
17
17
  - **Formatting**:
18
18
  - Auto-format ledgers (`bean-format` wrapper).
19
19
  - Global output formatting: `--format` support (`table`, `json`, `csv`) for all data commands.
@@ -94,7 +94,7 @@ bean report trial-balance main.beancount
94
94
  bean report holdings main.beancount
95
95
 
96
96
  # Audit a specific currency (Source of Exposure)
97
- bean report audit USD main.beancount
97
+ bean report audit --currency USD main.beancount
98
98
  ```
99
99
 
100
100
  > [!TIP]
@@ -136,7 +136,7 @@ bean transaction add main.beancount --json ... --draft
136
136
 
137
137
  ### Manage Accounts & Commodities
138
138
 
139
- All creation commands (`transaction add`, `account create`, `commodity create`) support batch processing via JSON arrays on STDIN.
139
+ All creation commands (`transaction add`, `account create`, `commodity create`) support batch processing via JSON arrays on STDIN. Use `--target` to override the destination file.
140
140
 
141
141
  ```bash
142
142
  # Batch add transactions from a file
@@ -146,19 +146,46 @@ cat txs.json | bean transaction add --json -
146
146
  bean --format json account list --file old.beancount | bean account create --json -
147
147
  ```
148
148
 
149
- **Standard CLI usage:**
149
+ **Accounts:**
150
150
  ```bash
151
- # List Accounts
151
+ # List accounts
152
152
  bean account list
153
153
 
154
- # Create Account
154
+ # Create account
155
155
  bean account create --name "Assets:NewBank" --currency "USD"
156
156
 
157
- # Create Commodity
157
+ # Add a balance assertion
158
+ bean account balance --json '{"date": "2024-01-01", "account": "Assets:Bank", "amount": {"number": 1000, "currency": "USD"}}'
159
+ ```
160
+
161
+ **Commodities:**
162
+ ```bash
163
+ # List all declared commodities
164
+ bean commodity list
165
+
166
+ # List by asset class
167
+ bean commodity list --asset-class stock
168
+
169
+ # Find currencies used in transactions but missing a commodity directive
170
+ bean commodity check
171
+
172
+ # Create a commodity
158
173
  bean commodity create "BTC" --name "Bitcoin"
174
+ ```
175
+
176
+ **Prices:**
177
+ ```bash
178
+ # Check for periods of missing price data
179
+ bean price check
180
+
181
+ # Check with weekly rate and 14-day tolerance
182
+ bean price check --rate weekly --tolerance 14
183
+
184
+ # Fetch latest quotes (dry run)
185
+ bean price fetch --dry-run
159
186
 
160
- # Fetch and Update Prices
161
- bean price --update
187
+ # Fetch and write new prices to the ledger
188
+ bean price fetch --update
162
189
  ```
163
190
 
164
191
  `beancount-cli` is specifically optimized for AI agents, providing both operational guidance and machine-readable interfaces.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "beancount-cli"
3
- version = "0.2.7"
3
+ version = "0.2.9"
4
4
  description = "A CLI tool to manage Beancount ledgers"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -0,0 +1 @@
1
+ __version__ = "0.2.9"
@@ -5,6 +5,7 @@ from beancount.core import amount, data, position
5
5
  from beancount_cli.models import (
6
6
  AccountName,
7
7
  AmountModel,
8
+ BalanceModel,
8
9
  CostModel,
9
10
  CurrencyCode,
10
11
  PostingModel,
@@ -98,3 +99,14 @@ def from_core_transaction(core: data.Transaction) -> TransactionModel:
98
99
  postings=[from_core_posting(p) for p in core.postings],
99
100
  meta=core.meta or {},
100
101
  )
102
+
103
+
104
+ def to_core_balance(model: BalanceModel) -> data.Balance:
105
+ return data.Balance(
106
+ meta=model.meta or {},
107
+ date=model.date,
108
+ account=str(model.account),
109
+ amount=to_core_amount(model.amount),
110
+ diff_amount=None,
111
+ tolerance=None,
112
+ )
@@ -5,8 +5,9 @@ import agentyper as typer
5
5
  from beancount_cli import __version__
6
6
  from beancount_cli.commands.account import app as acc_app
7
7
  from beancount_cli.commands.commodity import app as comm_app
8
+ from beancount_cli.commands.price import app as price_app
8
9
  from beancount_cli.commands.report import app as report_app
9
- from beancount_cli.commands.root import check, format_cmd, price, tree
10
+ from beancount_cli.commands.root import check, format_cmd, tree
10
11
  from beancount_cli.commands.transaction import app as tx_app
11
12
 
12
13
  app = typer.Agentyper(
@@ -19,11 +20,11 @@ app.add_typer(report_app, name="report")
19
20
  app.add_typer(tx_app, name="transaction")
20
21
  app.add_typer(acc_app, name="account")
21
22
  app.add_typer(comm_app, name="commodity")
22
-
23
23
  app.command(name="check")(check)
24
24
  app.command(name="tree")(tree)
25
25
  app.command(name="format")(format_cmd)
26
- app.command(name="price")(price)
26
+
27
+ app.add_typer(price_app, name="price")
27
28
 
28
29
 
29
30
  def main(args=None):
@@ -6,8 +6,13 @@ from pathlib import Path
6
6
  import agentyper as typer
7
7
  from pydantic import TypeAdapter
8
8
 
9
- from beancount_cli.commands.common import _is_table_format, console, get_ledger_file
10
- from beancount_cli.models import AccountModel
9
+ from beancount_cli.commands.common import (
10
+ _is_table_format,
11
+ console,
12
+ get_ledger_file,
13
+ read_json_input,
14
+ )
15
+ from beancount_cli.models import AccountModel, BalanceModel
11
16
  from beancount_cli.services import AccountService
12
17
 
13
18
  app = typer.Agentyper(help="Manage accounts.")
@@ -53,27 +58,23 @@ def account_create(
53
58
  json_data: str | None = typer.Option(
54
59
  None, "--json", "-j", help="JSON string data (or '-' to read from STDIN)"
55
60
  ),
61
+ target: Path | None = typer.Option(None, "--target", help="Override target file to write to"),
56
62
  ):
57
63
  """Create a new account."""
58
64
  actual_file = get_ledger_file(ledger_file or file)
59
65
  service = AccountService(actual_file)
60
66
 
61
67
  if json_data:
62
- if json_data == "-":
63
- content = sys.stdin.read()
64
- else:
65
- content = json_data
66
-
67
- data_input = json.loads(content)
68
+ data_input = json.loads(read_json_input(json_data))
68
69
  if isinstance(data_input, list):
69
70
  ta = TypeAdapter(list[AccountModel])
70
71
  models = ta.validate_python(data_input)
71
72
  for m in models:
72
- service.create_account(m)
73
+ service.create_account(m, target_file=target)
73
74
  console.print(f"[green]Account {m.name} created.[/green]")
74
75
  else:
75
76
  model = AccountModel(**data_input)
76
- service.create_account(model)
77
+ service.create_account(model, target_file=target)
77
78
  console.print(f"[green]Account {model.name} created.[/green]")
78
79
  else:
79
80
  if not name:
@@ -86,5 +87,26 @@ def account_create(
86
87
 
87
88
  currencies = [c.strip() for c in currency_opt.split(",")] if currency_opt else []
88
89
  model = AccountModel(name=name, open_date=d, currencies=currencies)
89
- service.create_account(model)
90
+ service.create_account(model, target_file=target)
90
91
  console.print(f"[green]Account {name} created.[/green]")
92
+
93
+
94
+ @app.command(name="balance")
95
+ def account_balance(
96
+ ledger_file: Path | None = typer.Argument(None, help="Path to ledger file"),
97
+ file: Path | None = typer.Option(
98
+ None, "--file", "-f", envvar="BEANCOUNT_FILE", help="Main beancount file"
99
+ ),
100
+ json_data: str = typer.Option(
101
+ ..., "--json", "-j", help="JSON string data (or '-' to read from STDIN)"
102
+ ),
103
+ target: Path | None = typer.Option(None, "--target", help="Override target file to write to"),
104
+ ):
105
+ """Add a balance directive for an account."""
106
+ actual_file = get_ledger_file(ledger_file or file)
107
+ service = AccountService(actual_file)
108
+
109
+ data_input = json.loads(read_json_input(json_data))
110
+ model = BalanceModel(**data_input)
111
+ service.add_balance(model, target_file=target)
112
+ console.print(f"[green]Balance check for {model.account} added.[/green]")
@@ -0,0 +1,100 @@
1
+ import json
2
+ import sys
3
+ from pathlib import Path
4
+
5
+ import agentyper as typer
6
+
7
+ from beancount_cli.commands.common import (
8
+ _is_table_format,
9
+ console,
10
+ get_ledger_file,
11
+ read_json_input,
12
+ )
13
+ from beancount_cli.services import CommodityService
14
+
15
+ app = typer.Agentyper(help="Manage commodities.")
16
+
17
+
18
+ @app.command(name="list")
19
+ def commodity_list(
20
+ ledger_file: Path | None = typer.Argument(None, help="Path to ledger file"),
21
+ file: Path | None = typer.Option(
22
+ None, "--file", "-f", envvar="BEANCOUNT_FILE", help="Main beancount file"
23
+ ),
24
+ asset_class: str | None = typer.Option(
25
+ None, "--asset-class", "-c", help="Filter by asset-class meta (e.g. stock, Cash)"
26
+ ),
27
+ ):
28
+ """List all commodities."""
29
+ actual_file = get_ledger_file(ledger_file or file)
30
+ service = CommodityService(actual_file)
31
+ commodities = service.list_commodities(asset_class=asset_class)
32
+
33
+ if _is_table_format():
34
+ data = [
35
+ {
36
+ "Currency": c.currency,
37
+ "Date": str(c.date) if c.date else "",
38
+ "Name": c.meta.get("name", "") if c.meta else "",
39
+ }
40
+ for c in commodities
41
+ ]
42
+ typer.output(data, title=f"Commodities ({len(commodities)})")
43
+ else:
44
+ typer.output(commodities, title=f"Commodities ({len(commodities)})")
45
+
46
+
47
+ @app.command(name="check")
48
+ def commodity_check(
49
+ ledger_file: Path | None = typer.Argument(None, help="Path to ledger file"),
50
+ file: Path | None = typer.Option(
51
+ None, "--file", "-f", envvar="BEANCOUNT_FILE", help="Main beancount file"
52
+ ),
53
+ ):
54
+ """Check for currencies used in transactions but missing a commodity directive."""
55
+ actual_file = get_ledger_file(ledger_file or file)
56
+ service = CommodityService(actual_file)
57
+ undeclared = service.get_undeclared_commodities()
58
+
59
+ if _is_table_format():
60
+ if not undeclared:
61
+ console.print("[green]All used currencies are declared.[/green]")
62
+ else:
63
+ typer.output(undeclared, title=f"Undeclared Commodities ({len(undeclared)})")
64
+ else:
65
+ typer.output(undeclared, title="Undeclared Commodities")
66
+
67
+
68
+ @app.command(name="create")
69
+ def commodity_create(
70
+ currency: str | None = typer.Argument(None, help="Currency code (e.g. USD)"),
71
+ ledger_file: Path | None = typer.Argument(None, help="Path to ledger file"),
72
+ file: Path | None = typer.Option(
73
+ None, "--file", "-f", envvar="BEANCOUNT_FILE", help="Main beancount file"
74
+ ),
75
+ name: str | None = typer.Option(None, "--name", "-n", help="Full name"),
76
+ json_data: str | None = typer.Option(
77
+ None, "--json", "-j", help="JSON string data (or '-' to read from STDIN)"
78
+ ),
79
+ ):
80
+ """Create a new commodity."""
81
+ actual_file = get_ledger_file(ledger_file or file)
82
+ service = CommodityService(actual_file)
83
+
84
+ if json_data:
85
+ data_input = json.loads(read_json_input(json_data))
86
+ items = data_input if isinstance(data_input, list) else [data_input]
87
+ for item in items:
88
+ curr = item.get("currency")
89
+ comm_name = item.get("name")
90
+ if not curr:
91
+ console.print(f"[yellow]Skipping invalid commodity entry: {item}[/yellow]")
92
+ continue
93
+ service.create_commodity(curr, name=comm_name)
94
+ console.print(f"[green]Commodity {curr} created.[/green]")
95
+ else:
96
+ if not currency:
97
+ console.print("[red]Error: currency argument is required if not using --json.[/red]")
98
+ sys.exit(typer.EXIT_VALIDATION)
99
+ service.create_commodity(currency, name=name)
100
+ console.print(f"[green]Commodity {currency} created.[/green]")
@@ -7,6 +7,14 @@ from rich.console import Console
7
7
  from rich.table import Table
8
8
 
9
9
  console = Console()
10
+ error_console = Console(stderr=True)
11
+
12
+
13
+ def read_json_input(json_data: str) -> str:
14
+ """Read JSON from a string or from STDIN when json_data is '-'."""
15
+ if json_data == "-":
16
+ return sys.stdin.read()
17
+ return json_data
10
18
 
11
19
 
12
20
  def get_ledger_file(override: str | Path | None = None) -> Path: