dvm-finance 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. dvm_finance-1.0.0/.gitignore +13 -0
  2. dvm_finance-1.0.0/LICENSE +21 -0
  3. dvm_finance-1.0.0/PKG-INFO +154 -0
  4. dvm_finance-1.0.0/README.md +112 -0
  5. dvm_finance-1.0.0/alembic/env.py +60 -0
  6. dvm_finance-1.0.0/alembic/script.py.mako +26 -0
  7. dvm_finance-1.0.0/alembic/versions/10222b47646b_initial_schema.py +218 -0
  8. dvm_finance-1.0.0/alembic.ini +39 -0
  9. dvm_finance-1.0.0/pyproject.toml +105 -0
  10. dvm_finance-1.0.0/src/abn_combined/__init__.py +3 -0
  11. dvm_finance-1.0.0/src/abn_combined/__main__.py +6 -0
  12. dvm_finance-1.0.0/src/abn_combined/api/__init__.py +1 -0
  13. dvm_finance-1.0.0/src/abn_combined/api/budgets.py +191 -0
  14. dvm_finance-1.0.0/src/abn_combined/api/cash_flow.py +114 -0
  15. dvm_finance-1.0.0/src/abn_combined/api/downloads.py +232 -0
  16. dvm_finance-1.0.0/src/abn_combined/api/rules.py +683 -0
  17. dvm_finance-1.0.0/src/abn_combined/api/snapshots.py +109 -0
  18. dvm_finance-1.0.0/src/abn_combined/api/tags.py +107 -0
  19. dvm_finance-1.0.0/src/abn_combined/api/transactions.py +253 -0
  20. dvm_finance-1.0.0/src/abn_combined/api/trends.py +58 -0
  21. dvm_finance-1.0.0/src/abn_combined/api/upload.py +83 -0
  22. dvm_finance-1.0.0/src/abn_combined/app.py +131 -0
  23. dvm_finance-1.0.0/src/abn_combined/cli.py +98 -0
  24. dvm_finance-1.0.0/src/abn_combined/constants.py +29 -0
  25. dvm_finance-1.0.0/src/abn_combined/core/__init__.py +1 -0
  26. dvm_finance-1.0.0/src/abn_combined/core/budget_report.py +132 -0
  27. dvm_finance-1.0.0/src/abn_combined/core/cash_flow.py +163 -0
  28. dvm_finance-1.0.0/src/abn_combined/core/categorizer.py +507 -0
  29. dvm_finance-1.0.0/src/abn_combined/core/dedup.py +120 -0
  30. dvm_finance-1.0.0/src/abn_combined/core/filters.py +472 -0
  31. dvm_finance-1.0.0/src/abn_combined/core/importer.py +120 -0
  32. dvm_finance-1.0.0/src/abn_combined/core/jobs.py +111 -0
  33. dvm_finance-1.0.0/src/abn_combined/core/legacy_migration.py +342 -0
  34. dvm_finance-1.0.0/src/abn_combined/core/models.py +229 -0
  35. dvm_finance-1.0.0/src/abn_combined/core/renames.py +198 -0
  36. dvm_finance-1.0.0/src/abn_combined/core/rule_prefill.py +99 -0
  37. dvm_finance-1.0.0/src/abn_combined/core/snapshots.py +606 -0
  38. dvm_finance-1.0.0/src/abn_combined/core/trends.py +317 -0
  39. dvm_finance-1.0.0/src/abn_combined/core/utils.py +67 -0
  40. dvm_finance-1.0.0/src/abn_combined/db.py +47 -0
  41. dvm_finance-1.0.0/src/abn_combined/downloaders/__init__.py +5 -0
  42. dvm_finance-1.0.0/src/abn_combined/downloaders/abn.py +290 -0
  43. dvm_finance-1.0.0/src/abn_combined/downloaders/paypal.py +614 -0
  44. dvm_finance-1.0.0/src/abn_combined/logging_config.py +42 -0
  45. dvm_finance-1.0.0/src/abn_combined/migrations.py +69 -0
  46. dvm_finance-1.0.0/src/abn_combined/parsers/__init__.py +52 -0
  47. dvm_finance-1.0.0/src/abn_combined/parsers/csv.py +81 -0
  48. dvm_finance-1.0.0/src/abn_combined/parsers/description.py +708 -0
  49. dvm_finance-1.0.0/src/abn_combined/parsers/mt940.py +353 -0
  50. dvm_finance-1.0.0/src/abn_combined/parsers/paypal.py +238 -0
  51. dvm_finance-1.0.0/src/abn_combined/parsers/seb.py +224 -0
  52. dvm_finance-1.0.0/src/abn_combined/parsers/utils.py +63 -0
  53. dvm_finance-1.0.0/src/abn_combined/parsers/wise.py +251 -0
  54. dvm_finance-1.0.0/src/abn_combined/parsers/xls.py +82 -0
  55. dvm_finance-1.0.0/src/abn_combined/settings.py +76 -0
  56. dvm_finance-1.0.0/src/abn_combined/web/static/app.css +109 -0
  57. dvm_finance-1.0.0/src/abn_combined/web/static/downloads.css +94 -0
  58. dvm_finance-1.0.0/src/abn_combined/web/static/js/phase2.js +279 -0
  59. dvm_finance-1.0.0/src/abn_combined/web/static/js/phase4.js +229 -0
  60. dvm_finance-1.0.0/src/abn_combined/web/static/js/rules.js +31 -0
  61. dvm_finance-1.0.0/src/abn_combined/web/static/js/transactions.js +16 -0
  62. dvm_finance-1.0.0/src/abn_combined/web/static/rules.css +99 -0
  63. dvm_finance-1.0.0/src/abn_combined/web/static/snapshots.css +25 -0
  64. dvm_finance-1.0.0/src/abn_combined/web/static/tables.css +96 -0
  65. dvm_finance-1.0.0/src/abn_combined/web/static/trends.css +145 -0
  66. dvm_finance-1.0.0/src/abn_combined/web/static/vendor/alpine.min.js +5 -0
  67. dvm_finance-1.0.0/src/abn_combined/web/static/vendor/htmx.min.js +1 -0
  68. dvm_finance-1.0.0/src/abn_combined/web/static/vendor/pico.min.css +4 -0
  69. dvm_finance-1.0.0/src/abn_combined/web/templates/_budgets_table.html +40 -0
  70. dvm_finance-1.0.0/src/abn_combined/web/templates/_cash_flow_table.html +30 -0
  71. dvm_finance-1.0.0/src/abn_combined/web/templates/_download_status.html +59 -0
  72. dvm_finance-1.0.0/src/abn_combined/web/templates/_rule_preview.html +156 -0
  73. dvm_finance-1.0.0/src/abn_combined/web/templates/_rule_report.html +145 -0
  74. dvm_finance-1.0.0/src/abn_combined/web/templates/_rules_row.html +86 -0
  75. dvm_finance-1.0.0/src/abn_combined/web/templates/_snapshot_import_report.html +103 -0
  76. dvm_finance-1.0.0/src/abn_combined/web/templates/_transaction_detail.html +37 -0
  77. dvm_finance-1.0.0/src/abn_combined/web/templates/_transactions_row.html +115 -0
  78. dvm_finance-1.0.0/src/abn_combined/web/templates/_transactions_table.html +89 -0
  79. dvm_finance-1.0.0/src/abn_combined/web/templates/_trends_table.html +75 -0
  80. dvm_finance-1.0.0/src/abn_combined/web/templates/_upload_summary.html +14 -0
  81. dvm_finance-1.0.0/src/abn_combined/web/templates/base.html +88 -0
  82. dvm_finance-1.0.0/src/abn_combined/web/templates/budgets.html +55 -0
  83. dvm_finance-1.0.0/src/abn_combined/web/templates/budgets_edit.html +34 -0
  84. dvm_finance-1.0.0/src/abn_combined/web/templates/cash_flow.html +49 -0
  85. dvm_finance-1.0.0/src/abn_combined/web/templates/download.html +130 -0
  86. dvm_finance-1.0.0/src/abn_combined/web/templates/placeholder.html +8 -0
  87. dvm_finance-1.0.0/src/abn_combined/web/templates/rules.html +74 -0
  88. dvm_finance-1.0.0/src/abn_combined/web/templates/rules_edit.html +268 -0
  89. dvm_finance-1.0.0/src/abn_combined/web/templates/rules_history.html +49 -0
  90. dvm_finance-1.0.0/src/abn_combined/web/templates/snapshots.html +104 -0
  91. dvm_finance-1.0.0/src/abn_combined/web/templates/tags.html +55 -0
  92. dvm_finance-1.0.0/src/abn_combined/web/templates/transactions.html +181 -0
  93. dvm_finance-1.0.0/src/abn_combined/web/templates/trends.html +57 -0
  94. dvm_finance-1.0.0/src/abn_combined/web/templates/upload.html +50 -0
