Spaces:
Sleeping
Sleeping
| """ | |
| Standalone DB connectivity test — run with: | |
| uv run python test_db.py | |
| Tests each layer independently so the exact failure point is visible. | |
| Loads DATABASE_URL from .env automatically. | |
| """ | |
| import asyncio | |
| import os | |
| import socket | |
| import sys | |
| from urllib.parse import urlparse | |
| from dotenv import load_dotenv | |
| load_dotenv(override=True) | |
| # asyncpg requires SelectorEventLoop on Windows — ProactorEventLoop (default in | |
| # Python 3.8+ on Windows) causes asyncpg to hang silently on connect. | |
| if sys.platform == "win32": | |
| asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) | |
| RAW_URL = os.environ.get("DATABASE_URL", "") | |
| # ── 0. Show what URL we are working with ────────────────────────────────────── | |
| print("\n── Step 0: URL check ──────────────────────────────────────────────────") | |
| if not RAW_URL: | |
| print("FAIL DATABASE_URL is not set in .env") | |
| sys.exit(1) | |
| parsed = urlparse(RAW_URL) | |
| masked = RAW_URL.replace(parsed.password or "", "***") if parsed.password else RAW_URL | |
| print(f" Raw scheme : {parsed.scheme}") | |
| print(f" Host : {parsed.hostname}") | |
| print(f" Port : {parsed.port or 5432}") | |
| print(f" Database : {parsed.path.lstrip('/')}") | |
| print(f" Full URL : {masked}") | |
| # asyncpg needs postgresql+asyncpg:// — detect and auto-fix for this test | |
| ASYNCPG_URL = RAW_URL | |
| if parsed.scheme == "postgresql" or parsed.scheme == "postgres": | |
| ASYNCPG_URL = RAW_URL.replace(parsed.scheme + "://", "postgresql+asyncpg://", 1) | |
| print(f"\n NOTE: scheme was '{parsed.scheme}://' — using 'postgresql+asyncpg://' for asyncpg test") | |
| print(f" → Fix your .env: change DATABASE_URL to start with 'postgresql+asyncpg://'") | |
| # ── 1. DNS resolution ───────────────────────────────────────────────────────── | |
| print("\n── Step 1: DNS resolution ─────────────────────────────────────────────") | |
| host = parsed.hostname | |
| try: | |
| ip = socket.gethostbyname(host) | |
| print(f" OK {host} → {ip}") | |
| except socket.gaierror as e: | |
| print(f" FAIL Cannot resolve {host}: {e}") | |
| sys.exit(1) | |
| # ── 2. TCP reachability ─────────────────────────────────────────────────────── | |
| print("\n── Step 2: TCP connect (port 5432) ────────────────────────────────────") | |
| port = parsed.port or 5432 | |
| try: | |
| sock = socket.create_connection((host, port), timeout=10) | |
| sock.close() | |
| print(f" OK TCP {host}:{port} reachable") | |
| except (socket.timeout, ConnectionRefusedError, OSError) as e: | |
| print(f" FAIL Cannot TCP-connect to {host}:{port}: {e}") | |
| print(" Common cause: corporate firewall blocks port 5432.") | |
| print(" Try Neon's pooler port 6543 or use a VPN/hotspot.") | |
| sys.exit(1) | |
| # ── 3a. TLS handshake (pure Python ssl — no asyncpg, no asyncio) ────────────── | |
| print("\n── Step 3a: TLS handshake (no asyncpg) ────────────────────────────────") | |
| import ssl as ssl_mod | |
| import threading | |
| def _tls_test(result): | |
| try: | |
| ctx = ssl_mod.SSLContext(ssl_mod.PROTOCOL_TLS_CLIENT) | |
| ctx.check_hostname = False | |
| ctx.verify_mode = ssl_mod.CERT_NONE | |
| raw = socket.create_connection((parsed.hostname, parsed.port or 5432), timeout=10) | |
| tls = ctx.wrap_socket(raw, server_hostname=parsed.hostname) | |
| result["ok"] = True | |
| result["ver"] = tls.version() | |
| tls.close() | |
| except Exception as e: | |
| result["err"] = f"{type(e).__name__}: {e}" | |
| _r = {} | |
| _t = threading.Thread(target=_tls_test, args=(_r,), daemon=True) | |
| _t.start() | |
| _t.join(timeout=12) | |
| if _t.is_alive(): | |
| print(" FAIL TLS timed out — SSL port blocked or Neon not responding to TLS") | |
| sys.exit(1) | |
| elif "err" in _r: | |
| print(f" FAIL TLS error: {_r['err']}") | |
| sys.exit(1) | |
| else: | |
| print(f" OK TLS handshake succeeded, protocol={_r['ver']}") | |
| # ── 3b. psycopg (psycopg3) sync connection — no asyncio, no Windows issues ──── | |
| print("\n── Step 3b: psycopg connection ────────────────────────────────────────") | |
| try: | |
| import psycopg | |
| except ImportError: | |
| print(" FAIL psycopg not installed — run: uv add 'psycopg[binary]'") | |
| sys.exit(1) | |
| _host = parsed.hostname | |
| _port = parsed.port or 5432 | |
| _user = parsed.username | |
| _passw = parsed.password | |
| _dbname = parsed.path.lstrip("/") | |
| def _psycopg_thread(result): | |
| try: | |
| conn = psycopg.connect( | |
| host=_host, port=_port, user=_user, | |
| password=_passw, dbname=_dbname, | |
| sslmode="require", connect_timeout=30, | |
| ) | |
| row = conn.execute("SELECT version()").fetchone() | |
| conn.close() | |
| result["version"] = row[0] | |
| except Exception as e: | |
| result["err"] = f"{type(e).__name__}: {e}" | |
| import time as _time | |
| _r2 = {} | |
| _t2 = threading.Thread(target=_psycopg_thread, args=(_r2,), daemon=True) | |
| _t2.start() | |
| print(" ... waiting", end="", flush=True) | |
| for _i in range(35): | |
| if not _t2.is_alive(): | |
| break | |
| _time.sleep(1) | |
| print(f" {_i+1}s", end="", flush=True) | |
| print() | |
| if _t2.is_alive(): | |
| print(" FAIL timed out after 35s — check Neon compute status") | |
| sys.exit(1) | |
| elif "err" in _r2: | |
| print(f" FAIL {_r2['err']}") | |
| sys.exit(1) | |
| else: | |
| print(f" OK {_r2['version'][:80]}") | |
| # ── 4. SQLAlchemy async engine (psycopg, main-thread asyncio) ───────────────── | |
| print("\n── Step 4: SQLAlchemy async engine ────────────────────────────────────") | |
| from sqlalchemy.ext.asyncio import create_async_engine | |
| from sqlalchemy import text as sa_text | |
| sa_url = (f"postgresql+psycopg://{parsed.username}:{parsed.password}" | |
| f"@{parsed.hostname}:{parsed.port or 5432}{parsed.path}") | |
| async def _sqlalchemy_async(): | |
| eng = create_async_engine( | |
| sa_url, | |
| connect_args={"sslmode": "require", "connect_timeout": 30}, | |
| pool_pre_ping=True, | |
| ) | |
| async with eng.connect() as conn: | |
| row = (await conn.execute(sa_text("SELECT version()"))).fetchone() | |
| await eng.dispose() | |
| return row[0] | |
| print(" ... connecting (connect_timeout=30s)", flush=True) | |
| try: | |
| val = asyncio.run(_sqlalchemy_async()) | |
| print(f" OK {val[:80]}") | |
| except Exception as e: | |
| print(f" FAIL {type(e).__name__}: {e}") | |
| sys.exit(1) | |
| print("\n══ All checks passed — Neon database is reachable and ready ══════════\n") | |