Skip to content

Commit 439b1b2

Browse files
committed
feat: refactor database session management and introduce global state for engine
1 parent b10a5af commit 439b1b2

6 files changed

Lines changed: 49 additions & 28 deletions

File tree

src/api/deps.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from typing import Annotated, Generator
22

3+
import state
34
from config import settings
4-
from database import engine
55
from fastapi import Depends, HTTPException
66
from fastapi.security import APIKeyHeader
77
from sqlmodel import Session
@@ -16,7 +16,7 @@ def get_db_session() -> Generator[Session, None, None]:
1616
"""
1717
Create a new database session and close the session after the operation has ended.
1818
"""
19-
with Session(engine) as session:
19+
with Session(state.engine) as session:
2020
yield session
2121

2222

src/api/endpoints/health.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
from database import check_health_safe, engine
1+
from database import check_health_safe
22
from fastapi import APIRouter
33
from starlette import status
4+
from state import engine
45

56
router = APIRouter()
67

src/database.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,3 @@ def create_db_and_tables(engine: Engine):
7878
Create database tables based on the defined SQLModel models.
7979
"""
8080
SQLModel.metadata.create_all(engine)
81-
82-
83-
engine = create_db_engine()

src/main.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
import os
22
from contextlib import asynccontextmanager
33

4+
import state
45
import uvicorn
56
from api.exception_handlers import validation_exception_handler
67
from api.router import router
78
from config import init_whatever, settings
8-
from database import engine
9+
from database import create_db_engine
910
from fastapi import FastAPI
1011
from fastapi.exceptions import RequestValidationError
1112
from fastapi.middleware.cors import CORSMiddleware
1213
from middlewares.logging import LogMiddleware
1314
from pydantic import ValidationError
15+
from utils.log import get_logger
16+
17+
logger = get_logger()
1418

1519

1620
@asynccontextmanager
@@ -20,9 +24,14 @@ async def lifespan(app: FastAPI):
2024
# It may be stored in app state object:
2125
# app.state.some_resource = SomeResource()
2226
# https://github.com/fastapi/fastapi/discussions/13029
27+
28+
state.engine = create_db_engine()
2329
yield
30+
2431
# Clean up
25-
engine.dispose()
32+
if state.engine:
33+
state.engine.dispose()
34+
logger.info("Database engine disposed")
2635

2736

2837
app = FastAPI(title=settings.SERVICE_NAME, version="0.1.0", lifespan=lifespan)

src/state.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""
2+
Global application state.
3+
Holds singleton instances that are initialized during app startup.
4+
"""
5+
6+
from sqlalchemy import Engine
7+
8+
# Global singleton instances
9+
engine: Engine | None = None

tests/conftest.py

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import os
2-
import tempfile
31
from pathlib import Path
42

53
import pytest
@@ -28,25 +26,26 @@ def data_folder_path():
2826
return Path(__file__).parent.parent / "data"
2927

3028

31-
@pytest.fixture(name="session")
32-
def session_fixture(request, monkeypatch) -> Session:
33-
"""Create a new database session for a test."""
29+
@pytest.fixture(scope="session")
30+
def db_engine(request, tmp_path_factory, session_mocker):
31+
"""
32+
Create a session-scoped database engine.
33+
Database is created once and migrations run once for all tests.
34+
"""
3435
# TODO: Use an in-memory SQLite database for faster tests if possible.
3536
# https://sqlmodel.tiangolo.com/tutorial/fastapi/tests/#memory-database
3637

3738
db_type = request.config.getoption("--db")
38-
3939
if db_type == "postgres":
4040
db_url = "postgresql://develop:develop_secret@localhost:5432/develop"
41-
path = None
4241
else:
4342
# Create a temporary database file for SQLite
44-
fd, path = tempfile.mkstemp(suffix=".db")
45-
os.close(fd)
46-
db_url = f"sqlite:///{path}"
43+
temp_dir = tmp_path_factory.mktemp("db")
44+
db_path = temp_dir / "test.db"
45+
db_url = f"sqlite:///{db_path}"
4746

4847
# Use monkeypatch to set DATABASE_URL environment variable
49-
monkeypatch.setattr("config.settings.DATABASE_URL", db_url)
48+
session_mocker.patch("config.settings.DATABASE_URL", db_url)
5049

5150
# Get path to alembic.ini
5251
src_dir = Path(__file__).parents[1] / "src"
@@ -61,12 +60,22 @@ def session_fixture(request, monkeypatch) -> Session:
6160
# TODO: Alternatively, you can create tables directly without migrations for simpler setups.
6261
# create_db_and_tables(engine)
6362

64-
# Ensure that changes made during tests do not persist and affect other tests using a nested transaction
65-
# This is needed for PostgreSQL since the SQLite is erased after each test by deleting the temp file
66-
connection = engine.connect()
63+
yield engine
64+
65+
# Clean up at the end of the test session
66+
engine.dispose()
67+
68+
69+
@pytest.fixture(name="session")
70+
def session_fixture(db_engine) -> Session:
71+
"""
72+
Create a new database session for a test, wrapped in a transaction that is rolled back after the test.
73+
"""
74+
75+
connection = db_engine.connect()
6776
transaction = connection.begin()
6877
session = Session(bind=connection)
69-
78+
# Ensure that changes made during tests do not persist and affect other tests using a nested transaction
7079
nested = connection.begin_nested()
7180

7281
@sa.event.listens_for(session, "after_transaction_end")
@@ -77,15 +86,11 @@ def end_savepoint(session, transaction):
7786

7887
yield session
7988

89+
# Rollback the transaction (this undoes all changes made during the test)
8090
session.close()
8191
transaction.rollback()
8292
connection.close()
8393

84-
# Clean up
85-
engine.dispose()
86-
if path:
87-
os.unlink(path)
88-
8994

9095
@pytest.fixture(name="client_with_db")
9196
def client_fixture(session: Session):

0 commit comments

Comments
 (0)