feat: auto-migrate SQLite data to Postgres on first deploy
- Add trumpsignal.db to git (one-time migration source) - entrypoint.sh detects empty Postgres and runs migration automatically - scripts/migrate_sqlite_to_postgres.py: posts, subscriptions, bot_trades - Add psycopg2-binary to requirements for migration script Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,6 @@ __pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
.env
|
||||
*.db
|
||||
*.db-shm
|
||||
*.db-wal
|
||||
.git/
|
||||
@@ -11,4 +10,3 @@ __pycache__/
|
||||
*.md
|
||||
test_*.py
|
||||
simulate_trades.py
|
||||
scripts/
|
||||
|
||||
+1
-2
@@ -7,8 +7,7 @@ env/
|
||||
.env
|
||||
*.pem
|
||||
|
||||
# 数据库
|
||||
*.db
|
||||
# 数据库 (trumpsignal.db 是例外,用于一次性迁移到 Postgres)
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
|
||||
@@ -4,5 +4,35 @@ set -e
|
||||
echo "[entrypoint] Running Alembic migrations..."
|
||||
alembic upgrade head
|
||||
|
||||
# One-time migration: if trumpsignal.db exists and posts table is empty,
|
||||
# migrate SQLite data into Postgres then remove the db file.
|
||||
if [ -f "trumpsignal.db" ]; then
|
||||
echo "[entrypoint] Found trumpsignal.db — checking if migration needed..."
|
||||
POST_COUNT=$(python -c "
|
||||
import asyncio, os
|
||||
os.environ.setdefault('DATABASE_URL', '')
|
||||
import psycopg2
|
||||
url = os.environ.get('DATABASE_URL','').replace('+asyncpg','')
|
||||
try:
|
||||
conn = psycopg2.connect(url)
|
||||
cur = conn.cursor()
|
||||
cur.execute('SELECT COUNT(*) FROM posts')
|
||||
print(cur.fetchone()[0])
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
print('0')
|
||||
" 2>/dev/null || echo "0")
|
||||
|
||||
if [ "$POST_COUNT" = "0" ]; then
|
||||
echo "[entrypoint] Postgres is empty — running SQLite migration..."
|
||||
python scripts/migrate_sqlite_to_postgres.py
|
||||
echo "[entrypoint] Migration done. Removing trumpsignal.db..."
|
||||
rm -f trumpsignal.db
|
||||
else
|
||||
echo "[entrypoint] Postgres already has data (${POST_COUNT} posts) — skipping migration."
|
||||
rm -f trumpsignal.db
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "[entrypoint] Starting API server..."
|
||||
exec uvicorn app.main:app --host 0.0.0.0 --port 8000
|
||||
|
||||
@@ -3,6 +3,7 @@ uvicorn[standard]==0.39.0
|
||||
sqlalchemy[asyncio]==2.0.49
|
||||
aiosqlite==0.22.1
|
||||
asyncpg==0.31.0
|
||||
psycopg2-binary==2.9.9
|
||||
alembic==1.16.5
|
||||
pydantic==2.13.2
|
||||
pydantic-settings==2.11.0
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
"""
|
||||
One-time migration: local trumpsignal.db (SQLite) → Postgres.
|
||||
Called automatically by entrypoint.sh on first deploy.
|
||||
Uses DATABASE_URL env var (same one uvicorn uses), just strips +asyncpg.
|
||||
"""
|
||||
import os
|
||||
import sqlite3
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import psycopg2
|
||||
import psycopg2.extras
|
||||
|
||||
SQLITE_PATH = os.path.join(os.path.dirname(__file__), '..', 'trumpsignal.db')
|
||||
|
||||
raw_url = os.environ.get('DATABASE_URL', '')
|
||||
if not raw_url:
|
||||
print("ERROR: DATABASE_URL not set")
|
||||
sys.exit(1)
|
||||
|
||||
# asyncpg URL → psycopg2 URL
|
||||
PG_URL = raw_url.replace('postgresql+asyncpg://', 'postgresql://')
|
||||
|
||||
|
||||
def parse_dt(v):
|
||||
if v is None:
|
||||
return None
|
||||
if isinstance(v, (int, float)):
|
||||
return datetime.fromtimestamp(v, tz=timezone.utc).replace(tzinfo=None)
|
||||
for fmt in ('%Y-%m-%d %H:%M:%S.%f', '%Y-%m-%d %H:%M:%S'):
|
||||
try:
|
||||
return datetime.strptime(v, fmt)
|
||||
except ValueError:
|
||||
continue
|
||||
return v
|
||||
|
||||
|
||||
def migrate():
|
||||
sq = sqlite3.connect(SQLITE_PATH)
|
||||
sq.row_factory = sqlite3.Row
|
||||
pg = psycopg2.connect(PG_URL)
|
||||
cur_sq = sq.cursor()
|
||||
cur_pg = pg.cursor()
|
||||
|
||||
# ── posts ─────────────────────────────────────────────────────────────────
|
||||
cur_sq.execute("SELECT * FROM posts")
|
||||
posts = cur_sq.fetchall()
|
||||
psycopg2.extras.execute_values(cur_pg, """
|
||||
INSERT INTO posts (
|
||||
id, external_id, text, source, published_at,
|
||||
sentiment, ai_confidence, relevant,
|
||||
price_impact_asset, price_impact_m5, price_impact_m15, price_impact_m1h,
|
||||
price_at_post, signal, ai_reasoning, prefilter_reason,
|
||||
analysis_version, created_at
|
||||
) VALUES %s ON CONFLICT (id) DO NOTHING
|
||||
""", [(
|
||||
r['id'], r['external_id'], r['text'], r['source'], parse_dt(r['published_at']),
|
||||
r['sentiment'], r['ai_confidence'], bool(r['relevant']),
|
||||
r['price_impact_asset'], r['price_impact_m5'], r['price_impact_m15'], r['price_impact_m1h'],
|
||||
r['price_at_post'], r['signal'], r['ai_reasoning'], r['prefilter_reason'],
|
||||
r['analysis_version'], parse_dt(r['created_at']),
|
||||
) for r in posts])
|
||||
pg.commit()
|
||||
print(f" ✓ {len(posts)} posts")
|
||||
|
||||
# ── subscriptions ─────────────────────────────────────────────────────────
|
||||
cur_sq.execute("SELECT * FROM subscriptions")
|
||||
subs = cur_sq.fetchall()
|
||||
psycopg2.extras.execute_values(cur_pg, """
|
||||
INSERT INTO subscriptions (
|
||||
id, wallet_address, hl_api_key, active, subscribed_at, created_at,
|
||||
leverage, position_size_usd, take_profit_pct, stop_loss_pct, min_confidence
|
||||
) VALUES %s ON CONFLICT (id) DO NOTHING
|
||||
""", [(
|
||||
r['id'], r['wallet_address'], r['hl_api_key'],
|
||||
bool(r['active']), parse_dt(r['subscribed_at']), parse_dt(r['created_at']),
|
||||
r['leverage'] if r['leverage'] is not None else 3,
|
||||
r['position_size_usd'] if r['position_size_usd'] is not None else 20.0,
|
||||
r['take_profit_pct'], r['stop_loss_pct'],
|
||||
r['min_confidence'] if r['min_confidence'] is not None else 80,
|
||||
) for r in subs])
|
||||
pg.commit()
|
||||
print(f" ✓ {len(subs)} subscriptions")
|
||||
|
||||
# ── bot_trades ────────────────────────────────────────────────────────────
|
||||
cur_sq.execute("SELECT * FROM bot_trades")
|
||||
trades = cur_sq.fetchall()
|
||||
psycopg2.extras.execute_values(cur_pg, """
|
||||
INSERT INTO bot_trades (
|
||||
id, asset, side, entry_price, exit_price, pnl_usd,
|
||||
hold_seconds, trigger_post_id, wallet_address,
|
||||
opened_at, closed_at, hl_order_id
|
||||
) VALUES %s ON CONFLICT (id) DO NOTHING
|
||||
""", [(
|
||||
r['id'], r['asset'], r['side'],
|
||||
r['entry_price'], r['exit_price'], r['pnl_usd'],
|
||||
r['hold_seconds'], r['trigger_post_id'], r['wallet_address'],
|
||||
parse_dt(r['opened_at']), parse_dt(r['closed_at']), r['hl_order_id'],
|
||||
) for r in trades])
|
||||
pg.commit()
|
||||
print(f" ✓ {len(trades)} bot_trades")
|
||||
|
||||
# ── reset sequences ───────────────────────────────────────────────────────
|
||||
for table, col in [('posts', 'id'), ('subscriptions', 'id'), ('bot_trades', 'id')]:
|
||||
cur_pg.execute(f"""
|
||||
SELECT setval(pg_get_serial_sequence('{table}', '{col}'),
|
||||
COALESCE((SELECT MAX({col}) FROM {table}), 1))
|
||||
""")
|
||||
pg.commit()
|
||||
|
||||
sq.close()
|
||||
pg.close()
|
||||
print("✅ SQLite → Postgres migration complete")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
migrate()
|
||||
Binary file not shown.
Reference in New Issue
Block a user