import logging import os from urllib.parse import quote_plus from sqlalchemy import create_engine, text from sqlalchemy.orm import declarative_base, sessionmaker logger = logging.getLogger(__name__) def load_env_file(path: str = ".env"): if not os.path.exists(path): return with open(path, "r", encoding="utf-8") as env_file: for raw_line in env_file: line = raw_line.strip() if not line or line.startswith("#") or "=" not in line: continue key, value = line.split("=", 1) key = key.strip() value = value.strip().strip('"').strip("'") if key and key not in os.environ: os.environ[key] = value load_env_file() def _build_pg_url(db_name: str) -> str: user = os.getenv("DB_USER", "") password = os.getenv("DB_PASSWORD", "") host = os.getenv("DB_HOST", "localhost") port = os.getenv("DB_PORT", "5432") return ( f"postgresql+psycopg2://{quote_plus(user)}:{quote_plus(password)}" f"@{host}:{port}/{db_name}" ) def _resolve_database_url() -> str: explicit_url = os.getenv("DATABASE_URL") if explicit_url: return explicit_url order_db = os.getenv("ORDER_DB_NAME") if order_db and os.getenv("DB_HOST"): return _build_pg_url(order_db) return "sqlite:///./app.db" DATABASE_URL = _resolve_database_url() connect_args = {} engine_kwargs = {} if DATABASE_URL.startswith("sqlite"): connect_args = {"check_same_thread": False, "timeout": 30} else: engine_kwargs.update(pool_pre_ping=True, pool_recycle=1800) engine = create_engine(DATABASE_URL, connect_args=connect_args, **engine_kwargs) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) Base = declarative_base() # --- Secondary engines for the other PostgreSQL databases ---------------- # These are exposed for code that needs to query item / return / checklist # databases. They are created lazily and only when PostgreSQL is configured. _secondary_engines: dict[str, object] = {} def get_secondary_engine(env_var: str): """Get (or create) an engine for ITEM_DB_NAME / RETURN_DB_NAME / CHECKLIST_DB_NAME.""" db_name = os.getenv(env_var) if not db_name or not os.getenv("DB_HOST"): return None if env_var in _secondary_engines: return _secondary_engines[env_var] eng = create_engine(_build_pg_url(db_name), pool_pre_ping=True, pool_recycle=1800) _secondary_engines[env_var] = eng return eng # Dependency for FastAPI to get DB session def get_db(): db = SessionLocal() try: yield db finally: db.close() def check_db_connection() -> tuple[bool, str]: """Ping the primary database. Returns (ok, message).""" try: with engine.connect() as conn: conn.execute(text("SELECT 1")) return True, "ok" except Exception as exc: # noqa: BLE001 return False, str(exc) def log_db_startup_status() -> None: safe_url = engine.url.render_as_string(hide_password=True) ok, msg = check_db_connection() if ok: logger.warning("[DB] Connected: %s", safe_url) else: logger.error("[DB] Connection FAILED: %s -> %s", safe_url, msg)