@@ -0,0 +1,13 @@
1
+ devdata/
2
+ __pycache__/
3
+ *.pyc
4
+ .coverage
5
+ htmlcov/
6
+ .pytest_cache/
7
+ dist/
8
+ *.egg-info/
9
+ .ruff_cache/
10
+ data/
11
+ *.db
12
+ .DS_Store
13
+ .chrome/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Divya van Mahajan
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.
@@ -0,0 +1,154 @@
1
+ Metadata-Version: 2.4
2
+ Name: dvm-finance
3
+ Version: 1.0.0
4
+ Summary: Integrated personal-finance app: download, parse, categorize, and review bank statements.
5
+ Project-URL: Homepage, https://github.com/divyavanmahajan/abn-combined
6
+ Project-URL: Repository, https://github.com/divyavanmahajan/abn-combined
7
+ Project-URL: Issues, https://github.com/divyavanmahajan/abn-combined/issues
8
+ Author: Divya van Mahajan
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: banking,budgeting,fastapi,finance,mt940,personal-finance,sqlite
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Framework :: FastAPI
14
+ Classifier: Intended Audience :: End Users/Desktop
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Office/Business :: Financial :: Accounting
20
+ Requires-Python: >=3.12
21
+ Requires-Dist: alembic
22
+ Requires-Dist: fastapi
23
+ Requires-Dist: jinja2
24
+ Requires-Dist: openpyxl
25
+ Requires-Dist: pandas
26
+ Requires-Dist: platformdirs
27
+ Requires-Dist: python-multipart
28
+ Requires-Dist: sqlalchemy>=2
29
+ Requires-Dist: structlog
30
+ Requires-Dist: uvicorn
31
+ Requires-Dist: xlrd
32
+ Provides-Extra: dev
33
+ Requires-Dist: httpx; extra == 'dev'
34
+ Requires-Dist: playwright; extra == 'dev'
35
+ Requires-Dist: pytest; extra == 'dev'
36
+ Requires-Dist: pytest-cov; extra == 'dev'
37
+ Requires-Dist: pytest-playwright; extra == 'dev'
38
+ Requires-Dist: ruff; extra == 'dev'
39
+ Provides-Extra: downloads
40
+ Requires-Dist: playwright; extra == 'downloads'
41
+ Description-Content-Type: text/markdown
42
+
43
+ # dvm-finance
44
+
45
+ Integrated personal-finance app: download, parse, categorize, and review bank
46
+ statements locally. Merges the abn-analyst analyzer and the abn-download
47
+ downloaders into a single, local, single-user web application. No login, no
48
+ charts, no LLM — rules + manual categorization, tables, and an audit trail.
49
+
50
+ ## Install & run (PyPI)
51
+
52
+ With PyPI, simply:
53
+
54
+ ```bash
55
+ pip install dvm-finance
56
+ dvm-finance
57
+ ```
58
+
59
+ Or with [uv](https://docs.astral.sh/uv/) for no-venv installation:
60
+
61
+ ```bash
62
+ uvx dvm-finance
63
+ ```
64
+
65
+ For development from a local checkout / wheel:
66
+
67
+ ```bash
68
+ uvx --from /path/to/abn-combined dvm-finance
69
+ uvx --from dist/dvm_finance-0.1.0-py3-none-any.whl dvm-finance
70
+ ```
71
+
72
+ ## First run
73
+
74
+ ```bash
75
+ dvm-finance # serves http://127.0.0.1:8000 and prints the URL
76
+ ```
77
+
78
+ On first start the app creates its data directory (SQLite DB, downloaded
79
+ statements, snapshot exports) and migrates the schema automatically. Defaults to
80
+ the platform user-data dir (macOS: `~/Library/Application Support/dvm-finance/`);
81
+ override with `--data-dir` or `DVM_FINANCE_DATA_DIR`. Other flags: `--port`
82
+ (default 8000) and `--host` (default `127.0.0.1`).
83
+
84
+ The app binds to `127.0.0.1` and has **no authentication** — do not widen the
85
+ bind address on an untrusted network.
86
+
87
+ An empty install offers three ways in: migrate a legacy database (below), upload
88
+ statement files on the **Upload** tab (MT940/STA, ABN XLS, generic CSV, PayPal
89
+ TXT, Wise CSV, SEB CSV), or download directly from the **Download** tab.
90
+
91
+ ## Migrating from abn-analyst
92
+
93
+ One-time, idempotent import of all transactions, rules + conditions, budgets and
94
+ manual categorizations from a legacy `abn_analyst.db` (opened read-only):
95
+
96
+ ```bash
97
+ dvm-finance migrate-legacy /path/to/abn_analyst.db
98
+ ```
99
+
100
+ Re-running skips already-present rows. A per-table summary is printed.
101
+
102
+ ## Sharing data with a second person
103
+
104
+ - **Export** (Snapshots tab): one click writes a versioned, gzipped-JSON snapshot
105
+ of transactions, rules, budgets and change reports, offered as a download.
106
+ - **Import** (their machine): upload the file on the Snapshots tab. The merge is
107
+ **incoming wins** — new records are inserted, conflicting records (including
108
+ manual categorizations and rule edits) are overwritten by the snapshot; local
109
+ records absent from the snapshot are never deleted. The database is backed up
110
+ first and an import report is stored for review.
111
+ - Second-user setup is two commands: `uvx dvm-finance`, then import a
112
+ snapshot in the browser.
113
+
114
+ **Security note**: snapshot files contain your full financial data and are **not
115
+ encrypted**. Exchange them only over a channel you trust.
116
+
117
+ ## Downloads
118
+
119
+ - **ABN AMRO**: click "Start ABN download" — a headed Chromium opens; authenticate
120
+ with the ABN AMRO app; statements are fetched, saved and imported automatically.
121
+ Requires Playwright browsers:
122
+
123
+ ```bash
124
+ playwright install chromium
125
+ ```
126
+
127
+ The app works fine without them (upload/rules/trends); the Download page shows
128
+ install instructions when they are missing.
129
+
130
+ - **PayPal**: start Chrome with remote debugging first, log in to PayPal, then
131
+ click "Start PayPal download":
132
+
133
+ ```bash
134
+ /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
135
+ --remote-debugging-port=9226 --user-data-dir=~/.chrome/debugdir \
136
+ https://www.paypal.com/reports/dlog
137
+ ```
138
+
139
+ ## Development
140
+
141
+ ```bash
142
+ source ~/venv/bin/activate
143
+ pip install -e ".[dev]"
144
+ playwright install chromium
145
+
146
+ ruff check . # lint
147
+ pytest # unit + integration (e2e/slow deselected)
148
+ pytest --cov=abn_combined # coverage (fails under 80%)
149
+ pytest -m e2e # Playwright end-to-end flows (headless Chromium)
150
+ dvm-finance --data-dir ./devdata # run against a scratch data dir
151
+ ```
152
+
153
+ See `docs/architecture.md`, `docs/developer.md` and `docs/product.md` for design
154
+ notes, and `docs/core-beliefs.md` for the project's golden principles.
@@ -0,0 +1,112 @@
1
+ # dvm-finance
2
+
3
+ Integrated personal-finance app: download, parse, categorize, and review bank
4
+ statements locally. Merges the abn-analyst analyzer and the abn-download
5
+ downloaders into a single, local, single-user web application. No login, no
6
+ charts, no LLM — rules + manual categorization, tables, and an audit trail.
7
+
8
+ ## Install & run (PyPI)
9
+
10
+ With PyPI, simply:
11
+
12
+ ```bash
13
+ pip install dvm-finance
14
+ dvm-finance
15
+ ```
16
+
17
+ Or with [uv](https://docs.astral.sh/uv/) for no-venv installation:
18
+
19
+ ```bash
20
+ uvx dvm-finance
21
+ ```
22
+
23
+ For development from a local checkout / wheel:
24
+
25
+ ```bash
26
+ uvx --from /path/to/abn-combined dvm-finance
27
+ uvx --from dist/dvm_finance-0.1.0-py3-none-any.whl dvm-finance
28
+ ```
29
+
30
+ ## First run
31
+
32
+ ```bash
33
+ dvm-finance # serves http://127.0.0.1:8000 and prints the URL
34
+ ```
35
+
36
+ On first start the app creates its data directory (SQLite DB, downloaded
37
+ statements, snapshot exports) and migrates the schema automatically. Defaults to
38
+ the platform user-data dir (macOS: `~/Library/Application Support/dvm-finance/`);
39
+ override with `--data-dir` or `DVM_FINANCE_DATA_DIR`. Other flags: `--port`
40
+ (default 8000) and `--host` (default `127.0.0.1`).
41
+
42
+ The app binds to `127.0.0.1` and has **no authentication** — do not widen the
43
+ bind address on an untrusted network.
44
+
45
+ An empty install offers three ways in: migrate a legacy database (below), upload
46
+ statement files on the **Upload** tab (MT940/STA, ABN XLS, generic CSV, PayPal
47
+ TXT, Wise CSV, SEB CSV), or download directly from the **Download** tab.
48
+
49
+ ## Migrating from abn-analyst
50
+
51
+ One-time, idempotent import of all transactions, rules + conditions, budgets and
52
+ manual categorizations from a legacy `abn_analyst.db` (opened read-only):
53
+
54
+ ```bash
55
+ dvm-finance migrate-legacy /path/to/abn_analyst.db
56
+ ```
57
+
58
+ Re-running skips already-present rows. A per-table summary is printed.
59
+
60
+ ## Sharing data with a second person
61
+
62
+ - **Export** (Snapshots tab): one click writes a versioned, gzipped-JSON snapshot
63
+ of transactions, rules, budgets and change reports, offered as a download.
64
+ - **Import** (their machine): upload the file on the Snapshots tab. The merge is
65
+ **incoming wins** — new records are inserted, conflicting records (including
66
+ manual categorizations and rule edits) are overwritten by the snapshot; local
67
+ records absent from the snapshot are never deleted. The database is backed up
68
+ first and an import report is stored for review.
69
+ - Second-user setup is two commands: `uvx dvm-finance`, then import a
70
+ snapshot in the browser.
71
+
72
+ **Security note**: snapshot files contain your full financial data and are **not
73
+ encrypted**. Exchange them only over a channel you trust.
74
+
75
+ ## Downloads
76
+
77
+ - **ABN AMRO**: click "Start ABN download" — a headed Chromium opens; authenticate
78
+ with the ABN AMRO app; statements are fetched, saved and imported automatically.
79
+ Requires Playwright browsers:
80
+
81
+ ```bash
82
+ playwright install chromium
83
+ ```
84
+
85
+ The app works fine without them (upload/rules/trends); the Download page shows
86
+ install instructions when they are missing.
87
+
88
+ - **PayPal**: start Chrome with remote debugging first, log in to PayPal, then
89
+ click "Start PayPal download":
90
+
91
+ ```bash
92
+ /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
93
+ --remote-debugging-port=9226 --user-data-dir=~/.chrome/debugdir \
94
+ https://www.paypal.com/reports/dlog
95
+ ```
96
+
97
+ ## Development
98
+
99
+ ```bash
100
+ source ~/venv/bin/activate
101
+ pip install -e ".[dev]"
102
+ playwright install chromium
103
+
104
+ ruff check . # lint
105
+ pytest # unit + integration (e2e/slow deselected)
106
+ pytest --cov=abn_combined # coverage (fails under 80%)
107
+ pytest -m e2e # Playwright end-to-end flows (headless Chromium)
108
+ dvm-finance --data-dir ./devdata # run against a scratch data dir
109
+ ```
110
+
111
+ See `docs/architecture.md`, `docs/developer.md` and `docs/product.md` for design
112
+ notes, and `docs/core-beliefs.md` for the project's golden principles.
@@ -0,0 +1,60 @@
1
+ """Alembic environment for abn-combined."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from logging.config import fileConfig
6
+
7
+ from sqlalchemy import engine_from_config, pool
8
+
9
+ from abn_combined.core.models import Base
10
+ from abn_combined.settings import Settings
11
+ from alembic import context
12
+
13
+ config = context.config
14
+
15
+ if config.config_file_name is not None:
16
+ fileConfig(config.config_file_name)
17
+
18
+ target_metadata = Base.metadata
19
+
20
+
21
+ def _database_url() -> str:
22
+ """Resolve the DB URL: programmatic override wins, else the data-dir DB."""
23
+ url = config.get_main_option("sqlalchemy.url")
24
+ if url:
25
+ return url
26
+ return Settings.create().database_url
27
+
28
+
29
+ def run_migrations_offline() -> None:
30
+ context.configure(
31
+ url=_database_url(),
32
+ target_metadata=target_metadata,
33
+ literal_binds=True,
34
+ dialect_opts={"paramstyle": "named"},
35
+ render_as_batch=True,
36
+ )
37
+ with context.begin_transaction():
38
+ context.run_migrations()
39
+
40
+
41
+ def run_migrations_online() -> None:
42
+ section = config.get_section(config.config_ini_section, {})
43
+ section["sqlalchemy.url"] = _database_url()
44
+ connectable = engine_from_config(
45
+ section, prefix="sqlalchemy.", poolclass=pool.NullPool
46
+ )
47
+ with connectable.connect() as connection:
48
+ context.configure(
49
+ connection=connection,
50
+ target_metadata=target_metadata,
51
+ render_as_batch=True,
52
+ )
53
+ with context.begin_transaction():
54
+ context.run_migrations()
55
+
56
+
57
+ if context.is_offline_mode():
58
+ run_migrations_offline()
59
+ else:
60
+ run_migrations_online()
@@ -0,0 +1,26 @@
1
+ """${message}
2
+
3
+ Revision ID: ${up_revision}
4
+ Revises: ${down_revision | comma,n}
5
+ Create Date: ${create_date}
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+ ${imports if imports else ""}
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = ${repr(up_revision)}
16
+ down_revision: Union[str, None] = ${repr(down_revision)}
17
+ branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
18
+ depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
19
+
20
+
21
+ def upgrade() -> None:
22
+ ${upgrades if upgrades else "pass"}
23
+
24
+
25
+ def downgrade() -> None:
26
+ ${downgrades if downgrades else "pass"}
@@ -0,0 +1,218 @@
1
+ """initial schema
2
+
3
+ Revision ID: 10222b47646b
4
+ Revises:
5
+ Create Date: 2026-07-07 03:16:39.912774
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = '10222b47646b'
16
+ down_revision: Union[str, None] = None
17
+ branch_labels: Union[str, Sequence[str], None] = None
18
+ depends_on: Union[str, Sequence[str], None] = None
19
+
20
+
21
+ def upgrade() -> None:
22
+ # ### commands auto generated by Alembic - please adjust! ###
23
+ op.create_table('budgets',
24
+ sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
25
+ sa.Column('category', sa.String(length=255), nullable=False),
26
+ sa.Column('amount', sa.Numeric(precision=10, scale=2), nullable=False),
27
+ sa.Column('period', sa.String(length=10), nullable=False),
28
+ sa.Column('start_date', sa.Date(), nullable=True),
29
+ sa.Column('end_date', sa.Date(), nullable=True),
30
+ sa.Column('notes', sa.Text(), nullable=True),
31
+ sa.Column('created_at', sa.Date(), nullable=True),
32
+ sa.Column('updated_at', sa.Date(), nullable=True),
33
+ sa.PrimaryKeyConstraint('id')
34
+ )
35
+ with op.batch_alter_table('budgets', schema=None) as batch_op:
36
+ batch_op.create_index('idx_budgets_category', ['category'], unique=False)
37
+ batch_op.create_index('idx_budgets_category_period', ['category', 'period'], unique=True)
38
+ batch_op.create_index('idx_budgets_period', ['period'], unique=False)
39
+
40
+ op.create_table('categorization_rules',
41
+ sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
42
+ sa.Column('uuid', sa.String(length=36), nullable=False),
43
+ sa.Column('priority', sa.Integer(), nullable=False),
44
+ sa.Column('rule_type', sa.String(length=50), nullable=False),
45
+ sa.Column('match_pattern', sa.String(length=50), nullable=False),
46
+ sa.Column('field_target', sa.String(length=50), nullable=True),
47
+ sa.Column('match_value', sa.String(length=500), nullable=False),
48
+ sa.Column('category', sa.String(length=100), nullable=False),
49
+ sa.Column('tags', sa.String(), nullable=True),
50
+ sa.Column('is_active', sa.Boolean(), nullable=False),
51
+ sa.Column('notes', sa.Text(), nullable=True),
52
+ sa.Column('filter_account', sa.String(), nullable=True),
53
+ sa.Column('filter_currency', sa.String(length=3), nullable=True),
54
+ sa.Column('filter_date_from', sa.Date(), nullable=True),
55
+ sa.Column('filter_date_to', sa.Date(), nullable=True),
56
+ sa.PrimaryKeyConstraint('id')
57
+ )
58
+ with op.batch_alter_table('categorization_rules', schema=None) as batch_op:
59
+ batch_op.create_index('idx_rules_priority_active', ['priority', 'is_active'], unique=False)
60
+ batch_op.create_index('idx_rules_type', ['rule_type'], unique=False)
61
+ batch_op.create_index(batch_op.f('ix_categorization_rules_uuid'), ['uuid'], unique=True)
62
+
63
+ op.create_table('download_state',
64
+ sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
65
+ sa.Column('source', sa.String(length=50), nullable=False),
66
+ sa.Column('account', sa.String(), nullable=True),
67
+ sa.Column('last_success_at', sa.DateTime(), nullable=True),
68
+ sa.Column('last_range_end', sa.Date(), nullable=True),
69
+ sa.PrimaryKeyConstraint('id')
70
+ )
71
+ with op.batch_alter_table('download_state', schema=None) as batch_op:
72
+ batch_op.create_index('idx_download_state_source_account', ['source', 'account'], unique=True)
73
+ batch_op.create_index(batch_op.f('ix_download_state_account'), ['account'], unique=False)
74
+ batch_op.create_index(batch_op.f('ix_download_state_source'), ['source'], unique=False)
75
+
76
+ op.create_table('rule_change_reports',
77
+ sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
78
+ sa.Column('created_at', sa.DateTime(), nullable=False),
79
+ sa.Column('rule_id', sa.Integer(), nullable=True),
80
+ sa.Column('rule_uuid', sa.String(length=36), nullable=True),
81
+ sa.Column('action', sa.String(length=20), nullable=False),
82
+ sa.Column('rule_before', sa.JSON(), nullable=True),
83
+ sa.Column('rule_after', sa.JSON(), nullable=True),
84
+ sa.Column('summary', sa.JSON(), nullable=True),
85
+ sa.PrimaryKeyConstraint('id')
86
+ )
87
+ with op.batch_alter_table('rule_change_reports', schema=None) as batch_op:
88
+ batch_op.create_index(batch_op.f('ix_rule_change_reports_created_at'), ['created_at'], unique=False)
89
+ batch_op.create_index(batch_op.f('ix_rule_change_reports_rule_id'), ['rule_id'], unique=False)
90
+ batch_op.create_index(batch_op.f('ix_rule_change_reports_rule_uuid'), ['rule_uuid'], unique=False)
91
+
92
+ op.create_table('snapshot_imports',
93
+ sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
94
+ sa.Column('created_at', sa.DateTime(), nullable=False),
95
+ sa.Column('source_machine_id', sa.String(), nullable=True),
96
+ sa.Column('schema_version', sa.Integer(), nullable=True),
97
+ sa.Column('counts', sa.JSON(), nullable=True),
98
+ sa.Column('overwrites', sa.JSON(), nullable=True),
99
+ sa.PrimaryKeyConstraint('id')
100
+ )
101
+ with op.batch_alter_table('snapshot_imports', schema=None) as batch_op:
102
+ batch_op.create_index(batch_op.f('ix_snapshot_imports_created_at'), ['created_at'], unique=False)
103
+
104
+ op.create_table('transactions',
105
+ sa.Column('id', sa.String(), nullable=False),
106
+ sa.Column('accountNumber', sa.String(), nullable=False),
107
+ sa.Column('mutationcode', sa.String(), nullable=True),
108
+ sa.Column('transactiondate', sa.Date(), nullable=False),
109
+ sa.Column('valuedate', sa.Date(), nullable=True),
110
+ sa.Column('startsaldo', sa.Numeric(precision=15, scale=2), nullable=True),
111
+ sa.Column('endsaldo', sa.Numeric(precision=15, scale=2), nullable=True),
112
+ sa.Column('amount', sa.Numeric(precision=15, scale=2), nullable=False),
113
+ sa.Column('description', sa.String(), nullable=True),
114
+ sa.Column('description_structured', sa.Text(), nullable=True),
115
+ sa.Column('category', sa.String(), nullable=True),
116
+ sa.Column('manual_category', sa.String(), nullable=True),
117
+ sa.Column('tags', sa.String(), nullable=True),
118
+ sa.Column('manual_tags', sa.String(), nullable=True),
119
+ sa.Column('categorization_source', sa.String(), nullable=True),
120
+ sa.Column('currency', sa.String(length=3), nullable=False),
121
+ sa.Column('source_file', sa.String(), nullable=True),
122
+ sa.Column('source_line', sa.Integer(), nullable=True),
123
+ sa.Column('transaction_type_code', sa.String(), nullable=True),
124
+ sa.Column('transaction_reference', sa.String(), nullable=True),
125
+ sa.Column('transaction_hash', sa.String(length=64), nullable=True),
126
+ sa.PrimaryKeyConstraint('id')
127
+ )
128
+ with op.batch_alter_table('transactions', schema=None) as batch_op:
129
+ batch_op.create_index('idx_account_date', ['accountNumber', 'transactiondate'], unique=False)
130
+ batch_op.create_index(batch_op.f('ix_transactions_accountNumber'), ['accountNumber'], unique=False)
131
+ batch_op.create_index(batch_op.f('ix_transactions_amount'), ['amount'], unique=False)
132
+ batch_op.create_index(batch_op.f('ix_transactions_category'), ['category'], unique=False)
133
+ batch_op.create_index(batch_op.f('ix_transactions_manual_category'), ['manual_category'], unique=False)
134
+ batch_op.create_index(batch_op.f('ix_transactions_transaction_hash'), ['transaction_hash'], unique=False)
135
+ batch_op.create_index(batch_op.f('ix_transactions_transactiondate'), ['transactiondate'], unique=False)
136
+
137
+ op.create_table('rule_change_items',
138
+ sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
139
+ sa.Column('report_id', sa.Integer(), nullable=False),
140
+ sa.Column('transaction_id', sa.String(), nullable=False),
141
+ sa.Column('old_category', sa.String(), nullable=True),
142
+ sa.Column('new_category', sa.String(), nullable=True),
143
+ sa.Column('old_tags', sa.String(), nullable=True),
144
+ sa.Column('new_tags', sa.String(), nullable=True),
145
+ sa.ForeignKeyConstraint(['report_id'], ['rule_change_reports.id'], ondelete='CASCADE'),
146
+ sa.PrimaryKeyConstraint('id')
147
+ )
148
+ with op.batch_alter_table('rule_change_items', schema=None) as batch_op:
149
+ batch_op.create_index(batch_op.f('ix_rule_change_items_report_id'), ['report_id'], unique=False)
150
+ batch_op.create_index(batch_op.f('ix_rule_change_items_transaction_id'), ['transaction_id'], unique=False)
151
+
152
+ op.create_table('rule_conditions',
153
+ sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
154
+ sa.Column('rule_id', sa.Integer(), nullable=False),
155
+ sa.Column('field_target', sa.String(length=50), nullable=False),
156
+ sa.Column('match_pattern', sa.String(length=50), nullable=False),
157
+ sa.Column('match_value', sa.String(length=500), nullable=False),
158
+ sa.Column('operator', sa.String(length=3), nullable=False),
159
+ sa.Column('sort_order', sa.Integer(), nullable=False),
160
+ sa.ForeignKeyConstraint(['rule_id'], ['categorization_rules.id'], ondelete='CASCADE'),
161
+ sa.PrimaryKeyConstraint('id')
162
+ )
163
+ with op.batch_alter_table('rule_conditions', schema=None) as batch_op:
164
+ batch_op.create_index(batch_op.f('ix_rule_conditions_rule_id'), ['rule_id'], unique=False)
165
+
166
+ # ### end Alembic commands ###
167
+
168
+
169
+ def downgrade() -> None:
170
+ # ### commands auto generated by Alembic - please adjust! ###
171
+ with op.batch_alter_table('rule_conditions', schema=None) as batch_op:
172
+ batch_op.drop_index(batch_op.f('ix_rule_conditions_rule_id'))
173
+
174
+ op.drop_table('rule_conditions')
175
+ with op.batch_alter_table('rule_change_items', schema=None) as batch_op:
176
+ batch_op.drop_index(batch_op.f('ix_rule_change_items_transaction_id'))
177
+ batch_op.drop_index(batch_op.f('ix_rule_change_items_report_id'))
178
+
179
+ op.drop_table('rule_change_items')
180
+ with op.batch_alter_table('transactions', schema=None) as batch_op:
181
+ batch_op.drop_index(batch_op.f('ix_transactions_transactiondate'))
182
+ batch_op.drop_index(batch_op.f('ix_transactions_transaction_hash'))
183
+ batch_op.drop_index(batch_op.f('ix_transactions_manual_category'))
184
+ batch_op.drop_index(batch_op.f('ix_transactions_category'))
185
+ batch_op.drop_index(batch_op.f('ix_transactions_amount'))
186
+ batch_op.drop_index(batch_op.f('ix_transactions_accountNumber'))
187
+ batch_op.drop_index('idx_account_date')
188
+
189
+ op.drop_table('transactions')
190
+ with op.batch_alter_table('snapshot_imports', schema=None) as batch_op:
191
+ batch_op.drop_index(batch_op.f('ix_snapshot_imports_created_at'))
192
+
193
+ op.drop_table('snapshot_imports')
194
+ with op.batch_alter_table('rule_change_reports', schema=None) as batch_op:
195
+ batch_op.drop_index(batch_op.f('ix_rule_change_reports_rule_uuid'))
196
+ batch_op.drop_index(batch_op.f('ix_rule_change_reports_rule_id'))
197
+ batch_op.drop_index(batch_op.f('ix_rule_change_reports_created_at'))
198
+
199
+ op.drop_table('rule_change_reports')
200
+ with op.batch_alter_table('download_state', schema=None) as batch_op:
201
+ batch_op.drop_index(batch_op.f('ix_download_state_source'))
202
+ batch_op.drop_index(batch_op.f('ix_download_state_account'))
203
+ batch_op.drop_index('idx_download_state_source_account')
204
+
205
+ op.drop_table('download_state')
206
+ with op.batch_alter_table('categorization_rules', schema=None) as batch_op:
207
+ batch_op.drop_index(batch_op.f('ix_categorization_rules_uuid'))
208
+ batch_op.drop_index('idx_rules_type')
209
+ batch_op.drop_index('idx_rules_priority_active')
210
+
211
+ op.drop_table('categorization_rules')
212
+ with op.batch_alter_table('budgets', schema=None) as batch_op:
213
+ batch_op.drop_index('idx_budgets_period')
214
+ batch_op.drop_index('idx_budgets_category_period')
215
+ batch_op.drop_index('idx_budgets_category')
216
+
217
+ op.drop_table('budgets')
218
+ # ### end Alembic commands ###
@@ -0,0 +1,39 @@
1
+ [alembic]
2
+ script_location = alembic
3
+ prepend_sys_path = src
4
+ # sqlalchemy.url is set programmatically at runtime (data-dir DB) and via env var
5
+ # ABN_COMBINED_DATA_DIR for offline autogenerate.
6
+
7
+ [loggers]
8
+ keys = root,sqlalchemy,alembic
9
+
10
+ [handlers]
11
+ keys = console
12
+
13
+ [formatters]
14
+ keys = generic
15
+
16
+ [logger_root]
17
+ level = WARN
18
+ handlers = console
19
+ qualname =
20
+
21
+ [logger_sqlalchemy]
22
+ level = WARN
23
+ handlers =
24
+ qualname = sqlalchemy.engine
25
+
26
+ [logger_alembic]
27
+ level = INFO
28
+ handlers =
29
+ qualname = alembic
30
+
31
+ [handler_console]
32
+ class = StreamHandler
33
+ args = (sys.stderr,)
34
+ level = NOTSET
35
+ formatter = generic
36
+
37
+ [formatter_generic]
38
+ format = %(levelname)-5.5s [%(name)s] %(message)s
39
+ datefmt = %H:%M:%S