vme-py 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vme_py-0.1.0/.gitignore +108 -0
- vme_py-0.1.0/LICENSE +21 -0
- vme_py-0.1.0/PKG-INFO +307 -0
- vme_py-0.1.0/README.md +269 -0
- vme_py-0.1.0/examples/budget-august.csv +25 -0
- vme_py-0.1.0/examples/expenses-multicurrency.json +20 -0
- vme_py-0.1.0/examples/rates-eur.csv +5 -0
- vme_py-0.1.0/examples/rates-pln.json +12 -0
- vme_py-0.1.0/examples/trip-multicurrency.csv +27 -0
- vme_py-0.1.0/pyproject.toml +83 -0
- vme_py-0.1.0/src/vme/__init__.py +34 -0
- vme_py-0.1.0/src/vme/cli.py +376 -0
- vme_py-0.1.0/src/vme/currencies.py +281 -0
- vme_py-0.1.0/src/vme/data_store.py +316 -0
- vme_py-0.1.0/src/vme/examples.py +66 -0
- vme_py-0.1.0/src/vme/io/__init__.py +41 -0
- vme_py-0.1.0/src/vme/io/base.py +96 -0
- vme_py-0.1.0/src/vme/io/camt.py +105 -0
- vme_py-0.1.0/src/vme/io/csv_loader.py +86 -0
- vme_py-0.1.0/src/vme/io/excel.py +53 -0
- vme_py-0.1.0/src/vme/io/json_loader.py +77 -0
- vme_py-0.1.0/src/vme/io/ofx.py +94 -0
- vme_py-0.1.0/src/vme/io/qif.py +80 -0
- vme_py-0.1.0/src/vme/models.py +174 -0
- vme_py-0.1.0/src/vme/plotting.py +538 -0
- vme_py-0.1.0/src/vme/sankey.py +179 -0
- vme_py-0.1.0/src/vme/theme.py +87 -0
- vme_py-0.1.0/src/vme/tools.py +366 -0
- vme_py-0.1.0/src/vme/visualizer.py +188 -0
- vme_py-0.1.0/tests/conftest.py +48 -0
- vme_py-0.1.0/tests/test_cli.py +210 -0
- vme_py-0.1.0/tests/test_data_store.py +229 -0
- vme_py-0.1.0/tests/test_io.py +217 -0
- vme_py-0.1.0/tests/test_sankey.py +176 -0
- vme_py-0.1.0/tests/test_tools.py +217 -0
- vme_py-0.1.0/tests/test_visualizer.py +221 -0
vme_py-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
*.egg-info/
|
|
24
|
+
.installed.cfg
|
|
25
|
+
*.egg
|
|
26
|
+
MANIFEST
|
|
27
|
+
|
|
28
|
+
# PyInstaller
|
|
29
|
+
# Usually these files are written by a python script from a template
|
|
30
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
31
|
+
*.manifest
|
|
32
|
+
*.spec
|
|
33
|
+
|
|
34
|
+
# Installer logs
|
|
35
|
+
pip-log.txt
|
|
36
|
+
pip-delete-this-directory.txt
|
|
37
|
+
|
|
38
|
+
# Unit test / coverage reports
|
|
39
|
+
htmlcov/
|
|
40
|
+
.tox/
|
|
41
|
+
.coverage
|
|
42
|
+
.coverage.*
|
|
43
|
+
.cache
|
|
44
|
+
nosetests.xml
|
|
45
|
+
coverage.xml
|
|
46
|
+
*.cover
|
|
47
|
+
.hypothesis/
|
|
48
|
+
.pytest_cache/
|
|
49
|
+
|
|
50
|
+
# Translations
|
|
51
|
+
*.mo
|
|
52
|
+
*.pot
|
|
53
|
+
|
|
54
|
+
# Django stuff:
|
|
55
|
+
*.log
|
|
56
|
+
local_settings.py
|
|
57
|
+
db.sqlite3
|
|
58
|
+
|
|
59
|
+
# Flask stuff:
|
|
60
|
+
instance/
|
|
61
|
+
.webassets-cache
|
|
62
|
+
|
|
63
|
+
# Scrapy stuff:
|
|
64
|
+
.scrapy
|
|
65
|
+
|
|
66
|
+
# Sphinx documentation
|
|
67
|
+
docs/_build/
|
|
68
|
+
|
|
69
|
+
# PyBuilder
|
|
70
|
+
target/
|
|
71
|
+
|
|
72
|
+
# Jupyter Notebook
|
|
73
|
+
.ipynb_checkpoints
|
|
74
|
+
|
|
75
|
+
# pyenv
|
|
76
|
+
.python-version
|
|
77
|
+
|
|
78
|
+
# celery beat schedule file
|
|
79
|
+
celerybeat-schedule
|
|
80
|
+
|
|
81
|
+
# SageMath parsed files
|
|
82
|
+
*.sage.py
|
|
83
|
+
|
|
84
|
+
# Environments
|
|
85
|
+
.env
|
|
86
|
+
.venv
|
|
87
|
+
env/
|
|
88
|
+
venv/
|
|
89
|
+
ENV/
|
|
90
|
+
env.bak/
|
|
91
|
+
venv.bak/
|
|
92
|
+
|
|
93
|
+
# Spyder project settings
|
|
94
|
+
.spyderproject
|
|
95
|
+
.spyproject
|
|
96
|
+
|
|
97
|
+
# Rope project settings
|
|
98
|
+
.ropeproject
|
|
99
|
+
|
|
100
|
+
# mkdocs documentation
|
|
101
|
+
/site
|
|
102
|
+
|
|
103
|
+
# mypy
|
|
104
|
+
.mypy_cache/
|
|
105
|
+
|
|
106
|
+
# Output written by usage.py (the checked-in examples are the other files there)
|
|
107
|
+
examples/output/usage-*.png
|
|
108
|
+
|
vme_py-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2019 Oskar Jarczyk
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
vme_py-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: vme-py
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Turn a list of budget expenses into a Sankey diagram you can share as a PNG.
|
|
5
|
+
Project-URL: Homepage, https://github.com/oskar-j/visualize-my-expenses
|
|
6
|
+
Project-URL: Source, https://github.com/oskar-j/visualize-my-expenses
|
|
7
|
+
Project-URL: Issues, https://github.com/oskar-j/visualize-my-expenses/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/oskar-j/visualize-my-expenses/releases
|
|
9
|
+
Author: Oskar Jarczyk
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: budget,expenses,personal-finance,plot,sankey,visualization
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
25
|
+
Classifier: Topic :: Office/Business :: Financial :: Accounting
|
|
26
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
27
|
+
Requires-Python: >=3.9
|
|
28
|
+
Requires-Dist: click>=8.0
|
|
29
|
+
Requires-Dist: matplotlib>=3.5
|
|
30
|
+
Provides-Extra: all
|
|
31
|
+
Requires-Dist: openpyxl>=3.0; extra == 'all'
|
|
32
|
+
Requires-Dist: plotly>=5.0; extra == 'all'
|
|
33
|
+
Provides-Extra: excel
|
|
34
|
+
Requires-Dist: openpyxl>=3.0; extra == 'excel'
|
|
35
|
+
Provides-Extra: html
|
|
36
|
+
Requires-Dist: plotly>=5.0; extra == 'html'
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
|
|
39
|
+
# visualize-my-expenses
|
|
40
|
+
|
|
41
|
+
[](https://pypi.org/project/vme-py/)
|
|
42
|
+
[](https://github.com/oskar-j/visualize-my-expenses/actions/workflows/ci.yml)
|
|
43
|
+
|
|
44
|
+
Turn a month of budget rows into a Sankey diagram you can share as a PNG.
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install vme-py
|
|
48
|
+
vme render budget.csv -c PLN -o august.png --month 2026-08
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+

|
|
52
|
+
|
|
53
|
+
Money flows left to right: income sources → your budget → categories → what you
|
|
54
|
+
actually bought. Whatever you did not spend leaves as **Savings / left over**, so
|
|
55
|
+
the picture always balances.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Install
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install vme-py
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The package is called `vme-py` on PyPI; the command it installs and the module
|
|
66
|
+
you import are both `vme`. If you only want the command, give it an environment
|
|
67
|
+
of its own:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
uv tool install vme-py # or: pipx install vme-py
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Python 3.9 or newer. The core needs only `click` and `matplotlib` — no browser,
|
|
74
|
+
no headless Chrome, no network. Two optional extras:
|
|
75
|
+
|
|
76
|
+
| Extra | Adds | For |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `vme-py[excel]` | `openpyxl` | reading `.xlsx` workbooks |
|
|
79
|
+
| `vme-py[html]` | `plotly` | writing an interactive `.html` version |
|
|
80
|
+
| `vme-py[all]` | both | |
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pip install "vme-py[all]"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
To work on the code, install from a clone instead:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
git clone https://github.com/oskar-j/visualize-my-expenses
|
|
90
|
+
cd visualize-my-expenses
|
|
91
|
+
uv sync # .venv as pinned by uv.lock: the package, both extras, the dev tools
|
|
92
|
+
# or: python -m venv .venv && source .venv/bin/activate && pip install -e ".[all]"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Try it in 30 seconds
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
vme sample budget.csv # writes an example file
|
|
99
|
+
vme render budget.csv -c PLN -o august.png # draws it
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Input formats
|
|
103
|
+
|
|
104
|
+
Point `vme` at whatever your bank or budget app exports; the format is guessed
|
|
105
|
+
from the file name, and `--format` overrides the guess.
|
|
106
|
+
|
|
107
|
+
| Format | Extensions | Notes |
|
|
108
|
+
|---|---|---|
|
|
109
|
+
| CSV / TSV | `.csv` `.tsv` `.txt` | separator and encoding are sniffed |
|
|
110
|
+
| JSON | `.json` | an array of rows, or `{"expenses": [...]}` |
|
|
111
|
+
| JSON Lines | `.jsonl` `.ndjson` | one row per line |
|
|
112
|
+
| OFX / QFX | `.ofx` `.qfx` | most US banks, Quicken, MS Money |
|
|
113
|
+
| QIF | `.qif` | older Quicken exports |
|
|
114
|
+
| ISO 20022 camt | `.xml` `.camt` | camt.052/053/054 SEPA statements |
|
|
115
|
+
| Excel | `.xlsx` `.xlsm` | needs the `excel` extra |
|
|
116
|
+
|
|
117
|
+
`vme formats` prints the same list.
|
|
118
|
+
|
|
119
|
+
Column names are matched loosely, so a bank export usually works untouched —
|
|
120
|
+
`Posted Date`, `Description`, `Transaction Amount`, `Debit/Credit`, `kwota` and
|
|
121
|
+
`waluta` are all understood. Amounts written as `1,234.56`, `1.234,56`,
|
|
122
|
+
`1 234,56`, `(12.00)`, `120.00-` or `$1,234.56` all parse.
|
|
123
|
+
|
|
124
|
+
The minimum a CSV needs is an amount:
|
|
125
|
+
|
|
126
|
+
```csv
|
|
127
|
+
date,category,label,amount,currency,kind
|
|
128
|
+
2026-08-01,Income,Salary,9800.00,PLN,income
|
|
129
|
+
2026-08-02,Housing,Rent,3200.00,PLN,expense
|
|
130
|
+
2026-08-04,Groceries,Biedronka,412.85,PLN,expense
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`category` groups, `label` is the detail inside the group, `kind` is `income` or
|
|
134
|
+
`expense`. Leave `kind` out and the direction is inferred: if any amount is
|
|
135
|
+
negative the file is read as a bank statement (negative = money out); if none
|
|
136
|
+
is, every row is spending.
|
|
137
|
+
|
|
138
|
+
## Multiple currencies
|
|
139
|
+
|
|
140
|
+
Each row carries its own currency, and `-c/--currency` says which one the
|
|
141
|
+
*picture* is drawn in. Anything else needs a rate — one unit of that currency
|
|
142
|
+
expressed in the report currency:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
vme render trip.csv -c PLN --rate EUR=4.30 --rate UAH=0.095 --rate USD=3.95
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+

|
|
149
|
+
|
|
150
|
+
Rates can live in a file instead (`--rates rates.json`), as JSON or CSV:
|
|
151
|
+
|
|
152
|
+
```json
|
|
153
|
+
{ "base": "PLN", "rates": { "EUR": 4.30, "USD": 3.95, "UAH": 0.095 } }
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
```csv
|
|
157
|
+
currency,rate
|
|
158
|
+
EUR,4.30
|
|
159
|
+
UAH,0.095
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
A `--rate` flag overrides the same currency in the file. Rates are never fetched
|
|
163
|
+
from the internet: the same input has to draw the same picture next month, and a
|
|
164
|
+
rate that quietly changes between two runs would break that.
|
|
165
|
+
|
|
166
|
+
Around 50 currencies — including EUR, PLN, UAH, CZK, GBP, USD, JPY and BTC —
|
|
167
|
+
know their own symbol, decimal places and separators, so amounts are written the
|
|
168
|
+
way that currency is normally written (`1 234,50 zł`, `€1,234.50`, `¥1,235`).
|
|
169
|
+
Anything else still works and prints its ISO code. `vme currencies` lists them.
|
|
170
|
+
|
|
171
|
+
Forget a rate and the error tells you what to add:
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
error: rows are in EUR (Euro), UAH (Ukrainian hryvnia) but the report is in PLN.
|
|
175
|
+
Give a rate for each one, for example: --rate EUR=4.30 (one EUR is worth 4.30 PLN)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Command line
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
vme render SOURCE draw a Sankey diagram
|
|
182
|
+
vme summary SOURCE print the same breakdown as a table
|
|
183
|
+
vme check SOURCE report anything that would stop it being plotted
|
|
184
|
+
vme formats list the input formats
|
|
185
|
+
vme currencies list the known currencies
|
|
186
|
+
vme sample FILE write an example file to start from
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Useful `render` options:
|
|
190
|
+
|
|
191
|
+
| Option | Does |
|
|
192
|
+
|---|---|
|
|
193
|
+
| `-o out.png` | `.png`, `.svg`, `.pdf` or `.html` — picked by extension |
|
|
194
|
+
| `-c PLN` | currency the report is drawn in |
|
|
195
|
+
| `-m 2026-08` | one month; also `-y 2026` and `-p START..END` |
|
|
196
|
+
| `--top 8` | keep the 8 biggest categories, fold the rest into "Other" |
|
|
197
|
+
| `--min-share 1` | fold anything under 1% of the total |
|
|
198
|
+
| `--max-labels 6` | most detail rows to show inside one category |
|
|
199
|
+
| `--no-detail` | stop at categories, skip the label column |
|
|
200
|
+
| `--theme dark` | dark background |
|
|
201
|
+
| `--width 1600 --dpi 200` | image size; height scales with the row count |
|
|
202
|
+
| `--transparent` | transparent background |
|
|
203
|
+
| `--sign statement` | force how a negative amount is read |
|
|
204
|
+
| `--open` | open the file when it is done |
|
|
205
|
+
|
|
206
|
+
Sharing a picture in a chat app: the defaults (1600px wide, 200 dpi, light
|
|
207
|
+
theme) are already sized for it. For a dark-mode chat, add `--theme dark`.
|
|
208
|
+
|
|
209
|
+

|
|
210
|
+
|
|
211
|
+
## Python API
|
|
212
|
+
|
|
213
|
+
`Visualizer` is the whole public surface.
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
from vme import Visualizer
|
|
217
|
+
|
|
218
|
+
Visualizer.from_file("august.csv", currency="PLN").create_png("august.png")
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Or build the rows yourself — dicts, namedtuples and `Expense` objects all work:
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
from vme import Expense, Visualizer
|
|
225
|
+
|
|
226
|
+
rows = [
|
|
227
|
+
Expense(category="Income", label="Salary", amount=9800, currency="PLN", kind="income"),
|
|
228
|
+
Expense(category="Housing", label="Rent", amount=3200, currency="PLN"),
|
|
229
|
+
Expense(category="Food", label="Lidl", amount=412, currency="PLN"),
|
|
230
|
+
]
|
|
231
|
+
|
|
232
|
+
v = Visualizer(rows, currency="PLN", title="August 2026", theme="light")
|
|
233
|
+
v.print_to_console()
|
|
234
|
+
v.create_png("august.png", width=1600, dpi=200)
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
| Method | Does |
|
|
238
|
+
|---|---|
|
|
239
|
+
| `Visualizer.from_file(path, fmt=None, ...)` | read any supported file |
|
|
240
|
+
| `create_png(path)` | write a PNG (also `.svg`, `.pdf`) |
|
|
241
|
+
| `create_html(path)` | write an interactive plotly page — needs the `html` extra |
|
|
242
|
+
| `save(path)` | pick the writer from the extension |
|
|
243
|
+
| `show_plot()` | open an interactive window |
|
|
244
|
+
| `print_to_console()` | the breakdown as text |
|
|
245
|
+
| `graph()` | the `SankeyGraph`, if you want to draw it yourself |
|
|
246
|
+
| `problems()` | what is wrong with the rows, as a list |
|
|
247
|
+
|
|
248
|
+
Constructor arguments: `currency`, `rates`, `period`, `sign`, `title`,
|
|
249
|
+
`subtitle`, `theme`, `top_categories`, `max_labels`, `min_share`, `detail`,
|
|
250
|
+
`show_savings`, `group_income`, `verbose`.
|
|
251
|
+
|
|
252
|
+
Below `Visualizer` sit `vme.io` (loading), `vme.sankey` (rows → graph),
|
|
253
|
+
`vme.plotting` (graph → picture) and `vme.currencies`; each is usable on its own.
|
|
254
|
+
|
|
255
|
+
## Development
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
uv sync # or: pip install -e ".[all]" pytest ruff
|
|
259
|
+
uv run pytest
|
|
260
|
+
uv run ruff check src tests
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
`uv.lock` pins the development environment, and CI installs from it with
|
|
264
|
+
`--locked`, so after changing a dependency in `pyproject.toml` run `uv lock` and
|
|
265
|
+
commit both files. The lock never reaches PyPI — `pip install vme-py` gets the
|
|
266
|
+
version ranges from `pyproject.toml` — and working from a clone with pip simply
|
|
267
|
+
ignores it.
|
|
268
|
+
|
|
269
|
+
Layout:
|
|
270
|
+
|
|
271
|
+
```
|
|
272
|
+
src/vme/
|
|
273
|
+
models.py Expense, Node, Link, SankeyGraph
|
|
274
|
+
currencies.py currency metadata, formatting, rate files
|
|
275
|
+
tools.py amount/date/direction parsing, row coercion
|
|
276
|
+
io/ one module per input format, self-registering
|
|
277
|
+
data_store.py row storage, sign conventions, conversion, validation
|
|
278
|
+
sankey.py rows -> graph (folding, savings branch)
|
|
279
|
+
plotting.py layout + the matplotlib and plotly backends
|
|
280
|
+
theme.py light and dark palettes
|
|
281
|
+
cli.py the click commands
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
The colour palettes are checked for colour-blind separation and for contrast
|
|
285
|
+
against their own background, and every node carries a visible label, so no
|
|
286
|
+
reading of the chart depends on telling two colours apart.
|
|
287
|
+
|
|
288
|
+
### Releasing
|
|
289
|
+
|
|
290
|
+
Every pull request is linted, tested (Linux on Python 3.9–3.14, plus macOS and
|
|
291
|
+
Windows) and built into a wheel that is installed and run. A merge to `master`
|
|
292
|
+
does all of that again and then publishes to PyPI — but only if the version in
|
|
293
|
+
`pyproject.toml` is not on PyPI yet. So a release is two steps:
|
|
294
|
+
|
|
295
|
+
1. in the pull request, bump the version with `uv version --bump patch` (or
|
|
296
|
+
`minor`), which updates `pyproject.toml` and `uv.lock` together — edit
|
|
297
|
+
`pyproject.toml` by hand and you need `uv lock` as well, or CI fails;
|
|
298
|
+
2. merge it.
|
|
299
|
+
|
|
300
|
+
CI uploads through PyPI's trusted publishing, so no API token is stored
|
|
301
|
+
anywhere; then it tags the commit `vX.Y.Z` and creates a GitHub release with the
|
|
302
|
+
built files. A merge that leaves the version alone publishes nothing. PyPI never
|
|
303
|
+
takes the same version twice, so a broken release is fixed by the next one.
|
|
304
|
+
|
|
305
|
+
## Licence
|
|
306
|
+
|
|
307
|
+
MIT — see [LICENSE](https://github.com/oskar-j/visualize-my-expenses/blob/master/LICENSE).
|
vme_py-0.1.0/README.md
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# visualize-my-expenses
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/vme-py/)
|
|
4
|
+
[](https://github.com/oskar-j/visualize-my-expenses/actions/workflows/ci.yml)
|
|
5
|
+
|
|
6
|
+
Turn a month of budget rows into a Sankey diagram you can share as a PNG.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install vme-py
|
|
10
|
+
vme render budget.csv -c PLN -o august.png --month 2026-08
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+
Money flows left to right: income sources → your budget → categories → what you
|
|
16
|
+
actually bought. Whatever you did not spend leaves as **Savings / left over**, so
|
|
17
|
+
the picture always balances.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install vme-py
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The package is called `vme-py` on PyPI; the command it installs and the module
|
|
28
|
+
you import are both `vme`. If you only want the command, give it an environment
|
|
29
|
+
of its own:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
uv tool install vme-py # or: pipx install vme-py
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Python 3.9 or newer. The core needs only `click` and `matplotlib` — no browser,
|
|
36
|
+
no headless Chrome, no network. Two optional extras:
|
|
37
|
+
|
|
38
|
+
| Extra | Adds | For |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| `vme-py[excel]` | `openpyxl` | reading `.xlsx` workbooks |
|
|
41
|
+
| `vme-py[html]` | `plotly` | writing an interactive `.html` version |
|
|
42
|
+
| `vme-py[all]` | both | |
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install "vme-py[all]"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
To work on the code, install from a clone instead:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
git clone https://github.com/oskar-j/visualize-my-expenses
|
|
52
|
+
cd visualize-my-expenses
|
|
53
|
+
uv sync # .venv as pinned by uv.lock: the package, both extras, the dev tools
|
|
54
|
+
# or: python -m venv .venv && source .venv/bin/activate && pip install -e ".[all]"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Try it in 30 seconds
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
vme sample budget.csv # writes an example file
|
|
61
|
+
vme render budget.csv -c PLN -o august.png # draws it
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Input formats
|
|
65
|
+
|
|
66
|
+
Point `vme` at whatever your bank or budget app exports; the format is guessed
|
|
67
|
+
from the file name, and `--format` overrides the guess.
|
|
68
|
+
|
|
69
|
+
| Format | Extensions | Notes |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| CSV / TSV | `.csv` `.tsv` `.txt` | separator and encoding are sniffed |
|
|
72
|
+
| JSON | `.json` | an array of rows, or `{"expenses": [...]}` |
|
|
73
|
+
| JSON Lines | `.jsonl` `.ndjson` | one row per line |
|
|
74
|
+
| OFX / QFX | `.ofx` `.qfx` | most US banks, Quicken, MS Money |
|
|
75
|
+
| QIF | `.qif` | older Quicken exports |
|
|
76
|
+
| ISO 20022 camt | `.xml` `.camt` | camt.052/053/054 SEPA statements |
|
|
77
|
+
| Excel | `.xlsx` `.xlsm` | needs the `excel` extra |
|
|
78
|
+
|
|
79
|
+
`vme formats` prints the same list.
|
|
80
|
+
|
|
81
|
+
Column names are matched loosely, so a bank export usually works untouched —
|
|
82
|
+
`Posted Date`, `Description`, `Transaction Amount`, `Debit/Credit`, `kwota` and
|
|
83
|
+
`waluta` are all understood. Amounts written as `1,234.56`, `1.234,56`,
|
|
84
|
+
`1 234,56`, `(12.00)`, `120.00-` or `$1,234.56` all parse.
|
|
85
|
+
|
|
86
|
+
The minimum a CSV needs is an amount:
|
|
87
|
+
|
|
88
|
+
```csv
|
|
89
|
+
date,category,label,amount,currency,kind
|
|
90
|
+
2026-08-01,Income,Salary,9800.00,PLN,income
|
|
91
|
+
2026-08-02,Housing,Rent,3200.00,PLN,expense
|
|
92
|
+
2026-08-04,Groceries,Biedronka,412.85,PLN,expense
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`category` groups, `label` is the detail inside the group, `kind` is `income` or
|
|
96
|
+
`expense`. Leave `kind` out and the direction is inferred: if any amount is
|
|
97
|
+
negative the file is read as a bank statement (negative = money out); if none
|
|
98
|
+
is, every row is spending.
|
|
99
|
+
|
|
100
|
+
## Multiple currencies
|
|
101
|
+
|
|
102
|
+
Each row carries its own currency, and `-c/--currency` says which one the
|
|
103
|
+
*picture* is drawn in. Anything else needs a rate — one unit of that currency
|
|
104
|
+
expressed in the report currency:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
vme render trip.csv -c PLN --rate EUR=4.30 --rate UAH=0.095 --rate USD=3.95
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+

|
|
111
|
+
|
|
112
|
+
Rates can live in a file instead (`--rates rates.json`), as JSON or CSV:
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{ "base": "PLN", "rates": { "EUR": 4.30, "USD": 3.95, "UAH": 0.095 } }
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
```csv
|
|
119
|
+
currency,rate
|
|
120
|
+
EUR,4.30
|
|
121
|
+
UAH,0.095
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
A `--rate` flag overrides the same currency in the file. Rates are never fetched
|
|
125
|
+
from the internet: the same input has to draw the same picture next month, and a
|
|
126
|
+
rate that quietly changes between two runs would break that.
|
|
127
|
+
|
|
128
|
+
Around 50 currencies — including EUR, PLN, UAH, CZK, GBP, USD, JPY and BTC —
|
|
129
|
+
know their own symbol, decimal places and separators, so amounts are written the
|
|
130
|
+
way that currency is normally written (`1 234,50 zł`, `€1,234.50`, `¥1,235`).
|
|
131
|
+
Anything else still works and prints its ISO code. `vme currencies` lists them.
|
|
132
|
+
|
|
133
|
+
Forget a rate and the error tells you what to add:
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
error: rows are in EUR (Euro), UAH (Ukrainian hryvnia) but the report is in PLN.
|
|
137
|
+
Give a rate for each one, for example: --rate EUR=4.30 (one EUR is worth 4.30 PLN)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Command line
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
vme render SOURCE draw a Sankey diagram
|
|
144
|
+
vme summary SOURCE print the same breakdown as a table
|
|
145
|
+
vme check SOURCE report anything that would stop it being plotted
|
|
146
|
+
vme formats list the input formats
|
|
147
|
+
vme currencies list the known currencies
|
|
148
|
+
vme sample FILE write an example file to start from
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Useful `render` options:
|
|
152
|
+
|
|
153
|
+
| Option | Does |
|
|
154
|
+
|---|---|
|
|
155
|
+
| `-o out.png` | `.png`, `.svg`, `.pdf` or `.html` — picked by extension |
|
|
156
|
+
| `-c PLN` | currency the report is drawn in |
|
|
157
|
+
| `-m 2026-08` | one month; also `-y 2026` and `-p START..END` |
|
|
158
|
+
| `--top 8` | keep the 8 biggest categories, fold the rest into "Other" |
|
|
159
|
+
| `--min-share 1` | fold anything under 1% of the total |
|
|
160
|
+
| `--max-labels 6` | most detail rows to show inside one category |
|
|
161
|
+
| `--no-detail` | stop at categories, skip the label column |
|
|
162
|
+
| `--theme dark` | dark background |
|
|
163
|
+
| `--width 1600 --dpi 200` | image size; height scales with the row count |
|
|
164
|
+
| `--transparent` | transparent background |
|
|
165
|
+
| `--sign statement` | force how a negative amount is read |
|
|
166
|
+
| `--open` | open the file when it is done |
|
|
167
|
+
|
|
168
|
+
Sharing a picture in a chat app: the defaults (1600px wide, 200 dpi, light
|
|
169
|
+
theme) are already sized for it. For a dark-mode chat, add `--theme dark`.
|
|
170
|
+
|
|
171
|
+

|
|
172
|
+
|
|
173
|
+
## Python API
|
|
174
|
+
|
|
175
|
+
`Visualizer` is the whole public surface.
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
from vme import Visualizer
|
|
179
|
+
|
|
180
|
+
Visualizer.from_file("august.csv", currency="PLN").create_png("august.png")
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Or build the rows yourself — dicts, namedtuples and `Expense` objects all work:
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
from vme import Expense, Visualizer
|
|
187
|
+
|
|
188
|
+
rows = [
|
|
189
|
+
Expense(category="Income", label="Salary", amount=9800, currency="PLN", kind="income"),
|
|
190
|
+
Expense(category="Housing", label="Rent", amount=3200, currency="PLN"),
|
|
191
|
+
Expense(category="Food", label="Lidl", amount=412, currency="PLN"),
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
v = Visualizer(rows, currency="PLN", title="August 2026", theme="light")
|
|
195
|
+
v.print_to_console()
|
|
196
|
+
v.create_png("august.png", width=1600, dpi=200)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
| Method | Does |
|
|
200
|
+
|---|---|
|
|
201
|
+
| `Visualizer.from_file(path, fmt=None, ...)` | read any supported file |
|
|
202
|
+
| `create_png(path)` | write a PNG (also `.svg`, `.pdf`) |
|
|
203
|
+
| `create_html(path)` | write an interactive plotly page — needs the `html` extra |
|
|
204
|
+
| `save(path)` | pick the writer from the extension |
|
|
205
|
+
| `show_plot()` | open an interactive window |
|
|
206
|
+
| `print_to_console()` | the breakdown as text |
|
|
207
|
+
| `graph()` | the `SankeyGraph`, if you want to draw it yourself |
|
|
208
|
+
| `problems()` | what is wrong with the rows, as a list |
|
|
209
|
+
|
|
210
|
+
Constructor arguments: `currency`, `rates`, `period`, `sign`, `title`,
|
|
211
|
+
`subtitle`, `theme`, `top_categories`, `max_labels`, `min_share`, `detail`,
|
|
212
|
+
`show_savings`, `group_income`, `verbose`.
|
|
213
|
+
|
|
214
|
+
Below `Visualizer` sit `vme.io` (loading), `vme.sankey` (rows → graph),
|
|
215
|
+
`vme.plotting` (graph → picture) and `vme.currencies`; each is usable on its own.
|
|
216
|
+
|
|
217
|
+
## Development
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
uv sync # or: pip install -e ".[all]" pytest ruff
|
|
221
|
+
uv run pytest
|
|
222
|
+
uv run ruff check src tests
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
`uv.lock` pins the development environment, and CI installs from it with
|
|
226
|
+
`--locked`, so after changing a dependency in `pyproject.toml` run `uv lock` and
|
|
227
|
+
commit both files. The lock never reaches PyPI — `pip install vme-py` gets the
|
|
228
|
+
version ranges from `pyproject.toml` — and working from a clone with pip simply
|
|
229
|
+
ignores it.
|
|
230
|
+
|
|
231
|
+
Layout:
|
|
232
|
+
|
|
233
|
+
```
|
|
234
|
+
src/vme/
|
|
235
|
+
models.py Expense, Node, Link, SankeyGraph
|
|
236
|
+
currencies.py currency metadata, formatting, rate files
|
|
237
|
+
tools.py amount/date/direction parsing, row coercion
|
|
238
|
+
io/ one module per input format, self-registering
|
|
239
|
+
data_store.py row storage, sign conventions, conversion, validation
|
|
240
|
+
sankey.py rows -> graph (folding, savings branch)
|
|
241
|
+
plotting.py layout + the matplotlib and plotly backends
|
|
242
|
+
theme.py light and dark palettes
|
|
243
|
+
cli.py the click commands
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
The colour palettes are checked for colour-blind separation and for contrast
|
|
247
|
+
against their own background, and every node carries a visible label, so no
|
|
248
|
+
reading of the chart depends on telling two colours apart.
|
|
249
|
+
|
|
250
|
+
### Releasing
|
|
251
|
+
|
|
252
|
+
Every pull request is linted, tested (Linux on Python 3.9–3.14, plus macOS and
|
|
253
|
+
Windows) and built into a wheel that is installed and run. A merge to `master`
|
|
254
|
+
does all of that again and then publishes to PyPI — but only if the version in
|
|
255
|
+
`pyproject.toml` is not on PyPI yet. So a release is two steps:
|
|
256
|
+
|
|
257
|
+
1. in the pull request, bump the version with `uv version --bump patch` (or
|
|
258
|
+
`minor`), which updates `pyproject.toml` and `uv.lock` together — edit
|
|
259
|
+
`pyproject.toml` by hand and you need `uv lock` as well, or CI fails;
|
|
260
|
+
2. merge it.
|
|
261
|
+
|
|
262
|
+
CI uploads through PyPI's trusted publishing, so no API token is stored
|
|
263
|
+
anywhere; then it tags the commit `vX.Y.Z` and creates a GitHub release with the
|
|
264
|
+
built files. A merge that leaves the version alone publishes nothing. PyPI never
|
|
265
|
+
takes the same version twice, so a broken release is fixed by the next one.
|
|
266
|
+
|
|
267
|
+
## Licence
|
|
268
|
+
|
|
269
|
+
MIT — see [LICENSE](https://github.com/oskar-j/visualize-my-expenses/blob/master/LICENSE).
|