mz_chatbot / scripts /check_env.py
Shurem's picture
shift mysql to neondb
ab8ae7f
Raw History Blame Contribute Delete
5.14 kB
"""
check_env.py β€” resolve and report the database configuration WITHOUT connecting.
Answers "is my .env actually valid?" before you run alembic. Stdlib only, so it
works before `uv sync`. Passwords are never printed.
python3 scripts/check_env.py
Common trap this catches: DATABASE_URL is a URL, so a password containing
@ : / ? # % must be percent-encoded β€” Neon's own connection string is already
encoded correctly, so this usually only bites if the password was edited by hand.
"""
from __future__ import annotations
import sys
from pathlib import Path
from urllib.parse import unquote, urlsplit
BACKEND = Path(__file__).resolve().parent.parent
REPO_ROOT = BACKEND.parent
NEEDS_ENCODING = set("@:/?#[]% ")
def load_env_file(path: Path) -> dict[str, str]:
"""Minimal .env reader β€” same precedence rule as config.py (first wins)."""
values: dict[str, str] = {}
if not path.exists():
return values
for raw in path.read_text(encoding="utf-8").splitlines():
line = raw.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, val = line.partition("=")
val = val.strip()
if len(val) >= 2 and val[0] == val[-1] and val[0] in "\"'":
val = val[1:-1]
values.setdefault(key.strip(), val)
return values
def main() -> int:
env = load_env_file(BACKEND / ".env")
for k, v in load_env_file(REPO_ROOT / ".env").items():
env.setdefault(k, v)
if not (BACKEND / ".env").exists():
print(f"ERROR: {BACKEND / '.env'} does not exist. Copy .env.example to .env.")
return 1
problems: list[str] = []
url = env.get("DATABASE_URL", "").strip()
print(f"backend .env : {BACKEND / '.env'}")
print(f"root .env : {REPO_ROOT / '.env'}"
f"{'' if (REPO_ROOT / '.env').exists() else ' (absent)'}")
print()
if not url:
problems.append("DATABASE_URL is empty β€” paste the Neon connection string in")
else:
parts = urlsplit(url)
driver, host, port = parts.scheme, parts.hostname, parts.port
user = unquote(parts.username or "")
raw_pw = parts.password or ""
db = parts.path.lstrip("/")
query = parts.query
print(f"driver : {driver}")
print(f"host : {host}")
print(f"port : {port or 5432}")
print(f"user : {user}")
print(f"password : {'set (' + str(len(raw_pw)) + ' chars as written)' if raw_pw else 'MISSING'}")
print(f"database : {db}")
print(f"query : {query or '(none)'}")
if driver != "postgresql+psycopg":
problems.append(
f"driver is {driver!r}; this backend expects postgresql+psycopg (psycopg v3)"
)
if not host:
problems.append("no host in DATABASE_URL")
if host and "neon.tech" not in host:
print(" (host does not look like a Neon endpoint β€” fine if intentional)")
if not user:
problems.append("no user in DATABASE_URL")
if not raw_pw:
problems.append("no password in DATABASE_URL")
if not db:
problems.append("no database name in DATABASE_URL (the path after the host)")
if "sslmode=require" not in query and "sslmode=verify-full" not in query:
problems.append("sslmode=require missing β€” Neon refuses plain-text connections")
# A raw special character in the password truncates the URL silently.
decoded = unquote(raw_pw)
if decoded != raw_pw and any(c in NEEDS_ENCODING for c in decoded):
print(" (password contains percent-encoded characters β€” good)")
elif any(c in NEEDS_ENCODING for c in raw_pw):
problems.append(
"password contains a character that must be percent-encoded "
f"({''.join(sorted(set(c for c in raw_pw if c in NEEDS_ENCODING)))}) β€” "
"urlsplit has probably mis-parsed this URL"
)
print()
print("service settings")
for key, default in (
("PUBLIC_BASE_URL", "http://127.0.0.1:8000"),
("CORS_ORIGINS", "http://localhost:3000,http://127.0.0.1:3000"),
("SCREENSHOT_DIR", "screenshots"),
):
val = env.get(key, "").strip()
print(f" {key:16s}: {val or default}{' (default)' if not val else ''}")
print(f" {'API_KEY':16s}: {'set β€” X-API-Key required' if env.get('API_KEY','').strip() else 'empty β€” auth disabled (fine for localhost)'}")
print()
print("third-party keys (presence only)")
for key in ("GEMINI_API_KEY", "HUNTER_API_KEY", "SIGNALHIRE_API_KEY", "APOLLO_API_KEY", "CALENDAR_LINK"):
print(f" {key:20s}: {'set' if env.get(key, '').strip() else 'not set'}")
print()
if problems:
print(f"{len(problems)} problem(s):")
for p in problems:
print(f" - {p}")
return len(problems)
print("config looks valid. Next: uv run alembic upgrade head")
return 0
if __name__ == "__main__":
sys.exit(main())