Files
template-tutoring-admin-das…/backend/app/db.py
T

68 lines
2.0 KiB
Python
Raw Normal View History

2026-09-14 23:53:29 +05:30
"""Database engine + session.
Reads DATABASE_URL from the environment (provisioned Postgres). The engine is
created with a short connect timeout and a probe: if Postgres is unreachable at
startup, we fall back to a local SQLite file so the app still runs (Preview,
offline dev). The rest of the app never chooses a database — it imports
``SessionLocal`` and ``engine`` from here.
"""
from __future__ import annotations
import os
from collections.abc import Generator
from sqlalchemy import create_engine, text
from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker
class Base(DeclarativeBase):
pass
def _normalize(url: str) -> str:
# SQLAlchemy + psycopg3 wants the postgresql+psycopg driver.
if url.startswith("postgresql://"):
return url.replace("postgresql://", "postgresql+psycopg://", 1)
if url.startswith("postgres://"):
return url.replace("postgres://", "postgresql+psycopg://", 1)
return url
def _make_engine():
url = os.getenv("DATABASE_URL", "").strip()
if url:
pg_url = _normalize(url)
try:
eng = create_engine(
pg_url,
pool_pre_ping=True,
connect_args={"connect_timeout": 5},
)
with eng.connect() as conn:
conn.execute(text("SELECT 1"))
print("[db] using Postgres")
return eng
except Exception as exc: # noqa: BLE001
print(f"[db] Postgres unreachable ({exc}); falling back to SQLite")
sqlite_path = os.path.join(os.path.dirname(__file__), "..", "app_fallback.db")
eng = create_engine(
f"sqlite:///{os.path.abspath(sqlite_path)}",
connect_args={"check_same_thread": False},
)
print("[db] using SQLite fallback")
return eng
engine = _make_engine()
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)
def get_db() -> Generator[Session, None, None]:
db = SessionLocal()
try:
yield db
finally:
db.close()