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.
Files changed (21) hide show
  1. statementproof-0.2.0/PKG-INFO +222 -0
  2. {statementproof-0.1.0 → statementproof-0.2.0}/pyproject.toml +7 -1
  3. statementproof-0.2.0/statementproof/README.md +197 -0
  4. {statementproof-0.1.0 → statementproof-0.2.0}/statementproof/__init__.py +1 -1
  5. {statementproof-0.1.0 → statementproof-0.2.0}/statementproof/cli.py +80 -1
  6. statementproof-0.2.0/statementproof/record.py +275 -0
  7. statementproof-0.2.0/statementproof.egg-info/PKG-INFO +222 -0
  8. {statementproof-0.1.0 → statementproof-0.2.0}/statementproof.egg-info/SOURCES.txt +1 -0
  9. statementproof-0.1.0/PKG-INFO +0 -117
  10. statementproof-0.1.0/statementproof/README.md +0 -96
  11. statementproof-0.1.0/statementproof.egg-info/PKG-INFO +0 -117
  12. {statementproof-0.1.0 → statementproof-0.2.0}/LICENSE +0 -0
  13. {statementproof-0.1.0 → statementproof-0.2.0}/README.md +0 -0
  14. {statementproof-0.1.0 → statementproof-0.2.0}/setup.cfg +0 -0
  15. {statementproof-0.1.0 → statementproof-0.2.0}/statementproof/__main__.py +0 -0
  16. {statementproof-0.1.0 → statementproof-0.2.0}/statementproof/extract.py +0 -0
  17. {statementproof-0.1.0 → statementproof-0.2.0}/statementproof/validate.py +0 -0
  18. {statementproof-0.1.0 → statementproof-0.2.0}/statementproof.egg-info/dependency_links.txt +0 -0
  19. {statementproof-0.1.0 → statementproof-0.2.0}/statementproof.egg-info/entry_points.txt +0 -0
  20. {statementproof-0.1.0 → statementproof-0.2.0}/statementproof.egg-info/requires.txt +0 -0
  21. {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.1.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.1.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)