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.
@@ -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
+ [![PyPI](https://img.shields.io/pypi/v/vme-py)](https://pypi.org/project/vme-py/)
42
+ [![CI](https://github.com/oskar-j/visualize-my-expenses/actions/workflows/ci.yml/badge.svg?branch=master)](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
+ ![A Sankey diagram of one month of spending](https://raw.githubusercontent.com/oskar-j/visualize-my-expenses/master/examples/output/budget-august.png)
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
+ ![A month of spending across four currencies](https://raw.githubusercontent.com/oskar-j/visualize-my-expenses/master/examples/output/trip-multicurrency.png)
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
+ ![The same data in the dark theme](https://raw.githubusercontent.com/oskar-j/visualize-my-expenses/master/examples/output/september-dark.png)
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
+ [![PyPI](https://img.shields.io/pypi/v/vme-py)](https://pypi.org/project/vme-py/)
4
+ [![CI](https://github.com/oskar-j/visualize-my-expenses/actions/workflows/ci.yml/badge.svg?branch=master)](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
+ ![A Sankey diagram of one month of spending](https://raw.githubusercontent.com/oskar-j/visualize-my-expenses/master/examples/output/budget-august.png)
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
+ ![A month of spending across four currencies](https://raw.githubusercontent.com/oskar-j/visualize-my-expenses/master/examples/output/trip-multicurrency.png)
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
+ ![The same data in the dark theme](https://raw.githubusercontent.com/oskar-j/visualize-my-expenses/master/examples/output/september-dark.png)
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).