bankstatementparser-writer-xlsx 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_writer_xlsx-0.0.10/LICENSE +189 -0
- bankstatementparser_writer_xlsx-0.0.10/PKG-INFO +250 -0
- bankstatementparser_writer_xlsx-0.0.10/README.md +220 -0
- bankstatementparser_writer_xlsx-0.0.10/bankstatementparser_writer_xlsx/__init__.py +30 -0
- bankstatementparser_writer_xlsx-0.0.10/bankstatementparser_writer_xlsx/writer.py +353 -0
- bankstatementparser_writer_xlsx-0.0.10/pyproject.toml +153 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work.
|
|
38
|
+
|
|
39
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
40
|
+
form, that is based on (or derived from) the Work and for which the
|
|
41
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
42
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
43
|
+
of this License, Derivative Works shall not include works that remain
|
|
44
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
45
|
+
the Work and Derivative Works thereof.
|
|
46
|
+
|
|
47
|
+
"Contribution" shall mean any work of authorship, including
|
|
48
|
+
the original version of the Work and any modifications or additions
|
|
49
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
50
|
+
submitted to the Licensor for inclusion in the Work by the copyright owner
|
|
51
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
52
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
53
|
+
means any form of electronic, verbal, or written communication sent
|
|
54
|
+
to the Licensor or its representatives, including but not limited to
|
|
55
|
+
communication on electronic mailing lists, source code control systems,
|
|
56
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
57
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
58
|
+
excluding communication that is conspicuously marked or otherwise
|
|
59
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
60
|
+
|
|
61
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
62
|
+
on behalf of whom a Contribution has been received by the Licensor and
|
|
63
|
+
subsequently incorporated within the Work.
|
|
64
|
+
|
|
65
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
66
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
67
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
68
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
69
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
70
|
+
Work and such Derivative Works in Source or Object form.
|
|
71
|
+
|
|
72
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
73
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
74
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
75
|
+
(except as stated in this section) patent license to make, have made,
|
|
76
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
77
|
+
where such license applies only to those patent claims licensable
|
|
78
|
+
by such Contributor that are necessarily infringed by their
|
|
79
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
80
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
81
|
+
institute patent litigation against any entity (including a
|
|
82
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
83
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
84
|
+
or contributory patent infringement, then any patent licenses
|
|
85
|
+
granted to You under this License for that Work shall terminate
|
|
86
|
+
as of the date such litigation is filed.
|
|
87
|
+
|
|
88
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
89
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
90
|
+
modifications, and in Source or Object form, provided that You
|
|
91
|
+
meet the following conditions:
|
|
92
|
+
|
|
93
|
+
(a) You must give any other recipients of the Work or
|
|
94
|
+
Derivative Works a copy of this License; and
|
|
95
|
+
|
|
96
|
+
(b) You must cause any modified files to carry prominent notices
|
|
97
|
+
stating that You changed the files; and
|
|
98
|
+
|
|
99
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
100
|
+
that You distribute, all copyright, patent, trademark, and
|
|
101
|
+
attribution notices from the Source form of the Work,
|
|
102
|
+
excluding those notices that do not pertain to any part of
|
|
103
|
+
the Derivative Works; and
|
|
104
|
+
|
|
105
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
106
|
+
distribution, then any Derivative Works that You distribute must
|
|
107
|
+
include a readable copy of the attribution notices contained
|
|
108
|
+
within such NOTICE file, excluding any notices that do not
|
|
109
|
+
pertain to any part of the Derivative Works, in at least one
|
|
110
|
+
of the following places: within a NOTICE text file distributed
|
|
111
|
+
as part of the Derivative Works; within the Source form or
|
|
112
|
+
documentation, if provided along with the Derivative Works; or,
|
|
113
|
+
within a display generated by the Derivative Works, if and
|
|
114
|
+
wherever such third-party notices normally appear. The contents
|
|
115
|
+
of the NOTICE file are for informational purposes only and
|
|
116
|
+
do not modify the License. You may add Your own attribution
|
|
117
|
+
notices within Derivative Works that You distribute, alongside
|
|
118
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
119
|
+
that such additional attribution notices cannot be construed
|
|
120
|
+
as modifying the License.
|
|
121
|
+
|
|
122
|
+
You may add Your own copyright statement to Your modifications and
|
|
123
|
+
may provide additional or different license terms and conditions
|
|
124
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
125
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
126
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
127
|
+
the conditions stated in this License.
|
|
128
|
+
|
|
129
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
130
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
131
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
132
|
+
this License, without any additional terms or conditions.
|
|
133
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
134
|
+
the terms of any separate license agreement you may have executed
|
|
135
|
+
with Licensor regarding such Contributions.
|
|
136
|
+
|
|
137
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
138
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
139
|
+
except as required for reasonable and customary use in describing the
|
|
140
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
141
|
+
|
|
142
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
143
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
144
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
145
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
146
|
+
implied, including, without limitation, any warranties or conditions
|
|
147
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
148
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
149
|
+
appropriateness of using or redistributing the Work and assume any
|
|
150
|
+
risks associated with Your exercise of permissions under this License.
|
|
151
|
+
|
|
152
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
153
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
154
|
+
unless required by applicable law (such as deliberate and grossly
|
|
155
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
156
|
+
liable to You for damages, including any direct, indirect, special,
|
|
157
|
+
incidental, or consequential damages of any character arising as a
|
|
158
|
+
result of this License or out of the use or inability to use the
|
|
159
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
160
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
161
|
+
other commercial damages or losses), even if such Contributor
|
|
162
|
+
has been advised of the possibility of such damages.
|
|
163
|
+
|
|
164
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
165
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
166
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
167
|
+
or other liability obligations and/or rights consistent with this
|
|
168
|
+
License. However, in accepting such obligations, You may act only
|
|
169
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
170
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
171
|
+
defend, and hold each Contributor harmless for any liability
|
|
172
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
173
|
+
of your accepting any such warranty or additional liability.
|
|
174
|
+
|
|
175
|
+
END OF TERMS AND CONDITIONS
|
|
176
|
+
|
|
177
|
+
Copyright 2023-2026 Sebastien Rousseau
|
|
178
|
+
|
|
179
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
180
|
+
you may not use this file except in compliance with the License.
|
|
181
|
+
You may obtain a copy of the License at
|
|
182
|
+
|
|
183
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
184
|
+
|
|
185
|
+
Unless required by applicable law or agreed to in writing, software
|
|
186
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
187
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
188
|
+
See the License for the specific language governing permissions and
|
|
189
|
+
limitations under the License.
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bankstatementparser-writer-xlsx
|
|
3
|
+
Version: 0.0.10
|
|
4
|
+
Summary: Excel (.xlsx) writer for bankstatementparser-parsed bank statements.
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: bank-statements,bankstatementparser,xlsx,excel,openpyxl,accounting,reconciliation
|
|
8
|
+
Author: Sebastien Rousseau
|
|
9
|
+
Author-email: sebastian.rousseau@gmail.com
|
|
10
|
+
Requires-Python: >=3.10,<4.0
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
22
|
+
Classifier: Topic :: Office/Business :: Financial :: Accounting
|
|
23
|
+
Requires-Dist: bankstatementparser (>=0.0.9)
|
|
24
|
+
Requires-Dist: openpyxl (>=3.1,<4)
|
|
25
|
+
Requires-Dist: pandas (>=2.0)
|
|
26
|
+
Project-URL: Homepage, https://bankstatementparser.com
|
|
27
|
+
Project-URL: Repository, https://github.com/sebastienrousseau/bankstatementparser-writer-xlsx
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# bankstatementparser-writer-xlsx: Excel writer for parsed bank statements
|
|
31
|
+
|
|
32
|
+
[![PyPI Version][pypi-badge]][pypi-url]
|
|
33
|
+
[![Python Versions][python-versions-badge]][pypi-url]
|
|
34
|
+
[![License][license-badge]][license-url]
|
|
35
|
+
[![Coverage][coverage-badge]][ci-url]
|
|
36
|
+
|
|
37
|
+
**An Excel `.xlsx` writer for data parsed by
|
|
38
|
+
[`bankstatementparser`][core]** — turn parsed transactions (a pandas
|
|
39
|
+
`DataFrame`, a list of `Transaction` objects, or a list of plain dicts)
|
|
40
|
+
into a polished workbook that accountants, auditors, and reconciliation
|
|
41
|
+
macros can open directly.
|
|
42
|
+
|
|
43
|
+
> **Latest release: v0.0.10** — single `write_xlsx(data, path, ...)`
|
|
44
|
+
> function, 100% line + branch coverage, 100% docstring coverage,
|
|
45
|
+
> `mypy --strict` clean.
|
|
46
|
+
|
|
47
|
+
## Contents
|
|
48
|
+
|
|
49
|
+
- [Overview](#overview)
|
|
50
|
+
- [Install](#install)
|
|
51
|
+
- [Quick start](#quick-start)
|
|
52
|
+
- [Input shapes](#input-shapes)
|
|
53
|
+
- [Value coercion](#value-coercion)
|
|
54
|
+
- [The summary sheet](#the-summary-sheet)
|
|
55
|
+
- [Examples](#examples)
|
|
56
|
+
- [When not to use this package](#when-not-to-use-this-package)
|
|
57
|
+
- [Development](#development)
|
|
58
|
+
- [Security](#security)
|
|
59
|
+
- [Documentation](#documentation)
|
|
60
|
+
- [License](#license)
|
|
61
|
+
- [Contributing](#contributing)
|
|
62
|
+
- [Acknowledgements](#acknowledgements)
|
|
63
|
+
|
|
64
|
+
## Overview
|
|
65
|
+
|
|
66
|
+
`bankstatementparser-writer-xlsx` is a small, focused companion to the
|
|
67
|
+
[`bankstatementparser`][core] library. It does one thing well: given
|
|
68
|
+
already-parsed bank-statement records, write a clean Excel workbook with
|
|
69
|
+
a bold header row, one row per transaction, auto-sized columns, and an
|
|
70
|
+
optional second `Summary` sheet.
|
|
71
|
+
|
|
72
|
+
The package consumes _parsed_ data — it does not read PDFs, CSVs, or
|
|
73
|
+
XML itself. Parsing (and the security surface that comes with untrusted
|
|
74
|
+
input) lives upstream in the [`bankstatementparser`][core] core.
|
|
75
|
+
|
|
76
|
+
## Install
|
|
77
|
+
|
|
78
|
+
`bankstatementparser-writer-xlsx` runs on macOS, Linux, and Windows and
|
|
79
|
+
requires **Python 3.10+**. It pulls in `bankstatementparser`,
|
|
80
|
+
`openpyxl`, and `pandas` automatically.
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pip install bankstatementparser-writer-xlsx
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Quick start
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from bankstatementparser import CsvStatementParser
|
|
90
|
+
from bankstatementparser_writer_xlsx import write_xlsx
|
|
91
|
+
|
|
92
|
+
parser = CsvStatementParser("statement.csv")
|
|
93
|
+
df = parser.parse() # a pandas DataFrame
|
|
94
|
+
write_xlsx(df, "statement.xlsx") # one polished workbook
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
That's an Excel workbook ready for your accountant. Add a summary sheet
|
|
98
|
+
in one extra argument:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from bankstatementparser import CsvStatementParser
|
|
102
|
+
from bankstatementparser_writer_xlsx import write_xlsx
|
|
103
|
+
|
|
104
|
+
parser = CsvStatementParser("statement.csv")
|
|
105
|
+
df = parser.parse()
|
|
106
|
+
write_xlsx(df, "statement.xlsx", summary=parser.get_summary())
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Input shapes
|
|
110
|
+
|
|
111
|
+
`write_xlsx(data, path, *, sheet_name="Transactions", summary=None)`
|
|
112
|
+
accepts three input shapes and normalises each to a flat table:
|
|
113
|
+
|
|
114
|
+
| Input | Column order |
|
|
115
|
+
| :--- | :--- |
|
|
116
|
+
| `pandas.DataFrame` (from a parser's `.parse()`) | the DataFrame's own column order |
|
|
117
|
+
| `list[bankstatementparser.Transaction]` | the stable `Transaction` field order |
|
|
118
|
+
| `list[dict]` | the union of keys, in first-seen order |
|
|
119
|
+
|
|
120
|
+
A header row (bold) is written to the `sheet_name` sheet, followed by
|
|
121
|
+
one row per record. Columns are auto-sized to their widest cell (capped
|
|
122
|
+
so wide descriptions don't run off-screen).
|
|
123
|
+
|
|
124
|
+
**Empty input** is accepted: an empty `list` writes an empty sheet (no
|
|
125
|
+
header), while an empty `DataFrame` that still carries column labels
|
|
126
|
+
writes a header-only sheet.
|
|
127
|
+
|
|
128
|
+
## Value coercion
|
|
129
|
+
|
|
130
|
+
Spreadsheet cells can't hold arbitrary Python objects, so the writer
|
|
131
|
+
coerces the rich types the parser emits:
|
|
132
|
+
|
|
133
|
+
| Python type | Written as |
|
|
134
|
+
| :--- | :--- |
|
|
135
|
+
| `decimal.Decimal` | `float` (Excel has no decimal type; floats aggregate natively) |
|
|
136
|
+
| `datetime.date` / `datetime.datetime` | native Excel date cell (unchanged) |
|
|
137
|
+
| `str`, `int`, `float`, `bool`, `None` | unchanged |
|
|
138
|
+
| anything else | `str(value)` |
|
|
139
|
+
|
|
140
|
+
## The summary sheet
|
|
141
|
+
|
|
142
|
+
If you pass `summary=` a mapping (for example a parser's
|
|
143
|
+
`get_summary()` result), the writer adds a second sheet titled
|
|
144
|
+
`Summary` with a bold `Key` / `Value` header and one row per item:
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
from decimal import Decimal
|
|
148
|
+
|
|
149
|
+
from bankstatementparser_writer_xlsx import write_xlsx
|
|
150
|
+
|
|
151
|
+
transactions = [
|
|
152
|
+
{"date": "2026-06-01", "description": "Salary", "amount": Decimal("3000.00")},
|
|
153
|
+
{"date": "2026-06-03", "description": "Coffee Shop", "amount": Decimal("-4.20")},
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
write_xlsx(
|
|
157
|
+
transactions,
|
|
158
|
+
"out.xlsx",
|
|
159
|
+
summary={
|
|
160
|
+
"account_id": "DE89370400440532013000",
|
|
161
|
+
"transaction_count": 128,
|
|
162
|
+
"total_amount": Decimal("12045.67"),
|
|
163
|
+
"currency": "EUR",
|
|
164
|
+
},
|
|
165
|
+
)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Examples
|
|
169
|
+
|
|
170
|
+
Five runnable examples live in [`examples/`](examples/) and are
|
|
171
|
+
exercised in CI on every commit. Together they cover every supported
|
|
172
|
+
input shape and option of `write_xlsx`:
|
|
173
|
+
|
|
174
|
+
- [`01_write_dataframe.py`](examples/01_write_dataframe.py) — write a
|
|
175
|
+
pandas `DataFrame` to a single sheet.
|
|
176
|
+
- [`02_write_transactions.py`](examples/02_write_transactions.py) —
|
|
177
|
+
write a list of `Transaction` objects in stable field order.
|
|
178
|
+
- [`03_write_dicts.py`](examples/03_write_dicts.py) — write a list of
|
|
179
|
+
plain `dict` records (union of keys).
|
|
180
|
+
- [`04_write_with_summary.py`](examples/04_write_with_summary.py) —
|
|
181
|
+
write a DataFrame plus a second `Summary` sheet via `summary=`.
|
|
182
|
+
- [`05_custom_sheet_name.py`](examples/05_custom_sheet_name.py) — rename
|
|
183
|
+
the transactions sheet with `sheet_name=`.
|
|
184
|
+
|
|
185
|
+
## When not to use this package
|
|
186
|
+
|
|
187
|
+
- **You need a custom sheet layout.** The single-sheet (+ optional
|
|
188
|
+
Summary) structure is intentionally simple. Compose your own
|
|
189
|
+
`openpyxl` workbook if you need pivot-ready, multi-sheet layouts.
|
|
190
|
+
- **You need `.xls` (legacy binary).** `openpyxl` writes `.xlsx` only;
|
|
191
|
+
convert downstream if you must.
|
|
192
|
+
- **You need encrypted output.** Out of scope; encrypt the produced
|
|
193
|
+
`.xlsx` downstream with a tool like `msoffcrypto-tool`.
|
|
194
|
+
- **You want to _read_ Excel.** This package is a writer.
|
|
195
|
+
|
|
196
|
+
## Development
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
git clone https://github.com/sebastienrousseau/bankstatementparser-writer-xlsx
|
|
200
|
+
cd bankstatementparser-writer-xlsx
|
|
201
|
+
poetry env use python3.12
|
|
202
|
+
poetry install
|
|
203
|
+
poetry run pytest # 100% line + branch coverage gate
|
|
204
|
+
poetry run ruff check bankstatementparser_writer_xlsx tests
|
|
205
|
+
poetry run mypy bankstatementparser_writer_xlsx
|
|
206
|
+
poetry run interrogate -c pyproject.toml bankstatementparser_writer_xlsx
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Security
|
|
210
|
+
|
|
211
|
+
`bankstatementparser-writer-xlsx` consumes already-parsed data, not raw
|
|
212
|
+
statement files — the PDF/CSV/XML parsing surface lives upstream in the
|
|
213
|
+
[`bankstatementparser`][core] core. Reporting practice, supported
|
|
214
|
+
versions, and supply-chain posture are documented in
|
|
215
|
+
[`SECURITY.md`](SECURITY.md).
|
|
216
|
+
|
|
217
|
+
## Documentation
|
|
218
|
+
|
|
219
|
+
- [`README.md`](README.md) — this file
|
|
220
|
+
- [`ARCHITECTURE.md`](ARCHITECTURE.md) — module map and design decisions
|
|
221
|
+
- [`CHANGELOG.md`](CHANGELOG.md) — release notes
|
|
222
|
+
- [`ROADMAP.md`](ROADMAP.md) — what's next
|
|
223
|
+
- [`SECURITY.md`](SECURITY.md) — disclosure + supported versions
|
|
224
|
+
- [`examples/`](examples/) — runnable scripts, exercised in CI
|
|
225
|
+
|
|
226
|
+
## License
|
|
227
|
+
|
|
228
|
+
Licensed under the [Apache License, Version 2.0][license-url]. Any
|
|
229
|
+
contribution submitted for inclusion shall be licensed as above, without
|
|
230
|
+
additional terms.
|
|
231
|
+
|
|
232
|
+
## Contributing
|
|
233
|
+
|
|
234
|
+
Contributions are welcome — open an issue or PR on
|
|
235
|
+
[the repository](https://github.com/sebastienrousseau/bankstatementparser-writer-xlsx).
|
|
236
|
+
|
|
237
|
+
## Acknowledgements
|
|
238
|
+
|
|
239
|
+
Built on the [`bankstatementparser`][core] library and
|
|
240
|
+
[openpyxl](https://openpyxl.readthedocs.io/).
|
|
241
|
+
|
|
242
|
+
[core]: https://github.com/sebastienrousseau/bankstatementparser
|
|
243
|
+
[pypi-url]: https://pypi.org/project/bankstatementparser-writer-xlsx/
|
|
244
|
+
[license-url]: https://opensource.org/license/apache-2-0/
|
|
245
|
+
[ci-url]: https://github.com/sebastienrousseau/bankstatementparser-writer-xlsx/actions/workflows/ci.yml
|
|
246
|
+
[pypi-badge]: https://img.shields.io/pypi/v/bankstatementparser-writer-xlsx.svg?style=for-the-badge
|
|
247
|
+
[python-versions-badge]: https://img.shields.io/pypi/pyversions/bankstatementparser-writer-xlsx.svg?style=for-the-badge
|
|
248
|
+
[license-badge]: https://img.shields.io/badge/License-Apache%202.0-blue.svg?style=for-the-badge
|
|
249
|
+
[coverage-badge]: https://img.shields.io/badge/Coverage-100%25-brightgreen?style=for-the-badge
|
|
250
|
+
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# bankstatementparser-writer-xlsx: Excel writer for parsed bank statements
|
|
2
|
+
|
|
3
|
+
[![PyPI Version][pypi-badge]][pypi-url]
|
|
4
|
+
[![Python Versions][python-versions-badge]][pypi-url]
|
|
5
|
+
[![License][license-badge]][license-url]
|
|
6
|
+
[![Coverage][coverage-badge]][ci-url]
|
|
7
|
+
|
|
8
|
+
**An Excel `.xlsx` writer for data parsed by
|
|
9
|
+
[`bankstatementparser`][core]** — turn parsed transactions (a pandas
|
|
10
|
+
`DataFrame`, a list of `Transaction` objects, or a list of plain dicts)
|
|
11
|
+
into a polished workbook that accountants, auditors, and reconciliation
|
|
12
|
+
macros can open directly.
|
|
13
|
+
|
|
14
|
+
> **Latest release: v0.0.10** — single `write_xlsx(data, path, ...)`
|
|
15
|
+
> function, 100% line + branch coverage, 100% docstring coverage,
|
|
16
|
+
> `mypy --strict` clean.
|
|
17
|
+
|
|
18
|
+
## Contents
|
|
19
|
+
|
|
20
|
+
- [Overview](#overview)
|
|
21
|
+
- [Install](#install)
|
|
22
|
+
- [Quick start](#quick-start)
|
|
23
|
+
- [Input shapes](#input-shapes)
|
|
24
|
+
- [Value coercion](#value-coercion)
|
|
25
|
+
- [The summary sheet](#the-summary-sheet)
|
|
26
|
+
- [Examples](#examples)
|
|
27
|
+
- [When not to use this package](#when-not-to-use-this-package)
|
|
28
|
+
- [Development](#development)
|
|
29
|
+
- [Security](#security)
|
|
30
|
+
- [Documentation](#documentation)
|
|
31
|
+
- [License](#license)
|
|
32
|
+
- [Contributing](#contributing)
|
|
33
|
+
- [Acknowledgements](#acknowledgements)
|
|
34
|
+
|
|
35
|
+
## Overview
|
|
36
|
+
|
|
37
|
+
`bankstatementparser-writer-xlsx` is a small, focused companion to the
|
|
38
|
+
[`bankstatementparser`][core] library. It does one thing well: given
|
|
39
|
+
already-parsed bank-statement records, write a clean Excel workbook with
|
|
40
|
+
a bold header row, one row per transaction, auto-sized columns, and an
|
|
41
|
+
optional second `Summary` sheet.
|
|
42
|
+
|
|
43
|
+
The package consumes _parsed_ data — it does not read PDFs, CSVs, or
|
|
44
|
+
XML itself. Parsing (and the security surface that comes with untrusted
|
|
45
|
+
input) lives upstream in the [`bankstatementparser`][core] core.
|
|
46
|
+
|
|
47
|
+
## Install
|
|
48
|
+
|
|
49
|
+
`bankstatementparser-writer-xlsx` runs on macOS, Linux, and Windows and
|
|
50
|
+
requires **Python 3.10+**. It pulls in `bankstatementparser`,
|
|
51
|
+
`openpyxl`, and `pandas` automatically.
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install bankstatementparser-writer-xlsx
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Quick start
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from bankstatementparser import CsvStatementParser
|
|
61
|
+
from bankstatementparser_writer_xlsx import write_xlsx
|
|
62
|
+
|
|
63
|
+
parser = CsvStatementParser("statement.csv")
|
|
64
|
+
df = parser.parse() # a pandas DataFrame
|
|
65
|
+
write_xlsx(df, "statement.xlsx") # one polished workbook
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
That's an Excel workbook ready for your accountant. Add a summary sheet
|
|
69
|
+
in one extra argument:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from bankstatementparser import CsvStatementParser
|
|
73
|
+
from bankstatementparser_writer_xlsx import write_xlsx
|
|
74
|
+
|
|
75
|
+
parser = CsvStatementParser("statement.csv")
|
|
76
|
+
df = parser.parse()
|
|
77
|
+
write_xlsx(df, "statement.xlsx", summary=parser.get_summary())
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Input shapes
|
|
81
|
+
|
|
82
|
+
`write_xlsx(data, path, *, sheet_name="Transactions", summary=None)`
|
|
83
|
+
accepts three input shapes and normalises each to a flat table:
|
|
84
|
+
|
|
85
|
+
| Input | Column order |
|
|
86
|
+
| :--- | :--- |
|
|
87
|
+
| `pandas.DataFrame` (from a parser's `.parse()`) | the DataFrame's own column order |
|
|
88
|
+
| `list[bankstatementparser.Transaction]` | the stable `Transaction` field order |
|
|
89
|
+
| `list[dict]` | the union of keys, in first-seen order |
|
|
90
|
+
|
|
91
|
+
A header row (bold) is written to the `sheet_name` sheet, followed by
|
|
92
|
+
one row per record. Columns are auto-sized to their widest cell (capped
|
|
93
|
+
so wide descriptions don't run off-screen).
|
|
94
|
+
|
|
95
|
+
**Empty input** is accepted: an empty `list` writes an empty sheet (no
|
|
96
|
+
header), while an empty `DataFrame` that still carries column labels
|
|
97
|
+
writes a header-only sheet.
|
|
98
|
+
|
|
99
|
+
## Value coercion
|
|
100
|
+
|
|
101
|
+
Spreadsheet cells can't hold arbitrary Python objects, so the writer
|
|
102
|
+
coerces the rich types the parser emits:
|
|
103
|
+
|
|
104
|
+
| Python type | Written as |
|
|
105
|
+
| :--- | :--- |
|
|
106
|
+
| `decimal.Decimal` | `float` (Excel has no decimal type; floats aggregate natively) |
|
|
107
|
+
| `datetime.date` / `datetime.datetime` | native Excel date cell (unchanged) |
|
|
108
|
+
| `str`, `int`, `float`, `bool`, `None` | unchanged |
|
|
109
|
+
| anything else | `str(value)` |
|
|
110
|
+
|
|
111
|
+
## The summary sheet
|
|
112
|
+
|
|
113
|
+
If you pass `summary=` a mapping (for example a parser's
|
|
114
|
+
`get_summary()` result), the writer adds a second sheet titled
|
|
115
|
+
`Summary` with a bold `Key` / `Value` header and one row per item:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from decimal import Decimal
|
|
119
|
+
|
|
120
|
+
from bankstatementparser_writer_xlsx import write_xlsx
|
|
121
|
+
|
|
122
|
+
transactions = [
|
|
123
|
+
{"date": "2026-06-01", "description": "Salary", "amount": Decimal("3000.00")},
|
|
124
|
+
{"date": "2026-06-03", "description": "Coffee Shop", "amount": Decimal("-4.20")},
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
write_xlsx(
|
|
128
|
+
transactions,
|
|
129
|
+
"out.xlsx",
|
|
130
|
+
summary={
|
|
131
|
+
"account_id": "DE89370400440532013000",
|
|
132
|
+
"transaction_count": 128,
|
|
133
|
+
"total_amount": Decimal("12045.67"),
|
|
134
|
+
"currency": "EUR",
|
|
135
|
+
},
|
|
136
|
+
)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Examples
|
|
140
|
+
|
|
141
|
+
Five runnable examples live in [`examples/`](examples/) and are
|
|
142
|
+
exercised in CI on every commit. Together they cover every supported
|
|
143
|
+
input shape and option of `write_xlsx`:
|
|
144
|
+
|
|
145
|
+
- [`01_write_dataframe.py`](examples/01_write_dataframe.py) — write a
|
|
146
|
+
pandas `DataFrame` to a single sheet.
|
|
147
|
+
- [`02_write_transactions.py`](examples/02_write_transactions.py) —
|
|
148
|
+
write a list of `Transaction` objects in stable field order.
|
|
149
|
+
- [`03_write_dicts.py`](examples/03_write_dicts.py) — write a list of
|
|
150
|
+
plain `dict` records (union of keys).
|
|
151
|
+
- [`04_write_with_summary.py`](examples/04_write_with_summary.py) —
|
|
152
|
+
write a DataFrame plus a second `Summary` sheet via `summary=`.
|
|
153
|
+
- [`05_custom_sheet_name.py`](examples/05_custom_sheet_name.py) — rename
|
|
154
|
+
the transactions sheet with `sheet_name=`.
|
|
155
|
+
|
|
156
|
+
## When not to use this package
|
|
157
|
+
|
|
158
|
+
- **You need a custom sheet layout.** The single-sheet (+ optional
|
|
159
|
+
Summary) structure is intentionally simple. Compose your own
|
|
160
|
+
`openpyxl` workbook if you need pivot-ready, multi-sheet layouts.
|
|
161
|
+
- **You need `.xls` (legacy binary).** `openpyxl` writes `.xlsx` only;
|
|
162
|
+
convert downstream if you must.
|
|
163
|
+
- **You need encrypted output.** Out of scope; encrypt the produced
|
|
164
|
+
`.xlsx` downstream with a tool like `msoffcrypto-tool`.
|
|
165
|
+
- **You want to _read_ Excel.** This package is a writer.
|
|
166
|
+
|
|
167
|
+
## Development
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
git clone https://github.com/sebastienrousseau/bankstatementparser-writer-xlsx
|
|
171
|
+
cd bankstatementparser-writer-xlsx
|
|
172
|
+
poetry env use python3.12
|
|
173
|
+
poetry install
|
|
174
|
+
poetry run pytest # 100% line + branch coverage gate
|
|
175
|
+
poetry run ruff check bankstatementparser_writer_xlsx tests
|
|
176
|
+
poetry run mypy bankstatementparser_writer_xlsx
|
|
177
|
+
poetry run interrogate -c pyproject.toml bankstatementparser_writer_xlsx
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Security
|
|
181
|
+
|
|
182
|
+
`bankstatementparser-writer-xlsx` consumes already-parsed data, not raw
|
|
183
|
+
statement files — the PDF/CSV/XML parsing surface lives upstream in the
|
|
184
|
+
[`bankstatementparser`][core] core. Reporting practice, supported
|
|
185
|
+
versions, and supply-chain posture are documented in
|
|
186
|
+
[`SECURITY.md`](SECURITY.md).
|
|
187
|
+
|
|
188
|
+
## Documentation
|
|
189
|
+
|
|
190
|
+
- [`README.md`](README.md) — this file
|
|
191
|
+
- [`ARCHITECTURE.md`](ARCHITECTURE.md) — module map and design decisions
|
|
192
|
+
- [`CHANGELOG.md`](CHANGELOG.md) — release notes
|
|
193
|
+
- [`ROADMAP.md`](ROADMAP.md) — what's next
|
|
194
|
+
- [`SECURITY.md`](SECURITY.md) — disclosure + supported versions
|
|
195
|
+
- [`examples/`](examples/) — runnable scripts, exercised in CI
|
|
196
|
+
|
|
197
|
+
## License
|
|
198
|
+
|
|
199
|
+
Licensed under the [Apache License, Version 2.0][license-url]. Any
|
|
200
|
+
contribution submitted for inclusion shall be licensed as above, without
|
|
201
|
+
additional terms.
|
|
202
|
+
|
|
203
|
+
## Contributing
|
|
204
|
+
|
|
205
|
+
Contributions are welcome — open an issue or PR on
|
|
206
|
+
[the repository](https://github.com/sebastienrousseau/bankstatementparser-writer-xlsx).
|
|
207
|
+
|
|
208
|
+
## Acknowledgements
|
|
209
|
+
|
|
210
|
+
Built on the [`bankstatementparser`][core] library and
|
|
211
|
+
[openpyxl](https://openpyxl.readthedocs.io/).
|
|
212
|
+
|
|
213
|
+
[core]: https://github.com/sebastienrousseau/bankstatementparser
|
|
214
|
+
[pypi-url]: https://pypi.org/project/bankstatementparser-writer-xlsx/
|
|
215
|
+
[license-url]: https://opensource.org/license/apache-2-0/
|
|
216
|
+
[ci-url]: https://github.com/sebastienrousseau/bankstatementparser-writer-xlsx/actions/workflows/ci.yml
|
|
217
|
+
[pypi-badge]: https://img.shields.io/pypi/v/bankstatementparser-writer-xlsx.svg?style=for-the-badge
|
|
218
|
+
[python-versions-badge]: https://img.shields.io/pypi/pyversions/bankstatementparser-writer-xlsx.svg?style=for-the-badge
|
|
219
|
+
[license-badge]: https://img.shields.io/badge/License-Apache%202.0-blue.svg?style=for-the-badge
|
|
220
|
+
[coverage-badge]: https://img.shields.io/badge/Coverage-100%25-brightgreen?style=for-the-badge
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Copyright (C) 2023-2026 Sebastien Rousseau.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
12
|
+
# implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
"""Excel (.xlsx) writer for parsed ``bankstatementparser`` data.
|
|
17
|
+
|
|
18
|
+
Exposes :func:`write_xlsx`, which serialises parsed bank-statement
|
|
19
|
+
records (a pandas DataFrame, a list of
|
|
20
|
+
:class:`bankstatementparser.Transaction` objects, or a list of plain
|
|
21
|
+
dicts) to a polished ``.xlsx`` workbook.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from .writer import write_xlsx
|
|
27
|
+
|
|
28
|
+
__all__ = ["write_xlsx", "__version__"]
|
|
29
|
+
|
|
30
|
+
__version__ = "0.0.10"
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
# Copyright (C) 2023-2026 Sebastien Rousseau.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
12
|
+
# implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
"""Excel ``.xlsx`` writer for parsed bank-statement data.
|
|
17
|
+
|
|
18
|
+
This module turns the output of a
|
|
19
|
+
[`bankstatementparser`](https://github.com/sebastienrousseau/bankstatementparser)
|
|
20
|
+
parser into a polished Excel workbook via
|
|
21
|
+
[openpyxl](https://openpyxl.readthedocs.io/).
|
|
22
|
+
|
|
23
|
+
The single public entry point is :func:`write_xlsx`, which accepts any
|
|
24
|
+
of three input shapes and normalises them to a flat, rectangular table:
|
|
25
|
+
|
|
26
|
+
* a :class:`pandas.DataFrame` (as returned by a parser's ``.parse()``),
|
|
27
|
+
* a list of :class:`bankstatementparser.Transaction` objects, or
|
|
28
|
+
* a list of plain ``dict`` row records.
|
|
29
|
+
|
|
30
|
+
The resulting workbook has a bold header row followed by one row per
|
|
31
|
+
record on the ``Transactions`` sheet, with column widths auto-sized for
|
|
32
|
+
readability. An optional ``summary`` mapping (e.g. the output of a
|
|
33
|
+
parser's ``get_summary()``) is written to a second ``Summary`` sheet as
|
|
34
|
+
key/value rows.
|
|
35
|
+
|
|
36
|
+
Value coercion
|
|
37
|
+
--------------
|
|
38
|
+
Cells must hold spreadsheet-friendly values, so the writer coerces the
|
|
39
|
+
rich Python types the parser emits:
|
|
40
|
+
|
|
41
|
+
* :class:`decimal.Decimal` is written as a ``float`` (Excel has no
|
|
42
|
+
decimal type; floats sort and aggregate natively in spreadsheets).
|
|
43
|
+
* :class:`datetime.date` and :class:`datetime.datetime` are written
|
|
44
|
+
unchanged — openpyxl serialises them as native Excel date cells.
|
|
45
|
+
* Every other value is written as-is when openpyxl accepts it
|
|
46
|
+
(``str``, ``int``, ``float``, ``bool``, ``None``) and otherwise
|
|
47
|
+
stringified via ``str()``.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
from __future__ import annotations
|
|
51
|
+
|
|
52
|
+
from collections.abc import Mapping, Sequence
|
|
53
|
+
from datetime import date, datetime
|
|
54
|
+
from decimal import Decimal
|
|
55
|
+
from pathlib import Path
|
|
56
|
+
from typing import Any
|
|
57
|
+
|
|
58
|
+
import pandas as pd
|
|
59
|
+
from openpyxl import Workbook
|
|
60
|
+
from openpyxl.styles import Font
|
|
61
|
+
from openpyxl.utils import get_column_letter
|
|
62
|
+
|
|
63
|
+
__all__ = ["write_xlsx"]
|
|
64
|
+
|
|
65
|
+
# Stable column order for ``Transaction`` rows. Mirrors the field order
|
|
66
|
+
# of ``bankstatementparser.Transaction`` so two runs over the same data
|
|
67
|
+
# always produce the same workbook layout.
|
|
68
|
+
_TRANSACTION_COLUMNS: tuple[str, ...] = (
|
|
69
|
+
"account_id",
|
|
70
|
+
"currency",
|
|
71
|
+
"amount",
|
|
72
|
+
"booking_date",
|
|
73
|
+
"value_date",
|
|
74
|
+
"description",
|
|
75
|
+
"normalized_description",
|
|
76
|
+
"reference",
|
|
77
|
+
"transaction_id",
|
|
78
|
+
"counterparty",
|
|
79
|
+
"source",
|
|
80
|
+
"source_index",
|
|
81
|
+
"source_method",
|
|
82
|
+
"confidence",
|
|
83
|
+
"category",
|
|
84
|
+
"transaction_hash",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Hard upper bound on auto-sized column width, in characters. Wide free
|
|
88
|
+
# text (descriptions) would otherwise push columns off-screen.
|
|
89
|
+
_MAX_COLUMN_WIDTH = 60
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def write_xlsx(
|
|
93
|
+
data: Any,
|
|
94
|
+
path: str | Path,
|
|
95
|
+
*,
|
|
96
|
+
sheet_name: str = "Transactions",
|
|
97
|
+
summary: Mapping[str, Any] | None = None,
|
|
98
|
+
) -> Path:
|
|
99
|
+
"""Write parsed bank-statement ``data`` to a ``.xlsx`` workbook.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
data: The records to serialise. One of:
|
|
103
|
+
|
|
104
|
+
* a :class:`pandas.DataFrame` (as returned by a
|
|
105
|
+
``bankstatementparser`` parser's ``.parse()`` method) —
|
|
106
|
+
its columns define the header order;
|
|
107
|
+
* a list of :class:`bankstatementparser.Transaction`
|
|
108
|
+
objects — serialised with a stable column order;
|
|
109
|
+
* a list of plain ``dict`` records — the header is the
|
|
110
|
+
union of keys in first-seen order.
|
|
111
|
+
|
|
112
|
+
An empty list (or empty DataFrame) is accepted and writes a
|
|
113
|
+
header-only sheet (header omitted entirely when no columns
|
|
114
|
+
can be determined).
|
|
115
|
+
path: The output ``.xlsx`` file path. The parent directory must
|
|
116
|
+
already exist; an existing file is overwritten.
|
|
117
|
+
sheet_name: The title of the sheet holding the transaction rows.
|
|
118
|
+
Defaults to ``"Transactions"``.
|
|
119
|
+
summary: Optional mapping (e.g. a parser's ``get_summary()``
|
|
120
|
+
result) written to a second ``"Summary"`` sheet as one
|
|
121
|
+
``key``/``value`` row per item, under a bold ``Key``/``Value``
|
|
122
|
+
header.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
The :class:`~pathlib.Path` of the written workbook.
|
|
126
|
+
|
|
127
|
+
Raises:
|
|
128
|
+
TypeError: If ``data`` is neither a DataFrame, nor a list/tuple
|
|
129
|
+
of ``Transaction`` objects, nor a list/tuple of ``dict``
|
|
130
|
+
records (and is not empty).
|
|
131
|
+
ValueError: If ``data`` is a non-empty sequence whose items mix
|
|
132
|
+
``dict`` and non-``dict`` types, or contain an unsupported
|
|
133
|
+
item type.
|
|
134
|
+
"""
|
|
135
|
+
columns, rows = _normalise(data)
|
|
136
|
+
|
|
137
|
+
workbook = Workbook()
|
|
138
|
+
sheet = workbook.active
|
|
139
|
+
sheet.title = sheet_name
|
|
140
|
+
_write_table(sheet, columns, rows)
|
|
141
|
+
|
|
142
|
+
if summary is not None:
|
|
143
|
+
summary_sheet = workbook.create_sheet("Summary")
|
|
144
|
+
_write_summary(summary_sheet, summary)
|
|
145
|
+
|
|
146
|
+
output = Path(path)
|
|
147
|
+
workbook.save(output)
|
|
148
|
+
return output
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _normalise(data: Any) -> tuple[list[str], list[list[Any]]]:
|
|
152
|
+
"""Normalise any supported ``data`` shape to columns and rows.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
data: The input passed to :func:`write_xlsx`.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
A ``(columns, rows)`` pair where ``columns`` is the ordered
|
|
159
|
+
header and ``rows`` is a list of cell-value lists aligned to it.
|
|
160
|
+
|
|
161
|
+
Raises:
|
|
162
|
+
TypeError: If ``data`` is of an unsupported top-level type.
|
|
163
|
+
ValueError: If a sequence mixes ``dict`` and non-``dict`` items
|
|
164
|
+
or contains an unsupported item type.
|
|
165
|
+
"""
|
|
166
|
+
if isinstance(data, pd.DataFrame):
|
|
167
|
+
return _normalise_dataframe(data)
|
|
168
|
+
if isinstance(data, list | tuple):
|
|
169
|
+
return _normalise_sequence(data)
|
|
170
|
+
raise TypeError(
|
|
171
|
+
"write_xlsx() expects a pandas DataFrame, a list of "
|
|
172
|
+
"Transaction objects, or a list of dict records; got "
|
|
173
|
+
f"{type(data).__name__}"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _normalise_dataframe(frame: Any) -> tuple[list[str], list[list[Any]]]:
|
|
178
|
+
"""Normalise a :class:`pandas.DataFrame` to columns and rows.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
frame: The DataFrame to convert. Its column order is preserved.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
A ``(columns, rows)`` pair with one row per DataFrame record.
|
|
185
|
+
"""
|
|
186
|
+
columns = [str(column) for column in frame.columns]
|
|
187
|
+
rows = [
|
|
188
|
+
[_coerce(value) for value in record]
|
|
189
|
+
for record in frame.itertuples(index=False, name=None)
|
|
190
|
+
]
|
|
191
|
+
return columns, rows
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _normalise_sequence(
|
|
195
|
+
records: Sequence[Any],
|
|
196
|
+
) -> tuple[list[str], list[list[Any]]]:
|
|
197
|
+
"""Normalise a list/tuple of Transactions or dicts to a table.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
records: A sequence of ``Transaction`` objects or ``dict``
|
|
201
|
+
records. May be empty.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
A ``(columns, rows)`` pair. An empty sequence yields empty
|
|
205
|
+
columns and rows.
|
|
206
|
+
|
|
207
|
+
Raises:
|
|
208
|
+
ValueError: If the items mix ``dict`` and non-``dict`` types or
|
|
209
|
+
an item is neither a ``dict`` nor a ``model_dump``-capable
|
|
210
|
+
object.
|
|
211
|
+
"""
|
|
212
|
+
if not records:
|
|
213
|
+
return [], []
|
|
214
|
+
|
|
215
|
+
dict_records = [_to_record(record) for record in records]
|
|
216
|
+
columns: list[str] = []
|
|
217
|
+
seen: set[str] = set()
|
|
218
|
+
for record in dict_records:
|
|
219
|
+
for key in record:
|
|
220
|
+
if key not in seen:
|
|
221
|
+
seen.add(key)
|
|
222
|
+
columns.append(key)
|
|
223
|
+
|
|
224
|
+
rows = [
|
|
225
|
+
[_coerce(record.get(column)) for column in columns]
|
|
226
|
+
for record in dict_records
|
|
227
|
+
]
|
|
228
|
+
return columns, rows
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _to_record(record: Any) -> dict[str, Any]:
|
|
232
|
+
"""Convert a single sequence item to an ordered ``dict`` record.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
record: A ``dict`` or a ``Transaction``-like object exposing a
|
|
236
|
+
``model_dump()`` method.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
An ordered ``dict`` of column name to value. ``Transaction``
|
|
240
|
+
objects use the stable column order in
|
|
241
|
+
:data:`_TRANSACTION_COLUMNS`; dicts preserve their own order.
|
|
242
|
+
|
|
243
|
+
Raises:
|
|
244
|
+
ValueError: If ``record`` is neither a ``dict`` nor exposes a
|
|
245
|
+
callable ``model_dump`` attribute.
|
|
246
|
+
"""
|
|
247
|
+
if isinstance(record, Mapping):
|
|
248
|
+
return dict(record)
|
|
249
|
+
model_dump = getattr(record, "model_dump", None)
|
|
250
|
+
if callable(model_dump):
|
|
251
|
+
dumped = model_dump()
|
|
252
|
+
ordered = {
|
|
253
|
+
key: dumped[key] for key in _TRANSACTION_COLUMNS if key in dumped
|
|
254
|
+
}
|
|
255
|
+
for key, value in dumped.items():
|
|
256
|
+
if key not in ordered:
|
|
257
|
+
ordered[key] = value
|
|
258
|
+
return ordered
|
|
259
|
+
raise ValueError(
|
|
260
|
+
"Sequence items must be dict records or Transaction objects "
|
|
261
|
+
f"(with a model_dump() method); got {type(record).__name__}"
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _coerce(value: Any) -> Any:
|
|
266
|
+
"""Coerce a Python value to a spreadsheet-friendly cell value.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
value: A value drawn from a record.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
``float`` for :class:`decimal.Decimal`; the value unchanged for
|
|
273
|
+
``date``/``datetime`` and for the scalar types openpyxl accepts
|
|
274
|
+
natively (``str``, ``int``, ``float``, ``bool``, ``None``);
|
|
275
|
+
``str(value)`` for anything else.
|
|
276
|
+
"""
|
|
277
|
+
if isinstance(value, bool):
|
|
278
|
+
return value
|
|
279
|
+
if isinstance(value, Decimal):
|
|
280
|
+
return float(value)
|
|
281
|
+
if isinstance(value, datetime | date):
|
|
282
|
+
return value
|
|
283
|
+
if value is None or isinstance(value, str | int | float):
|
|
284
|
+
return value
|
|
285
|
+
return str(value)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _write_table(
|
|
289
|
+
sheet: Any,
|
|
290
|
+
columns: list[str],
|
|
291
|
+
rows: list[list[Any]],
|
|
292
|
+
) -> None:
|
|
293
|
+
"""Write the header and data rows, then auto-size the columns.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
sheet: The target openpyxl worksheet.
|
|
297
|
+
columns: The ordered header labels. May be empty, in which case
|
|
298
|
+
nothing is written.
|
|
299
|
+
rows: The cell-value rows aligned to ``columns``.
|
|
300
|
+
"""
|
|
301
|
+
if not columns:
|
|
302
|
+
return
|
|
303
|
+
sheet.append(columns)
|
|
304
|
+
for cell in sheet[1]:
|
|
305
|
+
cell.font = Font(bold=True)
|
|
306
|
+
for row in rows:
|
|
307
|
+
sheet.append(row)
|
|
308
|
+
_autosize(sheet, columns, rows)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _write_summary(sheet: Any, summary: Mapping[str, Any]) -> None:
|
|
312
|
+
"""Write a ``summary`` mapping as key/value rows on ``sheet``.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
sheet: The target openpyxl worksheet (the ``Summary`` sheet).
|
|
316
|
+
summary: The mapping to serialise, one row per item under a
|
|
317
|
+
bold ``Key``/``Value`` header.
|
|
318
|
+
"""
|
|
319
|
+
sheet.append(["Key", "Value"])
|
|
320
|
+
for cell in sheet[1]:
|
|
321
|
+
cell.font = Font(bold=True)
|
|
322
|
+
keys = list(summary)
|
|
323
|
+
for key in keys:
|
|
324
|
+
sheet.append([str(key), _coerce(summary[key])])
|
|
325
|
+
_autosize(
|
|
326
|
+
sheet,
|
|
327
|
+
["Key", "Value"],
|
|
328
|
+
[[str(key), _coerce(summary[key])] for key in keys],
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _autosize(
|
|
333
|
+
sheet: Any,
|
|
334
|
+
columns: list[str],
|
|
335
|
+
rows: list[list[Any]],
|
|
336
|
+
) -> None:
|
|
337
|
+
"""Set each column width to fit its widest cell, within a cap.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
sheet: The worksheet whose column dimensions are adjusted.
|
|
341
|
+
columns: The header labels, used as the initial width seed.
|
|
342
|
+
rows: The data rows scanned for their widest cell per column.
|
|
343
|
+
"""
|
|
344
|
+
for index, header in enumerate(columns):
|
|
345
|
+
width = len(str(header))
|
|
346
|
+
for row in rows:
|
|
347
|
+
cell = row[index]
|
|
348
|
+
if cell is not None:
|
|
349
|
+
width = max(width, len(str(cell)))
|
|
350
|
+
letter = get_column_letter(index + 1)
|
|
351
|
+
sheet.column_dimensions[letter].width = min(
|
|
352
|
+
width + 2, _MAX_COLUMN_WIDTH
|
|
353
|
+
)
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "bankstatementparser-writer-xlsx"
|
|
3
|
+
version = "0.0.10"
|
|
4
|
+
description = "Excel (.xlsx) writer for bankstatementparser-parsed bank statements."
|
|
5
|
+
authors = ["Sebastien Rousseau <sebastian.rousseau@gmail.com>"]
|
|
6
|
+
license = "Apache-2.0"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
repository = "https://github.com/sebastienrousseau/bankstatementparser-writer-xlsx"
|
|
9
|
+
homepage = "https://bankstatementparser.com"
|
|
10
|
+
keywords = [
|
|
11
|
+
"bank-statements",
|
|
12
|
+
"bankstatementparser",
|
|
13
|
+
"xlsx",
|
|
14
|
+
"excel",
|
|
15
|
+
"openpyxl",
|
|
16
|
+
"accounting",
|
|
17
|
+
"reconciliation",
|
|
18
|
+
]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 3 - Alpha",
|
|
21
|
+
"Intended Audience :: Financial and Insurance Industry",
|
|
22
|
+
"License :: OSI Approved :: Apache Software License",
|
|
23
|
+
"Operating System :: OS Independent",
|
|
24
|
+
"Programming Language :: Python :: 3",
|
|
25
|
+
"Programming Language :: Python :: 3.10",
|
|
26
|
+
"Programming Language :: Python :: 3.11",
|
|
27
|
+
"Programming Language :: Python :: 3.12",
|
|
28
|
+
"Topic :: Office/Business :: Financial",
|
|
29
|
+
"Topic :: Office/Business :: Financial :: Accounting",
|
|
30
|
+
]
|
|
31
|
+
packages = [
|
|
32
|
+
{ include = "bankstatementparser_writer_xlsx" },
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[tool.poetry.dependencies]
|
|
36
|
+
python = ">=3.10,<4.0"
|
|
37
|
+
bankstatementparser = ">=0.0.9"
|
|
38
|
+
openpyxl = ">=3.1,<4"
|
|
39
|
+
pandas = ">=2.0"
|
|
40
|
+
|
|
41
|
+
[tool.poetry.group.dev.dependencies]
|
|
42
|
+
pytest = ">=9.0.3,<10.0.0"
|
|
43
|
+
pytest-cov = ">=6.0.0,<8.0.0"
|
|
44
|
+
black = "^26.3.1"
|
|
45
|
+
ruff = "^0.1.0"
|
|
46
|
+
mypy = "^1.11.0"
|
|
47
|
+
bandit = "^1.7.0"
|
|
48
|
+
interrogate = "^1.7.0"
|
|
49
|
+
|
|
50
|
+
[build-system]
|
|
51
|
+
requires = ["poetry-core"]
|
|
52
|
+
build-backend = "poetry.core.masonry.api"
|
|
53
|
+
|
|
54
|
+
[tool.black]
|
|
55
|
+
line-length = 79
|
|
56
|
+
target-version = ['py310']
|
|
57
|
+
|
|
58
|
+
[tool.pytest.ini_options]
|
|
59
|
+
testpaths = ["tests"]
|
|
60
|
+
python_files = "test_*.py"
|
|
61
|
+
python_classes = "Test*"
|
|
62
|
+
python_functions = "test_*"
|
|
63
|
+
addopts = ["-ra", "--strict-markers", "--tb=short"]
|
|
64
|
+
|
|
65
|
+
[tool.coverage.run]
|
|
66
|
+
branch = true
|
|
67
|
+
source = ["bankstatementparser_writer_xlsx"]
|
|
68
|
+
|
|
69
|
+
[tool.coverage.report]
|
|
70
|
+
exclude_also = [
|
|
71
|
+
"if __name__ == .__main__.:",
|
|
72
|
+
"pragma: no cover",
|
|
73
|
+
"@overload",
|
|
74
|
+
]
|
|
75
|
+
fail_under = 100
|
|
76
|
+
show_missing = true
|
|
77
|
+
skip_covered = false
|
|
78
|
+
|
|
79
|
+
[tool.interrogate]
|
|
80
|
+
fail-under = 100
|
|
81
|
+
ignore-init-method = true
|
|
82
|
+
ignore-semiprivate = false
|
|
83
|
+
ignore-private = false
|
|
84
|
+
ignore-magic = true
|
|
85
|
+
exclude = ["tests", "examples"]
|
|
86
|
+
|
|
87
|
+
[tool.ruff]
|
|
88
|
+
line-length = 79
|
|
89
|
+
target-version = "py310"
|
|
90
|
+
extend-exclude = ["*.md", "tmp_env", ".venv", ".eggs"]
|
|
91
|
+
|
|
92
|
+
[tool.ruff.lint]
|
|
93
|
+
select = ["E", "W", "F", "I", "B", "C4", "UP"]
|
|
94
|
+
ignore = ["E501"]
|
|
95
|
+
|
|
96
|
+
[tool.ruff.lint.per-file-ignores]
|
|
97
|
+
"__init__.py" = ["F401"]
|
|
98
|
+
|
|
99
|
+
[tool.ruff.format]
|
|
100
|
+
quote-style = "double"
|
|
101
|
+
indent-style = "space"
|
|
102
|
+
skip-magic-trailing-comma = false
|
|
103
|
+
|
|
104
|
+
[tool.mypy]
|
|
105
|
+
python_version = "3.10"
|
|
106
|
+
strict = true
|
|
107
|
+
warn_return_any = true
|
|
108
|
+
warn_unused_configs = true
|
|
109
|
+
disallow_untyped_defs = true
|
|
110
|
+
disallow_any_unimported = false
|
|
111
|
+
no_implicit_optional = true
|
|
112
|
+
warn_redundant_casts = true
|
|
113
|
+
warn_unused_ignores = true
|
|
114
|
+
warn_no_return = true
|
|
115
|
+
check_untyped_defs = true
|
|
116
|
+
strict_equality = true
|
|
117
|
+
exclude = [
|
|
118
|
+
"tmp_env/",
|
|
119
|
+
".venv/",
|
|
120
|
+
".eggs/",
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
[[tool.mypy.overrides]]
|
|
124
|
+
module = "bankstatementparser.*"
|
|
125
|
+
ignore_missing_imports = true
|
|
126
|
+
|
|
127
|
+
[[tool.mypy.overrides]]
|
|
128
|
+
module = "openpyxl"
|
|
129
|
+
ignore_missing_imports = true
|
|
130
|
+
|
|
131
|
+
[[tool.mypy.overrides]]
|
|
132
|
+
module = "openpyxl.*"
|
|
133
|
+
ignore_missing_imports = true
|
|
134
|
+
|
|
135
|
+
[[tool.mypy.overrides]]
|
|
136
|
+
module = "pandas.*"
|
|
137
|
+
ignore_missing_imports = true
|
|
138
|
+
|
|
139
|
+
[[tool.mypy.overrides]]
|
|
140
|
+
# The writer accepts heterogeneous record shapes (DataFrame, Transaction,
|
|
141
|
+
# dict) normalised through ``Any``-typed cells, so relax the two strict
|
|
142
|
+
# checks that signature would trip while keeping the rest of strict mode.
|
|
143
|
+
module = "bankstatementparser_writer_xlsx.*"
|
|
144
|
+
disallow_any_generics = false
|
|
145
|
+
warn_return_any = false
|
|
146
|
+
|
|
147
|
+
[[tool.mypy.overrides]]
|
|
148
|
+
module = "tests.*"
|
|
149
|
+
disallow_untyped_defs = false
|
|
150
|
+
disallow_incomplete_defs = false
|
|
151
|
+
disallow_untyped_calls = false
|
|
152
|
+
warn_unused_ignores = false
|
|
153
|
+
disable_error_code = ["arg-type", "return-value", "attr-defined", "assignment"]
|