fix: banner translations and deployment improvements
- Add translateCarName import from i18n.ts for proper multilingual support - Change default API language from 'ko' to 'en' for hero banners - Add checkbox column for Local Cars banner registration - Update Dockerfile with Playwright dependencies - Add PostgreSQL migration script - Add banner translation fix script 🤖 Generated with [Claude Code](https://claude.ai/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -2,16 +2,37 @@ FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies
|
||||
# Install system dependencies for PostgreSQL and Playwright
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
libpq-dev \
|
||||
# Playwright dependencies
|
||||
libnss3 \
|
||||
libnspr4 \
|
||||
libatk1.0-0 \
|
||||
libatk-bridge2.0-0 \
|
||||
libcups2 \
|
||||
libdrm2 \
|
||||
libdbus-1-3 \
|
||||
libxkbcommon0 \
|
||||
libatspi2.0-0 \
|
||||
libxcomposite1 \
|
||||
libxdamage1 \
|
||||
libxfixes3 \
|
||||
libxrandr2 \
|
||||
libgbm1 \
|
||||
libasound2 \
|
||||
libpango-1.0-0 \
|
||||
libcairo2 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Python dependencies
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Install Playwright browsers (Chromium only for smaller image)
|
||||
RUN playwright install chromium
|
||||
|
||||
# Copy application code
|
||||
COPY . .
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ def get_localized_field(obj, field: str, lang: str) -> Optional[str]:
|
||||
|
||||
@router.get("/", response_model=List[HeroBannerLocalizedResponse])
|
||||
def get_hero_banners(
|
||||
lang: str = Query("ko", regex="^(ko|en|mn|ru)$"),
|
||||
lang: str = Query("en", regex="^(ko|en|mn|ru)$"),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""활성 히어로 배너 목록 조회 (Public)"""
|
||||
|
||||
@@ -64,7 +64,10 @@ class Settings(BaseSettings):
|
||||
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
db_path = os.path.join(base_dir, "autonet.db")
|
||||
return f"sqlite:///{db_path}"
|
||||
return f"postgresql://{self.DB_USER}:{self.DB_PASSWORD}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}"
|
||||
# URL-encode password for special characters like @ # etc
|
||||
from urllib.parse import quote_plus
|
||||
encoded_password = quote_plus(self.DB_PASSWORD)
|
||||
return f"postgresql://{self.DB_USER}:{encoded_password}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}"
|
||||
|
||||
@property
|
||||
def REDIS_URL(self) -> str:
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.responses import RedirectResponse
|
||||
from contextlib import asynccontextmanager
|
||||
import os
|
||||
import asyncio
|
||||
@@ -15,6 +17,42 @@ from datetime import datetime, timedelta
|
||||
|
||||
app_settings = get_settings()
|
||||
|
||||
|
||||
class TrailingSlashMiddleware(BaseHTTPMiddleware):
|
||||
"""
|
||||
Middleware to normalize trailing slashes on API paths.
|
||||
Uses redirect to strip trailing slashes from non-root routes.
|
||||
|
||||
Routes defined with "/" (like /hero-banners/) keep trailing slash.
|
||||
Routes defined without "/" (like /cars) get redirected.
|
||||
"""
|
||||
# Routes that are defined WITH trailing slash (router.get("/"))
|
||||
TRAILING_SLASH_ROUTES = {
|
||||
"/api/hero-banners/",
|
||||
"/api/settings/",
|
||||
"/api/notifications/",
|
||||
"/api/vehicle-requests/",
|
||||
}
|
||||
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
path = request.url.path
|
||||
|
||||
# Skip if it's a known trailing-slash route
|
||||
if path in self.TRAILING_SLASH_ROUTES:
|
||||
return await call_next(request)
|
||||
|
||||
# Redirect trailing slash from other /api/* paths
|
||||
if path.startswith("/api/") and path.endswith("/") and len(path) > 5:
|
||||
new_path = path.rstrip("/")
|
||||
if request.url.query:
|
||||
new_url = f"{new_path}?{request.url.query}"
|
||||
else:
|
||||
new_url = new_path
|
||||
return RedirectResponse(url=new_url, status_code=307)
|
||||
|
||||
return await call_next(request)
|
||||
|
||||
|
||||
# Create tables
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
@@ -118,6 +156,9 @@ app = FastAPI(
|
||||
lifespan=lifespan
|
||||
)
|
||||
|
||||
# Trailing slash middleware (must be added before CORS)
|
||||
app.add_middleware(TrailingSlashMiddleware)
|
||||
|
||||
# CORS - credentials=True requires explicit origins (not "*")
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
@@ -126,6 +167,8 @@ app.add_middleware(
|
||||
"http://127.0.0.1:3000",
|
||||
"http://localhost:8000",
|
||||
"http://192.168.0.202:3000", # Local network
|
||||
"https://autonetsellcar.com", # Production
|
||||
"http://autonetsellcar.com", # Production (HTTP redirect)
|
||||
],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
|
||||
124
backend/fix_banner_translations.py
Normal file
124
backend/fix_banner_translations.py
Normal file
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Fix hero banner translations by updating title_en, title_mn, title_ru from title_ko
|
||||
"""
|
||||
import sqlite3
|
||||
import os
|
||||
|
||||
# Translation dictionary (subset of key terms)
|
||||
TRANSLATIONS = {
|
||||
# Makers
|
||||
'현대': {'en': 'Hyundai', 'mn': 'Хёндай', 'ru': 'Хёндай'},
|
||||
'기아': {'en': 'Kia', 'mn': 'Киа', 'ru': 'Киа'},
|
||||
'제네시스': {'en': 'Genesis', 'mn': 'Женезис', 'ru': 'Дженезис'},
|
||||
'쉐보레': {'en': 'Chevrolet', 'mn': 'Шевроле', 'ru': 'Шевроле'},
|
||||
'KG모빌리티': {'en': 'KG Mobility', 'mn': 'КЖ Мобилити', 'ru': 'КГ Мобилити'},
|
||||
'쌍용': {'en': 'SsangYong', 'mn': 'СсангЁнг', 'ru': 'СсангЙонг'},
|
||||
|
||||
# Models
|
||||
'모하비': {'en': 'Mohave', 'mn': 'Мохаве', 'ru': 'Мохаве'},
|
||||
'더 마스터': {'en': 'The Master', 'mn': 'Мастер', 'ru': 'Мастер'},
|
||||
'신형': {'en': 'New', 'mn': 'Шинэ', 'ru': 'Новый'},
|
||||
'더 뉴': {'en': 'The New', 'mn': 'Шинэ', 'ru': 'Новый'},
|
||||
'그랜드스타렉스': {'en': 'Grand Starex', 'mn': 'Гранд Старекс', 'ru': 'Гранд Старекс'},
|
||||
'스타렉스': {'en': 'Starex', 'mn': 'Старекс', 'ru': 'Старекс'},
|
||||
'싼타페': {'en': 'Santa Fe', 'mn': 'Санта Фе', 'ru': 'Санта Фе'},
|
||||
'스팅어': {'en': 'Stinger', 'mn': 'Стингер', 'ru': 'Стингер'},
|
||||
'마이스터': {'en': 'Meister', 'mn': 'Мейстер', 'ru': 'Мейстер'},
|
||||
'쏘렌토': {'en': 'Sorento', 'mn': 'Соренто', 'ru': 'Соренто'},
|
||||
'스포티지': {'en': 'Sportage', 'mn': 'Спортаж', 'ru': 'Спортаж'},
|
||||
'카니발': {'en': 'Carnival', 'mn': 'Карнивал', 'ru': 'Карнивал'},
|
||||
'셀토스': {'en': 'Seltos', 'mn': 'Селтос', 'ru': 'Селтос'},
|
||||
'투싼': {'en': 'Tucson', 'mn': 'Туксон', 'ru': 'Туксон'},
|
||||
'팰리세이드': {'en': 'Palisade', 'mn': 'Палисейд', 'ru': 'Палисейд'},
|
||||
'아반떼': {'en': 'Avante', 'mn': 'Аванте', 'ru': 'Аванте'},
|
||||
'쏘나타': {'en': 'Sonata', 'mn': 'Соната', 'ru': 'Соната'},
|
||||
'그랜저': {'en': 'Grandeur', 'mn': 'Грандёр', 'ru': 'Грандёр'},
|
||||
'코나': {'en': 'Kona', 'mn': 'Кона', 'ru': 'Кона'},
|
||||
'K5': {'en': 'K5', 'mn': 'K5', 'ru': 'K5'},
|
||||
'K3': {'en': 'K3', 'mn': 'K3', 'ru': 'K3'},
|
||||
'K7': {'en': 'K7', 'mn': 'K7', 'ru': 'K7'},
|
||||
'K8': {'en': 'K8', 'mn': 'K8', 'ru': 'K8'},
|
||||
'K9': {'en': 'K9', 'mn': 'K9', 'ru': 'K9'},
|
||||
'GV70': {'en': 'GV70', 'mn': 'GV70', 'ru': 'GV70'},
|
||||
'GV80': {'en': 'GV80', 'mn': 'GV80', 'ru': 'GV80'},
|
||||
'G70': {'en': 'G70', 'mn': 'G70', 'ru': 'G70'},
|
||||
'G80': {'en': 'G80', 'mn': 'G80', 'ru': 'G80'},
|
||||
'G90': {'en': 'G90', 'mn': 'G90', 'ru': 'G90'},
|
||||
}
|
||||
|
||||
# Sort keys by length (longest first) to avoid partial matches
|
||||
SORTED_KEYS = sorted(TRANSLATIONS.keys(), key=len, reverse=True)
|
||||
|
||||
|
||||
def translate(text: str, lang: str) -> str:
|
||||
"""Translate Korean text to target language"""
|
||||
if not text:
|
||||
return text
|
||||
|
||||
result = text
|
||||
for key in SORTED_KEYS:
|
||||
if key in result:
|
||||
translation = TRANSLATIONS[key].get(lang, key)
|
||||
result = result.replace(key, translation)
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
# Find database file
|
||||
db_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "autonet.db")
|
||||
|
||||
if not os.path.exists(db_path):
|
||||
print(f"Database not found at: {db_path}")
|
||||
return
|
||||
|
||||
print(f"Using database: {db_path}")
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Get all banners
|
||||
cursor.execute("SELECT id, title_ko, title_en, title_mn, title_ru FROM hero_banners")
|
||||
banners = cursor.fetchall()
|
||||
|
||||
print(f"\nFound {len(banners)} banners to update:\n")
|
||||
|
||||
for banner in banners:
|
||||
banner_id, title_ko, title_en, title_mn, title_ru = banner
|
||||
|
||||
if not title_ko:
|
||||
print(f"Banner {banner_id}: No Korean title, skipping")
|
||||
continue
|
||||
|
||||
new_title_en = translate(title_ko, 'en')
|
||||
new_title_mn = translate(title_ko, 'mn')
|
||||
new_title_ru = translate(title_ko, 'ru')
|
||||
|
||||
print(f"Banner {banner_id}:")
|
||||
print(f" KO: {title_ko}")
|
||||
print(f" EN: {title_en} -> {new_title_en}")
|
||||
print(f" MN: {title_mn} -> {new_title_mn}")
|
||||
print(f" RU: {title_ru} -> {new_title_ru}")
|
||||
print()
|
||||
|
||||
# Update the banner
|
||||
cursor.execute("""
|
||||
UPDATE hero_banners
|
||||
SET title_en = ?, title_mn = ?, title_ru = ?
|
||||
WHERE id = ?
|
||||
""", (new_title_en, new_title_mn, new_title_ru, banner_id))
|
||||
|
||||
conn.commit()
|
||||
print(f"Updated {len(banners)} banners successfully!")
|
||||
|
||||
# Verify
|
||||
print("\n--- Verification ---")
|
||||
cursor.execute("SELECT id, title_ko, title_en, title_mn FROM hero_banners")
|
||||
for row in cursor.fetchall():
|
||||
print(f"ID {row[0]}: {row[1]} -> EN: {row[2]}, MN: {row[3]}")
|
||||
|
||||
conn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
297
backend/migrate_to_postgres.py
Normal file
297
backend/migrate_to_postgres.py
Normal file
@@ -0,0 +1,297 @@
|
||||
"""
|
||||
SQLite to PostgreSQL Migration Script
|
||||
Handles boolean conversion and foreign key constraints
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
import sqlite3
|
||||
import psycopg2
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
SQLITE_PATH = os.path.join(os.path.dirname(__file__), "autonet.db")
|
||||
PG_CONFIG = {
|
||||
"host": "192.168.0.201",
|
||||
"port": 5432,
|
||||
"database": "autonet",
|
||||
"user": "admin",
|
||||
"password": "roskfl@1122"
|
||||
}
|
||||
|
||||
# Tables in dependency order (parents first)
|
||||
TABLES_ORDER = [
|
||||
# Base tables (no FK dependencies)
|
||||
"car_makers",
|
||||
"car_models",
|
||||
"translations",
|
||||
"system_settings",
|
||||
"cc_packages",
|
||||
"exchange_rates",
|
||||
"exchange_rate_history",
|
||||
"hero_banner_settings",
|
||||
# Users before user-dependent tables
|
||||
"users",
|
||||
# Car related
|
||||
"cars",
|
||||
"car_images",
|
||||
"car_options",
|
||||
"car_performance_checks",
|
||||
"car_specifications",
|
||||
"car_views",
|
||||
"performance_check_views",
|
||||
# Cache
|
||||
"car_cache",
|
||||
"car_detail_cache",
|
||||
"cache_request_queue",
|
||||
# Hero banners
|
||||
"hero_banners",
|
||||
# User activities
|
||||
"charge_history",
|
||||
"inquiries",
|
||||
"inquiry_messages",
|
||||
"vehicle_requests",
|
||||
"request_vehicles",
|
||||
"purchased_vehicles",
|
||||
"dealer_applications",
|
||||
"dealer_info",
|
||||
"vehicle_shares",
|
||||
"share_rewards",
|
||||
"withdrawal_requests",
|
||||
"referral_rewards",
|
||||
"notifications",
|
||||
"push_subscriptions",
|
||||
"user_notification_preferences",
|
||||
"verification_codes",
|
||||
"visitor_logs",
|
||||
"visitor_daily_stats",
|
||||
"visitor_sessions",
|
||||
]
|
||||
|
||||
def create_tables():
|
||||
"""Create tables in PostgreSQL"""
|
||||
print("\n[Step 1] Creating tables in PostgreSQL...")
|
||||
|
||||
os.environ["USE_SQLITE"] = "False"
|
||||
os.environ["DB_HOST"] = PG_CONFIG["host"]
|
||||
os.environ["DB_PORT"] = str(PG_CONFIG["port"])
|
||||
os.environ["DB_NAME"] = PG_CONFIG["database"]
|
||||
os.environ["DB_USER"] = PG_CONFIG["user"]
|
||||
os.environ["DB_PASSWORD"] = PG_CONFIG["password"]
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from app.database import Base
|
||||
from app.models import (
|
||||
CarMaker, CarModel, Car, CarImage, CarOption,
|
||||
User, CarView, PerformanceCheckView, ChargeHistory, VerificationCode,
|
||||
Inquiry, InquiryMessage, HeroBanner, HeroBannerSettings,
|
||||
Translation, CarCache, CarDetailCache, CacheRequestQueue,
|
||||
SystemSettings, VehicleRequest, RequestVehicle, PurchasedVehicle,
|
||||
DealerApplication, DealerInfo, VehicleShare, ShareReward,
|
||||
WithdrawalRequest, ReferralReward, Notification,
|
||||
PushSubscription, UserNotificationPreference,
|
||||
CarPerformanceCheck, CarSpecification,
|
||||
ExchangeRate, ExchangeRateHistory, CCPackage,
|
||||
VisitorLog, VisitorDailyStats, VisitorSession,
|
||||
)
|
||||
|
||||
encoded_pw = quote_plus(PG_CONFIG['password'])
|
||||
pg_url = f"postgresql://{PG_CONFIG['user']}:{encoded_pw}@{PG_CONFIG['host']}:{PG_CONFIG['port']}/{PG_CONFIG['database']}"
|
||||
engine = create_engine(pg_url, echo=False)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
print(" Tables created successfully!")
|
||||
|
||||
def get_boolean_columns(pg_cursor, table_name):
|
||||
"""Get list of boolean columns for a table"""
|
||||
pg_cursor.execute("""
|
||||
SELECT column_name FROM information_schema.columns
|
||||
WHERE table_schema = 'public' AND table_name = %s AND data_type = 'boolean'
|
||||
""", (table_name,))
|
||||
return [row[0] for row in pg_cursor.fetchall()]
|
||||
|
||||
def convert_row(row, columns, bool_cols):
|
||||
"""Convert SQLite row values for PostgreSQL (handle booleans)"""
|
||||
result = []
|
||||
for i, val in enumerate(row):
|
||||
col_name = columns[i]
|
||||
if col_name in bool_cols and val is not None:
|
||||
# Convert 0/1 to False/True
|
||||
result.append(bool(val))
|
||||
else:
|
||||
result.append(val)
|
||||
return tuple(result)
|
||||
|
||||
def migrate_table(sqlite_conn, pg_conn, table_name):
|
||||
"""Migrate a single table"""
|
||||
sqlite_cursor = sqlite_conn.cursor()
|
||||
pg_cursor = pg_conn.cursor()
|
||||
|
||||
# Get SQLite columns
|
||||
sqlite_cursor.execute(f"PRAGMA table_info({table_name})")
|
||||
sqlite_cols = [col[1] for col in sqlite_cursor.fetchall()]
|
||||
if not sqlite_cols:
|
||||
return 0
|
||||
|
||||
# Get data
|
||||
sqlite_cursor.execute(f"SELECT * FROM {table_name}")
|
||||
rows = sqlite_cursor.fetchall()
|
||||
if not rows:
|
||||
print(f" {table_name}: 0 rows (empty)")
|
||||
return 0
|
||||
|
||||
# Check PostgreSQL table exists
|
||||
pg_cursor.execute("""
|
||||
SELECT EXISTS (SELECT FROM information_schema.tables
|
||||
WHERE table_schema = 'public' AND table_name = %s)
|
||||
""", (table_name,))
|
||||
if not pg_cursor.fetchone()[0]:
|
||||
print(f" {table_name}: skipped (not in PostgreSQL)")
|
||||
return 0
|
||||
|
||||
# Get PostgreSQL columns
|
||||
pg_cursor.execute("""
|
||||
SELECT column_name FROM information_schema.columns
|
||||
WHERE table_schema = 'public' AND table_name = %s
|
||||
ORDER BY ordinal_position
|
||||
""", (table_name,))
|
||||
pg_cols = [row[0] for row in pg_cursor.fetchall()]
|
||||
|
||||
# Find common columns
|
||||
common_cols = [c for c in sqlite_cols if c in pg_cols]
|
||||
if not common_cols:
|
||||
print(f" {table_name}: skipped (no matching columns)")
|
||||
return 0
|
||||
|
||||
col_indices = [sqlite_cols.index(c) for c in common_cols]
|
||||
|
||||
# Get boolean columns
|
||||
bool_cols = set(get_boolean_columns(pg_cursor, table_name))
|
||||
|
||||
# Prepare query
|
||||
cols_str = ", ".join(common_cols)
|
||||
placeholders = ", ".join(["%s"] * len(common_cols))
|
||||
insert_sql = f"INSERT INTO {table_name} ({cols_str}) VALUES ({placeholders})"
|
||||
|
||||
try:
|
||||
# Truncate with CASCADE to handle FK
|
||||
pg_cursor.execute(f"TRUNCATE TABLE {table_name} CASCADE")
|
||||
|
||||
success = 0
|
||||
for row in rows:
|
||||
try:
|
||||
# Extract and convert row
|
||||
filtered = tuple(row[i] for i in col_indices)
|
||||
converted = convert_row(filtered, common_cols, bool_cols)
|
||||
pg_cursor.execute(insert_sql, converted)
|
||||
success += 1
|
||||
except Exception as e:
|
||||
pg_conn.rollback()
|
||||
# Truncate again after rollback
|
||||
pg_cursor.execute(f"TRUNCATE TABLE {table_name} CASCADE")
|
||||
print(f" {table_name}: error - {str(e)[:80]}")
|
||||
return 0
|
||||
|
||||
pg_conn.commit()
|
||||
print(f" {table_name}: {success}/{len(rows)} rows migrated")
|
||||
return success
|
||||
except Exception as e:
|
||||
pg_conn.rollback()
|
||||
print(f" {table_name}: error - {str(e)[:80]}")
|
||||
return 0
|
||||
|
||||
def reset_sequences(pg_conn):
|
||||
"""Reset sequences to max(id) + 1"""
|
||||
print("\n[Step 3] Resetting sequences...")
|
||||
pg_cursor = pg_conn.cursor()
|
||||
|
||||
# Get all tables with id column
|
||||
pg_cursor.execute("""
|
||||
SELECT table_name FROM information_schema.columns
|
||||
WHERE table_schema = 'public' AND column_name = 'id'
|
||||
""")
|
||||
tables = [row[0] for row in pg_cursor.fetchall()]
|
||||
|
||||
for table in tables:
|
||||
try:
|
||||
# Check if sequence exists
|
||||
seq_name = f"{table}_id_seq"
|
||||
pg_cursor.execute("""
|
||||
SELECT EXISTS (SELECT FROM pg_sequences WHERE schemaname = 'public' AND sequencename = %s)
|
||||
""", (seq_name,))
|
||||
if pg_cursor.fetchone()[0]:
|
||||
pg_cursor.execute(f"""
|
||||
SELECT setval('{seq_name}', COALESCE((SELECT MAX(id) FROM {table}), 0) + 1, false)
|
||||
""")
|
||||
except:
|
||||
pass
|
||||
|
||||
pg_conn.commit()
|
||||
print(" Sequences reset completed")
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("SQLite to PostgreSQL Migration")
|
||||
print("=" * 60)
|
||||
|
||||
# Step 1: Create tables
|
||||
try:
|
||||
create_tables()
|
||||
except Exception as e:
|
||||
print(f" Failed: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Step 2: Connect and migrate
|
||||
print("\n[Step 2] Migrating data...")
|
||||
|
||||
sqlite_conn = sqlite3.connect(SQLITE_PATH)
|
||||
|
||||
encoded_pw = quote_plus(PG_CONFIG['password'])
|
||||
pg_conn = psycopg2.connect(
|
||||
host=PG_CONFIG['host'],
|
||||
port=PG_CONFIG['port'],
|
||||
database=PG_CONFIG['database'],
|
||||
user=PG_CONFIG['user'],
|
||||
password=PG_CONFIG['password']
|
||||
)
|
||||
|
||||
# Disable FK checks during migration
|
||||
pg_cursor = pg_conn.cursor()
|
||||
pg_cursor.execute("SET session_replication_role = 'replica';")
|
||||
pg_conn.commit()
|
||||
|
||||
# Get SQLite tables
|
||||
sqlite_cursor = sqlite_conn.cursor()
|
||||
sqlite_cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name != 'sqlite_sequence'")
|
||||
all_tables = set(t[0] for t in sqlite_cursor.fetchall())
|
||||
|
||||
# Migrate in order
|
||||
total = 0
|
||||
migrated_tables = set()
|
||||
|
||||
for table in TABLES_ORDER:
|
||||
if table in all_tables:
|
||||
total += migrate_table(sqlite_conn, pg_conn, table)
|
||||
migrated_tables.add(table)
|
||||
|
||||
# Migrate remaining tables
|
||||
remaining = all_tables - migrated_tables
|
||||
for table in remaining:
|
||||
total += migrate_table(sqlite_conn, pg_conn, table)
|
||||
|
||||
# Re-enable FK checks
|
||||
pg_cursor.execute("SET session_replication_role = 'origin';")
|
||||
pg_conn.commit()
|
||||
|
||||
# Step 3: Reset sequences
|
||||
reset_sequences(pg_conn)
|
||||
|
||||
sqlite_conn.close()
|
||||
pg_conn.close()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print(f"Migration completed! Total rows: {total}")
|
||||
print("=" * 60)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,7 +1,7 @@
|
||||
fastapi
|
||||
uvicorn[standard]
|
||||
sqlalchemy
|
||||
# psycopg2-binary # Uncomment for PostgreSQL production
|
||||
psycopg2-binary # PostgreSQL production
|
||||
redis
|
||||
python-dotenv
|
||||
pydantic
|
||||
@@ -15,6 +15,8 @@ lxml
|
||||
alembic
|
||||
email-validator
|
||||
playwright # PDF capture for performance check reports
|
||||
img2pdf # Convert screenshots to PDF
|
||||
pillow # Image processing for PDF generation
|
||||
apscheduler # Scheduled tasks (exchange rate updates)
|
||||
stripe # Payment processing
|
||||
user-agents # Visitor tracking
|
||||
|
||||
Reference in New Issue
Block a user