canhoto 0.1.0__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.
- canhoto-0.1.0/.gitignore +14 -0
- canhoto-0.1.0/AGENTS.md +86 -0
- canhoto-0.1.0/LICENSE +21 -0
- canhoto-0.1.0/PKG-INFO +275 -0
- canhoto-0.1.0/README.md +244 -0
- canhoto-0.1.0/examples/parsers/README.md +107 -0
- canhoto-0.1.0/examples/parsers/demo_line_parser.py +178 -0
- canhoto-0.1.0/examples/parsers/fixtures/demo_statement.txt +5 -0
- canhoto-0.1.0/pyproject.toml +85 -0
- canhoto-0.1.0/src/canhoto/__init__.py +3 -0
- canhoto-0.1.0/src/canhoto/__main__.py +10 -0
- canhoto-0.1.0/src/canhoto/cli.py +386 -0
- canhoto-0.1.0/src/canhoto/core/__init__.py +1 -0
- canhoto-0.1.0/src/canhoto/core/breakdown.py +136 -0
- canhoto-0.1.0/src/canhoto/core/categorize.py +469 -0
- canhoto-0.1.0/src/canhoto/core/config.py +84 -0
- canhoto-0.1.0/src/canhoto/core/migrate.py +91 -0
- canhoto-0.1.0/src/canhoto/core/models.py +336 -0
- canhoto-0.1.0/src/canhoto/core/pdf_text.py +74 -0
- canhoto-0.1.0/src/canhoto/core/policy.py +48 -0
- canhoto-0.1.0/src/canhoto/core/redaction.py +117 -0
- canhoto-0.1.0/src/canhoto/core/store.py +593 -0
- canhoto-0.1.0/src/canhoto/core/user_rules.py +49 -0
- canhoto-0.1.0/src/canhoto/exporters/__init__.py +1 -0
- canhoto-0.1.0/src/canhoto/exporters/pdf_summary.py +433 -0
- canhoto-0.1.0/src/canhoto/exporters/protocol.py +23 -0
- canhoto-0.1.0/src/canhoto/mcp/__init__.py +1 -0
- canhoto-0.1.0/src/canhoto/mcp/__main__.py +6 -0
- canhoto-0.1.0/src/canhoto/mcp/allowlist.py +39 -0
- canhoto-0.1.0/src/canhoto/mcp/server.py +189 -0
- canhoto-0.1.0/src/canhoto/migrations/__init__.py +1 -0
- canhoto-0.1.0/src/canhoto/migrations/env.py +48 -0
- canhoto-0.1.0/src/canhoto/migrations/script.py.mako +26 -0
- canhoto-0.1.0/src/canhoto/migrations/versions/001_initial_schema.py +100 -0
- canhoto-0.1.0/src/canhoto/migrations/versions/002_user_rules.py +59 -0
- canhoto-0.1.0/src/canhoto/parsers/__init__.py +35 -0
- canhoto-0.1.0/src/canhoto/parsers/loader.py +191 -0
- canhoto-0.1.0/src/canhoto/parsers/protocol.py +40 -0
- canhoto-0.1.0/src/canhoto/parsers/scaffold.py +138 -0
- canhoto-0.1.0/src/canhoto/service.py +969 -0
- canhoto-0.1.0/tests/guardrails/test_mcp_allowlist.py +73 -0
- canhoto-0.1.0/tests/guardrails/test_models_contracts.py +131 -0
- canhoto-0.1.0/tests/guardrails/test_policy.py +66 -0
- canhoto-0.1.0/tests/guardrails/test_redaction.py +142 -0
- canhoto-0.1.0/tests/guardrails/test_review_batch.py +261 -0
- canhoto-0.1.0/tests/test_categorize.py +159 -0
- canhoto-0.1.0/tests/test_cli_ingest.py +106 -0
- canhoto-0.1.0/tests/test_config.py +146 -0
- canhoto-0.1.0/tests/test_doctor.py +208 -0
- canhoto-0.1.0/tests/test_export_pdf.py +378 -0
- canhoto-0.1.0/tests/test_ingest.py +170 -0
- canhoto-0.1.0/tests/test_mcp_server.py +128 -0
- canhoto-0.1.0/tests/test_merchant_memory.py +181 -0
- canhoto-0.1.0/tests/test_migrate.py +82 -0
- canhoto-0.1.0/tests/test_month_breakdown.py +269 -0
- canhoto-0.1.0/tests/test_parser_flow.py +290 -0
- canhoto-0.1.0/tests/test_parser_loader.py +250 -0
- canhoto-0.1.0/tests/test_review.py +124 -0
- canhoto-0.1.0/tests/test_store.py +258 -0
- canhoto-0.1.0/tests/test_user_rules.py +504 -0
canhoto-0.1.0/.gitignore
ADDED
canhoto-0.1.0/AGENTS.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Agent bootstrap — Canhoto
|
|
2
|
+
|
|
3
|
+
Read this before editing code.
|
|
4
|
+
|
|
5
|
+
## Product
|
|
6
|
+
|
|
7
|
+
| Layer | Value |
|
|
8
|
+
|---|---|
|
|
9
|
+
| Product | **Canhoto** (stub/counterfoil you keep) |
|
|
10
|
+
| Package / import | `canhoto` |
|
|
11
|
+
| CLI | `canhoto` |
|
|
12
|
+
| MCP | `canhoto-mcp` |
|
|
13
|
+
| Data | `~/.canhoto` / `$CANHOTO_DATA_DIR` |
|
|
14
|
+
| DB | `canhoto.db` |
|
|
15
|
+
|
|
16
|
+
User docs: [`README.md`](README.md).
|
|
17
|
+
|
|
18
|
+
## Layout
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
src/canhoto/
|
|
22
|
+
core/ # models, config, store, migrate, policy, redaction, categorize, …
|
|
23
|
+
migrations/ # Alembic scripts (SQLite)
|
|
24
|
+
parsers/ # Protocol, loader, scaffold (no bank logic)
|
|
25
|
+
exporters/ # pdf_summary
|
|
26
|
+
mcp/ # allowlist + Fast/MCP server → service only
|
|
27
|
+
service.py # façade for CLI + MCP
|
|
28
|
+
cli.py
|
|
29
|
+
examples/parsers/ # docs-only demo (not auto-loaded)
|
|
30
|
+
tests/guardrails/ # privacy/allowlist contracts
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Dependency direction: `cli` / `mcp` → `service` → `core` + ports.
|
|
34
|
+
|
|
35
|
+
## Hard rules
|
|
36
|
+
|
|
37
|
+
| Do | Do not |
|
|
38
|
+
|---|---|
|
|
39
|
+
| Keep money out of git | Commit statements, tokens, DB files |
|
|
40
|
+
| MCP tools ⊆ `MCP_TOOL_ALLOWLIST` | Add `sql_query` or full ledger dumps |
|
|
41
|
+
| Review via `to_review_item` + policy | Return raw `LedgerTransaction` to agents |
|
|
42
|
+
| Parser enable only after successful `parser_test` (non-empty txs) | Auto-download parsers; stamp OK on empty parse |
|
|
43
|
+
| PDF summary only | Full transaction listing PDF |
|
|
44
|
+
| Country-agnostic core | Hardcode one bank as the kernel |
|
|
45
|
+
| Prefer deleting dead code | Keep unused alternate trees |
|
|
46
|
+
|
|
47
|
+
## MCP happy path
|
|
48
|
+
|
|
49
|
+
1. `statement_preview` → `parser_scaffold` / `parser_write` → `parser_test` → `parser_enable`
|
|
50
|
+
2. `ingest`
|
|
51
|
+
3. `rule_list` → `run_rules` → `review_batch` loop → `set_categories` (and `set_merchant_category` as needed).
|
|
52
|
+
Store recurring decisions with `rule_add` and a note.
|
|
53
|
+
Rules never overwrite rows set by `set_categories`.
|
|
54
|
+
4. `month_breakdown` → `export_pdf`
|
|
55
|
+
|
|
56
|
+
`parser_write` requires `agent_view.allow_parser_writes=true`. CLI may always write local parsers.
|
|
57
|
+
|
|
58
|
+
## Data dir
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
$CANHOTO_DATA_DIR or ~/.canhoto/
|
|
62
|
+
config.json
|
|
63
|
+
canhoto.db
|
|
64
|
+
parsers/
|
|
65
|
+
exports/
|
|
66
|
+
raw/
|
|
67
|
+
fixtures/
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Commands
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
uv sync --extra dev
|
|
74
|
+
uv run pytest -q
|
|
75
|
+
uv run ruff check src/canhoto tests
|
|
76
|
+
uv run mypy -p canhoto
|
|
77
|
+
uv run canhoto --help
|
|
78
|
+
uv run canhoto-mcp # stdio; host-spawned
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Quality bar
|
|
82
|
+
|
|
83
|
+
- TDD for behavior changes; meaningful tests only
|
|
84
|
+
- No Google/Sheets in core
|
|
85
|
+
- Guardrails in `tests/guardrails/` must stay green
|
|
86
|
+
|
canhoto-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 luabagg
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
canhoto-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: canhoto
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Local bank statement ledger: parse, categorize, and export monthly PDF summaries (CLI and MCP)
|
|
5
|
+
Project-URL: Homepage, https://github.com/luabagg/canhoto
|
|
6
|
+
Project-URL: Documentation, https://github.com/luabagg/canhoto/blob/main/README.md
|
|
7
|
+
Project-URL: Issues, https://github.com/luabagg/canhoto/issues
|
|
8
|
+
Author: luabagg
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: cli,finance,ledger,mcp,pdf
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Requires-Dist: alembic>=1.19.1
|
|
22
|
+
Requires-Dist: fpdf2>=2.8.7
|
|
23
|
+
Requires-Dist: mcp>=1.0
|
|
24
|
+
Requires-Dist: pydantic>=2.0
|
|
25
|
+
Requires-Dist: pymupdf>=1.24.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# Canhoto
|
|
33
|
+
|
|
34
|
+
Canhoto keeps your bank and card statements on your computer. It parses
|
|
35
|
+
statements, categorizes spending, asks you about uncertain items, and exports a
|
|
36
|
+
monthly PDF summary.
|
|
37
|
+
|
|
38
|
+
> **Using an AI agent?** Connect `canhoto-mcp`. The agent gets a bounded set of
|
|
39
|
+
> tools to parse statements, review and categorize transactions, read monthly
|
|
40
|
+
> totals, and export reports. It never gets SQL access or a full ledger dump.
|
|
41
|
+
> The CLI gives you the same workflow by hand.
|
|
42
|
+
|
|
43
|
+
| | |
|
|
44
|
+
|---|---|
|
|
45
|
+
| Package | `canhoto` |
|
|
46
|
+
| CLI | `canhoto` |
|
|
47
|
+
| MCP | `canhoto-mcp` |
|
|
48
|
+
| Data | `~/.canhoto` or `$CANHOTO_DATA_DIR` |
|
|
49
|
+
|
|
50
|
+
## Install
|
|
51
|
+
|
|
52
|
+
Install the CLI and the MCP server from PyPI:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
uv tool install canhoto
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Or install from a checkout of this repository:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
uv tool install .
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Or run it in place:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
uv sync
|
|
68
|
+
uv run canhoto --help
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Then create the data directory and check it:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
canhoto init
|
|
75
|
+
canhoto doctor
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## How it works
|
|
79
|
+
|
|
80
|
+
```mermaid
|
|
81
|
+
flowchart TD
|
|
82
|
+
A[Statement PDF or text] --> B[Parser]
|
|
83
|
+
B --> C[Ingest]
|
|
84
|
+
C --> D[(Local SQLite ledger)]
|
|
85
|
+
D --> E[Your rules, built-in rules, merchant memory]
|
|
86
|
+
E --> F[Review uncertain items]
|
|
87
|
+
F --> G[Apply categories]
|
|
88
|
+
G --> H[Monthly breakdown]
|
|
89
|
+
H --> I[Summary PDF]
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Parsers only read statement rows. Canhoto handles categories, rules, merchant
|
|
93
|
+
memory, reports, and exports.
|
|
94
|
+
|
|
95
|
+
## Create a parser
|
|
96
|
+
|
|
97
|
+
Canhoto does not ship bank-specific parsers. Create one for your statement:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
canhoto parsers scaffold --id my_bank_card --type card --institution my_bank
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Edit `~/.canhoto/parsers/my_bank_card.py`, then test and enable it:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
canhoto parsers test --id my_bank_card --file ~/statements/sample.pdf
|
|
107
|
+
canhoto parsers enable --id my_bank_card
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
You can enable a parser only after a test extracts at least one transaction.
|
|
111
|
+
A failed test disables the parser until it passes again. See
|
|
112
|
+
[`examples/parsers/`](examples/parsers/) for a small example.
|
|
113
|
+
|
|
114
|
+
For password-protected PDFs, pass `--pdf-password` or set
|
|
115
|
+
`CANHOTO_PDF_PASSWORD`.
|
|
116
|
+
|
|
117
|
+
## Process a month
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
# Read statements into the ledger.
|
|
121
|
+
canhoto ingest ~/statements/*.pdf
|
|
122
|
+
|
|
123
|
+
# Apply your rules, the built-in rules, and merchant memory.
|
|
124
|
+
canhoto categorize rules --month 2026-06
|
|
125
|
+
|
|
126
|
+
# List the items that still need a decision.
|
|
127
|
+
canhoto review --month 2026-06
|
|
128
|
+
|
|
129
|
+
# Apply your decisions.
|
|
130
|
+
canhoto categorize apply --file patches.json
|
|
131
|
+
|
|
132
|
+
# See income, expenses, and category totals.
|
|
133
|
+
canhoto breakdown --month 2026-06
|
|
134
|
+
|
|
135
|
+
# Write the PDF summary.
|
|
136
|
+
canhoto export pdf 2026-06
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`patches.json` is a list of changes, one per transaction id from `review`:
|
|
140
|
+
|
|
141
|
+
```json
|
|
142
|
+
[
|
|
143
|
+
{ "id": "my_bank_card-2026-06-02-0001", "category": "Groceries",
|
|
144
|
+
"kind": "expense", "is_expense": true, "needs_review": false }
|
|
145
|
+
]
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Canhoto marks every row you change this way as `manual`. No automatic rule
|
|
149
|
+
changes a manual row again.
|
|
150
|
+
|
|
151
|
+
The PDF goes to `~/.canhoto/exports/2026-06-summary.pdf` by default. It shows
|
|
152
|
+
totals by category and the top merchants in each category. It never contains a
|
|
153
|
+
transaction list or raw statement descriptions.
|
|
154
|
+
|
|
155
|
+
### Teach Canhoto your rules
|
|
156
|
+
|
|
157
|
+
When a counterparty always means the same thing, store a rule. `categorize
|
|
158
|
+
rules` applies it to every future statement.
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
canhoto rules add --pattern "PIX RECEBIDO ACME LTDA" --direction in \
|
|
162
|
+
--min 1200 --max 1300 --category Income --kind income \
|
|
163
|
+
--note "Monthly pay from my company"
|
|
164
|
+
canhoto rules list
|
|
165
|
+
canhoto rules remove --id 3
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
| Option | Meaning |
|
|
169
|
+
|---|---|
|
|
170
|
+
| `--pattern` | Regular expression, case-insensitive, matched against the description. |
|
|
171
|
+
| `--direction` | `in` (money received), `out` (money sent), or `any`. |
|
|
172
|
+
| `--min`, `--max` | Amount range, inclusive, without the sign. Both are optional. |
|
|
173
|
+
| `--source-kind` | Match only `account` or `card` rows. |
|
|
174
|
+
| `--category` | The category to set. |
|
|
175
|
+
| `--kind` | `expense`, `income`, `transfer`, `internal_transfer`, `self_transfer`, or `card_payment`. Transfers do not count as income or spending. |
|
|
176
|
+
| `--review` | Set the category, but keep the row in the review queue. Use this when an amount range cannot separate two cases. |
|
|
177
|
+
| `--note` | Why the rule exists. Agents read it during review. Do not put secrets in it. |
|
|
178
|
+
| `--priority` | Lower numbers run first. The first matching rule wins. |
|
|
179
|
+
|
|
180
|
+
Your rules run before the built-in rules. They never change manual rows. When
|
|
181
|
+
you upgrade to the version with rules, Canhoto marks your existing reviewed
|
|
182
|
+
rows as manual.
|
|
183
|
+
|
|
184
|
+
To remember one merchant without a full rule, use merchant memory:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
canhoto categorize merchant --key CURSOR --category Subscriptions
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### PDF profiles
|
|
191
|
+
|
|
192
|
+
Choose a built-in style:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
canhoto export pdf 2026-06 --profile canhoto
|
|
196
|
+
canhoto export pdf 2026-06 --profile modern --output ~/Documents/2026-06.pdf
|
|
197
|
+
canhoto export pdf 2026-06 --profile minimal
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
- `canhoto`: receipt-style report with a category chart.
|
|
201
|
+
- `modern`: clean report with metric cards and a category chart.
|
|
202
|
+
- `minimal`: text-only report without a chart.
|
|
203
|
+
|
|
204
|
+
## MCP
|
|
205
|
+
|
|
206
|
+
The CLI and the MCP server use the same service layer. For agent-assisted use,
|
|
207
|
+
start the MCP server. The agent follows this flow:
|
|
208
|
+
|
|
209
|
+
`statement_preview` -> `parser_*` -> `ingest` -> `rule_list` -> `run_rules` ->
|
|
210
|
+
`review_batch` -> `set_categories` -> `month_breakdown` -> `export_pdf`
|
|
211
|
+
|
|
212
|
+
When you explain a recurring counterparty to the agent, it can store it with
|
|
213
|
+
`rule_add`. The MCP server exposes only domain tools. It does not give SQL
|
|
214
|
+
access or full ledger dumps.
|
|
215
|
+
|
|
216
|
+
To let an agent write parsers, add this to `~/.canhoto/config.json`:
|
|
217
|
+
|
|
218
|
+
```json
|
|
219
|
+
{ "agent_view": { "allow_parser_writes": true } }
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Example MCP host configuration:
|
|
223
|
+
|
|
224
|
+
```yaml
|
|
225
|
+
mcp_servers:
|
|
226
|
+
canhoto:
|
|
227
|
+
command: canhoto-mcp
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## CLI commands
|
|
231
|
+
|
|
232
|
+
```text
|
|
233
|
+
canhoto init | doctor
|
|
234
|
+
canhoto parsers scaffold|test|enable|list
|
|
235
|
+
canhoto ingest <files...> [--pdf-password PASSWORD]
|
|
236
|
+
canhoto categorize rules --month YYYY-MM
|
|
237
|
+
canhoto categorize apply --file patches.json
|
|
238
|
+
canhoto categorize merchant --key KEY --category CAT
|
|
239
|
+
canhoto rules add|list|remove
|
|
240
|
+
canhoto review --month YYYY-MM [--cursor ID] [--limit N]
|
|
241
|
+
canhoto breakdown --month YYYY-MM
|
|
242
|
+
canhoto export pdf YYYY-MM [--profile canhoto|modern|minimal] [--output PATH]
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Develop
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
uv sync --extra dev
|
|
249
|
+
uv run pytest -q
|
|
250
|
+
uv run ruff check src/canhoto tests
|
|
251
|
+
uv run mypy -p canhoto
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Or use the [`justfile`](justfile) with [Just](https://just.systems/).
|
|
255
|
+
|
|
256
|
+
### Release
|
|
257
|
+
|
|
258
|
+
Pushing a version tag publishes to PyPI through
|
|
259
|
+
[`.github/workflows/release.yml`](.github/workflows/release.yml). The workflow
|
|
260
|
+
tests, builds, smoke-tests the wheel and the source distribution, and publishes
|
|
261
|
+
with PyPI Trusted Publishing. The tag must match the version in
|
|
262
|
+
`pyproject.toml`.
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
uv version --bump patch # or minor / major
|
|
266
|
+
git commit -am "chore: release v$(uv version --short)"
|
|
267
|
+
git tag -a "v$(uv version --short)" -m "v$(uv version --short)"
|
|
268
|
+
git push origin main --tags
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Privacy
|
|
272
|
+
|
|
273
|
+
Your statements and database stay in the data directory. Do not commit
|
|
274
|
+
statements, tokens, database files, or `~/.canhoto`. Back up `canhoto.db` before
|
|
275
|
+
you upgrade: it holds your ledger and your rules.
|
canhoto-0.1.0/README.md
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# Canhoto
|
|
2
|
+
|
|
3
|
+
Canhoto keeps your bank and card statements on your computer. It parses
|
|
4
|
+
statements, categorizes spending, asks you about uncertain items, and exports a
|
|
5
|
+
monthly PDF summary.
|
|
6
|
+
|
|
7
|
+
> **Using an AI agent?** Connect `canhoto-mcp`. The agent gets a bounded set of
|
|
8
|
+
> tools to parse statements, review and categorize transactions, read monthly
|
|
9
|
+
> totals, and export reports. It never gets SQL access or a full ledger dump.
|
|
10
|
+
> The CLI gives you the same workflow by hand.
|
|
11
|
+
|
|
12
|
+
| | |
|
|
13
|
+
|---|---|
|
|
14
|
+
| Package | `canhoto` |
|
|
15
|
+
| CLI | `canhoto` |
|
|
16
|
+
| MCP | `canhoto-mcp` |
|
|
17
|
+
| Data | `~/.canhoto` or `$CANHOTO_DATA_DIR` |
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
Install the CLI and the MCP server from PyPI:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
uv tool install canhoto
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Or install from a checkout of this repository:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
uv tool install .
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or run it in place:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
uv sync
|
|
37
|
+
uv run canhoto --help
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Then create the data directory and check it:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
canhoto init
|
|
44
|
+
canhoto doctor
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## How it works
|
|
48
|
+
|
|
49
|
+
```mermaid
|
|
50
|
+
flowchart TD
|
|
51
|
+
A[Statement PDF or text] --> B[Parser]
|
|
52
|
+
B --> C[Ingest]
|
|
53
|
+
C --> D[(Local SQLite ledger)]
|
|
54
|
+
D --> E[Your rules, built-in rules, merchant memory]
|
|
55
|
+
E --> F[Review uncertain items]
|
|
56
|
+
F --> G[Apply categories]
|
|
57
|
+
G --> H[Monthly breakdown]
|
|
58
|
+
H --> I[Summary PDF]
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Parsers only read statement rows. Canhoto handles categories, rules, merchant
|
|
62
|
+
memory, reports, and exports.
|
|
63
|
+
|
|
64
|
+
## Create a parser
|
|
65
|
+
|
|
66
|
+
Canhoto does not ship bank-specific parsers. Create one for your statement:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
canhoto parsers scaffold --id my_bank_card --type card --institution my_bank
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Edit `~/.canhoto/parsers/my_bank_card.py`, then test and enable it:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
canhoto parsers test --id my_bank_card --file ~/statements/sample.pdf
|
|
76
|
+
canhoto parsers enable --id my_bank_card
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
You can enable a parser only after a test extracts at least one transaction.
|
|
80
|
+
A failed test disables the parser until it passes again. See
|
|
81
|
+
[`examples/parsers/`](examples/parsers/) for a small example.
|
|
82
|
+
|
|
83
|
+
For password-protected PDFs, pass `--pdf-password` or set
|
|
84
|
+
`CANHOTO_PDF_PASSWORD`.
|
|
85
|
+
|
|
86
|
+
## Process a month
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
# Read statements into the ledger.
|
|
90
|
+
canhoto ingest ~/statements/*.pdf
|
|
91
|
+
|
|
92
|
+
# Apply your rules, the built-in rules, and merchant memory.
|
|
93
|
+
canhoto categorize rules --month 2026-06
|
|
94
|
+
|
|
95
|
+
# List the items that still need a decision.
|
|
96
|
+
canhoto review --month 2026-06
|
|
97
|
+
|
|
98
|
+
# Apply your decisions.
|
|
99
|
+
canhoto categorize apply --file patches.json
|
|
100
|
+
|
|
101
|
+
# See income, expenses, and category totals.
|
|
102
|
+
canhoto breakdown --month 2026-06
|
|
103
|
+
|
|
104
|
+
# Write the PDF summary.
|
|
105
|
+
canhoto export pdf 2026-06
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`patches.json` is a list of changes, one per transaction id from `review`:
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
[
|
|
112
|
+
{ "id": "my_bank_card-2026-06-02-0001", "category": "Groceries",
|
|
113
|
+
"kind": "expense", "is_expense": true, "needs_review": false }
|
|
114
|
+
]
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Canhoto marks every row you change this way as `manual`. No automatic rule
|
|
118
|
+
changes a manual row again.
|
|
119
|
+
|
|
120
|
+
The PDF goes to `~/.canhoto/exports/2026-06-summary.pdf` by default. It shows
|
|
121
|
+
totals by category and the top merchants in each category. It never contains a
|
|
122
|
+
transaction list or raw statement descriptions.
|
|
123
|
+
|
|
124
|
+
### Teach Canhoto your rules
|
|
125
|
+
|
|
126
|
+
When a counterparty always means the same thing, store a rule. `categorize
|
|
127
|
+
rules` applies it to every future statement.
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
canhoto rules add --pattern "PIX RECEBIDO ACME LTDA" --direction in \
|
|
131
|
+
--min 1200 --max 1300 --category Income --kind income \
|
|
132
|
+
--note "Monthly pay from my company"
|
|
133
|
+
canhoto rules list
|
|
134
|
+
canhoto rules remove --id 3
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
| Option | Meaning |
|
|
138
|
+
|---|---|
|
|
139
|
+
| `--pattern` | Regular expression, case-insensitive, matched against the description. |
|
|
140
|
+
| `--direction` | `in` (money received), `out` (money sent), or `any`. |
|
|
141
|
+
| `--min`, `--max` | Amount range, inclusive, without the sign. Both are optional. |
|
|
142
|
+
| `--source-kind` | Match only `account` or `card` rows. |
|
|
143
|
+
| `--category` | The category to set. |
|
|
144
|
+
| `--kind` | `expense`, `income`, `transfer`, `internal_transfer`, `self_transfer`, or `card_payment`. Transfers do not count as income or spending. |
|
|
145
|
+
| `--review` | Set the category, but keep the row in the review queue. Use this when an amount range cannot separate two cases. |
|
|
146
|
+
| `--note` | Why the rule exists. Agents read it during review. Do not put secrets in it. |
|
|
147
|
+
| `--priority` | Lower numbers run first. The first matching rule wins. |
|
|
148
|
+
|
|
149
|
+
Your rules run before the built-in rules. They never change manual rows. When
|
|
150
|
+
you upgrade to the version with rules, Canhoto marks your existing reviewed
|
|
151
|
+
rows as manual.
|
|
152
|
+
|
|
153
|
+
To remember one merchant without a full rule, use merchant memory:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
canhoto categorize merchant --key CURSOR --category Subscriptions
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### PDF profiles
|
|
160
|
+
|
|
161
|
+
Choose a built-in style:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
canhoto export pdf 2026-06 --profile canhoto
|
|
165
|
+
canhoto export pdf 2026-06 --profile modern --output ~/Documents/2026-06.pdf
|
|
166
|
+
canhoto export pdf 2026-06 --profile minimal
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
- `canhoto`: receipt-style report with a category chart.
|
|
170
|
+
- `modern`: clean report with metric cards and a category chart.
|
|
171
|
+
- `minimal`: text-only report without a chart.
|
|
172
|
+
|
|
173
|
+
## MCP
|
|
174
|
+
|
|
175
|
+
The CLI and the MCP server use the same service layer. For agent-assisted use,
|
|
176
|
+
start the MCP server. The agent follows this flow:
|
|
177
|
+
|
|
178
|
+
`statement_preview` -> `parser_*` -> `ingest` -> `rule_list` -> `run_rules` ->
|
|
179
|
+
`review_batch` -> `set_categories` -> `month_breakdown` -> `export_pdf`
|
|
180
|
+
|
|
181
|
+
When you explain a recurring counterparty to the agent, it can store it with
|
|
182
|
+
`rule_add`. The MCP server exposes only domain tools. It does not give SQL
|
|
183
|
+
access or full ledger dumps.
|
|
184
|
+
|
|
185
|
+
To let an agent write parsers, add this to `~/.canhoto/config.json`:
|
|
186
|
+
|
|
187
|
+
```json
|
|
188
|
+
{ "agent_view": { "allow_parser_writes": true } }
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Example MCP host configuration:
|
|
192
|
+
|
|
193
|
+
```yaml
|
|
194
|
+
mcp_servers:
|
|
195
|
+
canhoto:
|
|
196
|
+
command: canhoto-mcp
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## CLI commands
|
|
200
|
+
|
|
201
|
+
```text
|
|
202
|
+
canhoto init | doctor
|
|
203
|
+
canhoto parsers scaffold|test|enable|list
|
|
204
|
+
canhoto ingest <files...> [--pdf-password PASSWORD]
|
|
205
|
+
canhoto categorize rules --month YYYY-MM
|
|
206
|
+
canhoto categorize apply --file patches.json
|
|
207
|
+
canhoto categorize merchant --key KEY --category CAT
|
|
208
|
+
canhoto rules add|list|remove
|
|
209
|
+
canhoto review --month YYYY-MM [--cursor ID] [--limit N]
|
|
210
|
+
canhoto breakdown --month YYYY-MM
|
|
211
|
+
canhoto export pdf YYYY-MM [--profile canhoto|modern|minimal] [--output PATH]
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Develop
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
uv sync --extra dev
|
|
218
|
+
uv run pytest -q
|
|
219
|
+
uv run ruff check src/canhoto tests
|
|
220
|
+
uv run mypy -p canhoto
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Or use the [`justfile`](justfile) with [Just](https://just.systems/).
|
|
224
|
+
|
|
225
|
+
### Release
|
|
226
|
+
|
|
227
|
+
Pushing a version tag publishes to PyPI through
|
|
228
|
+
[`.github/workflows/release.yml`](.github/workflows/release.yml). The workflow
|
|
229
|
+
tests, builds, smoke-tests the wheel and the source distribution, and publishes
|
|
230
|
+
with PyPI Trusted Publishing. The tag must match the version in
|
|
231
|
+
`pyproject.toml`.
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
uv version --bump patch # or minor / major
|
|
235
|
+
git commit -am "chore: release v$(uv version --short)"
|
|
236
|
+
git tag -a "v$(uv version --short)" -m "v$(uv version --short)"
|
|
237
|
+
git push origin main --tags
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Privacy
|
|
241
|
+
|
|
242
|
+
Your statements and database stay in the data directory. Do not commit
|
|
243
|
+
statements, tokens, database files, or `~/.canhoto`. Back up `canhoto.db` before
|
|
244
|
+
you upgrade: it holds your ledger and your rules.
|