from __future__ import annotations import os from urllib.parse import quote_plus from typing import Dict, Any, Optional from dotenv import load_dotenv from pathlib import Path env_path = Path(__file__).resolve().parent.parent / ".env" load_dotenv(dotenv_path=env_path, override=False) def build_database_url( *, backend: Optional[str] = None, url: Optional[str] = None, user: Optional[str] = None, password: Optional[str] = None, host: Optional[str] = None, port: Optional[str] = None, database: Optional[str] = None, driver: Optional[str] = None, dsn: Optional[str] = None, trusted: Optional[bool] = None, options: Optional[Dict[str, str]] = None, ) -> str: """ Build a SQLAlchemy URI. If `url` is provided, it wins. Supported backends: sqlite, postgresql, mysql, mssql """ if url: return url backend = (backend or "").lower().strip() options = options or {} if backend == 'sqlite': # Database can be a file path or ':memory:' db_path = database or 'inventory.db' if db_path == ':memory:': return 'sqlite:///:memory:' # Three slashes = relative file; four = absolute. We'll assume relative. return f"sqlite:///{db_path}" if backend in {'postgres', 'postgresql'}: driver = driver or 'psycopg' user = user or "" password = password or "" creds = f"{quote_plus(user)}:{quote_plus(password)}@" if user or password else "" host = host or "localhost" port = port or "5432" database = database or "inventory" qs = "" if options: qs = "?" + "&".join(f"{k}={quote_plus(v)}" for k, v in options.items()) return f"postgresql+{driver}://{creds}{host}:{port}/{database}{qs}" if backend in {"mysql"}: driver = driver or "pymysql" user = user or "" password = password or "" creds = f"{quote_plus(user)}:{quote_plus(password)}@" if user or password else "" host = host or "localhost" port = port or "3306" database = database or "inventory" qs = "" if options: qs = "?" + "&".join(f"{k}={quote_plus(v)}" for k, v in options.items()) return f"mysql+{driver}://{creds}{host}:{port}/{database}{qs}" if backend in {"mssql", "sqlserver", "sqlsrv"}: if dsn: qs = "" if options: qs = "?" + "&".join(f"{k}={quote_plus(v)}" for k, v in options.items()) return f"mssql+pyodbc://@{quote_plus(dsn)}{qs}" driver = driver or "ODBC Driver 18 for SQL Server" host = host or "localhost" port = port or "1433" database = database or "inventory" if trusted: opts = {"driver": driver, "Trusted_Connection": "yes", **options} qs = "?" + "&".join(f"{k}={quote_plus(v)}" for k, v in opts.items()) return f"mssql+pyodbc://{host}:{port}/{database}{qs}" user = user or "" password = password or "" creds = f"{quote_plus(user)}:{quote_plus(password)}@" if user or password else "" options = {"driver": driver, "TrustCertificate": "yes", **options} qs = "?" + "&".join(f"{k}={quote_plus(v)}" for k, v, in options.items()) return f"mssql+pyodbc://{creds}{host}:{port}/{database}{qs}" raise ValueError(f"Unsupported backend: {backend!r}") class Config: """ One config to feed both Flask and SQLAlchemy. Pulls sane defaults from env; override per-environment via subclasses. """ # Flask bits DEBUG = False TESTING = False SECRET_KEY = os.getenv("SECRET_KEY", "dev-not-secret") DATABASE_URL = build_database_url( url=os.getenv("DATABASE_URL", None), backend=os.getenv("DB_BACKEND", None), user=os.getenv("DB_USER", None), password=os.getenv("DB_PASS", None), host=os.getenv("DB_HOST", None), port=os.getenv("DB_PORT", None), database=os.getenv("DB_NAME", None), driver=os.getenv("DB_DRIVER", None), dsn=os.getenv("DB_DSN", None), trusted=bool(int(os.getenv("DB_TRUSTED", "0"))), options=None, ) SQLALCHEMY_ECHO = bool(int(os.getenv("DB_ECHO", "0"))) POOL_SIZE = int(os.getenv("DB_POOL_SIZE", "5")) MAX_OVERFLOW = int(os.getenv("DB_MAX_OVERFLOW", "10")) POOL_TIMEOUT = int(os.getenv("DB_POOL_TIMEOUT", "30")) POOL_RECYCLE = int(os.getenv("DB_POOL_RECYCLE", "1000")) POOL_PRE_PING = True SQLITE_PRAGMAS = { "journal_mode": os.getenv("SQLITE_JOURNAL_MODE", "WAL"), "foreign_keys": os.getenv("SQLITE_FOREIGN_KEYS", "ON"), "synchronous": os.getenv("SQLITE_SYNCHRONOUS", "NORMAL"), } @classmethod def engine_kwargs(cls) -> Dict[str, Any]: """ Create engine kwargs consistently for any backend """ url = cls.DATABASE_URL kwargs: Dict[str, Any] = { "echo": cls.SQLALCHEMY_ECHO, "pool_pre_ping": cls.POOL_PRE_PING, "future": True, } if url.startswith("sqlite://"): kwargs["connect_args"] = {"check_same_thread": False} elif url.startswith("mssql+pyodbc://"): pass kwargs.update( { "pool_size": cls.POOL_SIZE, "max_overflow": cls.MAX_OVERFLOW, "pool_timeout": cls.POOL_TIMEOUT, "pool_recycle": cls.POOL_RECYCLE, } ) return kwargs @classmethod def session_kwargs(cls) -> Dict[str, Any]: """ SessionMaker settings that won't bite you during normal request lifecycles. """ return { "autoflush": False, "autocommit": False, "expire_on_commit": False, "future": True, } class DevConfig(Config): DEBUG = True SQLALCHEMY_ECHO = bool(int(os.getenv("DB_ECHO", "1"))) class TestConfig(Config): TESTING = True DATABASE_URL = build_database_url(backend="sqlite", database=":memory:") SQLALCHEMY_ECHO = False class ProdConfig(Config): DEBUG = False SQLALCHEMY_ECHO = bool(int(os.getenv("DB_ECHO", "0")))