Spaces:
Running
Running
File size: 5,136 Bytes
ff3c2e9 ab8ae7f ff3c2e9 ab8ae7f ff3c2e9 ab8ae7f ff3c2e9 ab8ae7f ff3c2e9 ab8ae7f ff3c2e9 ab8ae7f ff3c2e9 ab8ae7f ff3c2e9 ab8ae7f ff3c2e9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | """
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())
|