commit
193b0f72bd
2 changed files with 541 additions and 0 deletions
-
368load_postgres.py
-
173load_redis.py
@ -0,0 +1,368 @@ |
|||||
|
import csv |
||||
|
import os |
||||
|
import psycopg2 |
||||
|
from psycopg2 import sql |
||||
|
import multiprocessing as mp |
||||
|
from concurrent.futures import ProcessPoolExecutor, as_completed |
||||
|
import time |
||||
|
import sys |
||||
|
import random |
||||
|
import tempfile |
||||
|
|
||||
|
# ═══════════════════════════════════════════════════════════════ |
||||
|
# تنظیمات |
||||
|
# ═══════════════════════════════════════════════════════════════ |
||||
|
DB_CONFIG = { |
||||
|
"host": "localhost", |
||||
|
"port": 5432, |
||||
|
"database": "numbers_db", |
||||
|
"user": "myuser", |
||||
|
"password": "mypassword" |
||||
|
} |
||||
|
|
||||
|
CSV_FILE = "numbers.csv" |
||||
|
TABLE_NAME = "phone_numbers" |
||||
|
COLUMN_NAME = "phone_number" |
||||
|
RANDOM_COLUMN_NAME = "random_code" |
||||
|
|
||||
|
NUM_WORKERS = 12 |
||||
|
BATCH_SIZE = 2_000_000 |
||||
|
|
||||
|
# ═══════════════════════════════════════════════════════════════ |
||||
|
# شروع توالی عددی |
||||
|
# ═══════════════════════════════════════════════════════════════ |
||||
|
PHONE_START = 100_000_000 # کمترین عدد ۹ رقمی |
||||
|
|
||||
|
|
||||
|
def optimize_connection(conn): |
||||
|
"""تنظیمات بهینه برای سرعت حداکثری""" |
||||
|
with conn.cursor() as cur: |
||||
|
cur.execute("SET work_mem = '512MB'") |
||||
|
cur.execute("SET synchronous_commit = OFF") |
||||
|
cur.execute("SET maintenance_work_mem = '2GB'") |
||||
|
cur.execute("SET max_parallel_workers_per_gather = 4") |
||||
|
cur.execute("SET random_page_cost = 1.1") |
||||
|
conn.commit() |
||||
|
|
||||
|
|
||||
|
def get_file_chunks(filename, num_chunks): |
||||
|
"""تقسیم فایل به بخشهای مساوی""" |
||||
|
print(f"Calculating chunks for {filename}...") |
||||
|
file_size = os.path.getsize(filename) |
||||
|
print(f"File size: {file_size / (1024 ** 3):.2f} GB") |
||||
|
chunk_size = file_size // num_chunks |
||||
|
chunks = [] |
||||
|
with open(filename, 'rb') as f: |
||||
|
f.readline() |
||||
|
start = f.tell() |
||||
|
for i in range(num_chunks): |
||||
|
if i < num_chunks - 1: |
||||
|
f.seek(chunk_size * (i + 1)) |
||||
|
f.readline() |
||||
|
end = f.tell() |
||||
|
chunks.append((start, end)) |
||||
|
start = end |
||||
|
else: |
||||
|
chunks.append((start, file_size)) |
||||
|
print(f"Created {len(chunks)} chunks") |
||||
|
return chunks |
||||
|
|
||||
|
|
||||
|
def count_total_rows(): |
||||
|
"""شمارش کل رکوردها""" |
||||
|
print("Counting total rows...") |
||||
|
count = 0 |
||||
|
with open(CSV_FILE, 'r') as f: |
||||
|
next(f) |
||||
|
for _ in f: |
||||
|
count += 1 |
||||
|
return count |
||||
|
|
||||
|
|
||||
|
def load_chunk(args): |
||||
|
"""هر Worker یک فایل temp میسازد و با COPY لود میکند""" |
||||
|
chunk_start, chunk_end, worker_id, rows_in_chunk = args |
||||
|
print(f"[Worker {worker_id}] Starting... chunk: {chunk_start} to {chunk_end}") |
||||
|
|
||||
|
conn = psycopg2.connect(**DB_CONFIG) |
||||
|
temp_file = None |
||||
|
try: |
||||
|
optimize_connection(conn) |
||||
|
print(f"[Worker {worker_id}] Connected to DB") |
||||
|
|
||||
|
start_time = time.time() |
||||
|
loaded = 0 |
||||
|
|
||||
|
# ═══════════════════════════════════════════════════════ |
||||
|
# محاسبه شروع عدد توالی برای این Worker |
||||
|
# ═══════════════════════════════════════════════════════ |
||||
|
phone_start = PHONE_START + (worker_id * rows_in_chunk) |
||||
|
current_phone = phone_start |
||||
|
print(f"[Worker {worker_id}] Phone range: {phone_start:,} to {phone_start + rows_in_chunk - 1:,}") |
||||
|
|
||||
|
temp_fd, temp_file = tempfile.mkstemp(suffix='.csv', prefix=f'worker_{worker_id}_') |
||||
|
os.close(temp_fd) |
||||
|
|
||||
|
with open(temp_file, 'w', buffering=8192 * 1024) as temp_f: |
||||
|
with open(CSV_FILE, 'r', buffering=8192 * 1024) as f: |
||||
|
f.seek(chunk_start) |
||||
|
|
||||
|
# رد کردن خط header اگر در ابتدای chunk باشد |
||||
|
if chunk_start > 0: |
||||
|
f.readline() |
||||
|
|
||||
|
while f.tell() < chunk_end: |
||||
|
line = f.readline() |
||||
|
if line.strip(): |
||||
|
# ═══════════════════════════════════════ |
||||
|
# استفاده از عدد توالی به جای خواندن از CSV |
||||
|
# ═══════════════════════════════════════ |
||||
|
random_code = random.randint(1000, 9999) |
||||
|
temp_f.write(f"{current_phone},{random_code}\n") |
||||
|
|
||||
|
current_phone += 1 |
||||
|
loaded += 1 |
||||
|
|
||||
|
if loaded % 5_000_000 == 0: |
||||
|
print(f"[Worker {worker_id}] Wrote {loaded:,} rows to temp file") |
||||
|
|
||||
|
print(f"[Worker {worker_id}] Temp file created: {loaded:,} rows") |
||||
|
|
||||
|
with open(temp_file, 'r') as f: |
||||
|
with conn.cursor() as cur: |
||||
|
cur.copy_expert( |
||||
|
f"COPY {TABLE_NAME} ({COLUMN_NAME}, {RANDOM_COLUMN_NAME}) FROM STDIN WITH CSV", |
||||
|
f |
||||
|
) |
||||
|
conn.commit() |
||||
|
|
||||
|
elapsed = time.time() - start_time |
||||
|
rate = loaded / elapsed if elapsed > 0 else 0 |
||||
|
print(f"[Worker {worker_id}] Done! {loaded:,} rows in {elapsed:.1f}s ({rate:,.0f} rows/sec)") |
||||
|
|
||||
|
return loaded |
||||
|
|
||||
|
except Exception as e: |
||||
|
print(f"[Worker {worker_id}] ERROR: {e}") |
||||
|
raise |
||||
|
finally: |
||||
|
conn.close() |
||||
|
if temp_file and os.path.exists(temp_file): |
||||
|
os.remove(temp_file) |
||||
|
|
||||
|
|
||||
|
def create_table(conn): |
||||
|
"""ساخت جدول Unlogged برای سرعت بیشتر""" |
||||
|
with conn.cursor() as cur: |
||||
|
cur.execute(f""" |
||||
|
DROP TABLE IF EXISTS {TABLE_NAME} CASCADE; |
||||
|
CREATE UNLOGGED TABLE {TABLE_NAME} ( |
||||
|
{COLUMN_NAME} BIGINT NOT NULL, |
||||
|
{RANDOM_COLUMN_NAME} INTEGER NOT NULL |
||||
|
) WITH (fillfactor = 100); |
||||
|
""") |
||||
|
conn.commit() |
||||
|
|
||||
|
|
||||
|
def index_exists(conn, index_name): |
||||
|
"""بررسی وجود ایندکس""" |
||||
|
with conn.cursor() as cur: |
||||
|
cur.execute(""" |
||||
|
SELECT EXISTS ( |
||||
|
SELECT 1 FROM pg_indexes |
||||
|
WHERE indexname = %s |
||||
|
) |
||||
|
""", (index_name,)) |
||||
|
return cur.fetchone()[0] |
||||
|
|
||||
|
|
||||
|
def create_index(conn, column_name): |
||||
|
"""ساخت ایندکس با بررسی و تأیید""" |
||||
|
index_name = f"idx_{TABLE_NAME}_{column_name}" |
||||
|
print(f"\n>>> Creating index on '{column_name}'...") |
||||
|
|
||||
|
if index_exists(conn, index_name): |
||||
|
print(f" Index '{index_name}' already exists, dropping...") |
||||
|
with conn.cursor() as cur: |
||||
|
cur.execute(f"DROP INDEX IF EXISTS {index_name}") |
||||
|
conn.commit() |
||||
|
|
||||
|
with conn.cursor() as cur: |
||||
|
start = time.time() |
||||
|
cur.execute(f"CREATE INDEX {index_name} ON {TABLE_NAME} ({column_name})") |
||||
|
conn.commit() |
||||
|
elapsed = time.time() - start |
||||
|
print(f" ✓ Index '{index_name}' created in {elapsed:.1f}s") |
||||
|
|
||||
|
if index_exists(conn, index_name): |
||||
|
print(f" ✓ Verified: Index '{index_name}' exists") |
||||
|
return True |
||||
|
else: |
||||
|
print(f" ✗ ERROR: Index '{index_name}' was not created!") |
||||
|
return False |
||||
|
|
||||
|
|
||||
|
def verify_indexes(): |
||||
|
"""تأیید نهایی وجود همه ایندکسها""" |
||||
|
conn = psycopg2.connect(**DB_CONFIG) |
||||
|
try: |
||||
|
print("\n" + "=" * 50) |
||||
|
print("INDEX VERIFICATION") |
||||
|
print("=" * 50) |
||||
|
with conn.cursor() as cur: |
||||
|
cur.execute(""" |
||||
|
SELECT indexname, indexdef |
||||
|
FROM pg_indexes |
||||
|
WHERE tablename = %s |
||||
|
ORDER BY indexname |
||||
|
""", (TABLE_NAME,)) |
||||
|
indexes = cur.fetchall() |
||||
|
|
||||
|
if not indexes: |
||||
|
print("✗ NO INDEXES FOUND!") |
||||
|
return False |
||||
|
|
||||
|
print(f"\nFound {len(indexes)} index(es):\n") |
||||
|
for idx_name, idx_def in indexes: |
||||
|
print(f" ✓ {idx_name}") |
||||
|
print(f" Definition: {idx_def}\n") |
||||
|
|
||||
|
phone_idx_exists = any('phone_number' in idx[1] for idx in indexes) |
||||
|
random_idx_exists = any('random_code' in idx[1] for idx in indexes) |
||||
|
|
||||
|
print("-" * 50) |
||||
|
print(f"phone_number indexed: {'✓ YES' if phone_idx_exists else '✗ NO'}") |
||||
|
print(f"random_code indexed: {'✓ YES' if random_idx_exists else '✗ NO'}") |
||||
|
print("-" * 50) |
||||
|
|
||||
|
return phone_idx_exists and random_idx_exists |
||||
|
finally: |
||||
|
conn.close() |
||||
|
|
||||
|
|
||||
|
def verify_data(start_check=100_000_000, sample_points=None): |
||||
|
"""بررسی صحت دادههای لود شده""" |
||||
|
if sample_points is None: |
||||
|
sample_points = [0, 10_000_000, 50_000_000, 100_000_000, 124_999_999] |
||||
|
|
||||
|
print("\n" + "=" * 50) |
||||
|
print("DATA VERIFICATION") |
||||
|
print("=" * 50) |
||||
|
|
||||
|
conn = psycopg2.connect(**DB_CONFIG) |
||||
|
try: |
||||
|
with conn.cursor() as cur: |
||||
|
for point in sample_points: |
||||
|
expected_phone = PHONE_START + point |
||||
|
cur.execute( |
||||
|
f"SELECT {COLUMN_NAME}, {RANDOM_COLUMN_NAME} FROM {TABLE_NAME} LIMIT 1 OFFSET %s", |
||||
|
(point,) |
||||
|
) |
||||
|
row = cur.fetchone() |
||||
|
if row: |
||||
|
status = "✓" if row[0] == expected_phone else "✗" |
||||
|
print(f" {status} Offset {point:>12,}: Expected {expected_phone:,}, Got {row[0]:,}") |
||||
|
finally: |
||||
|
conn.close() |
||||
|
|
||||
|
|
||||
|
def main(): |
||||
|
print("=" * 60) |
||||
|
print("PostgreSQL Bulk Loader - Sequential Phone Numbers") |
||||
|
print("=" * 60) |
||||
|
print(f"Phone number range: {PHONE_START:,} to {PHONE_START + 125_000_000 - 1:,}") |
||||
|
print(f"Total records: 125,000,000\n") |
||||
|
|
||||
|
num_workers = min(NUM_WORKERS, mp.cpu_count()) |
||||
|
total_rows = count_total_rows() |
||||
|
|
||||
|
print(f"CSV rows: {total_rows:,}") |
||||
|
print(f"Using {num_workers} workers") |
||||
|
print(f"Batch size: {BATCH_SIZE:,}\n") |
||||
|
|
||||
|
start_time = time.time() |
||||
|
|
||||
|
try: |
||||
|
# اتصال اولیه و ساخت جدول |
||||
|
conn = psycopg2.connect(**DB_CONFIG) |
||||
|
print("Connected to DB, creating UNLOGGED table...") |
||||
|
optimize_connection(conn) |
||||
|
create_table(conn) |
||||
|
conn.close() |
||||
|
print("Table ready\n") |
||||
|
|
||||
|
# تقسیم فایل |
||||
|
print("Calculating file chunks...") |
||||
|
chunks = get_file_chunks(CSV_FILE, num_workers) |
||||
|
|
||||
|
# ═══════════════════════════════════════════════════════ |
||||
|
# محاسبه تعداد رکوردها در هر chunk |
||||
|
# ═══════════════════════════════════════════════════════ |
||||
|
rows_per_chunk = total_rows // num_workers |
||||
|
|
||||
|
args = [ |
||||
|
(start, end, i, rows_per_chunk) |
||||
|
for i, (start, end) in enumerate(chunks) |
||||
|
] |
||||
|
|
||||
|
print("\nStarting workers...\n") |
||||
|
|
||||
|
# اجرای موازی |
||||
|
with ProcessPoolExecutor(max_workers=num_workers) as executor: |
||||
|
futures = [executor.submit(load_chunk, arg) for arg in args] |
||||
|
total = 0 |
||||
|
|
||||
|
for future in as_completed(futures): |
||||
|
try: |
||||
|
total += future.result() |
||||
|
except Exception as e: |
||||
|
print(f"Worker failed: {e}") |
||||
|
|
||||
|
elapsed = time.time() - start_time |
||||
|
rate = total / elapsed if elapsed > 0 else 0 |
||||
|
remaining = (total_rows - total) / rate if rate > 0 else 0 |
||||
|
percent = (total * 100) // total_rows |
||||
|
|
||||
|
print(f"\n>>> Progress: {total:,}/{total_rows:,} ({percent}%) | " |
||||
|
f"Speed: {rate:,.0f} rows/sec | " |
||||
|
f"ETA: {remaining / 60:.1f} min") |
||||
|
|
||||
|
print(f"\n✓ Total rows loaded: {total:,}") |
||||
|
|
||||
|
# ═══════════════════════════════════════════════════════ |
||||
|
# ساخت ایندکسها با تأیید |
||||
|
# ═══════════════════════════════════════════════════════ |
||||
|
print("\n" + "=" * 60) |
||||
|
print("CREATING INDEXES") |
||||
|
print("=" * 60) |
||||
|
|
||||
|
conn = psycopg2.connect(**DB_CONFIG) |
||||
|
success1 = create_index(conn, COLUMN_NAME) |
||||
|
conn.close() |
||||
|
|
||||
|
# ═══════════════════════════════════════════════════════ |
||||
|
# تأیید نهایی |
||||
|
# ═══════════════════════════════════════════════════════ |
||||
|
all_ok = verify_indexes() |
||||
|
|
||||
|
# بررسی صحت دادهها |
||||
|
verify_data() |
||||
|
|
||||
|
total_time = time.time() - start_time |
||||
|
|
||||
|
print(f"\n✓ Done! Total time: {total_time / 60:.1f} minutes") |
||||
|
print(f"✓ Average speed: {total_rows / total_time:,.0f} rows/sec") |
||||
|
|
||||
|
if all_ok: |
||||
|
print("\n✓✓✓ ALL INDEXES CREATED SUCCESSFULLY ✓✓✓") |
||||
|
else: |
||||
|
print("\n✗✗✗ SOME INDEXES MISSING! ✗✗✗") |
||||
|
|
||||
|
except Exception as e: |
||||
|
print(f"\n✗ Error: {e}") |
||||
|
import traceback |
||||
|
traceback.print_exc() |
||||
|
|
||||
|
|
||||
|
if __name__ == "__main__": |
||||
|
main() |
||||
@ -0,0 +1,173 @@ |
|||||
|
import os |
||||
|
import redis |
||||
|
import multiprocessing as mp |
||||
|
from concurrent.futures import ProcessPoolExecutor, as_completed |
||||
|
import time |
||||
|
import random |
||||
|
|
||||
|
# ───────────────────────────────────────────── |
||||
|
# کانفیگ Redis |
||||
|
# ───────────────────────────────────────────── |
||||
|
REDIS_CONFIG = { |
||||
|
"host": "localhost", |
||||
|
"port": 6379, |
||||
|
"db": 0, |
||||
|
"decode_responses": True, |
||||
|
"socket_connect_timeout": 5, |
||||
|
"socket_keepalive": True, |
||||
|
} |
||||
|
|
||||
|
NUM_WORKERS = 8 |
||||
|
BATCH_SIZE = 100_000 |
||||
|
|
||||
|
# ═══════════════════════════════════════════════════════════════ |
||||
|
# بازه اعداد - مطابق با دادههای لود شده |
||||
|
# ═══════════════════════════════════════════════════════════════ |
||||
|
TOTAL_RECORDS = 125_000_000 |
||||
|
PHONE_START = 100_000_000 |
||||
|
PHONE_END = PHONE_START + TOTAL_RECORDS - 1 # 224,999,999 |
||||
|
|
||||
|
|
||||
|
def optimize_redis(r): |
||||
|
"""تنظیمات بهینه Redis""" |
||||
|
r.config_set("maxmemory", "16gb") |
||||
|
r.config_set("maxmemory-policy", "allkeys-lru") |
||||
|
r.config_set("save", "") |
||||
|
print("✓ Redis optimized") |
||||
|
|
||||
|
|
||||
|
def load_chunk_to_redis(args): |
||||
|
"""لود یک بخش از بازه اعداد به Redis""" |
||||
|
worker_id, start_phone, end_phone = args |
||||
|
r = redis.Redis(**REDIS_CONFIG) |
||||
|
try: |
||||
|
print(f"[Worker {worker_id}] Starting... phone: {start_phone:,} to {end_phone:,}") |
||||
|
|
||||
|
loaded = 0 |
||||
|
batch_count = 0 |
||||
|
start_time = time.time() |
||||
|
|
||||
|
pipe = r.pipeline() |
||||
|
current_phone = start_phone |
||||
|
|
||||
|
while current_phone <= end_phone: |
||||
|
random_value = random.randint(1000, 9999) |
||||
|
pipe.set(str(current_phone), random_value) |
||||
|
loaded += 1 |
||||
|
current_phone += 1 |
||||
|
|
||||
|
if loaded >= BATCH_SIZE * (batch_count + 1): |
||||
|
pipe.execute() |
||||
|
batch_count += 1 |
||||
|
elapsed = time.time() - start_time |
||||
|
rate = loaded / elapsed if elapsed > 0 else 0 |
||||
|
print(f"[Worker {worker_id}] Batch {batch_count} done ({loaded:,} rows, {rate:,.0f} rows/sec)") |
||||
|
|
||||
|
if len(pipe.command_stack) > 0: |
||||
|
pipe.execute() |
||||
|
|
||||
|
elapsed = time.time() - start_time |
||||
|
rate = loaded / elapsed if elapsed > 0 else 0 |
||||
|
print(f"[Worker {worker_id}] Done! {loaded:,} rows in {elapsed:.1f}s ({rate:,.0f} rows/sec)") |
||||
|
return loaded |
||||
|
|
||||
|
except Exception as e: |
||||
|
print(f"[Worker {worker_id}] ERROR: {e}") |
||||
|
raise |
||||
|
finally: |
||||
|
r.close() |
||||
|
|
||||
|
|
||||
|
def main(): |
||||
|
print("=" * 60) |
||||
|
print("Redis Bulk Loader - Sequential Phone Numbers") |
||||
|
print("=" * 60) |
||||
|
print(f"Phone Range: {PHONE_START:,} to {PHONE_END:,}") |
||||
|
print(f"Total Records: {TOTAL_RECORDS:,}\n") |
||||
|
|
||||
|
num_workers = min(NUM_WORKERS, mp.cpu_count()) |
||||
|
print(f"Using {num_workers} workers\n") |
||||
|
|
||||
|
r = redis.Redis(**REDIS_CONFIG) |
||||
|
try: |
||||
|
optimize_redis(r) |
||||
|
print("Clearing existing keys...") |
||||
|
r.flushdb() |
||||
|
finally: |
||||
|
r.close() |
||||
|
|
||||
|
start_time = time.time() |
||||
|
|
||||
|
try: |
||||
|
# ═══════════════════════════════════════════════════════ |
||||
|
# تقسیم بازه اعداد بین workers |
||||
|
# ═══════════════════════════════════════════════════════ |
||||
|
records_per_worker = TOTAL_RECORDS // num_workers |
||||
|
args = [] |
||||
|
|
||||
|
for i in range(num_workers): |
||||
|
if i < num_workers - 1: |
||||
|
start_phone = PHONE_START + (i * records_per_worker) |
||||
|
end_phone = PHONE_START + ((i + 1) * records_per_worker) - 1 |
||||
|
else: |
||||
|
# آخرین worker باقیمانده را هم میگیرد |
||||
|
start_phone = PHONE_START + (i * records_per_worker) |
||||
|
end_phone = PHONE_END |
||||
|
|
||||
|
args.append((i, start_phone, end_phone)) |
||||
|
print(f"Worker {i}: {start_phone:,} to {end_phone:,} ({end_phone - start_phone + 1:,} records)") |
||||
|
|
||||
|
print("\nStarting workers...\n") |
||||
|
|
||||
|
with ProcessPoolExecutor(max_workers=num_workers) as executor: |
||||
|
futures = [executor.submit(load_chunk_to_redis, arg) for arg in args] |
||||
|
total = 0 |
||||
|
|
||||
|
for future in as_completed(futures): |
||||
|
try: |
||||
|
total += future.result() |
||||
|
except Exception as e: |
||||
|
print(f"Worker failed: {e}") |
||||
|
|
||||
|
elapsed = time.time() - start_time |
||||
|
rate = total / elapsed if elapsed > 0 else 0 |
||||
|
remaining = (TOTAL_RECORDS - total) / rate if rate > 0 else 0 |
||||
|
percent = (total * 100) // TOTAL_RECORDS |
||||
|
|
||||
|
print(f"\n>>> Progress: {total:,}/{TOTAL_RECORDS:,} ({percent}%) | " |
||||
|
f"Speed: {rate:,.0f} rows/sec | " |
||||
|
f"ETA: {remaining / 60:.1f} min") |
||||
|
|
||||
|
print(f"\n✓ Total rows loaded: {total:,}") |
||||
|
|
||||
|
# بررسی نهایی |
||||
|
r = redis.Redis(**REDIS_CONFIG) |
||||
|
try: |
||||
|
print("\n--- Sample Results ---") |
||||
|
sample_phones = [ |
||||
|
PHONE_START, |
||||
|
PHONE_START + 10_000_000, |
||||
|
PHONE_START + 50_000_000, |
||||
|
PHONE_END |
||||
|
] |
||||
|
for phone in sample_phones: |
||||
|
value = r.get(str(phone)) |
||||
|
print(f" {phone:,} → {value}") |
||||
|
|
||||
|
final_count = r.dbsize() |
||||
|
print(f"\n✓ Final count in Redis: {final_count:,}") |
||||
|
finally: |
||||
|
r.close() |
||||
|
|
||||
|
total_time = time.time() - start_time |
||||
|
print(f"\n✓ Done! Total time: {total_time / 60:.1f} minutes") |
||||
|
print(f"✓ Average speed: {TOTAL_RECORDS / total_time:,.0f} rows/sec") |
||||
|
|
||||
|
except Exception as e: |
||||
|
print(f"\n✗ Error: {e}") |
||||
|
import traceback |
||||
|
traceback.print_exc() |
||||
|
|
||||
|
|
||||
|
if __name__ == "__main__": |
||||
|
main() |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue