Add SQLite passive income dashboard with Excel import and deployment tools
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
"""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 ensure_asset(db, 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.')
|
||||
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:
|
||||
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;
|
||||
''')
|
||||
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)
|
||||
Reference in New Issue
Block a user