Files
finance-dashboard/app/database.py
T

81 lines
3.7 KiB
Python

"""Small SQLite layer. Each operation owns its connection and transaction."""
import os
import sqlite3
from contextlib import contextmanager
from pathlib import Path
from models import canonical_name, name_key, ASSET_TYPES
def db_path():
return Path(os.environ.get('FINANCE_DB_PATH', '/data/finance.db'))
@contextmanager
def connect(path=None):
db = sqlite3.connect(path or db_path(), timeout=5)
db.row_factory = sqlite3.Row
db.execute('PRAGMA foreign_keys = ON')
db.execute('PRAGMA busy_timeout = 5000')
try:
with db:
yield db
finally:
db.close()
def validate_asset(name, asset_type='other', ticker=None):
name = canonical_name(name)
if asset_type not in ASSET_TYPES:
raise ValueError('Ungültiger Positionstyp.')
ticker = (ticker or '').strip()
if len(ticker) > 30:
raise ValueError('Ticker darf maximal 30 Zeichen enthalten.')
return name, ticker or None
def ensure_asset(db, name, asset_type='other', ticker=None):
name, ticker = validate_asset(name, asset_type, ticker)
db.execute('INSERT INTO assets (name, normalized_name, ticker, asset_type) VALUES (?, ?, ?, ?) ON CONFLICT(normalized_name) DO NOTHING',
(name, name_key(name), ticker or None, asset_type))
return db.execute('SELECT id FROM assets WHERE normalized_name = ?', (name_key(name),)).fetchone()['id']
def initialize(path=None):
target = Path(path or db_path())
target.parent.mkdir(parents=True, exist_ok=True)
with connect(target) as db:
first_setup = db.execute("SELECT 1 FROM sqlite_master WHERE type='table' AND name='assets'").fetchone() is None
db.execute('PRAGMA journal_mode = WAL')
db.executescript('''
CREATE TABLE IF NOT EXISTS assets (
id INTEGER PRIMARY KEY, name TEXT NOT NULL,
normalized_name TEXT NOT NULL UNIQUE, ticker TEXT,
asset_type TEXT NOT NULL CHECK(asset_type IN ('stock','etf','bond','crypto','interest','other')),
active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0,1)),
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
);
CREATE TABLE IF NOT EXISTS income_entries (
id INTEGER PRIMARY KEY, date TEXT NOT NULL,
asset_id INTEGER NOT NULL REFERENCES assets(id) ON DELETE RESTRICT,
category TEXT NOT NULL CHECK(category IN ('dividend','interest','distribution','other')),
amount INTEGER NOT NULL CHECK(typeof(amount) = 'integer' AND abs(amount) <= 99999999999),
note TEXT, expected INTEGER NOT NULL DEFAULT 0 CHECK(expected IN (0,1)),
received INTEGER NOT NULL DEFAULT 1 CHECK(received IN (0,1)),
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
);
CREATE INDEX IF NOT EXISTS income_date ON income_entries(date DESC, id DESC);
CREATE INDEX IF NOT EXISTS income_asset ON income_entries(asset_id);
CREATE TABLE IF NOT EXISTS import_records (
fingerprint TEXT PRIMARY KEY,
entry_id INTEGER REFERENCES income_entries(id) ON DELETE SET NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
PRAGMA user_version = 1;
''')
if first_setup:
for name, kind in [('AGNC','stock'), ('Main Street Capital','stock'), ('Capital Southwest','stock'),
('Ares Capital','stock'), ('Realty Income','stock'), ('Enbridge','stock'),
('Bayer','stock'), ('STOXX Global Select Dividend 100','etf'), ('airBaltic','bond')]:
ensure_asset(db, name, kind)