bankstatementparser 0.0.9__tar.gz → 0.0.10__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.
- {bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/PKG-INFO +46 -12
- {bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/README.md +45 -11
- {bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/_llm_common.py +1 -0
- {bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/additional_parsers.py +4 -0
- {bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/api.py +6 -3
- {bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/cli.py +11 -10
- {bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/enrichment/categorizer.py +9 -3
- {bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/hybrid/evaluation.py +6 -0
- {bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/hybrid/llm_extractor.py +6 -3
- {bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/hybrid/ollama_direct.py +1 -1
- {bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/hybrid/orchestrator.py +22 -3
- {bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/hybrid/pdf_text.py +7 -5
- {bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/hybrid/verification.py +35 -16
- {bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/hybrid/vision.py +7 -5
- {bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/parallel.py +1 -1
- {bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/transaction_deduplicator.py +11 -0
- {bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/transaction_models.py +4 -0
- {bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/pyproject.toml +25 -3
- {bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/LICENSE +0 -0
- {bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/__init__.py +0 -0
- {bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/_amounts.py +0 -0
- {bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/bank_statement_parsers.py +0 -0
- {bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/base_parser.py +0 -0
- {bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/camt_parser.py +0 -0
- {bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/enrichment/__init__.py +0 -0
- {bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/enrichment/account_mapper.py +0 -0
- {bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/exceptions.py +0 -0
- {bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/export/__init__.py +0 -0
- {bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/export/ledger.py +0 -0
- {bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/hybrid/__init__.py +0 -0
- {bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/hybrid/prompts.py +0 -0
- {bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/hybrid/scanner.py +0 -0
- {bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/input_validator.py +0 -0
- {bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/pain001_parser.py +0 -0
- {bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/record_types.py +0 -0
- {bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/zip_security.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bankstatementparser
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.10
|
|
4
4
|
Summary: BankStatementParser is your essential tool for easy bank statement management. Designed with finance and treasury experts in mind, it offers a simple way to handle CAMT (ISO 20022) formats and more. Get quick, accurate insights from your financial data and spend less time on processing. It's the smart, hassle-free way to stay on top of your transactions.
|
|
5
5
|
License: Apache Software License
|
|
6
6
|
License-File: LICENSE
|
|
@@ -49,7 +49,7 @@ Description-Content-Type: text/markdown
|
|
|
49
49
|
|
|
50
50
|
<p align="center">
|
|
51
51
|
<a href="https://github.com/sebastienrousseau/bankstatementparser/actions"><img src="https://img.shields.io/github/actions/workflow/status/sebastienrousseau/bankstatementparser/quality-gates.yml?style=for-the-badge&logo=github" alt="Build" /></a>
|
|
52
|
-
<a href="https://pypi.org/project/bankstatementparser/"><img src="https://img.shields.io/pypi/pyversions/bankstatementparser.svg?style=for-the-badge&v=0.0.
|
|
52
|
+
<a href="https://pypi.org/project/bankstatementparser/"><img src="https://img.shields.io/pypi/pyversions/bankstatementparser.svg?style=for-the-badge&v=0.0.10" alt="PyPI" /></a>
|
|
53
53
|
<a href="https://pypi.org/project/bankstatementparser/"><img src="https://img.shields.io/pypi/dm/bankstatementparser.svg?style=for-the-badge" alt="PyPI Downloads" /></a>
|
|
54
54
|
<a href="https://codecov.io/github/sebastienrousseau/bankstatementparser?branch=main"><img src="https://img.shields.io/codecov/c/github/sebastienrousseau/bankstatementparser?style=for-the-badge" alt="Codecov" /></a>
|
|
55
55
|
<a href="LICENSE"><img src="https://img.shields.io/github/license/sebastienrousseau/bankstatementparser?style=for-the-badge" alt="License" /></a>
|
|
@@ -93,8 +93,9 @@ services unless they explicitly opt in.
|
|
|
93
93
|
- [Command Line](#command-line) — console script and REST API
|
|
94
94
|
- [Deduplication](#deduplication) — exact hashes + suspected matches
|
|
95
95
|
- [Export](#export) — CSV, JSON, Excel, Polars, hledger, beancount
|
|
96
|
-
- [Examples](#examples) —
|
|
96
|
+
- [Examples](#examples) — 23 runnable scripts (14 deterministic + 9 hybrid)
|
|
97
97
|
- [XML Tag Mapping](#xml-tag-mapping) — ISO 20022 tags to DataFrame columns
|
|
98
|
+
- [Ecosystem](#ecosystem) — companion packages (MCP, LSP, writers, loaders)
|
|
98
99
|
|
|
99
100
|
**Operational**
|
|
100
101
|
|
|
@@ -230,7 +231,7 @@ print(result.source_method) # "deterministic"
|
|
|
230
231
|
# Path B — text-LLM for digital PDFs (set BSP_HYBRID_MODEL=ollama/llama3)
|
|
231
232
|
result = smart_ingest("statement.pdf")
|
|
232
233
|
print(result.source_method) # "llm"
|
|
233
|
-
print(result.verification.status) # VERIFIED | DISCREPANCY | FAILED
|
|
234
|
+
print(result.verification.status) # VERIFIED | DISCREPANCY | UNVERIFIABLE | FAILED
|
|
234
235
|
|
|
235
236
|
# Path C — multimodal vision for scanned PDFs (set BSP_HYBRID_VISION_MODEL)
|
|
236
237
|
# auto-routed when pypdf cannot extract enough text
|
|
@@ -311,7 +312,7 @@ flowchart TD
|
|
|
311
312
|
Z --> V[verify_balance<br/>Golden Rule]
|
|
312
313
|
Y --> V
|
|
313
314
|
X --> V
|
|
314
|
-
V --> R[VERIFIED / DISCREPANCY / FAILED]
|
|
315
|
+
V --> R[VERIFIED / DISCREPANCY / UNVERIFIABLE / FAILED]
|
|
315
316
|
```
|
|
316
317
|
|
|
317
318
|
Every extracted row carries an immutable `transaction_hash`, an
|
|
@@ -349,7 +350,7 @@ for the full surface.
|
|
|
349
350
|
|
|
350
351
|
| Feature | Description |
|
|
351
352
|
|---|---|
|
|
352
|
-
| **Golden Rule verification** *(v0.0.5)* | Every result carries `opening + credits − debits == closing` status: `VERIFIED`, `DISCREPANCY`, or `FAILED`. |
|
|
353
|
+
| **Golden Rule verification** *(v0.0.5)* | Every result carries `opening + credits − debits == closing` status: `VERIFIED`, `DISCREPANCY`, `UNVERIFIABLE`, or `FAILED`. |
|
|
353
354
|
| **Multi-currency verification** *(v0.0.8)* | `verify_balance_multi_currency()` groups transactions by currency and runs the Golden Rule independently per group — no more false `DISCREPANCY` on multi-currency statements. |
|
|
354
355
|
| **Idempotent dedup** *(v0.0.5)* | Every `Transaction` carries a stable `transaction_hash` (MD5 of date + normalized description + amount). `Deduplicator.dedupe_by_hash()` makes incremental ingestion safe to re-run. |
|
|
355
356
|
| **Interactive review** *(v0.0.6)* | `--type review` CLI walks through discrepancies with accept/edit/skip/delete/quit. `IngestResult.to_json()` / `.from_json()` for stable round-trip with embedded audit trail. |
|
|
@@ -370,7 +371,7 @@ for the full surface.
|
|
|
370
371
|
|---|---|
|
|
371
372
|
| **PII redaction** | Names, IBANs, and addresses masked by default — opt in with `--show-pii` |
|
|
372
373
|
| **Secure ZIP** | `iter_secure_xml_entries()` rejects zip bombs, encrypted entries, and suspicious compression ratios |
|
|
373
|
-
| **Tested** |
|
|
374
|
+
| **Tested** | 844 tests, coverage gated at 100% in CI, property-based fuzzing with Hypothesis |
|
|
374
375
|
|
|
375
376
|
---
|
|
376
377
|
|
|
@@ -631,8 +632,8 @@ for currency, v in results.items():
|
|
|
631
632
|
|
|
632
633
|
## Examples
|
|
633
634
|
|
|
634
|
-
See [`examples/`](examples/README.md) for
|
|
635
|
-
(14 deterministic +
|
|
635
|
+
See [`examples/`](examples/README.md) for 23 runnable scripts
|
|
636
|
+
(14 deterministic + 9 hybrid):
|
|
636
637
|
|
|
637
638
|
### Deterministic parsers
|
|
638
639
|
|
|
@@ -661,9 +662,11 @@ See [`examples/`](examples/README.md) for 22 runnable scripts
|
|
|
661
662
|
| `hybrid/01_smart_ingest_deterministic.py` | Path A — `smart_ingest()` against a CAMT.053 fixture, $0 cost |
|
|
662
663
|
| `hybrid/02_smart_ingest_text_llm.py` | Path B — text-LLM extraction from a digital PDF (mock or live Ollama) |
|
|
663
664
|
| `hybrid/03_smart_ingest_vision.py` | Path C — multimodal vision extraction with `LOW_TEXT_DENSITY` auto-routing |
|
|
664
|
-
| `hybrid/04_golden_rule.py` |
|
|
665
|
+
| `hybrid/04_golden_rule.py` | `verify_balance()`, `verify_transactions()`, and `verify_continuity()` across `VERIFIED` / `DISCREPANCY` / `UNVERIFIABLE` outcomes |
|
|
665
666
|
| `hybrid/05_dedupe_recurring.py` | `normalize_description()` + `dedupe_by_hash()` for idempotent batching |
|
|
666
667
|
| `hybrid/06_cli_walkthrough.sh` | Four flavours of the new `--type ingest` CLI subcommand |
|
|
668
|
+
| `hybrid/06_cli_walkthrough.ps1` | PowerShell sibling of the bash walkthrough (native Windows) |
|
|
669
|
+
| `hybrid/07_scan_and_ingest.py` | Bulk directory ingest with `scan_and_ingest()` — cross-file dedup + continuity check |
|
|
667
670
|
|
|
668
671
|
See [`examples/hybrid/README.md`](examples/hybrid/README.md) for
|
|
669
672
|
the full walkthrough including a Mermaid flow diagram, the
|
|
@@ -681,6 +684,37 @@ reconciliation pipelines.
|
|
|
681
684
|
|
|
682
685
|
---
|
|
683
686
|
|
|
687
|
+
## Ecosystem
|
|
688
|
+
|
|
689
|
+
`bankstatementparser` is the core engine. Optional, independently
|
|
690
|
+
versioned companion packages build on it — each is its own repository
|
|
691
|
+
and Python package, so you install only what you need and the core
|
|
692
|
+
stays dependency-light.
|
|
693
|
+
|
|
694
|
+
| Package | PyPI | Role | What it adds |
|
|
695
|
+
|---|---|---|---|
|
|
696
|
+
| [`bankstatementparser-mcp`](https://github.com/sebastienrousseau/bankstatementparser-mcp) | [](https://pypi.org/project/bankstatementparser-mcp/) | AI agents | [Model Context Protocol](https://modelcontextprotocol.io) server — exposes detect / parse / validate / summarize as tools for Claude Desktop and other LLM clients |
|
|
697
|
+
| [`bankstatementparser-lsp`](https://github.com/sebastienrousseau/bankstatementparser-lsp) | [](https://pypi.org/project/bankstatementparser-lsp/) | Editors | Language Server with live MT940 diagnostics (missing tags, malformed balance / `:61:` lines) over [pygls](https://github.com/openlawlibrary/pygls) |
|
|
698
|
+
| [`bankstatementparser-writer-xlsx`](https://github.com/sebastienrousseau/bankstatementparser-writer-xlsx) | [](https://pypi.org/project/bankstatementparser-writer-xlsx/) | Output | Write parsed transactions (DataFrame, `Transaction` list, or dicts) to a polished Excel `.xlsx` workbook |
|
|
699
|
+
| [`bankstatementparser-loader-mt942`](https://github.com/sebastienrousseau/bankstatementparser-loader-mt942) | [](https://pypi.org/project/bankstatementparser-loader-mt942/) | Input | Parse SWIFT **MT942** interim transaction reports into `Transaction` objects (a format the core does not read) |
|
|
700
|
+
| [`bankstatementparser-loader-bai2`](https://github.com/sebastienrousseau/bankstatementparser-loader-bai2) | [](https://pypi.org/project/bankstatementparser-loader-bai2/) | Input | Parse **BAI2** cash-management files into `Transaction` objects (a format the core does not read) |
|
|
701
|
+
|
|
702
|
+
Loaders turn an additional source format into the same unified
|
|
703
|
+
`Transaction` model; writers take parsed data back out to another
|
|
704
|
+
target. Each companion pins `bankstatementparser` as a dependency and
|
|
705
|
+
ships its own 100%-coverage test suite.
|
|
706
|
+
|
|
707
|
+
```bash
|
|
708
|
+
# Mix and match: e.g. read an MT942 file, then export to Excel
|
|
709
|
+
pip install bankstatementparser-loader-mt942 bankstatementparser-writer-xlsx
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
Loaders hand back the same `Transaction` model the core parsers
|
|
713
|
+
produce, so `load_mt942_file(...)` then `write_xlsx(...)` composes
|
|
714
|
+
cleanly — see each companion's README for runnable examples.
|
|
715
|
+
|
|
716
|
+
---
|
|
717
|
+
|
|
684
718
|
## Project Layout
|
|
685
719
|
|
|
686
720
|
```text
|
|
@@ -690,9 +724,9 @@ bankstatementparser/enrichment/ Categorizer, AccountMapper, EnrichedTransaction
|
|
|
690
724
|
bankstatementparser/export/ hledger + beancount journal export
|
|
691
725
|
bankstatementparser/api.py REST API microservice (FastAPI)
|
|
692
726
|
docs/compliance/ ISO 13485 validation, risk register, traceability matrix
|
|
693
|
-
examples/ 14 deterministic +
|
|
727
|
+
examples/ 14 deterministic + 9 hybrid runnable example scripts
|
|
694
728
|
scripts/ SBOM generation, checksums, signature verification
|
|
695
|
-
tests/
|
|
729
|
+
tests/ 844 tests (unit, integration, property-based, security, hybrid mocks)
|
|
696
730
|
```
|
|
697
731
|
|
|
698
732
|
---
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
12
|
<a href="https://github.com/sebastienrousseau/bankstatementparser/actions"><img src="https://img.shields.io/github/actions/workflow/status/sebastienrousseau/bankstatementparser/quality-gates.yml?style=for-the-badge&logo=github" alt="Build" /></a>
|
|
13
|
-
<a href="https://pypi.org/project/bankstatementparser/"><img src="https://img.shields.io/pypi/pyversions/bankstatementparser.svg?style=for-the-badge&v=0.0.
|
|
13
|
+
<a href="https://pypi.org/project/bankstatementparser/"><img src="https://img.shields.io/pypi/pyversions/bankstatementparser.svg?style=for-the-badge&v=0.0.10" alt="PyPI" /></a>
|
|
14
14
|
<a href="https://pypi.org/project/bankstatementparser/"><img src="https://img.shields.io/pypi/dm/bankstatementparser.svg?style=for-the-badge" alt="PyPI Downloads" /></a>
|
|
15
15
|
<a href="https://codecov.io/github/sebastienrousseau/bankstatementparser?branch=main"><img src="https://img.shields.io/codecov/c/github/sebastienrousseau/bankstatementparser?style=for-the-badge" alt="Codecov" /></a>
|
|
16
16
|
<a href="LICENSE"><img src="https://img.shields.io/github/license/sebastienrousseau/bankstatementparser?style=for-the-badge" alt="License" /></a>
|
|
@@ -54,8 +54,9 @@ services unless they explicitly opt in.
|
|
|
54
54
|
- [Command Line](#command-line) — console script and REST API
|
|
55
55
|
- [Deduplication](#deduplication) — exact hashes + suspected matches
|
|
56
56
|
- [Export](#export) — CSV, JSON, Excel, Polars, hledger, beancount
|
|
57
|
-
- [Examples](#examples) —
|
|
57
|
+
- [Examples](#examples) — 23 runnable scripts (14 deterministic + 9 hybrid)
|
|
58
58
|
- [XML Tag Mapping](#xml-tag-mapping) — ISO 20022 tags to DataFrame columns
|
|
59
|
+
- [Ecosystem](#ecosystem) — companion packages (MCP, LSP, writers, loaders)
|
|
59
60
|
|
|
60
61
|
**Operational**
|
|
61
62
|
|
|
@@ -191,7 +192,7 @@ print(result.source_method) # "deterministic"
|
|
|
191
192
|
# Path B — text-LLM for digital PDFs (set BSP_HYBRID_MODEL=ollama/llama3)
|
|
192
193
|
result = smart_ingest("statement.pdf")
|
|
193
194
|
print(result.source_method) # "llm"
|
|
194
|
-
print(result.verification.status) # VERIFIED | DISCREPANCY | FAILED
|
|
195
|
+
print(result.verification.status) # VERIFIED | DISCREPANCY | UNVERIFIABLE | FAILED
|
|
195
196
|
|
|
196
197
|
# Path C — multimodal vision for scanned PDFs (set BSP_HYBRID_VISION_MODEL)
|
|
197
198
|
# auto-routed when pypdf cannot extract enough text
|
|
@@ -272,7 +273,7 @@ flowchart TD
|
|
|
272
273
|
Z --> V[verify_balance<br/>Golden Rule]
|
|
273
274
|
Y --> V
|
|
274
275
|
X --> V
|
|
275
|
-
V --> R[VERIFIED / DISCREPANCY / FAILED]
|
|
276
|
+
V --> R[VERIFIED / DISCREPANCY / UNVERIFIABLE / FAILED]
|
|
276
277
|
```
|
|
277
278
|
|
|
278
279
|
Every extracted row carries an immutable `transaction_hash`, an
|
|
@@ -310,7 +311,7 @@ for the full surface.
|
|
|
310
311
|
|
|
311
312
|
| Feature | Description |
|
|
312
313
|
|---|---|
|
|
313
|
-
| **Golden Rule verification** *(v0.0.5)* | Every result carries `opening + credits − debits == closing` status: `VERIFIED`, `DISCREPANCY`, or `FAILED`. |
|
|
314
|
+
| **Golden Rule verification** *(v0.0.5)* | Every result carries `opening + credits − debits == closing` status: `VERIFIED`, `DISCREPANCY`, `UNVERIFIABLE`, or `FAILED`. |
|
|
314
315
|
| **Multi-currency verification** *(v0.0.8)* | `verify_balance_multi_currency()` groups transactions by currency and runs the Golden Rule independently per group — no more false `DISCREPANCY` on multi-currency statements. |
|
|
315
316
|
| **Idempotent dedup** *(v0.0.5)* | Every `Transaction` carries a stable `transaction_hash` (MD5 of date + normalized description + amount). `Deduplicator.dedupe_by_hash()` makes incremental ingestion safe to re-run. |
|
|
316
317
|
| **Interactive review** *(v0.0.6)* | `--type review` CLI walks through discrepancies with accept/edit/skip/delete/quit. `IngestResult.to_json()` / `.from_json()` for stable round-trip with embedded audit trail. |
|
|
@@ -331,7 +332,7 @@ for the full surface.
|
|
|
331
332
|
|---|---|
|
|
332
333
|
| **PII redaction** | Names, IBANs, and addresses masked by default — opt in with `--show-pii` |
|
|
333
334
|
| **Secure ZIP** | `iter_secure_xml_entries()` rejects zip bombs, encrypted entries, and suspicious compression ratios |
|
|
334
|
-
| **Tested** |
|
|
335
|
+
| **Tested** | 844 tests, coverage gated at 100% in CI, property-based fuzzing with Hypothesis |
|
|
335
336
|
|
|
336
337
|
---
|
|
337
338
|
|
|
@@ -592,8 +593,8 @@ for currency, v in results.items():
|
|
|
592
593
|
|
|
593
594
|
## Examples
|
|
594
595
|
|
|
595
|
-
See [`examples/`](examples/README.md) for
|
|
596
|
-
(14 deterministic +
|
|
596
|
+
See [`examples/`](examples/README.md) for 23 runnable scripts
|
|
597
|
+
(14 deterministic + 9 hybrid):
|
|
597
598
|
|
|
598
599
|
### Deterministic parsers
|
|
599
600
|
|
|
@@ -622,9 +623,11 @@ See [`examples/`](examples/README.md) for 22 runnable scripts
|
|
|
622
623
|
| `hybrid/01_smart_ingest_deterministic.py` | Path A — `smart_ingest()` against a CAMT.053 fixture, $0 cost |
|
|
623
624
|
| `hybrid/02_smart_ingest_text_llm.py` | Path B — text-LLM extraction from a digital PDF (mock or live Ollama) |
|
|
624
625
|
| `hybrid/03_smart_ingest_vision.py` | Path C — multimodal vision extraction with `LOW_TEXT_DENSITY` auto-routing |
|
|
625
|
-
| `hybrid/04_golden_rule.py` |
|
|
626
|
+
| `hybrid/04_golden_rule.py` | `verify_balance()`, `verify_transactions()`, and `verify_continuity()` across `VERIFIED` / `DISCREPANCY` / `UNVERIFIABLE` outcomes |
|
|
626
627
|
| `hybrid/05_dedupe_recurring.py` | `normalize_description()` + `dedupe_by_hash()` for idempotent batching |
|
|
627
628
|
| `hybrid/06_cli_walkthrough.sh` | Four flavours of the new `--type ingest` CLI subcommand |
|
|
629
|
+
| `hybrid/06_cli_walkthrough.ps1` | PowerShell sibling of the bash walkthrough (native Windows) |
|
|
630
|
+
| `hybrid/07_scan_and_ingest.py` | Bulk directory ingest with `scan_and_ingest()` — cross-file dedup + continuity check |
|
|
628
631
|
|
|
629
632
|
See [`examples/hybrid/README.md`](examples/hybrid/README.md) for
|
|
630
633
|
the full walkthrough including a Mermaid flow diagram, the
|
|
@@ -642,6 +645,37 @@ reconciliation pipelines.
|
|
|
642
645
|
|
|
643
646
|
---
|
|
644
647
|
|
|
648
|
+
## Ecosystem
|
|
649
|
+
|
|
650
|
+
`bankstatementparser` is the core engine. Optional, independently
|
|
651
|
+
versioned companion packages build on it — each is its own repository
|
|
652
|
+
and Python package, so you install only what you need and the core
|
|
653
|
+
stays dependency-light.
|
|
654
|
+
|
|
655
|
+
| Package | PyPI | Role | What it adds |
|
|
656
|
+
|---|---|---|---|
|
|
657
|
+
| [`bankstatementparser-mcp`](https://github.com/sebastienrousseau/bankstatementparser-mcp) | [](https://pypi.org/project/bankstatementparser-mcp/) | AI agents | [Model Context Protocol](https://modelcontextprotocol.io) server — exposes detect / parse / validate / summarize as tools for Claude Desktop and other LLM clients |
|
|
658
|
+
| [`bankstatementparser-lsp`](https://github.com/sebastienrousseau/bankstatementparser-lsp) | [](https://pypi.org/project/bankstatementparser-lsp/) | Editors | Language Server with live MT940 diagnostics (missing tags, malformed balance / `:61:` lines) over [pygls](https://github.com/openlawlibrary/pygls) |
|
|
659
|
+
| [`bankstatementparser-writer-xlsx`](https://github.com/sebastienrousseau/bankstatementparser-writer-xlsx) | [](https://pypi.org/project/bankstatementparser-writer-xlsx/) | Output | Write parsed transactions (DataFrame, `Transaction` list, or dicts) to a polished Excel `.xlsx` workbook |
|
|
660
|
+
| [`bankstatementparser-loader-mt942`](https://github.com/sebastienrousseau/bankstatementparser-loader-mt942) | [](https://pypi.org/project/bankstatementparser-loader-mt942/) | Input | Parse SWIFT **MT942** interim transaction reports into `Transaction` objects (a format the core does not read) |
|
|
661
|
+
| [`bankstatementparser-loader-bai2`](https://github.com/sebastienrousseau/bankstatementparser-loader-bai2) | [](https://pypi.org/project/bankstatementparser-loader-bai2/) | Input | Parse **BAI2** cash-management files into `Transaction` objects (a format the core does not read) |
|
|
662
|
+
|
|
663
|
+
Loaders turn an additional source format into the same unified
|
|
664
|
+
`Transaction` model; writers take parsed data back out to another
|
|
665
|
+
target. Each companion pins `bankstatementparser` as a dependency and
|
|
666
|
+
ships its own 100%-coverage test suite.
|
|
667
|
+
|
|
668
|
+
```bash
|
|
669
|
+
# Mix and match: e.g. read an MT942 file, then export to Excel
|
|
670
|
+
pip install bankstatementparser-loader-mt942 bankstatementparser-writer-xlsx
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
Loaders hand back the same `Transaction` model the core parsers
|
|
674
|
+
produce, so `load_mt942_file(...)` then `write_xlsx(...)` composes
|
|
675
|
+
cleanly — see each companion's README for runnable examples.
|
|
676
|
+
|
|
677
|
+
---
|
|
678
|
+
|
|
645
679
|
## Project Layout
|
|
646
680
|
|
|
647
681
|
```text
|
|
@@ -651,9 +685,9 @@ bankstatementparser/enrichment/ Categorizer, AccountMapper, EnrichedTransaction
|
|
|
651
685
|
bankstatementparser/export/ hledger + beancount journal export
|
|
652
686
|
bankstatementparser/api.py REST API microservice (FastAPI)
|
|
653
687
|
docs/compliance/ ISO 13485 validation, risk register, traceability matrix
|
|
654
|
-
examples/ 14 deterministic +
|
|
688
|
+
examples/ 14 deterministic + 9 hybrid runnable example scripts
|
|
655
689
|
scripts/ SBOM generation, checksums, signature verification
|
|
656
|
-
tests/
|
|
690
|
+
tests/ 844 tests (unit, integration, property-based, security, hybrid mocks)
|
|
657
691
|
```
|
|
658
692
|
|
|
659
693
|
---
|
|
@@ -56,6 +56,7 @@ _warned_destinations: set[tuple[str, str]] = set()
|
|
|
56
56
|
|
|
57
57
|
|
|
58
58
|
def _is_local_api_base(api_base: Optional[str]) -> bool:
|
|
59
|
+
"""Return whether an API base URL points at the local machine."""
|
|
59
60
|
if not api_base:
|
|
60
61
|
return True
|
|
61
62
|
host = urlparse(api_base).hostname or ""
|
{bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/additional_parsers.py
RENAMED
|
@@ -118,6 +118,7 @@ CSV_COLUMN_GROUPS = {
|
|
|
118
118
|
|
|
119
119
|
|
|
120
120
|
def _normalized_name(name: str) -> str:
|
|
121
|
+
"""Fold a header name to lowercase ASCII alphanumerics for matching."""
|
|
121
122
|
# NFKD + ASCII-encode folds accented characters to their base
|
|
122
123
|
# letter (é -> e) so French/Spanish headers match their
|
|
123
124
|
# unaccented synonym entries.
|
|
@@ -130,6 +131,7 @@ def _normalized_name(name: str) -> str:
|
|
|
130
131
|
|
|
131
132
|
|
|
132
133
|
def _read_validated_text(file_name: str | Path) -> tuple[Path, str]:
|
|
134
|
+
"""Validate the path and read its contents as UTF-8 text."""
|
|
133
135
|
validator = InputValidator()
|
|
134
136
|
path = validator.validate_input_file_path(str(file_name))
|
|
135
137
|
try:
|
|
@@ -210,6 +212,7 @@ class CsvStatementParser(BankStatementParser):
|
|
|
210
212
|
self._parsed_df: pd.DataFrame | None = None
|
|
211
213
|
|
|
212
214
|
def _find_column(self, df: pd.DataFrame, logical_name: str) -> str | None:
|
|
215
|
+
"""Return the DataFrame column matching a logical name, if any."""
|
|
213
216
|
candidates = CSV_COLUMN_GROUPS[logical_name]
|
|
214
217
|
for column in df.columns:
|
|
215
218
|
column_name = str(column)
|
|
@@ -351,6 +354,7 @@ class OfxParser(BankStatementParser):
|
|
|
351
354
|
self._parsed_df: pd.DataFrame | None = None
|
|
352
355
|
|
|
353
356
|
def _tag_value(self, source: str, tag: str) -> str | None:
|
|
357
|
+
"""Return the stripped value of an OFX/SGML tag, or None."""
|
|
354
358
|
match = re.search(rf"<{tag}>([^<\r\n]+)", source, flags=re.IGNORECASE)
|
|
355
359
|
if match is None:
|
|
356
360
|
return None
|
|
@@ -89,6 +89,7 @@ class APIError(RuntimeError):
|
|
|
89
89
|
|
|
90
90
|
|
|
91
91
|
def _resolve_max_upload_bytes() -> int:
|
|
92
|
+
"""Resolve the max upload size from the environment or default."""
|
|
92
93
|
raw = os.environ.get(ENV_MAX_UPLOAD_BYTES)
|
|
93
94
|
if not raw:
|
|
94
95
|
return DEFAULT_MAX_UPLOAD_BYTES
|
|
@@ -121,6 +122,7 @@ def _safe_basename(filename: Optional[str]) -> str:
|
|
|
121
122
|
|
|
122
123
|
|
|
123
124
|
def _allowed_suffix(name: str) -> bool:
|
|
125
|
+
"""Return whether a filename has an allowed input extension."""
|
|
124
126
|
suffix = Path(name).suffix
|
|
125
127
|
if not suffix:
|
|
126
128
|
return False
|
|
@@ -170,7 +172,7 @@ def create_app(
|
|
|
170
172
|
_file_field = File(...)
|
|
171
173
|
|
|
172
174
|
@app.post("/ingest") # type: ignore[untyped-decorator]
|
|
173
|
-
async def ingest(
|
|
175
|
+
async def ingest(
|
|
174
176
|
file: UploadFile = _file_field,
|
|
175
177
|
) -> JSONResponse:
|
|
176
178
|
"""Upload a bank statement and get structured JSON back.
|
|
@@ -277,6 +279,7 @@ def _result_to_dict(result: Any) -> dict[str, Any]:
|
|
|
277
279
|
|
|
278
280
|
|
|
279
281
|
def _verification_dict(v: Any) -> Optional[dict[str, Any]]:
|
|
282
|
+
"""Serialize a verification result to a JSON-safe dict, or None."""
|
|
280
283
|
if v is None:
|
|
281
284
|
return None
|
|
282
285
|
return {
|
|
@@ -319,5 +322,5 @@ def main() -> None:
|
|
|
319
322
|
"Install with: pip install 'bankstatementparser[api]'"
|
|
320
323
|
) from exc
|
|
321
324
|
|
|
322
|
-
app = create_app()
|
|
323
|
-
uvicorn.run(app, host=args.host, port=args.port)
|
|
325
|
+
app = create_app()
|
|
326
|
+
uvicorn.run(app, host=args.host, port=args.port)
|
|
@@ -173,6 +173,7 @@ class BankStatementCLI:
|
|
|
173
173
|
"""
|
|
174
174
|
|
|
175
175
|
def get_camt_stats(parser: Any) -> list[dict[str, Any]]:
|
|
176
|
+
"""Return parser statistics as a list of dict rows."""
|
|
176
177
|
data = parser.get_statement_stats()
|
|
177
178
|
if isinstance(data, pd.DataFrame):
|
|
178
179
|
# list(DataFrame) would yield column names, not rows
|
|
@@ -375,7 +376,6 @@ class BankStatementCLI:
|
|
|
375
376
|
)
|
|
376
377
|
logger.error(f"Hybrid import failed: {exc}")
|
|
377
378
|
sys.exit(1)
|
|
378
|
-
return # pragma: no cover
|
|
379
379
|
|
|
380
380
|
try:
|
|
381
381
|
result = smart_ingest(str(file_path))
|
|
@@ -388,12 +388,10 @@ class BankStatementCLI:
|
|
|
388
388
|
)
|
|
389
389
|
logger.error(f"Hybrid runtime import failed: {exc}")
|
|
390
390
|
sys.exit(1)
|
|
391
|
-
return # pragma: no cover
|
|
392
391
|
except Exception as exc:
|
|
393
392
|
logger.error(f"Hybrid ingest failed: {exc}")
|
|
394
393
|
print(f"Error: hybrid ingest failed - {exc}")
|
|
395
394
|
sys.exit(1)
|
|
396
|
-
return # pragma: no cover
|
|
397
395
|
|
|
398
396
|
rows = [
|
|
399
397
|
{
|
|
@@ -450,9 +448,11 @@ class BankStatementCLI:
|
|
|
450
448
|
(when that file is JSON) or any other source that emits an
|
|
451
449
|
:class:`~bankstatementparser.hybrid.IngestResult` via its
|
|
452
450
|
``to_json()`` method. When the statement-level status is
|
|
453
|
-
:class:`VerificationStatus.DISCREPANCY
|
|
454
|
-
:class:`VerificationStatus.
|
|
455
|
-
|
|
451
|
+
:class:`VerificationStatus.DISCREPANCY`,
|
|
452
|
+
:class:`VerificationStatus.UNVERIFIABLE`, or
|
|
453
|
+
:class:`VerificationStatus.FAILED` (i.e. anything other than
|
|
454
|
+
``VERIFIED``), every transaction is reviewed. Independently,
|
|
455
|
+
``review_below`` routes individual
|
|
456
456
|
low-confidence rows into the same flow even when the
|
|
457
457
|
statement verified cleanly. The operator is prompted with a
|
|
458
458
|
single-character action menu:
|
|
@@ -493,7 +493,6 @@ class BankStatementCLI:
|
|
|
493
493
|
)
|
|
494
494
|
logger.error(f"Hybrid import failed: {exc}")
|
|
495
495
|
sys.exit(1)
|
|
496
|
-
return # pragma: no cover
|
|
497
496
|
|
|
498
497
|
try:
|
|
499
498
|
payload = file_path.read_text(encoding="utf-8")
|
|
@@ -502,7 +501,6 @@ class BankStatementCLI:
|
|
|
502
501
|
logger.error(f"Failed to load IngestResult: {exc}")
|
|
503
502
|
print(f"Error: cannot load IngestResult JSON - {exc}")
|
|
504
503
|
sys.exit(1)
|
|
505
|
-
return # pragma: no cover
|
|
506
504
|
|
|
507
505
|
v = result.verification
|
|
508
506
|
review_all = v is not None and v.status is not (
|
|
@@ -756,10 +754,13 @@ class BankStatementCLI:
|
|
|
756
754
|
try:
|
|
757
755
|
args = self.parser.parse_args()
|
|
758
756
|
except SystemExit:
|
|
759
|
-
# argparse failed, which means required arguments are missing
|
|
757
|
+
# argparse failed, which means required arguments are missing.
|
|
758
|
+
# The explicit return keeps the function from falling through
|
|
759
|
+
# to the (now unbound) ``args`` reference when ``sys.exit`` is
|
|
760
|
+
# patched in tests; in production ``sys.exit`` raises first.
|
|
760
761
|
print("Error: Missing required arguments")
|
|
761
762
|
sys.exit(1)
|
|
762
|
-
return
|
|
763
|
+
return
|
|
763
764
|
|
|
764
765
|
# Check if required arguments are present (safety check)
|
|
765
766
|
if (
|
|
@@ -178,6 +178,7 @@ def _build_messages(
|
|
|
178
178
|
transactions: list[Transaction],
|
|
179
179
|
schema: tuple[str, ...],
|
|
180
180
|
) -> list[dict[str, Any]]:
|
|
181
|
+
"""Build the system/user message pair for a categorization call."""
|
|
181
182
|
schema_block = "\n".join(f" - {c}" for c in schema)
|
|
182
183
|
rows = "\n".join(
|
|
183
184
|
_format_row(idx, tx) for idx, tx in enumerate(transactions)
|
|
@@ -218,6 +219,7 @@ def _sanitize_for_prompt(value: str) -> str:
|
|
|
218
219
|
|
|
219
220
|
|
|
220
221
|
def _format_row(index: int, tx: Transaction) -> str:
|
|
222
|
+
"""Render one transaction as a single prompt line."""
|
|
221
223
|
date = tx.booking_date.isoformat() if tx.booking_date else "????-??-??"
|
|
222
224
|
desc = _sanitize_for_prompt(tx.description or "(no description)")
|
|
223
225
|
return f" [{index}] {date} {tx.amount:>10} {desc[:80]}"
|
|
@@ -331,6 +333,7 @@ class Categorizer:
|
|
|
331
333
|
self,
|
|
332
334
|
chunk: list[Transaction],
|
|
333
335
|
) -> list[EnrichedTransaction]:
|
|
336
|
+
"""Categorize a single chunk of transactions via one LLM call."""
|
|
334
337
|
completion = self._resolve_completion()
|
|
335
338
|
messages = _build_messages(chunk, self.schema)
|
|
336
339
|
|
|
@@ -354,16 +357,17 @@ class Categorizer:
|
|
|
354
357
|
return _build_enriched(chunk, payload, self._schema_lookup)
|
|
355
358
|
|
|
356
359
|
def _resolve_completion(self) -> CompletionFn:
|
|
360
|
+
"""Return the injected completion callable or import LiteLLM's."""
|
|
357
361
|
if self.completion_fn is not None:
|
|
358
362
|
return self.completion_fn
|
|
359
|
-
try:
|
|
363
|
+
try:
|
|
360
364
|
from litellm import completion
|
|
361
|
-
except ImportError as exc:
|
|
365
|
+
except ImportError as exc:
|
|
362
366
|
raise CategorizerError(
|
|
363
367
|
"litellm is required for the enrichment module. "
|
|
364
368
|
"Install with: pip install bankstatementparser[enrichment]"
|
|
365
369
|
) from exc
|
|
366
|
-
return completion # type: ignore[no-any-return]
|
|
370
|
+
return completion # type: ignore[no-any-return]
|
|
367
371
|
|
|
368
372
|
|
|
369
373
|
# ---------------------------------------------------------------------------
|
|
@@ -377,6 +381,7 @@ def _extract_message_content(response: Any) -> str:
|
|
|
377
381
|
|
|
378
382
|
|
|
379
383
|
def _parse_json_payload(raw: str) -> dict[str, Any]:
|
|
384
|
+
"""Tolerantly parse a JSON object from a model response."""
|
|
380
385
|
return parse_json_payload(raw, error_cls=CategorizerError)
|
|
381
386
|
|
|
382
387
|
|
|
@@ -385,6 +390,7 @@ def _build_enriched(
|
|
|
385
390
|
payload: dict[str, Any],
|
|
386
391
|
schema_lookup: dict[str, str],
|
|
387
392
|
) -> list[EnrichedTransaction]:
|
|
393
|
+
"""Map an LLM results payload back onto the input chunk by index."""
|
|
388
394
|
raw_results = payload.get("results")
|
|
389
395
|
if not isinstance(raw_results, list):
|
|
390
396
|
raise CategorizerError("Enrichment payload missing 'results' list")
|
{bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/hybrid/evaluation.py
RENAMED
|
@@ -102,6 +102,7 @@ class EvalSummary:
|
|
|
102
102
|
|
|
103
103
|
|
|
104
104
|
def _to_decimal(value: object, *, context: str) -> Decimal:
|
|
105
|
+
"""Coerce a value to ``Decimal``, raising on malformed input."""
|
|
105
106
|
try:
|
|
106
107
|
return Decimal(str(value))
|
|
107
108
|
except InvalidOperation as exc:
|
|
@@ -109,12 +110,14 @@ def _to_decimal(value: object, *, context: str) -> Decimal:
|
|
|
109
110
|
|
|
110
111
|
|
|
111
112
|
def _optional_decimal(value: object, *, context: str) -> Optional[Decimal]:
|
|
113
|
+
"""Coerce a value to ``Decimal``, returning ``None`` when empty."""
|
|
112
114
|
if value is None or value == "":
|
|
113
115
|
return None
|
|
114
116
|
return _to_decimal(value, context=context)
|
|
115
117
|
|
|
116
118
|
|
|
117
119
|
def _optional_date(value: object, *, context: str) -> Optional[date]:
|
|
120
|
+
"""Parse an ISO date, returning ``None`` when empty and raising on bad input."""
|
|
118
121
|
if value is None or value == "":
|
|
119
122
|
return None
|
|
120
123
|
try:
|
|
@@ -243,6 +246,7 @@ def _match_rows(
|
|
|
243
246
|
def _description_matches(
|
|
244
247
|
expected: Optional[str], actual: Optional[str]
|
|
245
248
|
) -> bool:
|
|
249
|
+
"""Return whether two descriptions match after normalization."""
|
|
246
250
|
if not expected:
|
|
247
251
|
return not actual
|
|
248
252
|
if not actual:
|
|
@@ -253,6 +257,7 @@ def _description_matches(
|
|
|
253
257
|
|
|
254
258
|
|
|
255
259
|
def _ratio(numerator: int, denominator: int) -> float:
|
|
260
|
+
"""Return ``numerator / denominator``, or 1.0 when the denominator is zero."""
|
|
256
261
|
return numerator / denominator if denominator else 1.0
|
|
257
262
|
|
|
258
263
|
|
|
@@ -315,6 +320,7 @@ def score_extraction(
|
|
|
315
320
|
)
|
|
316
321
|
|
|
317
322
|
def _check(expected_value: object, actual_value: object) -> Optional[bool]:
|
|
323
|
+
"""Compare values, returning ``None`` when nothing is pinned."""
|
|
318
324
|
if expected_value is None:
|
|
319
325
|
return None
|
|
320
326
|
return expected_value == actual_value
|
{bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/hybrid/llm_extractor.py
RENAMED
|
@@ -127,6 +127,7 @@ class LLMExtractor:
|
|
|
127
127
|
return _build_result(payload, raw, source_text=statement_text)
|
|
128
128
|
|
|
129
129
|
def _resolve_completion(self) -> CompletionFn:
|
|
130
|
+
"""Return the injected completion callable or import a backend."""
|
|
130
131
|
if self._completion_fn is not None:
|
|
131
132
|
return self._completion_fn
|
|
132
133
|
# Auto-select the direct Ollama bridge for any ollama/* model
|
|
@@ -141,14 +142,14 @@ class LLMExtractor:
|
|
|
141
142
|
|
|
142
143
|
if is_ollama_model(self.model):
|
|
143
144
|
return ollama_direct_completion
|
|
144
|
-
try:
|
|
145
|
+
try:
|
|
145
146
|
from litellm import completion
|
|
146
|
-
except ImportError as exc:
|
|
147
|
+
except ImportError as exc:
|
|
147
148
|
raise LLMExtractorError(
|
|
148
149
|
"litellm is required for LLM extraction. "
|
|
149
150
|
"Install with: pip install bankstatementparser[hybrid]"
|
|
150
151
|
) from exc
|
|
151
|
-
return completion # type: ignore[no-any-return]
|
|
152
|
+
return completion # type: ignore[no-any-return]
|
|
152
153
|
|
|
153
154
|
|
|
154
155
|
def _extract_message_content(response: Any) -> str:
|
|
@@ -162,6 +163,7 @@ def _parse_json_payload(raw: str) -> dict[str, Any]:
|
|
|
162
163
|
|
|
163
164
|
|
|
164
165
|
def _to_decimal(value: Any) -> Optional[Decimal]:
|
|
166
|
+
"""Coerce a value to ``Decimal``, returning ``None`` when empty."""
|
|
165
167
|
if value in (None, ""):
|
|
166
168
|
return None
|
|
167
169
|
try:
|
|
@@ -203,6 +205,7 @@ def _build_result(
|
|
|
203
205
|
source_text: str = "",
|
|
204
206
|
source_method: SourceMethod = "llm",
|
|
205
207
|
) -> LLMExtractionResult:
|
|
208
|
+
"""Convert a parsed LLM payload into an :class:`LLMExtractionResult`."""
|
|
206
209
|
raw_txs = payload.get("transactions") or []
|
|
207
210
|
if not isinstance(raw_txs, list):
|
|
208
211
|
raise LLMExtractorError("LLM 'transactions' field must be a list")
|
{bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/hybrid/ollama_direct.py
RENAMED
|
@@ -91,7 +91,7 @@ def ollama_direct_completion(**kwargs: Any) -> dict[str, Any]:
|
|
|
91
91
|
"""
|
|
92
92
|
try:
|
|
93
93
|
import httpx
|
|
94
|
-
except ImportError as exc:
|
|
94
|
+
except ImportError as exc:
|
|
95
95
|
raise OllamaDirectError(
|
|
96
96
|
"httpx is required for the direct Ollama bridge. "
|
|
97
97
|
"Install with: pip install bankstatementparser[hybrid]"
|
{bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/hybrid/orchestrator.py
RENAMED
|
@@ -109,6 +109,7 @@ class IngestResult:
|
|
|
109
109
|
return json.dumps(self._to_dict(), indent=indent, sort_keys=False)
|
|
110
110
|
|
|
111
111
|
def _to_dict(self) -> dict[str, Any]:
|
|
112
|
+
"""Build the JSON-serializable dict representation."""
|
|
112
113
|
return {
|
|
113
114
|
"schema_version": 1,
|
|
114
115
|
"source_method": self.source_method,
|
|
@@ -146,6 +147,7 @@ class IngestResult:
|
|
|
146
147
|
|
|
147
148
|
@classmethod
|
|
148
149
|
def _from_dict(cls, data: dict[str, Any]) -> IngestResult:
|
|
150
|
+
"""Reconstruct an instance from a decoded JSON dict, validating fields."""
|
|
149
151
|
version = data.get("schema_version")
|
|
150
152
|
if version not in (None, 1):
|
|
151
153
|
raise ValueError(
|
|
@@ -185,6 +187,7 @@ class IngestResult:
|
|
|
185
187
|
def _verification_to_dict(
|
|
186
188
|
verification: Optional[BalanceVerification],
|
|
187
189
|
) -> Optional[dict[str, Any]]:
|
|
190
|
+
"""Serialize a :class:`BalanceVerification` to a JSON-safe dict."""
|
|
188
191
|
if verification is None:
|
|
189
192
|
return None
|
|
190
193
|
return {
|
|
@@ -203,6 +206,7 @@ def _verification_to_dict(
|
|
|
203
206
|
def _verification_from_dict(
|
|
204
207
|
data: Any,
|
|
205
208
|
) -> Optional[BalanceVerification]:
|
|
209
|
+
"""Reconstruct a :class:`BalanceVerification` from a decoded dict."""
|
|
206
210
|
if data is None:
|
|
207
211
|
return None
|
|
208
212
|
if not isinstance(data, dict):
|
|
@@ -229,12 +233,14 @@ def _verification_from_dict(
|
|
|
229
233
|
|
|
230
234
|
|
|
231
235
|
def _decimal_to_str(value: Optional[Decimal]) -> Optional[str]:
|
|
236
|
+
"""Encode a ``Decimal`` as a plain string, preserving ``None``."""
|
|
232
237
|
if value is None:
|
|
233
238
|
return None
|
|
234
239
|
return format(value, "f")
|
|
235
240
|
|
|
236
241
|
|
|
237
242
|
def _decimal_from_str(value: Any) -> Optional[Decimal]:
|
|
243
|
+
"""Decode a string back into a ``Decimal``, preserving ``None``/empty."""
|
|
238
244
|
if value is None or value == "":
|
|
239
245
|
return None
|
|
240
246
|
return Decimal(str(value))
|
|
@@ -313,10 +319,20 @@ def smart_ingest(
|
|
|
313
319
|
|
|
314
320
|
|
|
315
321
|
def _safe_detect(file_path: Path, warnings: list[str]) -> Optional[str]:
|
|
322
|
+
"""Detect the statement format, recording a warning on no match."""
|
|
316
323
|
try:
|
|
317
324
|
return detect_statement_format(str(file_path))
|
|
318
325
|
except Exception as exc:
|
|
319
|
-
|
|
326
|
+
logger.debug(
|
|
327
|
+
"Deterministic format detection did not match for %s: %s",
|
|
328
|
+
file_path,
|
|
329
|
+
exc,
|
|
330
|
+
)
|
|
331
|
+
warnings.append(
|
|
332
|
+
"No deterministic statement format matched "
|
|
333
|
+
"(not XML/CSV/OFX/QFX/MT940); routing to the hybrid "
|
|
334
|
+
"LLM/vision extraction pipeline"
|
|
335
|
+
)
|
|
320
336
|
return None
|
|
321
337
|
|
|
322
338
|
|
|
@@ -328,6 +344,7 @@ def _run_deterministic(
|
|
|
328
344
|
closing_balance: Optional[Decimal],
|
|
329
345
|
warnings: list[str],
|
|
330
346
|
) -> IngestResult:
|
|
347
|
+
"""Run the deterministic parser and wrap its output in an IngestResult."""
|
|
331
348
|
parser = create_parser(str(file_path), fmt)
|
|
332
349
|
raw = parser.parse()
|
|
333
350
|
transactions = _coerce_transactions(raw, source=fmt)
|
|
@@ -443,6 +460,7 @@ def _run_vision(
|
|
|
443
460
|
closing_balance: Optional[Decimal],
|
|
444
461
|
warnings: list[str],
|
|
445
462
|
) -> IngestResult:
|
|
463
|
+
"""Run the vision extractor and wrap its output in an IngestResult."""
|
|
446
464
|
vision_extractor = vision_extractor or VisionExtractor()
|
|
447
465
|
result = vision_extractor.extract(file_path)
|
|
448
466
|
return _build_ingest_result(
|
|
@@ -462,6 +480,7 @@ def _build_ingest_result(
|
|
|
462
480
|
closing_balance: Optional[Decimal],
|
|
463
481
|
warnings: list[str],
|
|
464
482
|
) -> IngestResult:
|
|
483
|
+
"""Assemble an IngestResult from an LLM result, running verification."""
|
|
465
484
|
effective_opening = (
|
|
466
485
|
opening_balance
|
|
467
486
|
if opening_balance is not None
|
|
@@ -498,13 +517,13 @@ def _coerce_transactions(raw: object, *, source: str) -> list[Transaction]:
|
|
|
498
517
|
if callable(to_dict):
|
|
499
518
|
try:
|
|
500
519
|
records = list(to_dict("records"))
|
|
501
|
-
except TypeError:
|
|
520
|
+
except TypeError:
|
|
502
521
|
records = []
|
|
503
522
|
elif isinstance(raw, list):
|
|
504
523
|
records = [dict(item) for item in raw if isinstance(item, dict)]
|
|
505
524
|
elif isinstance(raw, dict):
|
|
506
525
|
records = [dict(raw)]
|
|
507
|
-
else:
|
|
526
|
+
else:
|
|
508
527
|
records = []
|
|
509
528
|
|
|
510
529
|
transactions: list[Transaction] = []
|
{bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/hybrid/pdf_text.py
RENAMED
|
@@ -90,17 +90,18 @@ def extract_text_pages(
|
|
|
90
90
|
|
|
91
91
|
if engine == "pypdf":
|
|
92
92
|
pages = _extract_with_pypdf(pdf_path)
|
|
93
|
-
elif engine == "pdfplumber":
|
|
93
|
+
elif engine == "pdfplumber":
|
|
94
94
|
pages = _extract_with_pdfplumber(pdf_path)
|
|
95
|
-
else:
|
|
95
|
+
else:
|
|
96
96
|
raise PDFExtractionError(f"Unknown PDF engine: {engine}")
|
|
97
97
|
return [_strip_noise(page) for page in pages]
|
|
98
98
|
|
|
99
99
|
|
|
100
100
|
def _extract_with_pypdf(pdf_path: Path) -> list[str]:
|
|
101
|
+
"""Extract per-page text using the ``pypdf`` engine."""
|
|
101
102
|
try:
|
|
102
103
|
from pypdf import PdfReader
|
|
103
|
-
except ImportError as exc:
|
|
104
|
+
except ImportError as exc:
|
|
104
105
|
raise PDFExtractionError(
|
|
105
106
|
"pypdf is required for PDF extraction. "
|
|
106
107
|
"Install with: pip install bankstatementparser[hybrid]"
|
|
@@ -109,7 +110,7 @@ def _extract_with_pypdf(pdf_path: Path) -> list[str]:
|
|
|
109
110
|
try:
|
|
110
111
|
reader = PdfReader(str(pdf_path))
|
|
111
112
|
return [page.extract_text() or "" for page in reader.pages]
|
|
112
|
-
except Exception as exc:
|
|
113
|
+
except Exception as exc:
|
|
113
114
|
raise PDFExtractionError(
|
|
114
115
|
f"Failed to read PDF {pdf_path}: {exc}"
|
|
115
116
|
) from exc
|
|
@@ -117,7 +118,8 @@ def _extract_with_pypdf(pdf_path: Path) -> list[str]:
|
|
|
117
118
|
|
|
118
119
|
def _extract_with_pdfplumber(
|
|
119
120
|
pdf_path: Path,
|
|
120
|
-
) -> list[str]:
|
|
121
|
+
) -> list[str]:
|
|
122
|
+
"""Extract per-page text using the ``pdfplumber`` engine."""
|
|
121
123
|
try:
|
|
122
124
|
import pdfplumber
|
|
123
125
|
except ImportError as exc:
|
{bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/hybrid/verification.py
RENAMED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"""Golden Rule balance verification.
|
|
17
17
|
|
|
18
18
|
If ``opening + credits - debits != closing`` the statement is flagged
|
|
19
|
-
as ``Discrepancy`` (or ``
|
|
19
|
+
as ``Discrepancy`` (or ``Unverifiable`` when balances are missing). This is
|
|
20
20
|
the single most important integrity check in the hybrid pipeline.
|
|
21
21
|
"""
|
|
22
22
|
|
|
@@ -34,10 +34,25 @@ from ..transaction_models import Transaction
|
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
class VerificationStatus(str, Enum):
|
|
37
|
-
"""Verification outcome for a parsed statement.
|
|
37
|
+
"""Verification outcome for a parsed statement.
|
|
38
|
+
|
|
39
|
+
Members:
|
|
40
|
+
VERIFIED: The rule was applied and the books balance.
|
|
41
|
+
DISCREPANCY: The rule was applied and the books do **not**
|
|
42
|
+
balance.
|
|
43
|
+
UNVERIFIABLE: The rule could **not** be applied — e.g. a
|
|
44
|
+
missing opening/closing balance or fewer than two
|
|
45
|
+
statements for a continuity check. The statement is not
|
|
46
|
+
wrong; it simply cannot be checked, so it is routed to
|
|
47
|
+
human review.
|
|
48
|
+
FAILED: Reserved for a genuine verification error — the check
|
|
49
|
+
itself could not run. No current code path emits this; it
|
|
50
|
+
remains for forward compatibility and (de)serialization.
|
|
51
|
+
"""
|
|
38
52
|
|
|
39
53
|
VERIFIED = "verified"
|
|
40
54
|
DISCREPANCY = "discrepancy"
|
|
55
|
+
UNVERIFIABLE = "unverifiable"
|
|
41
56
|
FAILED = "failed"
|
|
42
57
|
|
|
43
58
|
|
|
@@ -76,7 +91,7 @@ def verify_balance(
|
|
|
76
91
|
|
|
77
92
|
Returns:
|
|
78
93
|
A :class:`BalanceVerification` describing the outcome. The
|
|
79
|
-
``status`` is ``
|
|
94
|
+
``status`` is ``UNVERIFIABLE`` when the rule cannot be applied
|
|
80
95
|
(missing balances), not when it disagrees.
|
|
81
96
|
"""
|
|
82
97
|
credits = Decimal("0")
|
|
@@ -91,7 +106,7 @@ def verify_balance(
|
|
|
91
106
|
|
|
92
107
|
if opening_balance is None or closing_balance is None:
|
|
93
108
|
return BalanceVerification(
|
|
94
|
-
status=VerificationStatus.
|
|
109
|
+
status=VerificationStatus.UNVERIFIABLE,
|
|
95
110
|
opening_balance=opening_balance,
|
|
96
111
|
closing_balance=closing_balance,
|
|
97
112
|
total_credits=credits,
|
|
@@ -152,10 +167,10 @@ def verify_balance_multi_currency(
|
|
|
152
167
|
Args:
|
|
153
168
|
transactions: Iterable of normalized transactions.
|
|
154
169
|
balances: Optional ``{currency: (opening, closing)}`` dict.
|
|
155
|
-
When ``None``, verification runs with ``
|
|
156
|
-
for every currency (no balances provided). When a
|
|
170
|
+
When ``None``, verification runs with ``UNVERIFIABLE``
|
|
171
|
+
status for every currency (no balances provided). When a
|
|
157
172
|
currency appears in the transactions but not in this
|
|
158
|
-
dict, its verification is also ``
|
|
173
|
+
dict, its verification is also ``UNVERIFIABLE``.
|
|
159
174
|
tolerance: Per-currency tolerance. Defaults to one cent.
|
|
160
175
|
|
|
161
176
|
Returns:
|
|
@@ -188,9 +203,11 @@ def aggregate_verifications(
|
|
|
188
203
|
) -> BalanceVerification:
|
|
189
204
|
"""Collapse per-currency results into one statement-level verdict.
|
|
190
205
|
|
|
191
|
-
The aggregate ``status`` is the worst per-currency outcome
|
|
192
|
-
``DISCREPANCY`` if any currency disagrees, else
|
|
193
|
-
currency
|
|
206
|
+
The aggregate ``status`` is the worst per-currency outcome
|
|
207
|
+
(worst-first): ``DISCREPANCY`` if any currency disagrees, else
|
|
208
|
+
``FAILED`` if any currency hit a genuine verification error, else
|
|
209
|
+
``UNVERIFIABLE`` if any currency could not be checked, else
|
|
210
|
+
``VERIFIED``.
|
|
194
211
|
|
|
195
212
|
``total_credits``/``total_debits``/``actual_delta`` are raw sums
|
|
196
213
|
across all currencies — useful as row-count-style magnitudes, not
|
|
@@ -218,6 +235,8 @@ def aggregate_verifications(
|
|
|
218
235
|
status = VerificationStatus.DISCREPANCY
|
|
219
236
|
elif VerificationStatus.FAILED in statuses:
|
|
220
237
|
status = VerificationStatus.FAILED
|
|
238
|
+
elif VerificationStatus.UNVERIFIABLE in statuses:
|
|
239
|
+
status = VerificationStatus.UNVERIFIABLE
|
|
221
240
|
else:
|
|
222
241
|
status = VerificationStatus.VERIFIED
|
|
223
242
|
|
|
@@ -286,13 +305,13 @@ def verify_continuity(
|
|
|
286
305
|
|
|
287
306
|
Returns:
|
|
288
307
|
A :class:`ContinuityResult`. ``status`` is ``DISCREPANCY``
|
|
289
|
-
if any link has a gap beyond tolerance, else ``
|
|
290
|
-
any link could not be checked (missing balance on either
|
|
308
|
+
if any link has a gap beyond tolerance, else ``UNVERIFIABLE``
|
|
309
|
+
if any link could not be checked (missing balance on either
|
|
291
310
|
side, or fewer than two statements), else ``VERIFIED``.
|
|
292
311
|
"""
|
|
293
312
|
if len(statements) < 2:
|
|
294
313
|
return ContinuityResult(
|
|
295
|
-
status=VerificationStatus.
|
|
314
|
+
status=VerificationStatus.UNVERIFIABLE,
|
|
296
315
|
breaks=(),
|
|
297
316
|
checked_links=0,
|
|
298
317
|
unchecked_links=0,
|
|
@@ -337,7 +356,7 @@ def verify_continuity(
|
|
|
337
356
|
)
|
|
338
357
|
if unchecked:
|
|
339
358
|
return ContinuityResult(
|
|
340
|
-
status=VerificationStatus.
|
|
359
|
+
status=VerificationStatus.UNVERIFIABLE,
|
|
341
360
|
breaks=(),
|
|
342
361
|
checked_links=checked,
|
|
343
362
|
unchecked_links=unchecked,
|
|
@@ -374,8 +393,8 @@ def verify_transactions(
|
|
|
374
393
|
|
|
375
394
|
The single ``opening_balance``/``closing_balance`` pair cannot
|
|
376
395
|
be attributed to one currency of a multi-currency statement, so
|
|
377
|
-
in that case every currency reports ``
|
|
378
|
-
rather than a spurious mismatch.
|
|
396
|
+
in that case every currency reports ``UNVERIFIABLE`` (cannot
|
|
397
|
+
verify) rather than a spurious mismatch.
|
|
379
398
|
|
|
380
399
|
Args:
|
|
381
400
|
transactions: Iterable of normalized transactions.
|
{bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/hybrid/vision.py
RENAMED
|
@@ -314,9 +314,10 @@ class VisionExtractor:
|
|
|
314
314
|
)
|
|
315
315
|
|
|
316
316
|
def _render_pages(self, pdf_path: Path) -> list[bytes]:
|
|
317
|
+
"""Render each PDF page (up to :attr:`max_pages`) as a PNG."""
|
|
317
318
|
try:
|
|
318
319
|
import pypdfium2 as pdfium
|
|
319
|
-
except ImportError as exc:
|
|
320
|
+
except ImportError as exc:
|
|
320
321
|
raise VisionExtractorError(
|
|
321
322
|
"pypdfium2 is required for vision extraction. "
|
|
322
323
|
"Install with: "
|
|
@@ -358,7 +359,7 @@ class VisionExtractor:
|
|
|
358
359
|
"""
|
|
359
360
|
try:
|
|
360
361
|
import pypdfium2 as pdfium
|
|
361
|
-
except ImportError as exc:
|
|
362
|
+
except ImportError as exc:
|
|
362
363
|
raise VisionExtractorError(
|
|
363
364
|
"pypdfium2 is required for vision extraction. "
|
|
364
365
|
"Install with: "
|
|
@@ -407,6 +408,7 @@ class VisionExtractor:
|
|
|
407
408
|
return all_strips
|
|
408
409
|
|
|
409
410
|
def _resolve_completion(self) -> CompletionFn:
|
|
411
|
+
"""Return the injected completion callable or import a backend."""
|
|
410
412
|
if self._completion_fn is not None:
|
|
411
413
|
return self._completion_fn
|
|
412
414
|
# Auto-select the direct Ollama bridge for any ollama/* model
|
|
@@ -421,15 +423,15 @@ class VisionExtractor:
|
|
|
421
423
|
|
|
422
424
|
if is_ollama_model(self.model):
|
|
423
425
|
return ollama_direct_completion
|
|
424
|
-
try:
|
|
426
|
+
try:
|
|
425
427
|
from litellm import completion
|
|
426
|
-
except ImportError as exc:
|
|
428
|
+
except ImportError as exc:
|
|
427
429
|
raise VisionExtractorError(
|
|
428
430
|
"litellm is required for vision extraction. "
|
|
429
431
|
"Install with: "
|
|
430
432
|
"pip install bankstatementparser[hybrid-vision]"
|
|
431
433
|
) from exc
|
|
432
|
-
return completion # type: ignore[no-any-return]
|
|
434
|
+
return completion # type: ignore[no-any-return]
|
|
433
435
|
|
|
434
436
|
|
|
435
437
|
STRIP_HEADER_SYSTEM_PROMPT = """You are a meticulous Financial Data
|
|
@@ -31,12 +31,14 @@ from .transaction_models import Transaction
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
def _days_between(left: date | None, right: date | None) -> int | None:
|
|
34
|
+
"""Return the absolute day gap between two dates, or None."""
|
|
34
35
|
if left is None or right is None:
|
|
35
36
|
return None
|
|
36
37
|
return abs((left - right).days)
|
|
37
38
|
|
|
38
39
|
|
|
39
40
|
def _description_similarity(left: Transaction, right: Transaction) -> float:
|
|
41
|
+
"""Return the 0.0-1.0 similarity of two normalized descriptions."""
|
|
40
42
|
if not left.normalized_description or not right.normalized_description:
|
|
41
43
|
return 0.0
|
|
42
44
|
return SequenceMatcher(
|
|
@@ -76,6 +78,8 @@ class DeduplicationResult(BaseModel):
|
|
|
76
78
|
|
|
77
79
|
@dataclass(frozen=True)
|
|
78
80
|
class _Candidate:
|
|
81
|
+
"""A transaction paired with its source index and primary hash."""
|
|
82
|
+
|
|
79
83
|
index: int
|
|
80
84
|
transaction: Transaction
|
|
81
85
|
primary_hash: str
|
|
@@ -247,6 +251,7 @@ class Deduplicator:
|
|
|
247
251
|
def _candidate_groups_by_primary(
|
|
248
252
|
self, candidates: list[_Candidate]
|
|
249
253
|
) -> dict[str, list[_Candidate]]:
|
|
254
|
+
"""Group candidates by their primary hash."""
|
|
250
255
|
groups: dict[str, list[_Candidate]] = defaultdict(list)
|
|
251
256
|
for candidate in candidates:
|
|
252
257
|
groups[candidate.primary_hash].append(candidate)
|
|
@@ -255,6 +260,7 @@ class Deduplicator:
|
|
|
255
260
|
def _find_exact_duplicates(
|
|
256
261
|
self, candidates: list[_Candidate]
|
|
257
262
|
) -> list[ExactDuplicateGroup]:
|
|
263
|
+
"""Return groups of candidates sharing a primary hash."""
|
|
258
264
|
groups = self._candidate_groups_by_primary(candidates)
|
|
259
265
|
exact_groups = []
|
|
260
266
|
for primary_hash, bucket in sorted(groups.items()):
|
|
@@ -281,6 +287,7 @@ class Deduplicator:
|
|
|
281
287
|
*,
|
|
282
288
|
excluded_indices: set[int],
|
|
283
289
|
) -> tuple[list[MatchGroup], set[int]]:
|
|
290
|
+
"""Find probable and temporal match groups for operator review."""
|
|
284
291
|
probable_groups, probable_indices = self._find_probable_matches(
|
|
285
292
|
candidates
|
|
286
293
|
)
|
|
@@ -299,6 +306,7 @@ class Deduplicator:
|
|
|
299
306
|
def _find_probable_matches(
|
|
300
307
|
self, candidates: list[_Candidate]
|
|
301
308
|
) -> tuple[list[MatchGroup], set[int]]:
|
|
309
|
+
"""Match candidates by hash collision and description similarity."""
|
|
302
310
|
groups = []
|
|
303
311
|
matched_indices: set[int] = set()
|
|
304
312
|
for bucket in self._candidate_groups_by_primary(candidates).values():
|
|
@@ -343,6 +351,7 @@ class Deduplicator:
|
|
|
343
351
|
def _find_temporal_matches(
|
|
344
352
|
self, candidates: list[_Candidate]
|
|
345
353
|
) -> tuple[list[MatchGroup], set[int]]:
|
|
354
|
+
"""Match candidates with equal amounts but shifted value dates."""
|
|
346
355
|
buckets: dict[tuple[str, str, str], list[_Candidate]] = defaultdict(
|
|
347
356
|
list
|
|
348
357
|
)
|
|
@@ -363,6 +372,7 @@ class Deduplicator:
|
|
|
363
372
|
component: list[_Candidate],
|
|
364
373
|
similarities: list[float],
|
|
365
374
|
) -> None:
|
|
375
|
+
"""Record a matched component as a temporal match group."""
|
|
366
376
|
matched_indices.update(candidate.index for candidate in component)
|
|
367
377
|
groups.append(self._temporal_group(component, similarities))
|
|
368
378
|
|
|
@@ -413,6 +423,7 @@ class Deduplicator:
|
|
|
413
423
|
component: list[_Candidate],
|
|
414
424
|
similarities: list[float],
|
|
415
425
|
) -> MatchGroup:
|
|
426
|
+
"""Build a temporal MatchGroup with a confidence and reason."""
|
|
416
427
|
max_delta = (
|
|
417
428
|
_days_between(
|
|
418
429
|
component[0].transaction.value_date,
|
{bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/transaction_models.py
RENAMED
|
@@ -34,6 +34,7 @@ from pydantic import (
|
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
def _coerce_decimal(value: object) -> Decimal:
|
|
37
|
+
"""Coerce a value into a Decimal amount, raising if empty."""
|
|
37
38
|
text = str(value).strip()
|
|
38
39
|
if not text:
|
|
39
40
|
raise ValueError("amount is required")
|
|
@@ -41,6 +42,7 @@ def _coerce_decimal(value: object) -> Decimal:
|
|
|
41
42
|
|
|
42
43
|
|
|
43
44
|
def _parse_date(value: object) -> date | None:
|
|
45
|
+
"""Parse a value into a date across common formats, or None."""
|
|
44
46
|
if value in (None, ""):
|
|
45
47
|
return None
|
|
46
48
|
if isinstance(value, date) and not isinstance(value, datetime):
|
|
@@ -98,6 +100,7 @@ def normalize_description(value: str | None) -> str:
|
|
|
98
100
|
|
|
99
101
|
|
|
100
102
|
def _first_value(record: Mapping[str, object], *keys: str) -> object | None:
|
|
103
|
+
"""Return the first non-empty value among the given keys, or None."""
|
|
101
104
|
for key in keys:
|
|
102
105
|
if key in record and record[key] not in (None, ""):
|
|
103
106
|
return record[key]
|
|
@@ -136,6 +139,7 @@ class BoundingBox(BaseModel):
|
|
|
136
139
|
|
|
137
140
|
@model_validator(mode="after")
|
|
138
141
|
def _check_non_inverted(self) -> BoundingBox:
|
|
142
|
+
"""Validate that the box corners are not inverted."""
|
|
139
143
|
if self.x0 > self.x1:
|
|
140
144
|
raise ValueError(
|
|
141
145
|
f"BoundingBox x0 ({self.x0}) must be <= x1 ({self.x1})"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "bankstatementparser"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.10"
|
|
4
4
|
description = "BankStatementParser is your essential tool for easy bank statement management. Designed with finance and treasury experts in mind, it offers a simple way to handle CAMT (ISO 20022) formats and more. Get quick, accurate insights from your financial data and spend less time on processing. It's the smart, hassle-free way to stay on top of your transactions."
|
|
5
5
|
authors = ["Sebastien Rousseau <sebastian.rousseau@gmail.com>"]
|
|
6
6
|
license = "Apache Software License"
|
|
@@ -31,9 +31,10 @@ pytest = ">=8.0.0,<10.0.0"
|
|
|
31
31
|
pytest-cov = "^7.1.0"
|
|
32
32
|
pytest-benchmark = "^5.1.0"
|
|
33
33
|
ruff = "^0.15.7"
|
|
34
|
-
mypy = "
|
|
34
|
+
mypy = ">=1.5,<3"
|
|
35
35
|
bandit = "^1.7.0"
|
|
36
36
|
hypothesis = ">=6.82,<7"
|
|
37
|
+
interrogate = "^1.7.0"
|
|
37
38
|
|
|
38
39
|
[tool.poetry.group.docs]
|
|
39
40
|
optional = true
|
|
@@ -162,6 +163,28 @@ markers = [
|
|
|
162
163
|
"slow: marks tests as slow-running performance or regression checks",
|
|
163
164
|
]
|
|
164
165
|
|
|
166
|
+
[tool.interrogate]
|
|
167
|
+
# Documentation coverage is enforced at 100% across the package, the
|
|
168
|
+
# developer/CI helper scripts, and the runnable examples. Test modules
|
|
169
|
+
# and the local virtualenv are excluded; everything else — including
|
|
170
|
+
# private and nested helpers — must carry a docstring.
|
|
171
|
+
fail-under = 100
|
|
172
|
+
ignore-init-method = true
|
|
173
|
+
ignore-magic = true
|
|
174
|
+
ignore-module = false
|
|
175
|
+
ignore-private = false
|
|
176
|
+
ignore-nested-functions = false
|
|
177
|
+
ignore-semiprivate = false
|
|
178
|
+
exclude = [
|
|
179
|
+
".venv",
|
|
180
|
+
"build",
|
|
181
|
+
"dist",
|
|
182
|
+
"tests",
|
|
183
|
+
"setup.py",
|
|
184
|
+
"docs",
|
|
185
|
+
]
|
|
186
|
+
verbose = 1
|
|
187
|
+
|
|
165
188
|
[tool.coverage.run]
|
|
166
189
|
source = ["bankstatementparser"]
|
|
167
190
|
branch = true
|
|
@@ -170,5 +193,4 @@ branch = true
|
|
|
170
193
|
exclude_lines = [
|
|
171
194
|
"pragma: no cover",
|
|
172
195
|
"if TYPE_CHECKING:",
|
|
173
|
-
"raise NotImplementedError",
|
|
174
196
|
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/enrichment/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/export/__init__.py
RENAMED
|
File without changes
|
{bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/export/ledger.py
RENAMED
|
File without changes
|
{bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/hybrid/__init__.py
RENAMED
|
File without changes
|
{bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/hybrid/prompts.py
RENAMED
|
File without changes
|
{bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/hybrid/scanner.py
RENAMED
|
File without changes
|
{bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/input_validator.py
RENAMED
|
File without changes
|
{bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/pain001_parser.py
RENAMED
|
File without changes
|
{bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/record_types.py
RENAMED
|
File without changes
|
{bankstatementparser-0.0.9 → bankstatementparser-0.0.10}/bankstatementparser/zip_security.py
RENAMED
|
File without changes
|