06/03 Optimize app
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Migration: add security columns.
|
||||
users — totp_secret, totp_enabled
|
||||
schwab_connections — refresh_token_expires_at
|
||||
|
||||
Run once: python scripts/add_security_columns.py
|
||||
Safe to re-run — skips columns that already exist.
|
||||
"""
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from app import create_app
|
||||
from app.extensions import db
|
||||
|
||||
app = create_app()
|
||||
|
||||
COLUMNS = [
|
||||
("users", "totp_secret",
|
||||
"VARCHAR(64) NULL DEFAULT NULL"),
|
||||
("users", "totp_enabled",
|
||||
"TINYINT(1) NOT NULL DEFAULT 0"),
|
||||
("schwab_connections", "refresh_token_expires_at",
|
||||
"DATETIME NULL DEFAULT NULL"),
|
||||
]
|
||||
|
||||
with app.app_context():
|
||||
with db.engine.connect() as conn:
|
||||
for table, column, definition in COLUMNS:
|
||||
exists = conn.execute(db.text(
|
||||
"SELECT COUNT(*) FROM information_schema.columns "
|
||||
"WHERE table_schema = DATABASE() "
|
||||
"AND table_name = :t AND column_name = :c"
|
||||
), {"t": table, "c": column}).scalar()
|
||||
|
||||
if exists:
|
||||
print(f" {table}.{column} already exists — skipped.")
|
||||
else:
|
||||
conn.execute(db.text(
|
||||
f"ALTER TABLE {table} ADD COLUMN {column} {definition}"
|
||||
))
|
||||
conn.commit()
|
||||
print(f" Added {table}.{column}.")
|
||||
|
||||
print("Done.")
|
||||
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Schwab daily auto-sync — balance, positions, and transactions.
|
||||
|
||||
Add to crontab (crontab -e):
|
||||
# Sync Schwab every day at 7 AM
|
||||
0 7 * * * /home/pfm/venv/bin/python /home/pfm/web/scripts/sync_schwab.py >> /home/pfm/web/logs/sync_schwab.log 2>&1
|
||||
|
||||
Or run manually:
|
||||
python scripts/sync_schwab.py
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from app import create_app
|
||||
from app.extensions import db
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s|%(levelname)s|%(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S',
|
||||
)
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
app = create_app()
|
||||
|
||||
with app.app_context():
|
||||
from app.models.schwab_connection import SchwabConnection, SchwabAccount
|
||||
from app.services.schwab_service import (
|
||||
sync_account_snapshot, sync_preview, import_transactions,
|
||||
)
|
||||
|
||||
connection = SchwabConnection.query.filter_by(is_active=True).first()
|
||||
if not connection:
|
||||
log.warning('[sync_schwab] No active Schwab connection — skipping.')
|
||||
sys.exit(0)
|
||||
|
||||
# Warn if refresh token is close to expiry
|
||||
if connection.refresh_token_expires_at:
|
||||
days_left = (connection.refresh_token_expires_at - datetime.utcnow()).days
|
||||
if days_left <= 2:
|
||||
log.warning('[sync_schwab] Schwab refresh token expires in %d day(s)! '
|
||||
'Log in and reconnect at /schwab/connect.', days_left)
|
||||
|
||||
accounts = SchwabAccount.query.filter(
|
||||
SchwabAccount.pfm_account_id != None,
|
||||
SchwabAccount.is_active == True,
|
||||
).all()
|
||||
|
||||
if not accounts:
|
||||
log.warning('[sync_schwab] No mapped Schwab accounts found.')
|
||||
sys.exit(0)
|
||||
|
||||
log.info('[sync_schwab] Syncing %d account(s)…', len(accounts))
|
||||
|
||||
for sa in accounts:
|
||||
sa.connection = connection # always use active connection
|
||||
|
||||
# ── 1. Balance + positions ────────────────────────────────────────────
|
||||
try:
|
||||
bal_updated, pos_synced = sync_account_snapshot(sa)
|
||||
log.info('[sync_schwab] %s — balance updated=%s positions=%d',
|
||||
sa.account_name, bal_updated, pos_synced)
|
||||
except Exception as e:
|
||||
log.error('[sync_schwab] snapshot failed for %s: %s', sa.account_name, e,
|
||||
exc_info=True)
|
||||
|
||||
# ── 2. New transactions ───────────────────────────────────────────────
|
||||
try:
|
||||
preview = sync_preview(sa)
|
||||
if preview:
|
||||
imported, skipped = import_transactions(preview, sa)
|
||||
log.info('[sync_schwab] %s — transactions: imported=%d skipped=%d',
|
||||
sa.account_name, imported, skipped)
|
||||
else:
|
||||
log.info('[sync_schwab] %s — no new transactions.', sa.account_name)
|
||||
except Exception as e:
|
||||
log.error('[sync_schwab] transaction sync failed for %s: %s',
|
||||
sa.account_name, e, exc_info=True)
|
||||
|
||||
log.info('[sync_schwab] Done.')
|
||||
Reference in New Issue
Block a user