statementproof 0.1.0__tar.gz → 0.2.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.
- statementproof-0.2.0/PKG-INFO +222 -0
- {statementproof-0.1.0 → statementproof-0.2.0}/pyproject.toml +7 -1
- statementproof-0.2.0/statementproof/README.md +197 -0
- {statementproof-0.1.0 → statementproof-0.2.0}/statementproof/__init__.py +1 -1
- {statementproof-0.1.0 → statementproof-0.2.0}/statementproof/cli.py +80 -1
- statementproof-0.2.0/statementproof/record.py +275 -0
- statementproof-0.2.0/statementproof.egg-info/PKG-INFO +222 -0
- {statementproof-0.1.0 → statementproof-0.2.0}/statementproof.egg-info/SOURCES.txt +1 -0
- statementproof-0.1.0/PKG-INFO +0 -117
- statementproof-0.1.0/statementproof/README.md +0 -96
- statementproof-0.1.0/statementproof.egg-info/PKG-INFO +0 -117
- {statementproof-0.1.0 → statementproof-0.2.0}/LICENSE +0 -0
- {statementproof-0.1.0 → statementproof-0.2.0}/README.md +0 -0
- {statementproof-0.1.0 → statementproof-0.2.0}/setup.cfg +0 -0
- {statementproof-0.1.0 → statementproof-0.2.0}/statementproof/__main__.py +0 -0
- {statementproof-0.1.0 → statementproof-0.2.0}/statementproof/extract.py +0 -0
- {statementproof-0.1.0 → statementproof-0.2.0}/statementproof/validate.py +0 -0
- {statementproof-0.1.0 → statementproof-0.2.0}/statementproof.egg-info/dependency_links.txt +0 -0
- {statementproof-0.1.0 → statementproof-0.2.0}/statementproof.egg-info/entry_points.txt +0 -0
- {statementproof-0.1.0 → statementproof-0.2.0}/statementproof.egg-info/requires.txt +0 -0
- {statementproof-0.1.0 → statementproof-0.2.0}/statementproof.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: statementproof
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Extract transactions from bank statement PDFs -- and prove the extraction is right, or say it isn't.
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/OrbitalKeyAi/statementproof
|
|
7
|
+
Project-URL: Documentation, https://orbitalkeyai.github.io/statementproof/
|
|
8
|
+
Project-URL: Source, https://github.com/OrbitalKeyAi/statementproof
|
|
9
|
+
Project-URL: Issues, https://github.com/OrbitalKeyAi/statementproof/issues
|
|
10
|
+
Keywords: bank-statement,pdf,csv,bookkeeping,accounting,reconciliation,extraction,converter,quickbooks,xero
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
13
|
+
Classifier: Topic :: Office/Business :: Financial :: Accounting
|
|
14
|
+
Classifier: Topic :: Text Processing :: Filters
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: pdfplumber>=0.10
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: reportlab>=4.0; extra == "dev"
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# statementproof
|
|
27
|
+
|
|
28
|
+
**Convert bank statement PDFs to CSV — and find out when the conversion is wrong.**
|
|
29
|
+
|
|
30
|
+
Most PDF-to-CSV converters hand you rows with no way to check them. The best of them
|
|
31
|
+
(monopoly) does check totals — but tells you only that *something* is wrong, somewhere. The expensive failure isn't a crash you notice — it's one debit read as a credit,
|
|
32
|
+
which reconciles to nothing and turns up weeks later inside a client's books.
|
|
33
|
+
|
|
34
|
+
A bank statement is one of the few documents that carries its own checksum: the balances.
|
|
35
|
+
statementproof uses them.
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
$ statementproof october.pdf
|
|
39
|
+
|
|
40
|
+
opening balance : 69.96
|
|
41
|
+
closing balance : 586.71
|
|
42
|
+
transactions : 21 extracted, 0 skipped
|
|
43
|
+
|
|
44
|
+
VERIFIED (21 transactions, checks: chain, aggregate)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
And when a row is misread:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
FAILED (21 transactions, checks: chain, aggregate)
|
|
51
|
+
[chain_break] row 14: running balance does not follow:
|
|
52
|
+
534.66 +37.07 should give 571.73, statement shows 497.59 (off by -74.14)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Install
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install statementproof
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Python 3.9+. One dependency (`pdfplumber`).
|
|
62
|
+
|
|
63
|
+
## Usage
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# check a statement and print the report
|
|
67
|
+
statementproof statement.pdf
|
|
68
|
+
|
|
69
|
+
# write the transactions to CSV
|
|
70
|
+
statementproof statement.pdf --csv transactions.csv
|
|
71
|
+
|
|
72
|
+
# layout report you can share — contains no financial data
|
|
73
|
+
statementproof statement.pdf --diagnostic
|
|
74
|
+
|
|
75
|
+
# a whole folder of statements, one summary
|
|
76
|
+
statementproof ./january-statements/
|
|
77
|
+
|
|
78
|
+
# write a reconciliation record you can file
|
|
79
|
+
statementproof ./january-statements/ --record january.html
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Exit code is `0` when verified, `1` otherwise, so it drops into a script. In batch mode a
|
|
83
|
+
file that could not be opened also exits non-zero — **unreadable is never counted as
|
|
84
|
+
passing.**
|
|
85
|
+
|
|
86
|
+
## The reconciliation record
|
|
87
|
+
|
|
88
|
+
`--record` writes a document recording *which exact file was checked, by whom, on what
|
|
89
|
+
date, and what the check found.* It opens in any browser and prints to PDF.
|
|
90
|
+
|
|
91
|
+
The load-bearing field is the **SHA-256 of the source PDF**. A record that names a file
|
|
92
|
+
proves nothing — filenames change and statements get re-exported. The hash binds the
|
|
93
|
+
record to one exact document, so if the PDF is later altered by a single byte, the record
|
|
94
|
+
visibly no longer describes the file it is attached to.
|
|
95
|
+
|
|
96
|
+
In batch mode the record also names every file that *could not* be read, in the printed
|
|
97
|
+
copy as well as the JSON. A record that quietly omits the statements it choked on would
|
|
98
|
+
assert a clean review of a folder it never finished reading.
|
|
99
|
+
|
|
100
|
+
## Three verdicts, and it will not bluff
|
|
101
|
+
|
|
102
|
+
| verdict | meaning |
|
|
103
|
+
|---|---|
|
|
104
|
+
| `VERIFIED` | the rows reproduce the statement's opening and closing balances, and each running balance follows from the last |
|
|
105
|
+
| `FAILED` | they don't — **with the row number, expected figure, and size of the gap** |
|
|
106
|
+
| `UNVERIFIED` | the statement carries no balances to check against. Output may be perfect; nothing proves it, so nothing is claimed |
|
|
107
|
+
|
|
108
|
+
A converter that returns a clean-looking CSV it cannot vouch for is the problem this
|
|
109
|
+
exists to solve, so `UNVERIFIED` is never dressed up as success.
|
|
110
|
+
|
|
111
|
+
## Your statement never leaves your machine
|
|
112
|
+
|
|
113
|
+
This tool asks you to point it at a document carrying your name, address, account
|
|
114
|
+
number, employer, and every merchant you used last month. "Nothing uploaded" can't be a
|
|
115
|
+
policy — it has to be a property of the code.
|
|
116
|
+
|
|
117
|
+
**No networking library is imported anywhere in this package**, and
|
|
118
|
+
[`tests/test_privacy.py`](statementproof/tests/test_privacy.py) parses every module's
|
|
119
|
+
AST and **fails the build if one ever is.** No file is written unless you name it.
|
|
120
|
+
|
|
121
|
+
The `--diagnostic` flag exists because a library of real statement *layouts* is what
|
|
122
|
+
would most improve this tool — and a layout can be described without describing anyone's
|
|
123
|
+
money. It reports column positions, column density, date-token **shapes** (`DD/DD`, not
|
|
124
|
+
`10/02`), and where extraction broke. It contains no amounts, balances, descriptions,
|
|
125
|
+
dates, account numbers, names, or even the filename. It prints to your screen so you can
|
|
126
|
+
read all of it before deciding whether to share it. The tool never sends it anywhere.
|
|
127
|
+
|
|
128
|
+
Those exclusions are asserted by tests against a known statement, not merely intended.
|
|
129
|
+
|
|
130
|
+
## How it works
|
|
131
|
+
|
|
132
|
+
**Geometry, not regex.** The most-cited converter failure is *"columns shift, debit and
|
|
133
|
+
credit values land in the wrong places."* That happens because `extract_text()` flattens
|
|
134
|
+
a two-dimensional page and discards the x-position — the only signal separating a debit
|
|
135
|
+
column from a credit column. statementproof reads each number's coordinates and clusters
|
|
136
|
+
the money columns by their right edges.
|
|
137
|
+
|
|
138
|
+
**Column roles from arithmetic, not headers.** Which column holds the running balance is
|
|
139
|
+
decided by behaviour, because header wording differs by bank and often vanishes on
|
|
140
|
+
continuation pages.
|
|
141
|
+
|
|
142
|
+
**Sign from the balance chain.** If the balance went down it was a debit, whether or not
|
|
143
|
+
a minus glyph survived extraction.
|
|
144
|
+
|
|
145
|
+
## Limitations — read these first
|
|
146
|
+
|
|
147
|
+
**Text-layer PDFs only.** A statement downloaded from your bank's website normally has a
|
|
148
|
+
text layer; a scan or a phone photo does not. Given one, statementproof says so rather
|
|
149
|
+
than inventing rows. **OCR is not built yet.**
|
|
150
|
+
|
|
151
|
+
**Arithmetic consistency, not correctness.** It cannot see an error that leaves the
|
|
152
|
+
totals intact:
|
|
153
|
+
|
|
154
|
+
- a page header picked up as a row with a `0.00` amount
|
|
155
|
+
- a wrong date or a truncated description
|
|
156
|
+
- two errors that cancel out
|
|
157
|
+
|
|
158
|
+
*"Provably arithmetically consistent"* is the honest claim. *"Provably correct"* is not,
|
|
159
|
+
and is not made here.
|
|
160
|
+
|
|
161
|
+
**It has not met your bank.** Seven layouts extract exactly — six written by the author,
|
|
162
|
+
one reproduced from a real bank's published specimen. That seventh immediately found
|
|
163
|
+
three bugs the other six could not, including `MM/DD` dates with no year, which took
|
|
164
|
+
extraction from 21/21 to 0/21 while the author's own suite still reported 6/6 passing.
|
|
165
|
+
**Passing tests you wrote yourself is worth very little.**
|
|
166
|
+
|
|
167
|
+
If it fails on your statement, run `--diagnostic` and
|
|
168
|
+
[open an issue](https://github.com/OrbitalKeyAi/statementproof/issues) with that output.
|
|
169
|
+
It contains no financial data, and each new layout makes the parser better for everyone.
|
|
170
|
+
|
|
171
|
+
## Tests
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
python statementproof/tests/make_statements.py # build synthetic corpus
|
|
175
|
+
python statementproof/tests/make_real_derived.py # build real-bank-derived layout
|
|
176
|
+
python statementproof/tests/score.py # score extraction vs ground truth
|
|
177
|
+
python statementproof/tests/test_failure_modes.py # validator behaviour
|
|
178
|
+
python statementproof/tests/test_privacy.py # privacy promises
|
|
179
|
+
python statementproof/tests/test_record.py # reconciliation record guarantees
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Current: **7/7 layouts extracted exactly · 9/9 failure-mode tests · 8/8 privacy checks ·
|
|
183
|
+
13/13 record guarantees · 0 false assurances.**
|
|
184
|
+
|
|
185
|
+
"False assurance" — reporting `VERIFIED` on a bad extraction — is the number that
|
|
186
|
+
matters. A validator that green-lights an error is worse than no validator.
|
|
187
|
+
|
|
188
|
+
## Commercial licence
|
|
189
|
+
|
|
190
|
+
The tool is free and MIT, and every figure it produces is real whether you pay or not.
|
|
191
|
+
|
|
192
|
+
What a licence changes is one thing: **it puts your firm's name on the record.** Without
|
|
193
|
+
it the record is stamped `UNLICENSED` — the numbers are still correct, but no one is named
|
|
194
|
+
as having performed the review, and an unsigned document is not evidence that anyone did.
|
|
195
|
+
|
|
196
|
+
**[Buy a licence — $39, one-time, perpetual](https://buy.stripe.com/4gM8wRe6NdON50s2kjgA802)**
|
|
197
|
+
· unlimited statements · unlimited machines · no subscription
|
|
198
|
+
|
|
199
|
+
Then set it once per machine:
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
setx STATEMENTPROOF_LICENCE "Your Firm Name" # Windows
|
|
203
|
+
export STATEMENTPROOF_LICENCE="Your Firm Name" # macOS / Linux
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
There is no key server, no activation call, and no check-in — the package still contains
|
|
207
|
+
no network code of any kind. The licence is an attestation, not a lock. Since the source
|
|
208
|
+
is public, any check could be deleted in seconds; pretending otherwise would be exactly
|
|
209
|
+
the kind of bluffing this tool exists to refuse. Editing the stamp out would produce a
|
|
210
|
+
record naming a licence holder who did not buy one, which is forging your own audit
|
|
211
|
+
evidence, and no payment system prevents that either.
|
|
212
|
+
|
|
213
|
+
## Related
|
|
214
|
+
|
|
215
|
+
**[sqliteproof](https://github.com/OrbitalKeyAi/sqliteproof)** — the same idea for SQLite
|
|
216
|
+
databases. `PRAGMA integrity_check` tells you a page number; sqliteproof tells you which
|
|
217
|
+
tables survived, how many rows were lost, and what is safe to export. Free, MIT, no
|
|
218
|
+
dependencies.
|
|
219
|
+
|
|
220
|
+
## License
|
|
221
|
+
|
|
222
|
+
MIT.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "statementproof"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Extract transactions from bank statement PDFs -- and prove the extraction is right, or say it isn't."
|
|
9
9
|
readme = "statementproof/README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -26,6 +26,12 @@ dependencies = [
|
|
|
26
26
|
"pdfplumber>=0.10",
|
|
27
27
|
]
|
|
28
28
|
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/OrbitalKeyAi/statementproof"
|
|
31
|
+
Documentation = "https://orbitalkeyai.github.io/statementproof/"
|
|
32
|
+
Source = "https://github.com/OrbitalKeyAi/statementproof"
|
|
33
|
+
Issues = "https://github.com/OrbitalKeyAi/statementproof/issues"
|
|
34
|
+
|
|
29
35
|
[project.optional-dependencies]
|
|
30
36
|
dev = ["reportlab>=4.0"]
|
|
31
37
|
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# statementproof
|
|
2
|
+
|
|
3
|
+
**Convert bank statement PDFs to CSV — and find out when the conversion is wrong.**
|
|
4
|
+
|
|
5
|
+
Most PDF-to-CSV converters hand you rows with no way to check them. The best of them
|
|
6
|
+
(monopoly) does check totals — but tells you only that *something* is wrong, somewhere. The expensive failure isn't a crash you notice — it's one debit read as a credit,
|
|
7
|
+
which reconciles to nothing and turns up weeks later inside a client's books.
|
|
8
|
+
|
|
9
|
+
A bank statement is one of the few documents that carries its own checksum: the balances.
|
|
10
|
+
statementproof uses them.
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
$ statementproof october.pdf
|
|
14
|
+
|
|
15
|
+
opening balance : 69.96
|
|
16
|
+
closing balance : 586.71
|
|
17
|
+
transactions : 21 extracted, 0 skipped
|
|
18
|
+
|
|
19
|
+
VERIFIED (21 transactions, checks: chain, aggregate)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
And when a row is misread:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
FAILED (21 transactions, checks: chain, aggregate)
|
|
26
|
+
[chain_break] row 14: running balance does not follow:
|
|
27
|
+
534.66 +37.07 should give 571.73, statement shows 497.59 (off by -74.14)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install statementproof
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Python 3.9+. One dependency (`pdfplumber`).
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# check a statement and print the report
|
|
42
|
+
statementproof statement.pdf
|
|
43
|
+
|
|
44
|
+
# write the transactions to CSV
|
|
45
|
+
statementproof statement.pdf --csv transactions.csv
|
|
46
|
+
|
|
47
|
+
# layout report you can share — contains no financial data
|
|
48
|
+
statementproof statement.pdf --diagnostic
|
|
49
|
+
|
|
50
|
+
# a whole folder of statements, one summary
|
|
51
|
+
statementproof ./january-statements/
|
|
52
|
+
|
|
53
|
+
# write a reconciliation record you can file
|
|
54
|
+
statementproof ./january-statements/ --record january.html
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Exit code is `0` when verified, `1` otherwise, so it drops into a script. In batch mode a
|
|
58
|
+
file that could not be opened also exits non-zero — **unreadable is never counted as
|
|
59
|
+
passing.**
|
|
60
|
+
|
|
61
|
+
## The reconciliation record
|
|
62
|
+
|
|
63
|
+
`--record` writes a document recording *which exact file was checked, by whom, on what
|
|
64
|
+
date, and what the check found.* It opens in any browser and prints to PDF.
|
|
65
|
+
|
|
66
|
+
The load-bearing field is the **SHA-256 of the source PDF**. A record that names a file
|
|
67
|
+
proves nothing — filenames change and statements get re-exported. The hash binds the
|
|
68
|
+
record to one exact document, so if the PDF is later altered by a single byte, the record
|
|
69
|
+
visibly no longer describes the file it is attached to.
|
|
70
|
+
|
|
71
|
+
In batch mode the record also names every file that *could not* be read, in the printed
|
|
72
|
+
copy as well as the JSON. A record that quietly omits the statements it choked on would
|
|
73
|
+
assert a clean review of a folder it never finished reading.
|
|
74
|
+
|
|
75
|
+
## Three verdicts, and it will not bluff
|
|
76
|
+
|
|
77
|
+
| verdict | meaning |
|
|
78
|
+
|---|---|
|
|
79
|
+
| `VERIFIED` | the rows reproduce the statement's opening and closing balances, and each running balance follows from the last |
|
|
80
|
+
| `FAILED` | they don't — **with the row number, expected figure, and size of the gap** |
|
|
81
|
+
| `UNVERIFIED` | the statement carries no balances to check against. Output may be perfect; nothing proves it, so nothing is claimed |
|
|
82
|
+
|
|
83
|
+
A converter that returns a clean-looking CSV it cannot vouch for is the problem this
|
|
84
|
+
exists to solve, so `UNVERIFIED` is never dressed up as success.
|
|
85
|
+
|
|
86
|
+
## Your statement never leaves your machine
|
|
87
|
+
|
|
88
|
+
This tool asks you to point it at a document carrying your name, address, account
|
|
89
|
+
number, employer, and every merchant you used last month. "Nothing uploaded" can't be a
|
|
90
|
+
policy — it has to be a property of the code.
|
|
91
|
+
|
|
92
|
+
**No networking library is imported anywhere in this package**, and
|
|
93
|
+
[`tests/test_privacy.py`](statementproof/tests/test_privacy.py) parses every module's
|
|
94
|
+
AST and **fails the build if one ever is.** No file is written unless you name it.
|
|
95
|
+
|
|
96
|
+
The `--diagnostic` flag exists because a library of real statement *layouts* is what
|
|
97
|
+
would most improve this tool — and a layout can be described without describing anyone's
|
|
98
|
+
money. It reports column positions, column density, date-token **shapes** (`DD/DD`, not
|
|
99
|
+
`10/02`), and where extraction broke. It contains no amounts, balances, descriptions,
|
|
100
|
+
dates, account numbers, names, or even the filename. It prints to your screen so you can
|
|
101
|
+
read all of it before deciding whether to share it. The tool never sends it anywhere.
|
|
102
|
+
|
|
103
|
+
Those exclusions are asserted by tests against a known statement, not merely intended.
|
|
104
|
+
|
|
105
|
+
## How it works
|
|
106
|
+
|
|
107
|
+
**Geometry, not regex.** The most-cited converter failure is *"columns shift, debit and
|
|
108
|
+
credit values land in the wrong places."* That happens because `extract_text()` flattens
|
|
109
|
+
a two-dimensional page and discards the x-position — the only signal separating a debit
|
|
110
|
+
column from a credit column. statementproof reads each number's coordinates and clusters
|
|
111
|
+
the money columns by their right edges.
|
|
112
|
+
|
|
113
|
+
**Column roles from arithmetic, not headers.** Which column holds the running balance is
|
|
114
|
+
decided by behaviour, because header wording differs by bank and often vanishes on
|
|
115
|
+
continuation pages.
|
|
116
|
+
|
|
117
|
+
**Sign from the balance chain.** If the balance went down it was a debit, whether or not
|
|
118
|
+
a minus glyph survived extraction.
|
|
119
|
+
|
|
120
|
+
## Limitations — read these first
|
|
121
|
+
|
|
122
|
+
**Text-layer PDFs only.** A statement downloaded from your bank's website normally has a
|
|
123
|
+
text layer; a scan or a phone photo does not. Given one, statementproof says so rather
|
|
124
|
+
than inventing rows. **OCR is not built yet.**
|
|
125
|
+
|
|
126
|
+
**Arithmetic consistency, not correctness.** It cannot see an error that leaves the
|
|
127
|
+
totals intact:
|
|
128
|
+
|
|
129
|
+
- a page header picked up as a row with a `0.00` amount
|
|
130
|
+
- a wrong date or a truncated description
|
|
131
|
+
- two errors that cancel out
|
|
132
|
+
|
|
133
|
+
*"Provably arithmetically consistent"* is the honest claim. *"Provably correct"* is not,
|
|
134
|
+
and is not made here.
|
|
135
|
+
|
|
136
|
+
**It has not met your bank.** Seven layouts extract exactly — six written by the author,
|
|
137
|
+
one reproduced from a real bank's published specimen. That seventh immediately found
|
|
138
|
+
three bugs the other six could not, including `MM/DD` dates with no year, which took
|
|
139
|
+
extraction from 21/21 to 0/21 while the author's own suite still reported 6/6 passing.
|
|
140
|
+
**Passing tests you wrote yourself is worth very little.**
|
|
141
|
+
|
|
142
|
+
If it fails on your statement, run `--diagnostic` and
|
|
143
|
+
[open an issue](https://github.com/OrbitalKeyAi/statementproof/issues) with that output.
|
|
144
|
+
It contains no financial data, and each new layout makes the parser better for everyone.
|
|
145
|
+
|
|
146
|
+
## Tests
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
python statementproof/tests/make_statements.py # build synthetic corpus
|
|
150
|
+
python statementproof/tests/make_real_derived.py # build real-bank-derived layout
|
|
151
|
+
python statementproof/tests/score.py # score extraction vs ground truth
|
|
152
|
+
python statementproof/tests/test_failure_modes.py # validator behaviour
|
|
153
|
+
python statementproof/tests/test_privacy.py # privacy promises
|
|
154
|
+
python statementproof/tests/test_record.py # reconciliation record guarantees
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Current: **7/7 layouts extracted exactly · 9/9 failure-mode tests · 8/8 privacy checks ·
|
|
158
|
+
13/13 record guarantees · 0 false assurances.**
|
|
159
|
+
|
|
160
|
+
"False assurance" — reporting `VERIFIED` on a bad extraction — is the number that
|
|
161
|
+
matters. A validator that green-lights an error is worse than no validator.
|
|
162
|
+
|
|
163
|
+
## Commercial licence
|
|
164
|
+
|
|
165
|
+
The tool is free and MIT, and every figure it produces is real whether you pay or not.
|
|
166
|
+
|
|
167
|
+
What a licence changes is one thing: **it puts your firm's name on the record.** Without
|
|
168
|
+
it the record is stamped `UNLICENSED` — the numbers are still correct, but no one is named
|
|
169
|
+
as having performed the review, and an unsigned document is not evidence that anyone did.
|
|
170
|
+
|
|
171
|
+
**[Buy a licence — $39, one-time, perpetual](https://buy.stripe.com/4gM8wRe6NdON50s2kjgA802)**
|
|
172
|
+
· unlimited statements · unlimited machines · no subscription
|
|
173
|
+
|
|
174
|
+
Then set it once per machine:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
setx STATEMENTPROOF_LICENCE "Your Firm Name" # Windows
|
|
178
|
+
export STATEMENTPROOF_LICENCE="Your Firm Name" # macOS / Linux
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
There is no key server, no activation call, and no check-in — the package still contains
|
|
182
|
+
no network code of any kind. The licence is an attestation, not a lock. Since the source
|
|
183
|
+
is public, any check could be deleted in seconds; pretending otherwise would be exactly
|
|
184
|
+
the kind of bluffing this tool exists to refuse. Editing the stamp out would produce a
|
|
185
|
+
record naming a licence holder who did not buy one, which is forging your own audit
|
|
186
|
+
evidence, and no payment system prevents that either.
|
|
187
|
+
|
|
188
|
+
## Related
|
|
189
|
+
|
|
190
|
+
**[sqliteproof](https://github.com/OrbitalKeyAi/sqliteproof)** — the same idea for SQLite
|
|
191
|
+
databases. `PRAGMA integrity_check` tells you a page number; sqliteproof tells you which
|
|
192
|
+
tables survived, how many rows were lost, and what is safe to export. Free, MIT, no
|
|
193
|
+
dependencies.
|
|
194
|
+
|
|
195
|
+
## License
|
|
196
|
+
|
|
197
|
+
MIT.
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""statementproof - verifiable bank statement extraction."""
|
|
2
|
-
__version__ = "0.
|
|
2
|
+
__version__ = "0.2.0"
|
|
@@ -99,17 +99,85 @@ def build_diagnostic(pdf_path, txns, diag, opening, closing, res):
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
|
|
102
|
+
def run_batch(args):
|
|
103
|
+
"""Validate every PDF in a folder, then emit one record covering all of them.
|
|
104
|
+
|
|
105
|
+
A firm processes many clients' statements a month, and the free single-file path makes
|
|
106
|
+
that a hundred terminal invocations nobody keeps a trace of. What matters here is that
|
|
107
|
+
**a file that fails to open is reported, not skipped silently** -- a batch summary that
|
|
108
|
+
quietly omits the statements it choked on is precisely the false assurance this whole
|
|
109
|
+
tool exists to refuse.
|
|
110
|
+
"""
|
|
111
|
+
from . import record as _rec
|
|
112
|
+
|
|
113
|
+
pdfs = sorted(os.path.join(args.pdf, f) for f in os.listdir(args.pdf)
|
|
114
|
+
if f.lower().endswith(".pdf"))
|
|
115
|
+
if not pdfs:
|
|
116
|
+
print("error: no PDFs in %s" % args.pdf, file=sys.stderr)
|
|
117
|
+
return 2
|
|
118
|
+
|
|
119
|
+
if not args.quiet:
|
|
120
|
+
print(BANNER)
|
|
121
|
+
print(" %d statement(s) in %s\n" % (len(pdfs), args.pdf))
|
|
122
|
+
|
|
123
|
+
records, failed_to_read = [], []
|
|
124
|
+
for p in pdfs:
|
|
125
|
+
name = os.path.basename(p)
|
|
126
|
+
try:
|
|
127
|
+
opening, closing = find_balances(p)
|
|
128
|
+
txns, diag = parse(p, balance_hints=(opening, closing))
|
|
129
|
+
res = validate(txns, opening, closing)
|
|
130
|
+
except Exception as e:
|
|
131
|
+
failed_to_read.append((name, "%s: %s" % (type(e).__name__, e)))
|
|
132
|
+
if not args.quiet:
|
|
133
|
+
print(" %-38s COULD NOT READ (%s)" % (name[:38], type(e).__name__))
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
records.append(_rec.build(p, txns, res, opening, closing, diag))
|
|
137
|
+
if not args.quiet:
|
|
138
|
+
print(" %-38s %-13s %3d txns" % (name[:38], res.verdict, len(txns)))
|
|
139
|
+
|
|
140
|
+
batch = _rec.build_batch(records)
|
|
141
|
+
if failed_to_read:
|
|
142
|
+
batch["summary"]["unreadable"] = [{"file": n, "error": e} for n, e in failed_to_read]
|
|
143
|
+
|
|
144
|
+
if not args.quiet:
|
|
145
|
+
print()
|
|
146
|
+
for verdict, n in batch["summary"]["by_verdict"].items():
|
|
147
|
+
print(" %-13s %d" % (verdict, n))
|
|
148
|
+
if failed_to_read:
|
|
149
|
+
print(" %-13s %d <-- not validated; they are not counted as passing"
|
|
150
|
+
% ("UNREADABLE", len(failed_to_read)))
|
|
151
|
+
|
|
152
|
+
if args.record:
|
|
153
|
+
_rec.write(batch, args.record)
|
|
154
|
+
if not args.quiet:
|
|
155
|
+
print("\n wrote batch reconciliation record to %s" % args.record)
|
|
156
|
+
if batch["licence"]["status"] != "licensed":
|
|
157
|
+
print(" (UNLICENSED -- set %s to name the reviewer.)" % _rec.LICENCE_ENV)
|
|
158
|
+
|
|
159
|
+
# Non-zero if anything failed or could not be read, so it drops into a firm's scripts.
|
|
160
|
+
bad = batch["summary"]["by_verdict"].get("FAILED", 0) + len(failed_to_read)
|
|
161
|
+
return 1 if bad else 0
|
|
162
|
+
|
|
163
|
+
|
|
102
164
|
def main(argv=None):
|
|
103
165
|
ap = argparse.ArgumentParser(
|
|
104
166
|
prog="statementproof",
|
|
105
167
|
description="Validate bank statement PDF extraction. Runs locally; uploads nothing.")
|
|
106
|
-
ap.add_argument("pdf", help="path to a bank statement PDF")
|
|
168
|
+
ap.add_argument("pdf", help="path to a bank statement PDF, or a folder of them")
|
|
107
169
|
ap.add_argument("--csv", metavar="OUT", help="write extracted transactions to CSV")
|
|
170
|
+
ap.add_argument("--record", metavar="OUT",
|
|
171
|
+
help="write a reconciliation record (.html or .json) -- the artifact "
|
|
172
|
+
"recording which file was checked, by whom, and what was found")
|
|
108
173
|
ap.add_argument("--diagnostic", action="store_true",
|
|
109
174
|
help="print a shareable layout report containing no financial data")
|
|
110
175
|
ap.add_argument("--quiet", action="store_true", help="verdict line only")
|
|
111
176
|
args = ap.parse_args(argv)
|
|
112
177
|
|
|
178
|
+
if os.path.isdir(args.pdf):
|
|
179
|
+
return run_batch(args)
|
|
180
|
+
|
|
113
181
|
if not os.path.isfile(args.pdf):
|
|
114
182
|
print("error: no such file: %s" % args.pdf, file=sys.stderr)
|
|
115
183
|
return 2
|
|
@@ -173,6 +241,17 @@ def main(argv=None):
|
|
|
173
241
|
if not args.quiet:
|
|
174
242
|
print("\n wrote %d rows to %s" % (len(txns), args.csv))
|
|
175
243
|
|
|
244
|
+
if args.record:
|
|
245
|
+
from . import record as _rec
|
|
246
|
+
r = _rec.build(args.pdf, txns, res, opening, closing, diag)
|
|
247
|
+
_rec.write(r, args.record)
|
|
248
|
+
if not args.quiet:
|
|
249
|
+
print("\n wrote reconciliation record to %s" % args.record)
|
|
250
|
+
if r["licence"]["status"] != "licensed":
|
|
251
|
+
print(" (UNLICENSED -- the figures are real, but no licence holder is named,")
|
|
252
|
+
print(" so it does not record who performed the review. Set %s.)"
|
|
253
|
+
% _rec.LICENCE_ENV)
|
|
254
|
+
|
|
176
255
|
if args.diagnostic:
|
|
177
256
|
d = build_diagnostic(args.pdf, txns, diag, opening, closing, res)
|
|
178
257
|
print("\n" + "=" * 68)
|