Files
SlabUploader/backend/app/db/engine.py
T
grokbot_ty e5514fe3bf feat(#27): Gate B [07/10]: FastAPI health endpoint + app factory (H-1)
- App factory pattern (FUND-8): create_app() produces fresh app per test
- Logging config (structured, no secrets)
- Config validation: SLAB_AES_KEY required, WOO_BASE_URL HTTPS-only
- GET /api/health returns 200 {status: ok, version, woo_reachable: false}
- SQLite + alembic skeleton: full DATA-MODEL schema, initial migration
- API contract tests via httpx TestClient: 5 cases all green
  - test_returns_200, test_returns_ok_status, test_woo_reachable_false
  - test_response_shape, test_with_test_client_lifespan (FUND-9/10)
2026-09-07 07:53:36 +00:00

85 lines
2.1 KiB
Python

"""SQLite engine and session management.
Uses SQLAlchemy async engine with aiosqlite driver.
For tests, pass `test_db_url` via the settings or use a test fixture
to point at an in-memory SQLite database.
"""
import logging
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from app.db.models import Base
logger = logging.getLogger(__name__)
# Default DB path for production
DEFAULT_DB_URL = "sqlite+aiosqlite:///data/slab.db"
_engine: object = None
_session_factory: object = None
_test_url: str | None = None
def set_test_url(url: str) -> None:
"""Override the database URL for testing. Must be called before create_app."""
global _test_url
_test_url = url
def get_db_url() -> str:
"""Return the database URL, preferring test URL if set."""
if _test_url:
return _test_url
return DEFAULT_DB_URL
async def init_db() -> None:
"""Initialize the async engine, session factory, and create tables.
For in-memory SQLite tests, creates tables directly.
For file-based SQLite, attempts alembic migrations first, falling
back to direct table creation.
"""
global _engine, _session_factory
db_url = get_db_url()
logger.info("Connecting to %s", db_url)
_engine = create_async_engine(
db_url,
echo=False,
connect_args={"check_same_thread": False},
)
_session_factory = async_sessionmaker(
_engine,
class_=AsyncSession,
expire_on_commit=False,
autocommit=False,
autoflush=False,
)
# Create all tables (works for both in-memory and file-based)
async with _engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
logger.info("Database initialized")
async def close_db_connection() -> None:
"""Dispose of the engine (closes all connections)."""
global _engine
if _engine is not None:
await _engine.dispose()
_engine = None
async def get_session() -> AsyncSession:
"""Get a database session."""
if _session_factory is None:
raise RuntimeError("Database not initialized. Call init_db() first.")
return _session_factory()