-
Notifications
You must be signed in to change notification settings - Fork 0
Scaffold async database connection #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,34 @@ | ||||||
| import os | ||||||
| from collections.abc import AsyncGenerator | ||||||
|
|
||||||
| from dotenv import load_dotenv | ||||||
| from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine | ||||||
| from sqlmodel import SQLModel | ||||||
|
|
||||||
| load_dotenv() | ||||||
|
|
||||||
| DATABASE_URL = os.environ.get("DATABASE_URL") | ||||||
| if not DATABASE_URL: | ||||||
| raise ValueError( | ||||||
| "DATABASE_URL environment variable is required. " | ||||||
| "See .env.example for setup instructions." | ||||||
| ) | ||||||
|
|
||||||
| engine = create_async_engine( | ||||||
| DATABASE_URL, echo=os.environ.get("ENVIRONMENT") == "development" | ||||||
| ) | ||||||
|
|
||||||
| # keeps objects usable after commit (relevant for async sessions) | ||||||
| _session_factory = async_sessionmaker(engine, expire_on_commit=False) | ||||||
|
|
||||||
|
|
||||||
| async def get_session() -> AsyncGenerator[AsyncSession]: | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📝 Type annotation: The
Suggested change
Why this matters:
Pyright should catch this — if it doesn't, double-check your pyright config is active.
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. in Python 3.13 the None second argument is the default, so writing it out is redundant |
||||||
| """FastAPI dependency — yields one session per request, closed on exit.""" | ||||||
| async with _session_factory() as session: | ||||||
| yield session | ||||||
|
|
||||||
|
|
||||||
| async def create_db_and_tables() -> None: | ||||||
| """Create every table registered in SQLModel.metadata (runs on startup).""" | ||||||
| async with engine.begin() as conn: | ||||||
| await conn.run_sync(SQLModel.metadata.create_all) | ||||||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,20 @@ | ||||||||
| from collections.abc import AsyncGenerator | ||||||||
| from contextlib import asynccontextmanager | ||||||||
|
|
||||||||
| from fastapi import FastAPI | ||||||||
|
|
||||||||
| from app.database import create_db_and_tables | ||||||||
|
|
||||||||
|
|
||||||||
| @asynccontextmanager | ||||||||
| async def lifespan(app: FastAPI) -> AsyncGenerator[None]: | ||||||||
| await create_db_and_tables() | ||||||||
| yield | ||||||||
|
|
||||||||
|
|
||||||||
| app = FastAPI(lifespan=lifespan) | ||||||||
|
|
||||||||
|
|
||||||||
| @app.get("/health") | ||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Consistency: For consistency with the rest of the codebase (which uses async throughout), consider making this
Suggested change
Why: While FastAPI handles sync functions fine by running them in a thread pool, using
Not required, but a good habit in an async-first project. |
||||||||
| async def health() -> dict[str, str]: | ||||||||
| return {"status": "ok"} | ||||||||
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Learning note:
expire_on_commit=Falseis important for async SQLAlchemy/SQLModel usage.Why: By default, SQLAlchemy expires all objects after a commit, requiring a fresh query to access attributes. With async code, you can't lazily reload attributes after the session is closed (you'd need
await). Setting this toFalsekeeps objects usable after commit.Tradeoff: You won't automatically see updates from other sessions, but that's fine for request-scoped sessions where each request gets a fresh session anyway.
Worth adding a brief comment here since it's a common async gotcha.